From db5c93f96dc661b4c87dc198275967232f7edf19 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Nov 2024 18:36:24 +0100 Subject: [PATCH 001/103] Bump version to 2024.12.0b0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 514c2154611..93a909f6aad 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index e281a2429d0..a9597458d66 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.12.0.dev0" +version = "2024.12.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 3fff3003f24869ca1e12f8df430d8f201b53bb51 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Wed, 27 Nov 2024 22:37:15 +0100 Subject: [PATCH 002/103] Add missing data_description for lamarzocco OptionsFlow (#131708) --- homeassistant/components/lamarzocco/strings.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lamarzocco/strings.json b/homeassistant/components/lamarzocco/strings.json index f98d5c2a700..666eb7f4a84 100644 --- a/homeassistant/components/lamarzocco/strings.json +++ b/homeassistant/components/lamarzocco/strings.json @@ -67,8 +67,10 @@ "step": { "init": { "data": { - "title": "Update Configuration", "use_bluetooth": "Use Bluetooth" + }, + "data_description": { + "use_bluetooth": "Should the integration try to use Bluetooth to control the machine?" } } } From 897abc114e453127fd8bb81b043ce24e30f8e828 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 28 Nov 2024 01:34:36 +0100 Subject: [PATCH 003/103] Bump music assistant client 1.0.8 (#131739) --- homeassistant/components/music_assistant/manifest.json | 2 +- homeassistant/components/music_assistant/media_player.py | 6 +++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/music_assistant/fixtures/players.json | 6 +++--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/music_assistant/manifest.json b/homeassistant/components/music_assistant/manifest.json index 65e6652407f..f5cdcf50673 100644 --- a/homeassistant/components/music_assistant/manifest.json +++ b/homeassistant/components/music_assistant/manifest.json @@ -7,6 +7,6 @@ "documentation": "https://www.home-assistant.io/integrations/music_assistant", "iot_class": "local_push", "loggers": ["music_assistant"], - "requirements": ["music-assistant-client==1.0.5"], + "requirements": ["music-assistant-client==1.0.8"], "zeroconf": ["_mass._tcp.local."] } diff --git a/homeassistant/components/music_assistant/media_player.py b/homeassistant/components/music_assistant/media_player.py index 07d6ddeee03..d1d707c92e1 100644 --- a/homeassistant/components/music_assistant/media_player.py +++ b/homeassistant/components/music_assistant/media_player.py @@ -193,7 +193,7 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity): super().__init__(mass, player_id) self._attr_icon = self.player.icon.replace("mdi-", "mdi:") self._attr_supported_features = SUPPORTED_FEATURES - if PlayerFeature.SYNC in self.player.supported_features: + if PlayerFeature.SET_MEMBERS in self.player.supported_features: self._attr_supported_features |= MediaPlayerEntityFeature.GROUPING if PlayerFeature.VOLUME_MUTE in self.player.supported_features: self._attr_supported_features |= MediaPlayerEntityFeature.VOLUME_MUTE @@ -407,12 +407,12 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity): if (mass_player_id := hass_state.attributes.get("mass_player_id")) is None: continue player_ids.append(mass_player_id) - await self.mass.players.player_command_sync_many(self.player_id, player_ids) + await self.mass.players.player_command_group_many(self.player_id, player_ids) @catch_musicassistant_error async def async_unjoin_player(self) -> None: """Remove this player from any group.""" - await self.mass.players.player_command_unsync(self.player_id) + await self.mass.players.player_command_ungroup(self.player_id) @catch_musicassistant_error async def _async_handle_play_media( diff --git a/requirements_all.txt b/requirements_all.txt index 5decd2975fd..6151da007c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1409,7 +1409,7 @@ mozart-api==4.1.1.116.3 mullvad-api==1.0.0 # homeassistant.components.music_assistant -music-assistant-client==1.0.5 +music-assistant-client==1.0.8 # homeassistant.components.tts mutagen==1.47.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f824a1f212..f88bda8215a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1178,7 +1178,7 @@ mozart-api==4.1.1.116.3 mullvad-api==1.0.0 # homeassistant.components.music_assistant -music-assistant-client==1.0.5 +music-assistant-client==1.0.8 # homeassistant.components.tts mutagen==1.47.0 diff --git a/tests/components/music_assistant/fixtures/players.json b/tests/components/music_assistant/fixtures/players.json index b7ff304a7ee..2d8b88d0e8e 100644 --- a/tests/components/music_assistant/fixtures/players.json +++ b/tests/components/music_assistant/fixtures/players.json @@ -16,7 +16,7 @@ "volume_set", "volume_mute", "pause", - "sync", + "set_members", "power", "enqueue" ], @@ -57,7 +57,7 @@ "volume_set", "volume_mute", "pause", - "sync", + "set_members", "power", "enqueue" ], @@ -109,7 +109,7 @@ "volume_set", "volume_mute", "pause", - "sync", + "set_members", "power", "enqueue" ], From 74a3d11aeae67e08e8621e971f7810f0a42db9c1 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Wed, 27 Nov 2024 13:36:17 -0800 Subject: [PATCH 004/103] Add a missing rainbird data description (#131740) --- homeassistant/components/rainbird/strings.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/rainbird/strings.json b/homeassistant/components/rainbird/strings.json index 25d3a962b36..6f92b1bdb97 100644 --- a/homeassistant/components/rainbird/strings.json +++ b/homeassistant/components/rainbird/strings.json @@ -40,6 +40,9 @@ "title": "[%key:component::rainbird::config::step::user::title%]", "data": { "duration": "Default irrigation time in minutes" + }, + "data_description": { + "duration": "The default duration the sprinkler will run when turned on." } } } From c9d3ba900ec05cf1ba9fbf08fe33580fba167856 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Nov 2024 12:15:44 -0800 Subject: [PATCH 005/103] Bump aiohttp to 3.11.8 (#131744) --- homeassistant/components/http/__init__.py | 3 ++- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/http/__init__.py b/homeassistant/components/http/__init__.py index 3b18b44862a..95cdee9ab9e 100644 --- a/homeassistant/components/http/__init__.py +++ b/homeassistant/components/http/__init__.py @@ -326,7 +326,8 @@ class HomeAssistantApplication(web.Application): protocol, writer, task, - loop=self._loop, + # loop will never be None when called from aiohttp + loop=self._loop, # type: ignore[arg-type] client_max_size=self._client_max_size, ) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a4beb141911..0819990cffc 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ aiodiscover==2.1.0 aiodns==3.2.0 aiohasupervisor==0.2.1 aiohttp-fast-zlib==0.2.0 -aiohttp==3.11.7 +aiohttp==3.11.8 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 diff --git a/pyproject.toml b/pyproject.toml index a9597458d66..5f4c8747023 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.2.1", - "aiohttp==3.11.7", + "aiohttp==3.11.8", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.0", "aiozoneinfo==0.2.1", diff --git a/requirements.txt b/requirements.txt index 5ca03592107..28034d80394 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.2.0 aiohasupervisor==0.2.1 -aiohttp==3.11.7 +aiohttp==3.11.8 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.0 aiozoneinfo==0.2.1 From 47e7c4f1c140d8b4b0d2fe14928d617b265f4261 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Nov 2024 16:35:49 -0800 Subject: [PATCH 006/103] Bump orjson to 3.10.12 (#131752) changelog: https://github.com/ijl/orjson/compare/3.10.11...3.10.12 --- 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 0819990cffc..691d80f31bf 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -41,7 +41,7 @@ ifaddr==0.2.0 Jinja2==3.1.4 lru-dict==1.3.0 mutagen==1.47.0 -orjson==3.10.11 +orjson==3.10.12 packaging>=23.1 paho-mqtt==1.6.1 Pillow==11.0.0 diff --git a/pyproject.toml b/pyproject.toml index 5f4c8747023..68b0a7e5a59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ dependencies = [ "Pillow==11.0.0", "propcache==0.2.0", "pyOpenSSL==24.2.1", - "orjson==3.10.11", + "orjson==3.10.12", "packaging>=23.1", "psutil-home-assistant==0.0.1", "python-slugify==8.0.4", diff --git a/requirements.txt b/requirements.txt index 28034d80394..2cbdeb14b98 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,7 +30,7 @@ cryptography==43.0.1 Pillow==11.0.0 propcache==0.2.0 pyOpenSSL==24.2.1 -orjson==3.10.11 +orjson==3.10.12 packaging>=23.1 psutil-home-assistant==0.0.1 python-slugify==8.0.4 From 509311ac193980b9d0c1c4261d1d0bc6c16fee55 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 28 Nov 2024 08:07:19 +0100 Subject: [PATCH 007/103] Remove Spotify audio feature sensors (#131754) --- homeassistant/components/spotify/__init__.py | 2 +- .../components/spotify/coordinator.py | 22 - homeassistant/components/spotify/icons.json | 35 -- homeassistant/components/spotify/sensor.py | 179 ------ homeassistant/components/spotify/strings.json | 41 -- tests/components/spotify/conftest.py | 2 - .../spotify/fixtures/audio_features.json | 20 - .../spotify/snapshots/test_diagnostics.ambr | 14 - .../spotify/snapshots/test_sensor.ambr | 595 ------------------ tests/components/spotify/test_sensor.py | 66 -- 10 files changed, 1 insertion(+), 975 deletions(-) delete mode 100644 homeassistant/components/spotify/sensor.py delete mode 100644 tests/components/spotify/fixtures/audio_features.json delete mode 100644 tests/components/spotify/snapshots/test_sensor.ambr delete mode 100644 tests/components/spotify/test_sensor.py diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index cfcc9011b37..37580ac432d 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -29,7 +29,7 @@ from .util import ( spotify_uri_from_media_browser_url, ) -PLATFORMS = [Platform.MEDIA_PLAYER, Platform.SENSOR] +PLATFORMS = [Platform.MEDIA_PLAYER] __all__ = [ "async_browse_media", diff --git a/homeassistant/components/spotify/coordinator.py b/homeassistant/components/spotify/coordinator.py index 9e62d5f137e..a7c95e31245 100644 --- a/homeassistant/components/spotify/coordinator.py +++ b/homeassistant/components/spotify/coordinator.py @@ -7,14 +7,12 @@ from typing import TYPE_CHECKING from spotifyaio import ( ContextType, - ItemType, PlaybackState, Playlist, SpotifyClient, SpotifyConnectionError, UserProfile, ) -from spotifyaio.models import AudioFeatures from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant @@ -39,7 +37,6 @@ class SpotifyCoordinatorData: current_playback: PlaybackState | None position_updated_at: datetime | None playlist: Playlist | None - audio_features: AudioFeatures | None dj_playlist: bool = False @@ -65,7 +62,6 @@ class SpotifyCoordinator(DataUpdateCoordinator[SpotifyCoordinatorData]): ) self.client = client self._playlist: Playlist | None = None - self._currently_loaded_track: str | None = None async def _async_setup(self) -> None: """Set up the coordinator.""" @@ -84,28 +80,11 @@ class SpotifyCoordinator(DataUpdateCoordinator[SpotifyCoordinatorData]): current_playback=None, position_updated_at=None, playlist=None, - audio_features=None, ) # Record the last updated time, because Spotify's timestamp property is unreliable # and doesn't actually return the fetch time as is mentioned in the API description position_updated_at = dt_util.utcnow() - audio_features: AudioFeatures | None = None - if (item := current.item) is not None and item.type == ItemType.TRACK: - if item.uri != self._currently_loaded_track: - try: - audio_features = await self.client.get_audio_features(item.uri) - except SpotifyConnectionError: - _LOGGER.debug( - "Unable to load audio features for track '%s'. " - "Continuing without audio features", - item.uri, - ) - audio_features = None - else: - self._currently_loaded_track = item.uri - else: - audio_features = self.data.audio_features dj_playlist = False if (context := current.context) is not None: if self._playlist is None or self._playlist.uri != context.uri: @@ -128,6 +107,5 @@ class SpotifyCoordinator(DataUpdateCoordinator[SpotifyCoordinatorData]): current_playback=current, position_updated_at=position_updated_at, playlist=self._playlist, - audio_features=audio_features, dj_playlist=dj_playlist, ) diff --git a/homeassistant/components/spotify/icons.json b/homeassistant/components/spotify/icons.json index e1b08127e43..00c63141eae 100644 --- a/homeassistant/components/spotify/icons.json +++ b/homeassistant/components/spotify/icons.json @@ -4,41 +4,6 @@ "spotify": { "default": "mdi:spotify" } - }, - "sensor": { - "song_tempo": { - "default": "mdi:metronome" - }, - "danceability": { - "default": "mdi:dance-ballroom" - }, - "energy": { - "default": "mdi:lightning-bolt" - }, - "mode": { - "default": "mdi:music" - }, - "speechiness": { - "default": "mdi:speaker-message" - }, - "acousticness": { - "default": "mdi:guitar-acoustic" - }, - "instrumentalness": { - "default": "mdi:guitar-electric" - }, - "valence": { - "default": "mdi:emoticon-happy" - }, - "liveness": { - "default": "mdi:music-note" - }, - "time_signature": { - "default": "mdi:music-clef-treble" - }, - "key": { - "default": "mdi:music-clef-treble" - } } } } diff --git a/homeassistant/components/spotify/sensor.py b/homeassistant/components/spotify/sensor.py deleted file mode 100644 index 3486a911b0d..00000000000 --- a/homeassistant/components/spotify/sensor.py +++ /dev/null @@ -1,179 +0,0 @@ -"""Sensor platform for Spotify.""" - -from collections.abc import Callable -from dataclasses import dataclass - -from spotifyaio.models import AudioFeatures, Key - -from homeassistant.components.sensor import ( - SensorDeviceClass, - SensorEntity, - SensorEntityDescription, -) -from homeassistant.const import PERCENTAGE -from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity_platform import AddEntitiesCallback - -from .coordinator import SpotifyConfigEntry, SpotifyCoordinator -from .entity import SpotifyEntity - - -@dataclass(frozen=True, kw_only=True) -class SpotifyAudioFeaturesSensorEntityDescription(SensorEntityDescription): - """Describes Spotify sensor entity.""" - - value_fn: Callable[[AudioFeatures], float | str | None] - - -KEYS: dict[Key, str] = { - Key.C: "C", - Key.C_SHARP_D_FLAT: "C♯/D♭", - Key.D: "D", - Key.D_SHARP_E_FLAT: "D♯/E♭", - Key.E: "E", - Key.F: "F", - Key.F_SHARP_G_FLAT: "F♯/G♭", - Key.G: "G", - Key.G_SHARP_A_FLAT: "G♯/A♭", - Key.A: "A", - Key.A_SHARP_B_FLAT: "A♯/B♭", - Key.B: "B", -} - -KEY_OPTIONS = list(KEYS.values()) - - -def _get_key(audio_features: AudioFeatures) -> str | None: - if audio_features.key is None: - return None - return KEYS[audio_features.key] - - -AUDIO_FEATURE_SENSORS: tuple[SpotifyAudioFeaturesSensorEntityDescription, ...] = ( - SpotifyAudioFeaturesSensorEntityDescription( - key="bpm", - translation_key="song_tempo", - native_unit_of_measurement="bpm", - suggested_display_precision=0, - value_fn=lambda audio_features: audio_features.tempo, - ), - SpotifyAudioFeaturesSensorEntityDescription( - key="danceability", - translation_key="danceability", - native_unit_of_measurement=PERCENTAGE, - suggested_display_precision=0, - value_fn=lambda audio_features: audio_features.danceability * 100, - entity_registry_enabled_default=False, - ), - SpotifyAudioFeaturesSensorEntityDescription( - key="energy", - translation_key="energy", - native_unit_of_measurement=PERCENTAGE, - suggested_display_precision=0, - value_fn=lambda audio_features: audio_features.energy * 100, - entity_registry_enabled_default=False, - ), - SpotifyAudioFeaturesSensorEntityDescription( - key="mode", - translation_key="mode", - device_class=SensorDeviceClass.ENUM, - options=["major", "minor"], - value_fn=lambda audio_features: audio_features.mode.name.lower(), - entity_registry_enabled_default=False, - ), - SpotifyAudioFeaturesSensorEntityDescription( - key="speechiness", - translation_key="speechiness", - native_unit_of_measurement=PERCENTAGE, - suggested_display_precision=0, - value_fn=lambda audio_features: audio_features.speechiness * 100, - entity_registry_enabled_default=False, - ), - SpotifyAudioFeaturesSensorEntityDescription( - key="acousticness", - translation_key="acousticness", - native_unit_of_measurement=PERCENTAGE, - suggested_display_precision=0, - value_fn=lambda audio_features: audio_features.acousticness * 100, - entity_registry_enabled_default=False, - ), - SpotifyAudioFeaturesSensorEntityDescription( - key="instrumentalness", - translation_key="instrumentalness", - native_unit_of_measurement=PERCENTAGE, - suggested_display_precision=0, - value_fn=lambda audio_features: audio_features.instrumentalness * 100, - entity_registry_enabled_default=False, - ), - SpotifyAudioFeaturesSensorEntityDescription( - key="liveness", - translation_key="liveness", - native_unit_of_measurement=PERCENTAGE, - suggested_display_precision=0, - value_fn=lambda audio_features: audio_features.liveness * 100, - entity_registry_enabled_default=False, - ), - SpotifyAudioFeaturesSensorEntityDescription( - key="valence", - translation_key="valence", - native_unit_of_measurement=PERCENTAGE, - suggested_display_precision=0, - value_fn=lambda audio_features: audio_features.valence * 100, - entity_registry_enabled_default=False, - ), - SpotifyAudioFeaturesSensorEntityDescription( - key="time_signature", - translation_key="time_signature", - device_class=SensorDeviceClass.ENUM, - options=["3/4", "4/4", "5/4", "6/4", "7/4"], - value_fn=lambda audio_features: f"{audio_features.time_signature}/4", - entity_registry_enabled_default=False, - ), - SpotifyAudioFeaturesSensorEntityDescription( - key="key", - translation_key="key", - device_class=SensorDeviceClass.ENUM, - options=KEY_OPTIONS, - value_fn=_get_key, - entity_registry_enabled_default=False, - ), -) - - -async def async_setup_entry( - hass: HomeAssistant, - entry: SpotifyConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Set up Spotify sensor based on a config entry.""" - coordinator = entry.runtime_data.coordinator - - async_add_entities( - SpotifyAudioFeatureSensor(coordinator, description) - for description in AUDIO_FEATURE_SENSORS - ) - - -class SpotifyAudioFeatureSensor(SpotifyEntity, SensorEntity): - """Representation of a Spotify sensor.""" - - entity_description: SpotifyAudioFeaturesSensorEntityDescription - - def __init__( - self, - coordinator: SpotifyCoordinator, - entity_description: SpotifyAudioFeaturesSensorEntityDescription, - ) -> None: - """Initialize.""" - super().__init__(coordinator) - self._attr_unique_id = ( - f"{coordinator.current_user.user_id}_{entity_description.key}" - ) - self.entity_description = entity_description - - @property - def native_value(self) -> float | str | None: - """Return the state of the sensor.""" - if (audio_features := self.coordinator.data.audio_features) is None: - return None - return self.entity_description.value_fn(audio_features) diff --git a/homeassistant/components/spotify/strings.json b/homeassistant/components/spotify/strings.json index faf20d740d9..90e573a1706 100644 --- a/homeassistant/components/spotify/strings.json +++ b/homeassistant/components/spotify/strings.json @@ -30,46 +30,5 @@ "info": { "api_endpoint_reachable": "Spotify API endpoint reachable" } - }, - "entity": { - "sensor": { - "song_tempo": { - "name": "Song tempo" - }, - "danceability": { - "name": "Song danceability" - }, - "energy": { - "name": "Song energy" - }, - "mode": { - "name": "Song mode", - "state": { - "minor": "Minor", - "major": "Major" - } - }, - "speechiness": { - "name": "Song speechiness" - }, - "acousticness": { - "name": "Song acousticness" - }, - "instrumentalness": { - "name": "Song instrumentalness" - }, - "valence": { - "name": "Song valence" - }, - "liveness": { - "name": "Song liveness" - }, - "time_signature": { - "name": "Song time signature" - }, - "key": { - "name": "Song key" - } - } } } diff --git a/tests/components/spotify/conftest.py b/tests/components/spotify/conftest.py index d3fc418f1cd..cc1f423246c 100644 --- a/tests/components/spotify/conftest.py +++ b/tests/components/spotify/conftest.py @@ -9,7 +9,6 @@ from spotifyaio.models import ( Album, Artist, ArtistResponse, - AudioFeatures, CategoriesResponse, Category, CategoryPlaylistResponse, @@ -140,7 +139,6 @@ def mock_spotify() -> Generator[AsyncMock]: ("album.json", "get_album", Album), ("artist.json", "get_artist", Artist), ("show.json", "get_show", Show), - ("audio_features.json", "get_audio_features", AudioFeatures), ): getattr(client, method).return_value = obj.from_json( load_fixture(fixture, DOMAIN) diff --git a/tests/components/spotify/fixtures/audio_features.json b/tests/components/spotify/fixtures/audio_features.json deleted file mode 100644 index 52dfee060f7..00000000000 --- a/tests/components/spotify/fixtures/audio_features.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "danceability": 0.696, - "energy": 0.905, - "key": 3, - "loudness": -2.743, - "mode": 1, - "speechiness": 0.103, - "acousticness": 0.011, - "instrumentalness": 0.000905, - "liveness": 0.302, - "valence": 0.625, - "tempo": 114.944, - "type": "audio_features", - "id": "11dFghVXANMlKmJXsNCbNl", - "uri": "spotify:track:11dFghVXANMlKmJXsNCbNl", - "track_href": "https://api.spotify.com/v1/tracks/11dFghVXANMlKmJXsNCbNl", - "analysis_url": "https://api.spotify.com/v1/audio-analysis/11dFghVXANMlKmJXsNCbNl", - "duration_ms": 207960, - "time_signature": 4 -} diff --git a/tests/components/spotify/snapshots/test_diagnostics.ambr b/tests/components/spotify/snapshots/test_diagnostics.ambr index 161b6025ff3..40502562da3 100644 --- a/tests/components/spotify/snapshots/test_diagnostics.ambr +++ b/tests/components/spotify/snapshots/test_diagnostics.ambr @@ -14,20 +14,6 @@ }), ]), 'playback': dict({ - 'audio_features': dict({ - 'acousticness': 0.011, - 'danceability': 0.696, - 'energy': 0.905, - 'instrumentalness': 0.000905, - 'key': 3, - 'liveness': 0.302, - 'loudness': -2.743, - 'mode': 1, - 'speechiness': 0.103, - 'tempo': 114.944, - 'time_signature': 4, - 'valence': 0.625, - }), 'current_playback': dict({ 'context': dict({ 'context_type': 'playlist', diff --git a/tests/components/spotify/snapshots/test_sensor.ambr b/tests/components/spotify/snapshots/test_sensor.ambr deleted file mode 100644 index ce77dda479f..00000000000 --- a/tests/components/spotify/snapshots/test_sensor.ambr +++ /dev/null @@ -1,595 +0,0 @@ -# serializer version: 1 -# name: test_entities[sensor.spotify_spotify_1_song_acousticness-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.spotify_spotify_1_song_acousticness', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Song acousticness', - 'platform': 'spotify', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'acousticness', - 'unique_id': '1112264111_acousticness', - 'unit_of_measurement': '%', - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_acousticness-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Spotify spotify_1 Song acousticness', - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.spotify_spotify_1_song_acousticness', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '1.1', - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_danceability-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.spotify_spotify_1_song_danceability', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Song danceability', - 'platform': 'spotify', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'danceability', - 'unique_id': '1112264111_danceability', - 'unit_of_measurement': '%', - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_danceability-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Spotify spotify_1 Song danceability', - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.spotify_spotify_1_song_danceability', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '69.6', - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_energy-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.spotify_spotify_1_song_energy', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Song energy', - 'platform': 'spotify', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'energy', - 'unique_id': '1112264111_energy', - 'unit_of_measurement': '%', - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_energy-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Spotify spotify_1 Song energy', - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.spotify_spotify_1_song_energy', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '90.5', - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_instrumentalness-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.spotify_spotify_1_song_instrumentalness', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Song instrumentalness', - 'platform': 'spotify', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'instrumentalness', - 'unique_id': '1112264111_instrumentalness', - 'unit_of_measurement': '%', - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_instrumentalness-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Spotify spotify_1 Song instrumentalness', - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.spotify_spotify_1_song_instrumentalness', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '0.0905', - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_key-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'C', - 'C♯/D♭', - 'D', - 'D♯/E♭', - 'E', - 'F', - 'F♯/G♭', - 'G', - 'G♯/A♭', - 'A', - 'A♯/B♭', - 'B', - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.spotify_spotify_1_song_key', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Song key', - 'platform': 'spotify', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'key', - 'unique_id': '1112264111_key', - 'unit_of_measurement': None, - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_key-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'Spotify spotify_1 Song key', - 'options': list([ - 'C', - 'C♯/D♭', - 'D', - 'D♯/E♭', - 'E', - 'F', - 'F♯/G♭', - 'G', - 'G♯/A♭', - 'A', - 'A♯/B♭', - 'B', - ]), - }), - 'context': , - 'entity_id': 'sensor.spotify_spotify_1_song_key', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'D♯/E♭', - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_liveness-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.spotify_spotify_1_song_liveness', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Song liveness', - 'platform': 'spotify', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'liveness', - 'unique_id': '1112264111_liveness', - 'unit_of_measurement': '%', - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_liveness-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Spotify spotify_1 Song liveness', - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.spotify_spotify_1_song_liveness', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '30.2', - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_mode-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - 'major', - 'minor', - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.spotify_spotify_1_song_mode', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Song mode', - 'platform': 'spotify', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'mode', - 'unique_id': '1112264111_mode', - 'unit_of_measurement': None, - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_mode-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'Spotify spotify_1 Song mode', - 'options': list([ - 'major', - 'minor', - ]), - }), - 'context': , - 'entity_id': 'sensor.spotify_spotify_1_song_mode', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'major', - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_speechiness-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.spotify_spotify_1_song_speechiness', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Song speechiness', - 'platform': 'spotify', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'speechiness', - 'unique_id': '1112264111_speechiness', - 'unit_of_measurement': '%', - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_speechiness-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Spotify spotify_1 Song speechiness', - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.spotify_spotify_1_song_speechiness', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '10.3', - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_tempo-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.spotify_spotify_1_song_tempo', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Song tempo', - 'platform': 'spotify', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'song_tempo', - 'unique_id': '1112264111_bpm', - 'unit_of_measurement': 'bpm', - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_tempo-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Spotify spotify_1 Song tempo', - 'unit_of_measurement': 'bpm', - }), - 'context': , - 'entity_id': 'sensor.spotify_spotify_1_song_tempo', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '114.944', - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_time_signature-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': dict({ - 'options': list([ - '3/4', - '4/4', - '5/4', - '6/4', - '7/4', - ]), - }), - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.spotify_spotify_1_song_time_signature', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Song time signature', - 'platform': 'spotify', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'time_signature', - 'unique_id': '1112264111_time_signature', - 'unit_of_measurement': None, - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_time_signature-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'enum', - 'friendly_name': 'Spotify spotify_1 Song time signature', - 'options': list([ - '3/4', - '4/4', - '5/4', - '6/4', - '7/4', - ]), - }), - 'context': , - 'entity_id': 'sensor.spotify_spotify_1_song_time_signature', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '4/4', - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_valence-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'sensor', - 'entity_category': None, - 'entity_id': 'sensor.spotify_spotify_1_song_valence', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - 'sensor': dict({ - 'suggested_display_precision': 0, - }), - }), - 'original_device_class': None, - 'original_icon': None, - 'original_name': 'Song valence', - 'platform': 'spotify', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': 'valence', - 'unique_id': '1112264111_valence', - 'unit_of_measurement': '%', - }) -# --- -# name: test_entities[sensor.spotify_spotify_1_song_valence-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'friendly_name': 'Spotify spotify_1 Song valence', - 'unit_of_measurement': '%', - }), - 'context': , - 'entity_id': 'sensor.spotify_spotify_1_song_valence', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': '62.5', - }) -# --- diff --git a/tests/components/spotify/test_sensor.py b/tests/components/spotify/test_sensor.py deleted file mode 100644 index 11ce361034a..00000000000 --- a/tests/components/spotify/test_sensor.py +++ /dev/null @@ -1,66 +0,0 @@ -"""Tests for the Spotify sensor platform.""" - -from unittest.mock import MagicMock, patch - -import pytest -from spotifyaio import PlaybackState -from syrupy import SnapshotAssertion - -from homeassistant.components.spotify import DOMAIN -from homeassistant.const import STATE_UNKNOWN, Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er - -from . import setup_integration - -from tests.common import MockConfigEntry, load_fixture, snapshot_platform - - -@pytest.mark.usefixtures("setup_credentials") -@pytest.mark.usefixtures("entity_registry_enabled_by_default") -async def test_entities( - hass: HomeAssistant, - mock_spotify: MagicMock, - mock_config_entry: MockConfigEntry, - entity_registry: er.EntityRegistry, - snapshot: SnapshotAssertion, -) -> None: - """Test the Spotify entities.""" - with patch("homeassistant.components.spotify.PLATFORMS", [Platform.SENSOR]): - await setup_integration(hass, mock_config_entry) - - await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) - - -@pytest.mark.usefixtures("setup_credentials") -async def test_audio_features_unavailable( - hass: HomeAssistant, - mock_spotify: MagicMock, - mock_config_entry: MockConfigEntry, - entity_registry: er.EntityRegistry, - snapshot: SnapshotAssertion, -) -> None: - """Test the Spotify entities.""" - mock_spotify.return_value.get_audio_features.return_value = None - - await setup_integration(hass, mock_config_entry) - - assert hass.states.get("sensor.spotify_spotify_1_song_tempo").state == STATE_UNKNOWN - - -@pytest.mark.usefixtures("setup_credentials") -async def test_audio_features_unknown_during_podcast( - hass: HomeAssistant, - mock_spotify: MagicMock, - mock_config_entry: MockConfigEntry, - entity_registry: er.EntityRegistry, - snapshot: SnapshotAssertion, -) -> None: - """Test the Spotify audio features sensor during a podcast.""" - mock_spotify.return_value.get_playback.return_value = PlaybackState.from_json( - load_fixture("playback_episode.json", DOMAIN) - ) - - await setup_integration(hass, mock_config_entry) - - assert hass.states.get("sensor.spotify_spotify_1_song_tempo").state == STATE_UNKNOWN From f02d2344fc9a3229d655653abb02ea3ca8b14708 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Nov 2024 13:55:51 -0800 Subject: [PATCH 008/103] Bump uiprotect to 6.6.3 (#131764) --- 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 a8ad956a667..9a76ba6f984 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==6.6.2", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==6.6.3", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 6151da007c4..e4314e257f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2897,7 +2897,7 @@ typedmonarchmoney==0.3.1 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==6.6.2 +uiprotect==6.6.3 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f88bda8215a..6b6594e29bf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2310,7 +2310,7 @@ typedmonarchmoney==0.3.1 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==6.6.2 +uiprotect==6.6.3 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 2fc01a02dbbb7c69d324d98394246a7767c12d35 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Wed, 27 Nov 2024 22:55:33 +0100 Subject: [PATCH 009/103] Bump pylamarzocco to 1.2.12 (#131765) --- homeassistant/components/lamarzocco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lamarzocco/manifest.json b/homeassistant/components/lamarzocco/manifest.json index a71da7c4754..43b1c7deb47 100644 --- a/homeassistant/components/lamarzocco/manifest.json +++ b/homeassistant/components/lamarzocco/manifest.json @@ -36,5 +36,5 @@ "integration_type": "device", "iot_class": "cloud_polling", "loggers": ["pylamarzocco"], - "requirements": ["pylamarzocco==1.2.11"] + "requirements": ["pylamarzocco==1.2.12"] } diff --git a/requirements_all.txt b/requirements_all.txt index e4314e257f2..c7980fb9bdf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2027,7 +2027,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lamarzocco -pylamarzocco==1.2.11 +pylamarzocco==1.2.12 # homeassistant.components.lastfm pylast==5.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6b6594e29bf..714e9d39be4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1632,7 +1632,7 @@ pykrakenapi==0.1.8 pykulersky==0.5.2 # homeassistant.components.lamarzocco -pylamarzocco==1.2.11 +pylamarzocco==1.2.12 # homeassistant.components.lastfm pylast==5.1.0 From c9dde419a205608092c5e63427da9f04f1125deb Mon Sep 17 00:00:00 2001 From: Manu <4445816+tr4nt0r@users.noreply.github.com> Date: Thu, 28 Nov 2024 01:35:23 +0100 Subject: [PATCH 010/103] Fix rounding of attributes in Habitica integration (#131772) --- homeassistant/components/habitica/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/habitica/util.py b/homeassistant/components/habitica/util.py index 03acb08baf9..b2b4430c490 100644 --- a/homeassistant/components/habitica/util.py +++ b/homeassistant/components/habitica/util.py @@ -174,7 +174,7 @@ def get_attribute_points( ) return { - "level": min(round(user["stats"]["lvl"] / 2), 50), + "level": min(floor(user["stats"]["lvl"] / 2), 50), "equipment": equipment, "class": class_bonus, "allocated": user["stats"][attribute], From 71376229f63b138dd2fc6345f6e4687349f9d475 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Nov 2024 15:29:29 -0800 Subject: [PATCH 011/103] Bump aioesphomeapi to 27.0.3 (#131773) --- homeassistant/components/esphome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 5524e87e2a8..77a3164d94c 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -16,7 +16,7 @@ "loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"], "mqtt": ["esphome/discover/#"], "requirements": [ - "aioesphomeapi==27.0.2", + "aioesphomeapi==27.0.3", "esphome-dashboard-api==1.2.3", "bleak-esphome==1.1.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index c7980fb9bdf..2284fa386b5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -243,7 +243,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==27.0.2 +aioesphomeapi==27.0.3 # homeassistant.components.flo aioflo==2021.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 714e9d39be4..27ffd600131 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -231,7 +231,7 @@ aioelectricitymaps==0.4.0 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==27.0.2 +aioesphomeapi==27.0.3 # homeassistant.components.flo aioflo==2021.11.0 From 0a3a3edf7714a57d59b09400c3410c539a3a9347 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Wed, 27 Nov 2024 19:34:57 -0500 Subject: [PATCH 012/103] Bump ZHA to 0.0.41 (#131776) --- 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 ded37fc4713..1fbbd83bb9c 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -21,7 +21,7 @@ "zha", "universal_silabs_flasher" ], - "requirements": ["universal-silabs-flasher==0.0.25", "zha==0.0.40"], + "requirements": ["universal-silabs-flasher==0.0.25", "zha==0.0.41"], "usb": [ { "vid": "10C4", diff --git a/requirements_all.txt b/requirements_all.txt index 2284fa386b5..cc6ada55f72 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3081,7 +3081,7 @@ zeroconf==0.136.2 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.40 +zha==0.0.41 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.13 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27ffd600131..452e6143c34 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2464,7 +2464,7 @@ zeroconf==0.136.2 zeversolar==0.3.2 # homeassistant.components.zha -zha==0.0.40 +zha==0.0.41 # homeassistant.components.zwave_js zwave-js-server-python==0.59.1 From b8c4ce932ce7a0b2de058c4fa032a31595e370a2 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Thu, 28 Nov 2024 08:06:31 +0100 Subject: [PATCH 013/103] Fix Home Connect microwave programs (#131782) --- homeassistant/components/home_connect/select.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/home_connect/select.py b/homeassistant/components/home_connect/select.py index 172b959b145..fdd1f38bf97 100644 --- a/homeassistant/components/home_connect/select.py +++ b/homeassistant/components/home_connect/select.py @@ -140,12 +140,12 @@ TRANSLATION_KEYS_PROGRAMS_MAP = { "Cooking.Oven.Program.HeatingMode.HotAir80Steam", "Cooking.Oven.Program.HeatingMode.HotAir100Steam", "Cooking.Oven.Program.HeatingMode.SabbathProgramme", - "Cooking.Oven.Program.Microwave90Watt", - "Cooking.Oven.Program.Microwave180Watt", - "Cooking.Oven.Program.Microwave360Watt", - "Cooking.Oven.Program.Microwave600Watt", - "Cooking.Oven.Program.Microwave900Watt", - "Cooking.Oven.Program.Microwave1000Watt", + "Cooking.Oven.Program.Microwave.90Watt", + "Cooking.Oven.Program.Microwave.180Watt", + "Cooking.Oven.Program.Microwave.360Watt", + "Cooking.Oven.Program.Microwave.600Watt", + "Cooking.Oven.Program.Microwave.900Watt", + "Cooking.Oven.Program.Microwave.1000Watt", "Cooking.Oven.Program.Microwave.Max", "Cooking.Oven.Program.HeatingMode.WarmingDrawer", "LaundryCare.Washer.Program.Cotton", From 3af0bc2c33f8b7ec8179fe4b0997ac981c80fad0 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Nov 2024 08:44:28 +0100 Subject: [PATCH 014/103] Bump version to 2024.12.0b1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 93a909f6aad..bf84dbb6ff9 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 68b0a7e5a59..419d04fdf25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.12.0b0" +version = "2024.12.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 99f8dbd278c71031a16084f727844e29772e7f51 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Thu, 28 Nov 2024 09:58:16 +0100 Subject: [PATCH 015/103] Bump bimmer_connected to 0.17.0 (#131352) --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index ed0919a5dcf..d1ca735ce55 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "iot_class": "cloud_polling", "loggers": ["bimmer_connected"], - "requirements": ["bimmer-connected[china]==0.16.4"] + "requirements": ["bimmer-connected[china]==0.17.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index cc6ada55f72..d6699f420f5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -582,7 +582,7 @@ beautifulsoup4==4.12.3 # beewi-smartclim==0.0.10 # homeassistant.components.bmw_connected_drive -bimmer-connected[china]==0.16.4 +bimmer-connected[china]==0.17.0 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 452e6143c34..dc79e890073 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -516,7 +516,7 @@ base36==0.1.1 beautifulsoup4==4.12.3 # homeassistant.components.bmw_connected_drive -bimmer-connected[china]==0.16.4 +bimmer-connected[china]==0.17.0 # homeassistant.components.eq3btsmart # homeassistant.components.esphome From 7ab1bfcf1ffff63a7b3454bdabffc2eaaf45596a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 27 Nov 2024 21:12:42 +0100 Subject: [PATCH 016/103] Improve recorder history queries (#131702) * Improve recorder history queries * Remove some comments * Update StatesManager._oldest_ts when adding pending state * Update after review * Improve tests * Improve post-purge logic * Avoid calling dt_util.utc_to_timestamp in new code --------- Co-authored-by: J. Nick Koston --- homeassistant/components/history/__init__.py | 7 ++-- homeassistant/components/history/helpers.py | 13 ++++---- .../components/history/websocket_api.py | 7 ++-- homeassistant/components/recorder/core.py | 1 + .../components/recorder/history/legacy.py | 18 +++++------ .../components/recorder/history/modern.py | 31 +++++++++--------- homeassistant/components/recorder/purge.py | 3 ++ homeassistant/components/recorder/queries.py | 9 ++++++ .../recorder/table_managers/states.py | 32 +++++++++++++++++++ homeassistant/components/recorder/tasks.py | 2 -- tests/components/recorder/test_purge.py | 17 ++++++++++ 11 files changed, 102 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 365be06fd2d..7241e1fac9a 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -22,7 +22,7 @@ import homeassistant.util.dt as dt_util from . import websocket_api from .const import DOMAIN -from .helpers import entities_may_have_state_changes_after, has_recorder_run_after +from .helpers import entities_may_have_state_changes_after, has_states_before CONF_ORDER = "use_include_order" @@ -107,7 +107,10 @@ class HistoryPeriodView(HomeAssistantView): no_attributes = "no_attributes" in request.query if ( - (end_time and not has_recorder_run_after(hass, end_time)) + # has_states_before will return True if there are states older than + # end_time. If it's false, we know there are no states in the + # database up until end_time. + (end_time and not has_states_before(hass, end_time)) or not include_start_time_state and entity_ids and not entities_may_have_state_changes_after( diff --git a/homeassistant/components/history/helpers.py b/homeassistant/components/history/helpers.py index bd477e7e4ed..2010b7373ff 100644 --- a/homeassistant/components/history/helpers.py +++ b/homeassistant/components/history/helpers.py @@ -6,7 +6,6 @@ from collections.abc import Iterable from datetime import datetime as dt from homeassistant.components.recorder import get_instance -from homeassistant.components.recorder.models import process_timestamp from homeassistant.core import HomeAssistant @@ -26,8 +25,10 @@ def entities_may_have_state_changes_after( return False -def has_recorder_run_after(hass: HomeAssistant, run_time: dt) -> bool: - """Check if the recorder has any runs after a specific time.""" - return run_time >= process_timestamp( - get_instance(hass).recorder_runs_manager.first.start - ) +def has_states_before(hass: HomeAssistant, run_time: dt) -> bool: + """Check if the recorder has states as old or older than run_time. + + Returns True if there may be such states. + """ + oldest_ts = get_instance(hass).states_manager.oldest_ts + return oldest_ts is not None and run_time.timestamp() >= oldest_ts diff --git a/homeassistant/components/history/websocket_api.py b/homeassistant/components/history/websocket_api.py index c85d975c3c9..35f8ed5f1ac 100644 --- a/homeassistant/components/history/websocket_api.py +++ b/homeassistant/components/history/websocket_api.py @@ -39,7 +39,7 @@ from homeassistant.util.async_ import create_eager_task import homeassistant.util.dt as dt_util from .const import EVENT_COALESCE_TIME, MAX_PENDING_HISTORY_STATES -from .helpers import entities_may_have_state_changes_after, has_recorder_run_after +from .helpers import entities_may_have_state_changes_after, has_states_before _LOGGER = logging.getLogger(__name__) @@ -142,7 +142,10 @@ async def ws_get_history_during_period( no_attributes = msg["no_attributes"] if ( - (end_time and not has_recorder_run_after(hass, end_time)) + # has_states_before will return True if there are states older than + # end_time. If it's false, we know there are no states in the + # database up until end_time. + (end_time and not has_states_before(hass, end_time)) or not include_start_time_state and entity_ids and not entities_may_have_state_changes_after( diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 6ba64d4a571..8c2e1c9e006 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -1424,6 +1424,7 @@ class Recorder(threading.Thread): with session_scope(session=self.get_session()) as session: end_incomplete_runs(session, self.recorder_runs_manager.recording_start) self.recorder_runs_manager.start(session) + self.states_manager.load_from_db(session) self._open_event_session() diff --git a/homeassistant/components/recorder/history/legacy.py b/homeassistant/components/recorder/history/legacy.py index b59fc43c3d0..3a0fe79455b 100644 --- a/homeassistant/components/recorder/history/legacy.py +++ b/homeassistant/components/recorder/history/legacy.py @@ -22,9 +22,9 @@ from homeassistant.core import HomeAssistant, State, split_entity_id from homeassistant.helpers.recorder import get_instance import homeassistant.util.dt as dt_util -from ..db_schema import RecorderRuns, StateAttributes, States +from ..db_schema import StateAttributes, States from ..filters import Filters -from ..models import process_timestamp, process_timestamp_to_utc_isoformat +from ..models import process_timestamp_to_utc_isoformat from ..models.legacy import LegacyLazyState, legacy_row_to_compressed_state from ..util import execute_stmt_lambda_element, session_scope from .const import ( @@ -436,7 +436,7 @@ def get_last_state_changes( def _get_states_for_entities_stmt( - run_start: datetime, + run_start_ts: float, utc_point_in_time: datetime, entity_ids: list[str], no_attributes: bool, @@ -447,7 +447,6 @@ def _get_states_for_entities_stmt( ) # We got an include-list of entities, accelerate the query by filtering already # in the inner query. - run_start_ts = process_timestamp(run_start).timestamp() utc_point_in_time_ts = dt_util.utc_to_timestamp(utc_point_in_time) stmt += lambda q: q.join( ( @@ -483,7 +482,7 @@ def _get_rows_with_session( session: Session, utc_point_in_time: datetime, entity_ids: list[str], - run: RecorderRuns | None = None, + *, no_attributes: bool = False, ) -> Iterable[Row]: """Return the states at a specific point in time.""" @@ -495,17 +494,16 @@ def _get_rows_with_session( ), ) - if run is None: - run = get_instance(hass).recorder_runs_manager.get(utc_point_in_time) + oldest_ts = get_instance(hass).states_manager.oldest_ts - if run is None or process_timestamp(run.start) > utc_point_in_time: - # History did not run before utc_point_in_time + if oldest_ts is None or oldest_ts > utc_point_in_time.timestamp(): + # We don't have any states for the requested time return [] # We have more than one entity to look at so we need to do a query on states # since the last recorder run started. stmt = _get_states_for_entities_stmt( - run.start, utc_point_in_time, entity_ids, no_attributes + oldest_ts, utc_point_in_time, entity_ids, no_attributes ) return execute_stmt_lambda_element(session, stmt) diff --git a/homeassistant/components/recorder/history/modern.py b/homeassistant/components/recorder/history/modern.py index b44bec0d0ee..902f1b5dc24 100644 --- a/homeassistant/components/recorder/history/modern.py +++ b/homeassistant/components/recorder/history/modern.py @@ -34,7 +34,6 @@ from ..models import ( LazyState, datetime_to_timestamp_or_none, extract_metadata_ids, - process_timestamp, row_to_compressed_state, ) from ..util import execute_stmt_lambda_element, session_scope @@ -246,9 +245,9 @@ def get_significant_states_with_session( if metadata_id is not None and split_entity_id(entity_id)[0] in SIGNIFICANT_DOMAINS ] - run_start_ts: float | None = None + oldest_ts: float | None = None if include_start_time_state and not ( - run_start_ts := _get_run_start_ts_for_utc_point_in_time(hass, start_time) + oldest_ts := _get_oldest_possible_ts(hass, start_time) ): include_start_time_state = False start_time_ts = dt_util.utc_to_timestamp(start_time) @@ -264,7 +263,7 @@ def get_significant_states_with_session( significant_changes_only, no_attributes, include_start_time_state, - run_start_ts, + oldest_ts, ), track_on=[ bool(single_metadata_id), @@ -411,9 +410,9 @@ def state_changes_during_period( entity_id_to_metadata_id: dict[str, int | None] = { entity_id: single_metadata_id } - run_start_ts: float | None = None + oldest_ts: float | None = None if include_start_time_state and not ( - run_start_ts := _get_run_start_ts_for_utc_point_in_time(hass, start_time) + oldest_ts := _get_oldest_possible_ts(hass, start_time) ): include_start_time_state = False start_time_ts = dt_util.utc_to_timestamp(start_time) @@ -426,7 +425,7 @@ def state_changes_during_period( no_attributes, limit, include_start_time_state, - run_start_ts, + oldest_ts, has_last_reported, ), track_on=[ @@ -600,17 +599,17 @@ def _get_start_time_state_for_entities_stmt( ) -def _get_run_start_ts_for_utc_point_in_time( +def _get_oldest_possible_ts( hass: HomeAssistant, utc_point_in_time: datetime ) -> float | None: - """Return the start time of a run.""" - run = get_instance(hass).recorder_runs_manager.get(utc_point_in_time) - if ( - run is not None - and (run_start := process_timestamp(run.start)) < utc_point_in_time - ): - return run_start.timestamp() - # History did not run before utc_point_in_time but we still + """Return the oldest possible timestamp. + + Returns None if there are no states as old as utc_point_in_time. + """ + + oldest_ts = get_instance(hass).states_manager.oldest_ts + if oldest_ts is not None and oldest_ts < utc_point_in_time.timestamp(): + return oldest_ts return None diff --git a/homeassistant/components/recorder/purge.py b/homeassistant/components/recorder/purge.py index 329f48e5455..28a5a2ed32d 100644 --- a/homeassistant/components/recorder/purge.py +++ b/homeassistant/components/recorder/purge.py @@ -123,6 +123,9 @@ def purge_old_data( _purge_old_entity_ids(instance, session) _purge_old_recorder_runs(instance, session, purge_before) + with session_scope(session=instance.get_session(), read_only=True) as session: + instance.recorder_runs_manager.load_from_db(session) + instance.states_manager.load_from_db(session) if repack: repack_database(instance) return True diff --git a/homeassistant/components/recorder/queries.py b/homeassistant/components/recorder/queries.py index 2e4b588a0b0..8ca7bef2691 100644 --- a/homeassistant/components/recorder/queries.py +++ b/homeassistant/components/recorder/queries.py @@ -637,6 +637,15 @@ def find_states_to_purge( ) +def find_oldest_state() -> StatementLambdaElement: + """Find the last_updated_ts of the oldest state.""" + return lambda_stmt( + lambda: select(States.last_updated_ts).where( + States.state_id.in_(select(func.min(States.state_id))) + ) + ) + + def find_short_term_statistics_to_purge( purge_before: datetime, max_bind_vars: int ) -> StatementLambdaElement: diff --git a/homeassistant/components/recorder/table_managers/states.py b/homeassistant/components/recorder/table_managers/states.py index d5cef759c54..fafcfa0ea61 100644 --- a/homeassistant/components/recorder/table_managers/states.py +++ b/homeassistant/components/recorder/table_managers/states.py @@ -2,7 +2,15 @@ from __future__ import annotations +from collections.abc import Sequence +from typing import Any, cast + +from sqlalchemy.engine.row import Row +from sqlalchemy.orm.session import Session + from ..db_schema import States +from ..queries import find_oldest_state +from ..util import execute_stmt_lambda_element class StatesManager: @@ -13,6 +21,12 @@ class StatesManager: self._pending: dict[str, States] = {} self._last_committed_id: dict[str, int] = {} self._last_reported: dict[int, float] = {} + self._oldest_ts: float | None = None + + @property + def oldest_ts(self) -> float | None: + """Return the oldest timestamp.""" + return self._oldest_ts def pop_pending(self, entity_id: str) -> States | None: """Pop a pending state. @@ -44,6 +58,8 @@ class StatesManager: recorder thread. """ self._pending[entity_id] = state + if self._oldest_ts is None: + self._oldest_ts = state.last_updated_ts def update_pending_last_reported( self, state_id: int, last_reported_timestamp: float @@ -74,6 +90,22 @@ class StatesManager: """ self._last_committed_id.clear() self._pending.clear() + self._oldest_ts = None + + def load_from_db(self, session: Session) -> None: + """Update the cache. + + Must run in the recorder thread. + """ + result = cast( + Sequence[Row[Any]], + execute_stmt_lambda_element(session, find_oldest_state()), + ) + if not result: + ts = None + else: + ts = result[0].last_updated_ts + self._oldest_ts = ts def evict_purged_state_ids(self, purged_state_ids: set[int]) -> None: """Evict purged states from the committed states. diff --git a/homeassistant/components/recorder/tasks.py b/homeassistant/components/recorder/tasks.py index 783f0a80b8e..fa10c12aa68 100644 --- a/homeassistant/components/recorder/tasks.py +++ b/homeassistant/components/recorder/tasks.py @@ -120,8 +120,6 @@ class PurgeTask(RecorderTask): if purge.purge_old_data( instance, self.purge_before, self.repack, self.apply_filter ): - with instance.get_session() as session: - instance.recorder_runs_manager.load_from_db(session) # We always need to do the db cleanups after a purge # is finished to ensure the WAL checkpoint and other # tasks happen after a vacuum. diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index ca160e5201b..f721a260c14 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -112,6 +112,9 @@ async def test_purge_big_database(hass: HomeAssistant, recorder_mock: Recorder) async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> None: """Test deleting old states.""" + assert recorder_mock.states_manager.oldest_ts is None + oldest_ts = recorder_mock.states_manager.oldest_ts + await _add_test_states(hass) # make sure we start with 6 states @@ -127,6 +130,10 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> events = session.query(Events).filter(Events.event_type == "state_changed") assert events.count() == 0 + assert recorder_mock.states_manager.oldest_ts != oldest_ts + assert recorder_mock.states_manager.oldest_ts == states[0].last_updated_ts + oldest_ts = recorder_mock.states_manager.oldest_ts + assert "test.recorder2" in recorder_mock.states_manager._last_committed_id purge_before = dt_util.utcnow() - timedelta(days=4) @@ -140,6 +147,8 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> repack=False, ) assert not finished + # states_manager.oldest_ts is not updated until after the purge is complete + assert recorder_mock.states_manager.oldest_ts == oldest_ts with session_scope(hass=hass) as session: states = session.query(States) @@ -162,6 +171,8 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> finished = purge_old_data(recorder_mock, purge_before, repack=False) assert finished + # states_manager.oldest_ts should now be updated + assert recorder_mock.states_manager.oldest_ts != oldest_ts with session_scope(hass=hass) as session: states = session.query(States) @@ -169,6 +180,10 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> assert states.count() == 2 assert state_attributes.count() == 1 + assert recorder_mock.states_manager.oldest_ts != oldest_ts + assert recorder_mock.states_manager.oldest_ts == states[0].last_updated_ts + oldest_ts = recorder_mock.states_manager.oldest_ts + assert "test.recorder2" in recorder_mock.states_manager._last_committed_id # run purge_old_data again @@ -181,6 +196,8 @@ async def test_purge_old_states(hass: HomeAssistant, recorder_mock: Recorder) -> repack=False, ) assert not finished + # states_manager.oldest_ts is not updated until after the purge is complete + assert recorder_mock.states_manager.oldest_ts == oldest_ts with session_scope(hass=hass) as session: assert states.count() == 0 From 80bc70771e91f8332f23b333d1c94b6a48699cfd Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 28 Nov 2024 12:34:06 +0100 Subject: [PATCH 017/103] Remove Spotify featured playlists and categories from media browser (#131758) --- .../components/spotify/browse_media.py | 72 ---------- tests/components/spotify/conftest.py | 14 -- .../spotify/fixtures/categories.json | 36 ----- .../components/spotify/fixtures/category.json | 12 -- .../spotify/fixtures/category_playlists.json | 84 ------------ .../spotify/fixtures/featured_playlists.json | 85 ------------ .../spotify/snapshots/test_media_browser.ambr | 125 ------------------ .../components/spotify/test_media_browser.py | 3 - 8 files changed, 431 deletions(-) delete mode 100644 tests/components/spotify/fixtures/categories.json delete mode 100644 tests/components/spotify/fixtures/category.json delete mode 100644 tests/components/spotify/fixtures/category_playlists.json delete mode 100644 tests/components/spotify/fixtures/featured_playlists.json diff --git a/homeassistant/components/spotify/browse_media.py b/homeassistant/components/spotify/browse_media.py index 403ec608a7c..1ae5804ea66 100644 --- a/homeassistant/components/spotify/browse_media.py +++ b/homeassistant/components/spotify/browse_media.py @@ -101,8 +101,6 @@ class BrowsableMedia(StrEnum): CURRENT_USER_RECENTLY_PLAYED = "current_user_recently_played" CURRENT_USER_TOP_ARTISTS = "current_user_top_artists" CURRENT_USER_TOP_TRACKS = "current_user_top_tracks" - CATEGORIES = "categories" - FEATURED_PLAYLISTS = "featured_playlists" NEW_RELEASES = "new_releases" @@ -115,8 +113,6 @@ LIBRARY_MAP = { BrowsableMedia.CURRENT_USER_RECENTLY_PLAYED.value: "Recently played", BrowsableMedia.CURRENT_USER_TOP_ARTISTS.value: "Top Artists", BrowsableMedia.CURRENT_USER_TOP_TRACKS.value: "Top Tracks", - BrowsableMedia.CATEGORIES.value: "Categories", - BrowsableMedia.FEATURED_PLAYLISTS.value: "Featured Playlists", BrowsableMedia.NEW_RELEASES.value: "New Releases", } @@ -153,18 +149,6 @@ CONTENT_TYPE_MEDIA_CLASS: dict[str, Any] = { "parent": MediaClass.DIRECTORY, "children": MediaClass.TRACK, }, - BrowsableMedia.FEATURED_PLAYLISTS.value: { - "parent": MediaClass.DIRECTORY, - "children": MediaClass.PLAYLIST, - }, - BrowsableMedia.CATEGORIES.value: { - "parent": MediaClass.DIRECTORY, - "children": MediaClass.GENRE, - }, - "category_playlists": { - "parent": MediaClass.DIRECTORY, - "children": MediaClass.PLAYLIST, - }, BrowsableMedia.NEW_RELEASES.value: { "parent": MediaClass.DIRECTORY, "children": MediaClass.ALBUM, @@ -354,32 +338,6 @@ async def build_item_response( # noqa: C901 elif media_content_type == BrowsableMedia.CURRENT_USER_TOP_TRACKS: if top_tracks := await spotify.get_top_tracks(): items = [_get_track_item_payload(track) for track in top_tracks] - elif media_content_type == BrowsableMedia.FEATURED_PLAYLISTS: - if featured_playlists := await spotify.get_featured_playlists(): - items = [ - _get_playlist_item_payload(playlist) for playlist in featured_playlists - ] - elif media_content_type == BrowsableMedia.CATEGORIES: - if categories := await spotify.get_categories(): - items = [ - { - "id": category.category_id, - "name": category.name, - "type": "category_playlists", - "uri": category.category_id, - "thumbnail": category.icons[0].url if category.icons else None, - } - for category in categories - ] - elif media_content_type == "category_playlists": - if ( - playlists := await spotify.get_category_playlists( - category_id=media_content_id - ) - ) and (category := await spotify.get_category(media_content_id)): - title = category.name - image = category.icons[0].url if category.icons else None - items = [_get_playlist_item_payload(playlist) for playlist in playlists] elif media_content_type == BrowsableMedia.NEW_RELEASES: if new_releases := await spotify.get_new_releases(): items = [_get_album_item_payload(album) for album in new_releases] @@ -429,36 +387,6 @@ async def build_item_response( # noqa: C901 _LOGGER.debug("Unknown media type received: %s", media_content_type) return None - if media_content_type == BrowsableMedia.CATEGORIES: - media_item = BrowseMedia( - can_expand=True, - can_play=False, - children_media_class=media_class["children"], - media_class=media_class["parent"], - media_content_id=media_content_id, - media_content_type=f"{MEDIA_PLAYER_PREFIX}{media_content_type}", - title=LIBRARY_MAP.get(media_content_id, "Unknown"), - ) - - media_item.children = [] - for item in items: - if (item_id := item["id"]) is None: - _LOGGER.debug("Missing ID for media item: %s", item) - continue - media_item.children.append( - BrowseMedia( - can_expand=True, - can_play=False, - children_media_class=MediaClass.TRACK, - media_class=MediaClass.PLAYLIST, - media_content_id=item_id, - media_content_type=f"{MEDIA_PLAYER_PREFIX}category_playlists", - thumbnail=item["thumbnail"], - title=item["name"], - ) - ) - return media_item - if title is None: title = LIBRARY_MAP.get(media_content_id, "Unknown") diff --git a/tests/components/spotify/conftest.py b/tests/components/spotify/conftest.py index cc1f423246c..67d4eac3960 100644 --- a/tests/components/spotify/conftest.py +++ b/tests/components/spotify/conftest.py @@ -9,11 +9,7 @@ from spotifyaio.models import ( Album, Artist, ArtistResponse, - CategoriesResponse, - Category, - CategoryPlaylistResponse, Devices, - FeaturedPlaylistResponse, NewReleasesResponse, NewReleasesResponseInner, PlaybackState, @@ -134,7 +130,6 @@ def mock_spotify() -> Generator[AsyncMock]: PlaybackState, ), ("current_user.json", "get_current_user", UserProfile), - ("category.json", "get_category", Category), ("playlist.json", "get_playlist", Playlist), ("album.json", "get_album", Album), ("artist.json", "get_artist", Artist), @@ -146,15 +141,6 @@ def mock_spotify() -> Generator[AsyncMock]: client.get_followed_artists.return_value = ArtistResponse.from_json( load_fixture("followed_artists.json", DOMAIN) ).artists.items - client.get_featured_playlists.return_value = FeaturedPlaylistResponse.from_json( - load_fixture("featured_playlists.json", DOMAIN) - ).playlists.items - client.get_categories.return_value = CategoriesResponse.from_json( - load_fixture("categories.json", DOMAIN) - ).categories.items - client.get_category_playlists.return_value = CategoryPlaylistResponse.from_json( - load_fixture("category_playlists.json", DOMAIN) - ).playlists.items client.get_new_releases.return_value = NewReleasesResponse.from_json( load_fixture("new_releases.json", DOMAIN) ).albums.items diff --git a/tests/components/spotify/fixtures/categories.json b/tests/components/spotify/fixtures/categories.json deleted file mode 100644 index ed873c95c30..00000000000 --- a/tests/components/spotify/fixtures/categories.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "categories": { - "href": "https://api.spotify.com/v1/browse/categories?offset=0&limit=20&locale=en-US,en;q%3D0.5", - "items": [ - { - "href": "https://api.spotify.com/v1/browse/categories/0JQ5DAt0tbjZptfcdMSKl3", - "id": "0JQ5DAt0tbjZptfcdMSKl3", - "icons": [ - { - "height": 274, - "url": "https://t.scdn.co/images/728ed47fc1674feb95f7ac20236eb6d7.jpeg", - "width": 274 - } - ], - "name": "Made For You" - }, - { - "href": "https://api.spotify.com/v1/browse/categories/0JQ5DAqbMKFz6FAsUtgAab", - "id": "0JQ5DAqbMKFz6FAsUtgAab", - "icons": [ - { - "height": 274, - "url": "https://t.scdn.co/images/728ed47fc1674feb95f7ac20236eb6d7.jpeg", - "width": 274 - } - ], - "name": "New Releases" - } - ], - "limit": 20, - "next": "https://api.spotify.com/v1/browse/categories?offset=20&limit=20&locale=en-US,en;q%3D0.5", - "offset": 0, - "previous": null, - "total": 56 - } -} diff --git a/tests/components/spotify/fixtures/category.json b/tests/components/spotify/fixtures/category.json deleted file mode 100644 index d60605cf94f..00000000000 --- a/tests/components/spotify/fixtures/category.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "href": "https://api.spotify.com/v1/browse/categories/0JQ5DAqbMKFRY5ok2pxXJ0", - "id": "0JQ5DAqbMKFRY5ok2pxXJ0", - "icons": [ - { - "height": 274, - "url": "https://t.scdn.co/media/original/dinner_1b6506abba0ba52c54e6d695c8571078_274x274.jpg", - "width": 274 - } - ], - "name": "Cooking & Dining" -} diff --git a/tests/components/spotify/fixtures/category_playlists.json b/tests/components/spotify/fixtures/category_playlists.json deleted file mode 100644 index c2262708d5a..00000000000 --- a/tests/components/spotify/fixtures/category_playlists.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "playlists": { - "href": "https://api.spotify.com/v1/browse/categories/0JQ5DAqbMKFRY5ok2pxXJ0/playlists?country=NL&offset=0&limit=20", - "items": [ - { - "collaborative": false, - "description": "Lekker eten en lang natafelen? Daar hoort muziek bij.", - "external_urls": { - "spotify": "https://open.spotify.com/playlist/37i9dQZF1DX7yhuKT9G4qk" - }, - "href": "https://api.spotify.com/v1/playlists/37i9dQZF1DX7yhuKT9G4qk", - "id": "37i9dQZF1DX7yhuKT9G4qk", - "images": [ - { - "height": null, - "url": "https://i.scdn.co/image/ab67706f0000000343319faa9428405f3312b588", - "width": null - } - ], - "name": "eten met vrienden", - "owner": { - "display_name": "Spotify", - "external_urls": { - "spotify": "https://open.spotify.com/user/spotify" - }, - "href": "https://api.spotify.com/v1/users/spotify", - "id": "spotify", - "type": "user", - "uri": "spotify:user:spotify" - }, - "primary_color": null, - "public": null, - "snapshot_id": "MTcwMTY5Njk3NywwMDAwMDAwMDkyY2JjZDA1MjA2YTBmNzMxMmFlNGI0YzRhMjg0ZWZl", - "tracks": { - "href": "https://api.spotify.com/v1/playlists/37i9dQZF1DX7yhuKT9G4qk/tracks", - "total": 313 - }, - "type": "playlist", - "uri": "spotify:playlist:37i9dQZF1DX7yhuKT9G4qk" - }, - { - "collaborative": false, - "description": "From new retro to classic country blues, honky tonk, rockabilly, and more.", - "external_urls": { - "spotify": "https://open.spotify.com/playlist/37i9dQZF1DXbvE0SE0Cczh" - }, - "href": "https://api.spotify.com/v1/playlists/37i9dQZF1DXbvE0SE0Cczh", - "id": "37i9dQZF1DXbvE0SE0Cczh", - "images": [ - { - "height": null, - "url": "https://i.scdn.co/image/ab67706f00000003b93c270883619dde61725fc8", - "width": null - } - ], - "name": "Jukebox Joint", - "owner": { - "display_name": "Spotify", - "external_urls": { - "spotify": "https://open.spotify.com/user/spotify" - }, - "href": "https://api.spotify.com/v1/users/spotify", - "id": "spotify", - "type": "user", - "uri": "spotify:user:spotify" - }, - "primary_color": null, - "public": null, - "snapshot_id": "MTY4NjkxODgwMiwwMDAwMDAwMGUwNWRkNjY5N2UzM2Q4NzI4NzRiZmNhMGVmMzAyZTA5", - "tracks": { - "href": "https://api.spotify.com/v1/playlists/37i9dQZF1DXbvE0SE0Cczh/tracks", - "total": 60 - }, - "type": "playlist", - "uri": "spotify:playlist:37i9dQZF1DXbvE0SE0Cczh" - } - ], - "limit": 20, - "next": "https://api.spotify.com/v1/browse/categories/0JQ5DAqbMKFRY5ok2pxXJ0/playlists?country=NL&offset=20&limit=20", - "offset": 0, - "previous": null, - "total": 46 - } -} diff --git a/tests/components/spotify/fixtures/featured_playlists.json b/tests/components/spotify/fixtures/featured_playlists.json deleted file mode 100644 index 5e6e53a7ee1..00000000000 --- a/tests/components/spotify/fixtures/featured_playlists.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "message": "Popular Playlists", - "playlists": { - "href": "https://api.spotify.com/v1/browse/featured-playlists?country=NL×tamp=2023-12-18T18%3A35%3A35&offset=0&limit=20", - "items": [ - { - "collaborative": false, - "description": "De ideale playlist voor het fijne kerstgevoel bij de boom!", - "external_urls": { - "spotify": "https://open.spotify.com/playlist/37i9dQZF1DX4dopZ9vOp1t" - }, - "href": "https://api.spotify.com/v1/playlists/37i9dQZF1DX4dopZ9vOp1t", - "id": "37i9dQZF1DX4dopZ9vOp1t", - "images": [ - { - "height": null, - "url": "https://i.scdn.co/image/ab67706f000000037d14c267b8ee5fea2246a8fe", - "width": null - } - ], - "name": "Kerst Hits 2023", - "owner": { - "display_name": "Spotify", - "external_urls": { - "spotify": "https://open.spotify.com/user/spotify" - }, - "href": "https://api.spotify.com/v1/users/spotify", - "id": "spotify", - "type": "user", - "uri": "spotify:user:spotify" - }, - "primary_color": null, - "public": null, - "snapshot_id": "MTcwMjU2ODI4MSwwMDAwMDAwMDE1ZGRiNzI3OGY4OGU2MzA1MWNkZGMyNTdmNDUwMTc1", - "tracks": { - "href": "https://api.spotify.com/v1/playlists/37i9dQZF1DX4dopZ9vOp1t/tracks", - "total": 298 - }, - "type": "playlist", - "uri": "spotify:playlist:37i9dQZF1DX4dopZ9vOp1t" - }, - { - "collaborative": false, - "description": "De 50 populairste hits van Nederland. Cover: Jack Harlow", - "external_urls": { - "spotify": "https://open.spotify.com/playlist/37i9dQZF1DWSBi5svWQ9Nk" - }, - "href": "https://api.spotify.com/v1/playlists/37i9dQZF1DWSBi5svWQ9Nk", - "id": "37i9dQZF1DWSBi5svWQ9Nk", - "images": [ - { - "height": null, - "url": "https://i.scdn.co/image/ab67706f00000003f7b99051789611a49101c1cf", - "width": null - } - ], - "name": "Top Hits NL", - "owner": { - "display_name": "Spotify", - "external_urls": { - "spotify": "https://open.spotify.com/user/spotify" - }, - "href": "https://api.spotify.com/v1/users/spotify", - "id": "spotify", - "type": "user", - "uri": "spotify:user:spotify" - }, - "primary_color": null, - "public": null, - "snapshot_id": "MTcwMjU5NDgwMCwwMDAwMDAwMDU4NWY2MTE4NmU4NmIwMDdlMGE4ZGRkOTZkN2U2MzAx", - "tracks": { - "href": "https://api.spotify.com/v1/playlists/37i9dQZF1DWSBi5svWQ9Nk/tracks", - "total": 50 - }, - "type": "playlist", - "uri": "spotify:playlist:37i9dQZF1DWSBi5svWQ9Nk" - } - ], - "limit": 20, - "next": "https://api.spotify.com/v1/browse/featured-playlists?country=NL×tamp=2023-12-18T18%3A35%3A35&offset=20&limit=20", - "offset": 0, - "previous": null, - "total": 24 - } -} diff --git a/tests/components/spotify/snapshots/test_media_browser.ambr b/tests/components/spotify/snapshots/test_media_browser.ambr index e1ff42cb7c8..764dc7a10e1 100644 --- a/tests/components/spotify/snapshots/test_media_browser.ambr +++ b/tests/components/spotify/snapshots/test_media_browser.ambr @@ -84,26 +84,6 @@ 'thumbnail': None, 'title': 'Top Tracks', }), - dict({ - 'can_expand': True, - 'can_play': False, - 'children_media_class': , - 'media_class': , - 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/categories', - 'media_content_type': 'spotify://categories', - 'thumbnail': None, - 'title': 'Categories', - }), - dict({ - 'can_expand': True, - 'can_play': False, - 'children_media_class': , - 'media_class': , - 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/featured_playlists', - 'media_content_type': 'spotify://featured_playlists', - 'thumbnail': None, - 'title': 'Featured Playlists', - }), dict({ 'can_expand': True, 'can_play': False, @@ -299,76 +279,6 @@ 'title': 'Pitbull', }) # --- -# name: test_browsing[categories-categories] - dict({ - 'can_expand': True, - 'can_play': False, - 'children': list([ - dict({ - 'can_expand': True, - 'can_play': False, - 'children_media_class': , - 'media_class': , - 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/0JQ5DAt0tbjZptfcdMSKl3', - 'media_content_type': 'spotify://category_playlists', - 'thumbnail': 'https://t.scdn.co/images/728ed47fc1674feb95f7ac20236eb6d7.jpeg', - 'title': 'Made For You', - }), - dict({ - 'can_expand': True, - 'can_play': False, - 'children_media_class': , - 'media_class': , - 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/0JQ5DAqbMKFz6FAsUtgAab', - 'media_content_type': 'spotify://category_playlists', - 'thumbnail': 'https://t.scdn.co/images/728ed47fc1674feb95f7ac20236eb6d7.jpeg', - 'title': 'New Releases', - }), - ]), - 'children_media_class': , - 'media_class': , - 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/categories', - 'media_content_type': 'spotify://categories', - 'not_shown': 0, - 'thumbnail': None, - 'title': 'Categories', - }) -# --- -# name: test_browsing[category_playlists-dinner] - dict({ - 'can_expand': True, - 'can_play': False, - 'children': list([ - dict({ - 'can_expand': True, - 'can_play': True, - 'children_media_class': , - 'media_class': , - 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/spotify:playlist:37i9dQZF1DX7yhuKT9G4qk', - 'media_content_type': 'spotify://playlist', - 'thumbnail': 'https://i.scdn.co/image/ab67706f0000000343319faa9428405f3312b588', - 'title': 'eten met vrienden', - }), - dict({ - 'can_expand': True, - 'can_play': True, - 'children_media_class': , - 'media_class': , - 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/spotify:playlist:37i9dQZF1DXbvE0SE0Cczh', - 'media_content_type': 'spotify://playlist', - 'thumbnail': 'https://i.scdn.co/image/ab67706f00000003b93c270883619dde61725fc8', - 'title': 'Jukebox Joint', - }), - ]), - 'children_media_class': , - 'media_class': , - 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/dinner', - 'media_content_type': 'spotify://category_playlists', - 'not_shown': 0, - 'thumbnail': 'https://t.scdn.co/media/original/dinner_1b6506abba0ba52c54e6d695c8571078_274x274.jpg', - 'title': 'Cooking & Dining', - }) -# --- # name: test_browsing[current_user_followed_artists-current_user_followed_artists] dict({ 'can_expand': True, @@ -649,41 +559,6 @@ 'title': 'Top Tracks', }) # --- -# name: test_browsing[featured_playlists-featured_playlists] - dict({ - 'can_expand': True, - 'can_play': False, - 'children': list([ - dict({ - 'can_expand': True, - 'can_play': True, - 'children_media_class': , - 'media_class': , - 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/spotify:playlist:37i9dQZF1DX4dopZ9vOp1t', - 'media_content_type': 'spotify://playlist', - 'thumbnail': 'https://i.scdn.co/image/ab67706f000000037d14c267b8ee5fea2246a8fe', - 'title': 'Kerst Hits 2023', - }), - dict({ - 'can_expand': True, - 'can_play': True, - 'children_media_class': , - 'media_class': , - 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/spotify:playlist:37i9dQZF1DWSBi5svWQ9Nk', - 'media_content_type': 'spotify://playlist', - 'thumbnail': 'https://i.scdn.co/image/ab67706f00000003f7b99051789611a49101c1cf', - 'title': 'Top Hits NL', - }), - ]), - 'children_media_class': , - 'media_class': , - 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/featured_playlists', - 'media_content_type': 'spotify://featured_playlists', - 'not_shown': 0, - 'thumbnail': None, - 'title': 'Featured Playlists', - }) -# --- # name: test_browsing[new_releases-new_releases] dict({ 'can_expand': True, diff --git a/tests/components/spotify/test_media_browser.py b/tests/components/spotify/test_media_browser.py index dcacc23bbee..ff3404dcfe9 100644 --- a/tests/components/spotify/test_media_browser.py +++ b/tests/components/spotify/test_media_browser.py @@ -112,9 +112,6 @@ async def test_browse_media_playlists( ("current_user_recently_played", "current_user_recently_played"), ("current_user_top_artists", "current_user_top_artists"), ("current_user_top_tracks", "current_user_top_tracks"), - ("featured_playlists", "featured_playlists"), - ("categories", "categories"), - ("category_playlists", "dinner"), ("new_releases", "new_releases"), ("playlist", "spotify:playlist:3cEYpjA9oz9GiPac4AsH4n"), ("album", "spotify:album:3IqzqH6ShrRtie9Yd2ODyG"), From 3ca49dc8a6e8d4ef1055f5dbf33ec8f9c9c284a9 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Nov 2024 09:18:00 +0100 Subject: [PATCH 018/103] Bump samsungtvws to 2.7.1 (#131784) --- homeassistant/components/samsungtv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index d25501b356d..041e9b8fe9b 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -37,7 +37,7 @@ "requirements": [ "getmac==0.9.4", "samsungctl[websocket]==0.7.1", - "samsungtvws[async,encrypted]==2.7.0", + "samsungtvws[async,encrypted]==2.7.1", "wakeonlan==2.1.0", "async-upnp-client==0.41.0" ], diff --git a/requirements_all.txt b/requirements_all.txt index d6699f420f5..fc13e72c128 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2610,7 +2610,7 @@ rxv==0.7.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws[async,encrypted]==2.7.0 +samsungtvws[async,encrypted]==2.7.1 # homeassistant.components.sanix sanix==1.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dc79e890073..0923b497575 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2086,7 +2086,7 @@ rxv==0.7.0 samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv -samsungtvws[async,encrypted]==2.7.0 +samsungtvws[async,encrypted]==2.7.1 # homeassistant.components.sanix sanix==1.0.6 From e2cda54473312be901bb88b0c693a4b4d67dbcd9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Nov 2024 12:25:16 +0100 Subject: [PATCH 019/103] Ensure custom integrations are assigned the custom IQS scale (#131795) --- homeassistant/loader.py | 3 +++ tests/test_loader.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 4313cd2d6e0..1fa9d0cd49d 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -830,6 +830,9 @@ class Integration: @cached_property def quality_scale(self) -> str | None: """Return Integration Quality Scale.""" + # Custom integrations default to "custom" quality scale. + if not self.is_built_in or self.overwrites_built_in: + return "custom" return self.manifest.get("quality_scale") @cached_property diff --git a/tests/test_loader.py b/tests/test_loader.py index a39bd63ad0d..4c3c4eb309f 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -547,6 +547,7 @@ def test_integration_properties(hass: HomeAssistant) -> None: ], "mqtt": ["hue/discovery"], "version": "1.0.0", + "quality_scale": "gold", }, ) assert integration.name == "Philips Hue" @@ -585,6 +586,7 @@ def test_integration_properties(hass: HomeAssistant) -> None: assert integration.is_built_in is True assert integration.overwrites_built_in is False assert integration.version == "1.0.0" + assert integration.quality_scale == "gold" integration = loader.Integration( hass, @@ -595,6 +597,7 @@ def test_integration_properties(hass: HomeAssistant) -> None: "domain": "hue", "dependencies": ["test-dep"], "requirements": ["test-req==1.0.0"], + "quality_scale": "gold", }, ) assert integration.is_built_in is False @@ -607,6 +610,7 @@ def test_integration_properties(hass: HomeAssistant) -> None: assert integration.ssdp is None assert integration.mqtt is None assert integration.version is None + assert integration.quality_scale == "custom" integration = loader.Integration( hass, From 9677c6e24c0131104b1217497795f504ad463adb Mon Sep 17 00:00:00 2001 From: Norbert Rittel Date: Thu, 28 Nov 2024 13:44:40 +0100 Subject: [PATCH 020/103] Remove wrong plural "s" in 'todo.remove_item' action (#131814) --- homeassistant/components/todo/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/todo/strings.json b/homeassistant/components/todo/strings.json index 45e378c3de5..245e5c82fc8 100644 --- a/homeassistant/components/todo/strings.json +++ b/homeassistant/components/todo/strings.json @@ -78,7 +78,7 @@ "fields": { "item": { "name": "Item name", - "description": "The name for the to-do list items." + "description": "The name for the to-do list item." } } } From e08b71086faa0ba91b969e85e7735530d15c5522 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Thu, 28 Nov 2024 13:41:30 +0100 Subject: [PATCH 021/103] Fix more flaky translation checks (#131824) --- tests/components/stt/test_init.py | 4 ++++ tests/components/tts/test_init.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/components/stt/test_init.py b/tests/components/stt/test_init.py index 92225123995..3d5daab2bec 100644 --- a/tests/components/stt/test_init.py +++ b/tests/components/stt/test_init.py @@ -34,6 +34,7 @@ from tests.common import ( mock_integration, mock_platform, mock_restore_cache, + reset_translation_cache, ) from tests.typing import ClientSessionGenerator, WebSocketGenerator @@ -518,6 +519,9 @@ async def test_default_engine_prefer_cloud_entity( assert provider_engine.name == "test" assert async_default_engine(hass) == "stt.cloud_stt_entity" + # Reset the `cloud` translations cache to avoid flaky translation checks + reset_translation_cache(hass, ["cloud"]) + async def test_get_engine_legacy( hass: HomeAssistant, tmp_path: Path, mock_provider: MockSTTProvider diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 9d8dbf3ef94..0b01a24720d 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -1990,5 +1990,5 @@ async def test_default_engine_prefer_cloud_entity( assert provider_engine == "test" assert tts.async_default_engine(hass) == "tts.cloud_tts_entity" - # Reset the `cloud` translations cache + # Reset the `cloud` translations cache to avoid flaky translation checks reset_translation_cache(hass, ["cloud"]) From be25b9d4d0632391894e6860fa55641898169ed8 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 28 Nov 2024 13:45:10 +0100 Subject: [PATCH 022/103] Bump spotifyaio to 0.8.10 (#131827) --- .../components/spotify/browse_media.py | 35 +- .../components/spotify/manifest.json | 4 +- .../components/spotify/media_player.py | 2 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/spotify/fixtures/playlist.json | 466 ++++++++++++++++++ .../spotify/snapshots/test_diagnostics.ambr | 63 +++ .../spotify/snapshots/test_media_browser.ambr | 10 + 8 files changed, 566 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/spotify/browse_media.py b/homeassistant/components/spotify/browse_media.py index 1ae5804ea66..81cdfdfb3cf 100644 --- a/homeassistant/components/spotify/browse_media.py +++ b/homeassistant/components/spotify/browse_media.py @@ -14,6 +14,7 @@ from spotifyaio import ( SpotifyClient, Track, ) +from spotifyaio.models import ItemType, SimplifiedEpisode import yarl from homeassistant.components.media_player import ( @@ -90,6 +91,16 @@ def _get_track_item_payload( } +def _get_episode_item_payload(episode: SimplifiedEpisode) -> ItemPayload: + return { + "id": episode.episode_id, + "name": episode.name, + "type": MediaType.EPISODE, + "uri": episode.uri, + "thumbnail": fetch_image_url(episode.images), + } + + class BrowsableMedia(StrEnum): """Enum of browsable media.""" @@ -345,10 +356,15 @@ async def build_item_response( # noqa: C901 if playlist := await spotify.get_playlist(media_content_id): title = playlist.name image = playlist.images[0].url if playlist.images else None - items = [ - _get_track_item_payload(playlist_track.track) - for playlist_track in playlist.tracks.items - ] + for playlist_item in playlist.tracks.items: + if playlist_item.track.type is ItemType.TRACK: + if TYPE_CHECKING: + assert isinstance(playlist_item.track, Track) + items.append(_get_track_item_payload(playlist_item.track)) + elif playlist_item.track.type is ItemType.EPISODE: + if TYPE_CHECKING: + assert isinstance(playlist_item.track, SimplifiedEpisode) + items.append(_get_episode_item_payload(playlist_item.track)) elif media_content_type == MediaType.ALBUM: if album := await spotify.get_album(media_content_id): title = album.name @@ -370,16 +386,7 @@ async def build_item_response( # noqa: C901 ): title = show.name image = show.images[0].url if show.images else None - items = [ - { - "id": episode.episode_id, - "name": episode.name, - "type": MediaType.EPISODE, - "uri": episode.uri, - "thumbnail": fetch_image_url(episode.images), - } - for episode in show_episodes - ] + items = [_get_episode_item_payload(episode) for episode in show_episodes] try: media_class = CONTENT_TYPE_MEDIA_CLASS[media_content_type] diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index e7b24cb3e1d..6c5b7382bbb 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/spotify", "integration_type": "service", "iot_class": "cloud_polling", - "loggers": ["spotipy"], - "requirements": ["spotifyaio==0.8.8"], + "loggers": ["spotifyaio"], + "requirements": ["spotifyaio==0.8.10"], "zeroconf": ["_spotify-connect._tcp.local."] } diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 7687936fe4c..20a634efb42 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -361,6 +361,8 @@ class SpotifyMediaPlayer(SpotifyEntity, MediaPlayerEntity): """Select playback device.""" for device in self.devices.data: if device.name == source: + if TYPE_CHECKING: + assert device.device_id is not None await self.coordinator.client.transfer_playback(device.device_id) return diff --git a/requirements_all.txt b/requirements_all.txt index fc13e72c128..9a4c93ee96e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2719,7 +2719,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.3 # homeassistant.components.spotify -spotifyaio==0.8.8 +spotifyaio==0.8.10 # homeassistant.components.sql sqlparse==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0923b497575..b17bd38a849 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2171,7 +2171,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.3 # homeassistant.components.spotify -spotifyaio==0.8.8 +spotifyaio==0.8.10 # homeassistant.components.sql sqlparse==0.5.0 diff --git a/tests/components/spotify/fixtures/playlist.json b/tests/components/spotify/fixtures/playlist.json index 36c28cc814b..5680ac9109c 100644 --- a/tests/components/spotify/fixtures/playlist.json +++ b/tests/components/spotify/fixtures/playlist.json @@ -514,6 +514,472 @@ "uri": "spotify:track:2E2znCPaS8anQe21GLxcvJ", "is_local": false } + }, + { + "added_at": "2024-11-28T11:20:58Z", + "added_by": { + "external_urls": { + "spotify": "https://open.spotify.com/user/1112264649" + }, + "href": "https://api.spotify.com/v1/users/1112264649", + "id": "1112264649", + "type": "user", + "uri": "spotify:user:1112264649" + }, + "is_local": false, + "primary_color": null, + "track": { + "explicit": false, + "audio_preview_url": "https://podz-content.spotifycdn.com/audio/clips/06lRxUmh8UNVTByuyxLYqh/clip_132296_192296.mp3", + "description": "Patreon: https://www.patreon.com/safetythirdMerch: https://safetythird.shopYouTube: https://www.youtube.com/@safetythird/Advertising Inquiries: https://redcircle.com/brandsPrivacy & Opt-Out: https://redcircle.com/privacy", + "duration_ms": 3690161, + "episode": true, + "external_urls": { + "spotify": "https://open.spotify.com/episode/3o0RYoo5iOMKSmEbunsbvW" + }, + "href": "https://api.spotify.com/v1/episodes/3o0RYoo5iOMKSmEbunsbvW", + "html_description": "

Patreon: https://www.patreon.com/safetythird

Merch: https://safetythird.shop

YouTube: https://www.youtube.com/@safetythird/



Advertising Inquiries: https://redcircle.com/brands

Privacy & Opt-Out: https://redcircle.com/privacy", + "id": "3o0RYoo5iOMKSmEbunsbvW", + "images": [ + { + "height": 640, + "url": "https://i.scdn.co/image/ab6765630000ba8ac7bedd27a4413b1abf926d8a", + "width": 640 + }, + { + "height": 300, + "url": "https://i.scdn.co/image/ab67656300005f1fc7bedd27a4413b1abf926d8a", + "width": 300 + }, + { + "height": 64, + "url": "https://i.scdn.co/image/ab6765630000f68dc7bedd27a4413b1abf926d8a", + "width": 64 + } + ], + "is_externally_hosted": false, + "language": "en-US", + "languages": ["en-US"], + "name": "My Squirrel Has Brain Damage - Safety Third 119", + "release_date": "2024-07-26", + "release_date_precision": "day", + "available_markets": [ + "AR", + "AU", + "AT", + "BE", + "BO", + "BR", + "BG", + "CA", + "CL", + "CO", + "CR", + "CY", + "CZ", + "DK", + "DO", + "DE", + "EC", + "EE", + "SV", + "FI", + "FR", + "GR", + "GT", + "HN", + "HK", + "HU", + "IS", + "IE", + "IT", + "LV", + "LT", + "LU", + "MY", + "MT", + "MX", + "NL", + "NZ", + "NI", + "NO", + "PA", + "PY", + "PE", + "PH", + "PL", + "PT", + "SG", + "SK", + "ES", + "SE", + "CH", + "TW", + "TR", + "UY", + "US", + "GB", + "AD", + "LI", + "MC", + "ID", + "JP", + "TH", + "VN", + "RO", + "IL", + "ZA", + "SA", + "AE", + "BH", + "QA", + "OM", + "KW", + "EG", + "MA", + "DZ", + "TN", + "LB", + "JO", + "PS", + "IN", + "BY", + "KZ", + "MD", + "UA", + "AL", + "BA", + "HR", + "ME", + "MK", + "RS", + "SI", + "KR", + "BD", + "PK", + "LK", + "GH", + "KE", + "NG", + "TZ", + "UG", + "AG", + "AM", + "BS", + "BB", + "BZ", + "BT", + "BW", + "BF", + "CV", + "CW", + "DM", + "FJ", + "GM", + "GE", + "GD", + "GW", + "GY", + "HT", + "JM", + "KI", + "LS", + "LR", + "MW", + "MV", + "ML", + "MH", + "FM", + "NA", + "NR", + "NE", + "PW", + "PG", + "PR", + "WS", + "SM", + "ST", + "SN", + "SC", + "SL", + "SB", + "KN", + "LC", + "VC", + "SR", + "TL", + "TO", + "TT", + "TV", + "VU", + "AZ", + "BN", + "BI", + "KH", + "CM", + "TD", + "KM", + "GQ", + "SZ", + "GA", + "GN", + "KG", + "LA", + "MO", + "MR", + "MN", + "NP", + "RW", + "TG", + "UZ", + "ZW", + "BJ", + "MG", + "MU", + "MZ", + "AO", + "CI", + "DJ", + "ZM", + "CD", + "CG", + "IQ", + "LY", + "TJ", + "VE", + "ET", + "XK" + ], + "show": { + "available_markets": [ + "AR", + "AU", + "AT", + "BE", + "BO", + "BR", + "BG", + "CA", + "CL", + "CO", + "CR", + "CY", + "CZ", + "DK", + "DO", + "DE", + "EC", + "EE", + "SV", + "FI", + "FR", + "GR", + "GT", + "HN", + "HK", + "HU", + "IS", + "IE", + "IT", + "LV", + "LT", + "LU", + "MY", + "MT", + "MX", + "NL", + "NZ", + "NI", + "NO", + "PA", + "PY", + "PE", + "PH", + "PL", + "PT", + "SG", + "SK", + "ES", + "SE", + "CH", + "TW", + "TR", + "UY", + "US", + "GB", + "AD", + "LI", + "MC", + "ID", + "JP", + "TH", + "VN", + "RO", + "IL", + "ZA", + "SA", + "AE", + "BH", + "QA", + "OM", + "KW", + "EG", + "MA", + "DZ", + "TN", + "LB", + "JO", + "PS", + "IN", + "BY", + "KZ", + "MD", + "UA", + "AL", + "BA", + "HR", + "ME", + "MK", + "RS", + "SI", + "KR", + "BD", + "PK", + "LK", + "GH", + "KE", + "NG", + "TZ", + "UG", + "AG", + "AM", + "BS", + "BB", + "BZ", + "BT", + "BW", + "BF", + "CV", + "CW", + "DM", + "FJ", + "GM", + "GE", + "GD", + "GW", + "GY", + "HT", + "JM", + "KI", + "LS", + "LR", + "MW", + "MV", + "ML", + "MH", + "FM", + "NA", + "NR", + "NE", + "PW", + "PG", + "PR", + "WS", + "SM", + "ST", + "SN", + "SC", + "SL", + "SB", + "KN", + "LC", + "VC", + "SR", + "TL", + "TO", + "TT", + "TV", + "VU", + "AZ", + "BN", + "BI", + "KH", + "CM", + "TD", + "KM", + "GQ", + "SZ", + "GA", + "GN", + "KG", + "LA", + "MO", + "MR", + "MN", + "NP", + "RW", + "TG", + "UZ", + "ZW", + "BJ", + "MG", + "MU", + "MZ", + "AO", + "CI", + "DJ", + "ZM", + "CD", + "CG", + "IQ", + "LY", + "TJ", + "VE", + "ET", + "XK" + ], + "copyrights": [], + "description": "Safety Third is a weekly show hosted by William Osman, NileRed, The Backyard Scientist, Allen Pan, and a couple other YouTube \"Scientists\". Sometimes we have guests, sometimes it's just us, but always: safety is our number three priority.", + "explicit": true, + "external_urls": { + "spotify": "https://open.spotify.com/show/1Y9ExMgMxoBVrgrfU7u0nD" + }, + "href": "https://api.spotify.com/v1/shows/1Y9ExMgMxoBVrgrfU7u0nD", + "html_description": "

Safety Third is a weekly show hosted by William Osman, NileRed, The Backyard Scientist, Allen Pan, and a couple other YouTube "Scientists". Sometimes we have guests, sometimes it's just us, but always: safety is our number three priority.

", + "id": "1Y9ExMgMxoBVrgrfU7u0nD", + "images": [ + { + "height": 640, + "url": "https://i.scdn.co/image/ab6765630000ba8ac7bedd27a4413b1abf926d8a", + "width": 640 + }, + { + "height": 300, + "url": "https://i.scdn.co/image/ab67656300005f1fc7bedd27a4413b1abf926d8a", + "width": 300 + }, + { + "height": 64, + "url": "https://i.scdn.co/image/ab6765630000f68dc7bedd27a4413b1abf926d8a", + "width": 64 + } + ], + "is_externally_hosted": false, + "languages": ["en-US"], + "media_type": "audio", + "name": "Safety Third", + "publisher": "Safety Third ", + "total_episodes": 120, + "type": "show", + "uri": "spotify:show:1Y9ExMgMxoBVrgrfU7u0nD" + }, + "track": false, + "type": "episode", + "uri": "spotify:episode:3o0RYoo5iOMKSmEbunsbvW" + }, + "video_thumbnail": { + "url": null + } } ] } diff --git a/tests/components/spotify/snapshots/test_diagnostics.ambr b/tests/components/spotify/snapshots/test_diagnostics.ambr index 40502562da3..0ac375d18e3 100644 --- a/tests/components/spotify/snapshots/test_diagnostics.ambr +++ b/tests/components/spotify/snapshots/test_diagnostics.ambr @@ -409,6 +409,69 @@ 'uri': 'spotify:track:2E2znCPaS8anQe21GLxcvJ', }), }), + dict({ + 'track': dict({ + 'description': 'Patreon: https://www.patreon.com/safetythirdMerch: https://safetythird.shopYouTube: https://www.youtube.com/@safetythird/Advertising Inquiries: https://redcircle.com/brandsPrivacy & Opt-Out: https://redcircle.com/privacy', + 'duration_ms': 3690161, + 'episode_id': '3o0RYoo5iOMKSmEbunsbvW', + 'explicit': False, + 'external_urls': dict({ + 'spotify': 'https://open.spotify.com/episode/3o0RYoo5iOMKSmEbunsbvW', + }), + 'href': 'https://api.spotify.com/v1/episodes/3o0RYoo5iOMKSmEbunsbvW', + 'images': list([ + dict({ + 'height': 640, + 'url': 'https://i.scdn.co/image/ab6765630000ba8ac7bedd27a4413b1abf926d8a', + 'width': 640, + }), + dict({ + 'height': 300, + 'url': 'https://i.scdn.co/image/ab67656300005f1fc7bedd27a4413b1abf926d8a', + 'width': 300, + }), + dict({ + 'height': 64, + 'url': 'https://i.scdn.co/image/ab6765630000f68dc7bedd27a4413b1abf926d8a', + 'width': 64, + }), + ]), + 'name': 'My Squirrel Has Brain Damage - Safety Third 119', + 'release_date': '2024-07-26', + 'release_date_precision': 'day', + 'show': dict({ + 'description': 'Safety Third is a weekly show hosted by William Osman, NileRed, The Backyard Scientist, Allen Pan, and a couple other YouTube "Scientists". Sometimes we have guests, sometimes it\'s just us, but always: safety is our number three priority.', + 'external_urls': dict({ + 'spotify': 'https://open.spotify.com/show/1Y9ExMgMxoBVrgrfU7u0nD', + }), + 'href': 'https://api.spotify.com/v1/shows/1Y9ExMgMxoBVrgrfU7u0nD', + 'images': list([ + dict({ + 'height': 640, + 'url': 'https://i.scdn.co/image/ab6765630000ba8ac7bedd27a4413b1abf926d8a', + 'width': 640, + }), + dict({ + 'height': 300, + 'url': 'https://i.scdn.co/image/ab67656300005f1fc7bedd27a4413b1abf926d8a', + 'width': 300, + }), + dict({ + 'height': 64, + 'url': 'https://i.scdn.co/image/ab6765630000f68dc7bedd27a4413b1abf926d8a', + 'width': 64, + }), + ]), + 'name': 'Safety Third', + 'publisher': 'Safety Third ', + 'show_id': '1Y9ExMgMxoBVrgrfU7u0nD', + 'total_episodes': 120, + 'uri': 'spotify:show:1Y9ExMgMxoBVrgrfU7u0nD', + }), + 'type': 'episode', + 'uri': 'spotify:episode:3o0RYoo5iOMKSmEbunsbvW', + }), + }), ]), }), 'uri': 'spotify:playlist:3cEYpjA9oz9GiPac4AsH4n', diff --git a/tests/components/spotify/snapshots/test_media_browser.ambr b/tests/components/spotify/snapshots/test_media_browser.ambr index 764dc7a10e1..6b217977227 100644 --- a/tests/components/spotify/snapshots/test_media_browser.ambr +++ b/tests/components/spotify/snapshots/test_media_browser.ambr @@ -649,6 +649,16 @@ 'thumbnail': 'https://i.scdn.co/image/ab67616d0000b27304e57d181ff062f8339d6c71', 'title': 'You Are So Beautiful', }), + dict({ + 'can_expand': False, + 'can_play': True, + 'children_media_class': None, + 'media_class': , + 'media_content_id': 'spotify://01j5tx5a0ff6g5v0qjx6hbc94t/spotify:episode:3o0RYoo5iOMKSmEbunsbvW', + 'media_content_type': 'spotify://episode', + 'thumbnail': 'https://i.scdn.co/image/ab6765630000ba8ac7bedd27a4413b1abf926d8a', + 'title': 'My Squirrel Has Brain Damage - Safety Third 119', + }), ]), 'children_media_class': , 'media_class': , From 157198bf419153c5e1eaab0530d0b9d668e420fc Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 28 Nov 2024 13:45:51 +0100 Subject: [PATCH 023/103] Make wake word selection part of configuration (#131832) --- homeassistant/components/esphome/select.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/esphome/select.py b/homeassistant/components/esphome/select.py index ab7654478a7..71a21186d3d 100644 --- a/homeassistant/components/esphome/select.py +++ b/homeassistant/components/esphome/select.py @@ -10,6 +10,7 @@ from homeassistant.components.assist_pipeline.select import ( ) from homeassistant.components.assist_satellite import AssistSatelliteConfiguration from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.const import EntityCategory from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import restore_state from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -100,7 +101,9 @@ class EsphomeAssistSatelliteWakeWordSelect( """Wake word selector for esphome devices.""" entity_description = SelectEntityDescription( - key="wake_word", translation_key="wake_word" + key="wake_word", + translation_key="wake_word", + entity_category=EntityCategory.CONFIG, ) _attr_should_poll = False _attr_current_option: str | None = None From 9d48f36754379881373f6421917c381ad01ffab5 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Thu, 28 Nov 2024 18:30:05 +0100 Subject: [PATCH 024/103] Allow empty trigger sentence responses in conversations (#131849) allow empty trigger sentence responses --- homeassistant/components/assist_pipeline/pipeline.py | 2 +- tests/components/conversation/test_init.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 96beaf792a7..5bbc81adb86 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -1040,7 +1040,7 @@ class PipelineRun: := await conversation.async_handle_sentence_triggers( self.hass, user_input ) - ): + ) is not None: # Sentence trigger matched trigger_response = intent.IntentResponse( self.pipeline.conversation_language diff --git a/tests/components/conversation/test_init.py b/tests/components/conversation/test_init.py index 0100e62cf81..6900ba2d419 100644 --- a/tests/components/conversation/test_init.py +++ b/tests/components/conversation/test_init.py @@ -236,12 +236,17 @@ async def test_prepare_agent( assert len(mock_prepare.mock_calls) == 1 -async def test_async_handle_sentence_triggers(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("response_template", "expected_response"), + [("response {{ trigger.device_id }}", "response 1234"), ("", "")], +) +async def test_async_handle_sentence_triggers( + hass: HomeAssistant, response_template: str, expected_response: str +) -> None: """Test handling sentence triggers with async_handle_sentence_triggers.""" assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "conversation", {}) - response_template = "response {{ trigger.device_id }}" assert await async_setup_component( hass, "automation", @@ -260,7 +265,6 @@ async def test_async_handle_sentence_triggers(hass: HomeAssistant) -> None: # Device id will be available in response template device_id = "1234" - expected_response = f"response {device_id}" actual_response = await async_handle_sentence_triggers( hass, ConversationInput( From eeb63d42a01662e012e8d5af52102ca0b767be06 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 28 Nov 2024 18:04:00 +0100 Subject: [PATCH 025/103] Bump pyatv to 0.16.0 (#131852) --- homeassistant/components/apple_tv/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index b4e1b354878..b10a14af32b 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/apple_tv", "iot_class": "local_push", "loggers": ["pyatv", "srptools"], - "requirements": ["pyatv==0.15.1"], + "requirements": ["pyatv==0.16.0"], "zeroconf": [ "_mediaremotetv._tcp.local.", "_companion-link._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 9a4c93ee96e..535a5bbc34b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1778,7 +1778,7 @@ pyatag==0.3.5.3 pyatmo==8.1.0 # homeassistant.components.apple_tv -pyatv==0.15.1 +pyatv==0.16.0 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b17bd38a849..077847d260b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1449,7 +1449,7 @@ pyatag==0.3.5.3 pyatmo==8.1.0 # homeassistant.components.apple_tv -pyatv==0.15.1 +pyatv==0.16.0 # homeassistant.components.aussie_broadband pyaussiebb==0.0.15 From ac4ae0430ee62d411bc21f729e3a2fedd12e8cb4 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 28 Nov 2024 20:50:53 +0100 Subject: [PATCH 026/103] Update frontend to 20241127.1 (#131855) --- 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 3063d3d8440..7bd500f17ea 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==20241127.0"] + "requirements": ["home-assistant-frontend==20241127.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 691d80f31bf..cb3f51476c8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ habluetooth==3.6.0 hass-nabucasa==0.85.0 hassil==2.0.4 home-assistant-bluetooth==1.13.0 -home-assistant-frontend==20241127.0 +home-assistant-frontend==20241127.1 home-assistant-intents==2024.11.27 httpx==0.27.2 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 535a5bbc34b..e0dbe09526e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1130,7 +1130,7 @@ hole==0.8.0 holidays==0.61 # homeassistant.components.frontend -home-assistant-frontend==20241127.0 +home-assistant-frontend==20241127.1 # homeassistant.components.conversation home-assistant-intents==2024.11.27 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 077847d260b..4083eb83844 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -956,7 +956,7 @@ hole==0.8.0 holidays==0.61 # homeassistant.components.frontend -home-assistant-frontend==20241127.0 +home-assistant-frontend==20241127.1 # homeassistant.components.conversation home-assistant-intents==2024.11.27 From dd186723416a74f960edfc97a02003c79e14267b Mon Sep 17 00:00:00 2001 From: Madhan Date: Thu, 28 Nov 2024 18:48:38 +0000 Subject: [PATCH 027/103] Bump PyMetEireann to 2024.11.0 (#131860) Co-authored-by: Joostlek --- homeassistant/components/met_eireann/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/met_eireann/manifest.json b/homeassistant/components/met_eireann/manifest.json index 72afc6977dd..7b913df4d3c 100644 --- a/homeassistant/components/met_eireann/manifest.json +++ b/homeassistant/components/met_eireann/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/met_eireann", "iot_class": "cloud_polling", "loggers": ["meteireann"], - "requirements": ["PyMetEireann==2021.8.0"] + "requirements": ["PyMetEireann==2024.11.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index e0dbe09526e..0226fa8d924 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -60,7 +60,7 @@ PyFronius==0.7.3 PyLoadAPI==1.3.2 # homeassistant.components.met_eireann -PyMetEireann==2021.8.0 +PyMetEireann==2024.11.0 # homeassistant.components.met # homeassistant.components.norway_air diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4083eb83844..ac180f8c650 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -57,7 +57,7 @@ PyFronius==0.7.3 PyLoadAPI==1.3.2 # homeassistant.components.met_eireann -PyMetEireann==2021.8.0 +PyMetEireann==2024.11.0 # homeassistant.components.met # homeassistant.components.norway_air From 2ea0c547883f56ea390fd755313e977e9d3af20c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 28 Nov 2024 20:52:51 +0100 Subject: [PATCH 028/103] Only download translation strings we have defined (#131864) --- script/translations/deduplicate.py | 3 +-- script/translations/develop.py | 25 +------------------- script/translations/download.py | 37 +++++++++++++++++++++++++++++- script/translations/util.py | 23 +++++++++++++++++++ 4 files changed, 61 insertions(+), 27 deletions(-) diff --git a/script/translations/deduplicate.py b/script/translations/deduplicate.py index 8cc4cee3b10..f92f90115ce 100644 --- a/script/translations/deduplicate.py +++ b/script/translations/deduplicate.py @@ -7,8 +7,7 @@ from pathlib import Path from homeassistant.const import Platform from . import upload -from .develop import flatten_translations -from .util import get_base_arg_parser, load_json_from_path +from .util import flatten_translations, get_base_arg_parser, load_json_from_path def get_arguments() -> argparse.Namespace: diff --git a/script/translations/develop.py b/script/translations/develop.py index 00465e1bc24..9e3a2ded046 100644 --- a/script/translations/develop.py +++ b/script/translations/develop.py @@ -9,7 +9,7 @@ import sys from . import download, upload from .const import INTEGRATIONS_DIR -from .util import get_base_arg_parser +from .util import flatten_translations, get_base_arg_parser def valid_integration(integration): @@ -32,29 +32,6 @@ def get_arguments() -> argparse.Namespace: return parser.parse_args() -def flatten_translations(translations): - """Flatten all translations.""" - stack = [iter(translations.items())] - key_stack = [] - flattened_translations = {} - while stack: - for k, v in stack[-1]: - key_stack.append(k) - if isinstance(v, dict): - stack.append(iter(v.items())) - break - if isinstance(v, str): - common_key = "::".join(key_stack) - flattened_translations[common_key] = v - key_stack.pop() - else: - stack.pop() - if key_stack: - key_stack.pop() - - return flattened_translations - - def substitute_translation_references(integration_strings, flattened_translations): """Recursively processes all translation strings for the integration.""" result = {} diff --git a/script/translations/download.py b/script/translations/download.py index 756de46fb61..3fa7065d058 100755 --- a/script/translations/download.py +++ b/script/translations/download.py @@ -7,10 +7,11 @@ import json from pathlib import Path import re import subprocess +from typing import Any from .const import CLI_2_DOCKER_IMAGE, CORE_PROJECT_ID, INTEGRATIONS_DIR from .error import ExitApp -from .util import get_lokalise_token, load_json_from_path +from .util import flatten_translations, get_lokalise_token, load_json_from_path FILENAME_FORMAT = re.compile(r"strings\.(?P\w+)\.json") DOWNLOAD_DIR = Path("build/translations-download").absolute() @@ -103,7 +104,15 @@ def save_language_translations(lang, translations): f"Skipping {lang} for {component}, as the integration doesn't seem to exist." ) continue + if not ( + Path("homeassistant") / "components" / component / "strings.json" + ).exists(): + print( + f"Skipping {lang} for {component}, as the integration doesn't have a strings.json file." + ) + continue path.parent.mkdir(parents=True, exist_ok=True) + base_translations = pick_keys(component, base_translations) save_json(path, base_translations) if "platform" not in component_translations: @@ -131,6 +140,32 @@ def delete_old_translations(): fil.unlink() +def get_current_keys(component: str) -> dict[str, Any]: + """Get the current keys for a component.""" + strings_path = Path("homeassistant") / "components" / component / "strings.json" + return load_json_from_path(strings_path) + + +def pick_keys(component: str, translations: dict[str, Any]) -> dict[str, Any]: + """Pick the keys that are in the current strings.""" + flat_translations = flatten_translations(translations) + flat_current_keys = flatten_translations(get_current_keys(component)) + flatten_result = {} + for key in flat_current_keys: + if key in flat_translations: + flatten_result[key] = flat_translations[key] + result = {} + for key, value in flatten_result.items(): + parts = key.split("::") + d = result + for part in parts[:-1]: + if part not in d: + d[part] = {} + d = d[part] + d[parts[-1]] = value + return result + + def run(): """Run the script.""" DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True) diff --git a/script/translations/util.py b/script/translations/util.py index 8892bb46b7a..d78b2c4faff 100644 --- a/script/translations/util.py +++ b/script/translations/util.py @@ -66,3 +66,26 @@ def load_json_from_path(path: pathlib.Path) -> Any: return json.loads(path.read_text()) except json.JSONDecodeError as err: raise JSONDecodeErrorWithPath(err.msg, err.doc, err.pos, path) from err + + +def flatten_translations(translations): + """Flatten all translations.""" + stack = [iter(translations.items())] + key_stack = [] + flattened_translations = {} + while stack: + for k, v in stack[-1]: + key_stack.append(k) + if isinstance(v, dict): + stack.append(iter(v.items())) + break + if isinstance(v, str): + common_key = "::".join(key_stack) + flattened_translations[common_key] = v + key_stack.pop() + else: + stack.pop() + if key_stack: + key_stack.pop() + + return flattened_translations From ee960933dba5f0d2433a54bf96dc4c4d36ab461e Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Thu, 28 Nov 2024 11:51:23 -0800 Subject: [PATCH 029/103] Fix flaky test in history stats (#131869) --- tests/components/history_stats/test_sensor.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 694c5c20707..d60203676e6 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -459,7 +459,11 @@ async def test_async_on_entire_period( def _fake_states(*args, **kwargs): return { "binary_sensor.test_on_id": [ - ha.State("binary_sensor.test_on_id", "on", last_changed=start_time), + ha.State( + "binary_sensor.test_on_id", + "on", + last_changed=(start_time - timedelta(seconds=10)), + ), ha.State("binary_sensor.test_on_id", "on", last_changed=t0), ha.State("binary_sensor.test_on_id", "on", last_changed=t1), ha.State("binary_sensor.test_on_id", "on", last_changed=t2), From f97d96e3aeafd95834627997bfc969d8bc8a4cd4 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Thu, 28 Nov 2024 21:01:00 +0100 Subject: [PATCH 030/103] Add captcha to BMW ConfigFlow (#131351) Co-authored-by: Franck Nijhof --- .../bmw_connected_drive/config_flow.py | 71 ++++++++-- .../components/bmw_connected_drive/const.py | 5 + .../bmw_connected_drive/coordinator.py | 5 - .../bmw_connected_drive/strings.json | 10 ++ .../bmw_connected_drive/__init__.py | 9 +- .../snapshots/test_diagnostics.ambr | 6 +- .../bmw_connected_drive/test_config_flow.py | 121 ++++++++++-------- 7 files changed, 153 insertions(+), 74 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/config_flow.py b/homeassistant/components/bmw_connected_drive/config_flow.py index 409bfdca6f1..8831895c71e 100644 --- a/homeassistant/components/bmw_connected_drive/config_flow.py +++ b/homeassistant/components/bmw_connected_drive/config_flow.py @@ -27,9 +27,18 @@ from homeassistant.const import CONF_PASSWORD, CONF_REGION, CONF_SOURCE, CONF_US from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig +from homeassistant.util.ssl import get_default_context from . import DOMAIN -from .const import CONF_ALLOWED_REGIONS, CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN +from .const import ( + CONF_ALLOWED_REGIONS, + CONF_CAPTCHA_REGIONS, + CONF_CAPTCHA_TOKEN, + CONF_CAPTCHA_URL, + CONF_GCID, + CONF_READ_ONLY, + CONF_REFRESH_TOKEN, +) DATA_SCHEMA = vol.Schema( { @@ -41,7 +50,14 @@ DATA_SCHEMA = vol.Schema( translation_key="regions", ) ), - } + }, + extra=vol.REMOVE_EXTRA, +) +CAPTCHA_SCHEMA = vol.Schema( + { + vol.Required(CONF_CAPTCHA_TOKEN): str, + }, + extra=vol.REMOVE_EXTRA, ) @@ -54,6 +70,8 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, data[CONF_USERNAME], data[CONF_PASSWORD], get_region_from_name(data[CONF_REGION]), + hcaptcha_token=data.get(CONF_CAPTCHA_TOKEN), + verify=get_default_context(), ) try: @@ -79,15 +97,17 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 + data: dict[str, Any] = {} + _existing_entry_data: Mapping[str, Any] | None = None async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Handle the initial step.""" - errors: dict[str, str] = {} + errors: dict[str, str] = self.data.pop("errors", {}) - if user_input is not None: + if user_input is not None and not errors: unique_id = f"{user_input[CONF_REGION]}-{user_input[CONF_USERNAME]}" await self.async_set_unique_id(unique_id) @@ -96,22 +116,35 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN): else: self._abort_if_unique_id_configured() + # Store user input for later use + self.data.update(user_input) + + # North America and Rest of World require captcha token + if ( + self.data.get(CONF_REGION) in CONF_CAPTCHA_REGIONS + and CONF_CAPTCHA_TOKEN not in self.data + ): + return await self.async_step_captcha() + info = None try: - info = await validate_input(self.hass, user_input) - entry_data = { - **user_input, - CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN), - CONF_GCID: info.get(CONF_GCID), - } + info = await validate_input(self.hass, self.data) except MissingCaptcha: errors["base"] = "missing_captcha" except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" + finally: + self.data.pop(CONF_CAPTCHA_TOKEN, None) if info: + entry_data = { + **self.data, + CONF_REFRESH_TOKEN: info.get(CONF_REFRESH_TOKEN), + CONF_GCID: info.get(CONF_GCID), + } + if self.source == SOURCE_REAUTH: return self.async_update_reload_and_abort( self._get_reauth_entry(), data=entry_data @@ -128,7 +161,7 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN): schema = self.add_suggested_values_to_schema( DATA_SCHEMA, - self._existing_entry_data, + self._existing_entry_data or self.data, ) return self.async_show_form(step_id="user", data_schema=schema, errors=errors) @@ -147,6 +180,22 @@ class BMWConfigFlow(ConfigFlow, domain=DOMAIN): self._existing_entry_data = self._get_reconfigure_entry().data return await self.async_step_user() + async def async_step_captcha( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Show captcha form.""" + if user_input and user_input.get(CONF_CAPTCHA_TOKEN): + self.data[CONF_CAPTCHA_TOKEN] = user_input[CONF_CAPTCHA_TOKEN].strip() + return await self.async_step_user(self.data) + + return self.async_show_form( + step_id="captcha", + data_schema=CAPTCHA_SCHEMA, + description_placeholders={ + "captcha_url": CONF_CAPTCHA_URL.format(region=self.data[CONF_REGION]) + }, + ) + @staticmethod @callback def async_get_options_flow( diff --git a/homeassistant/components/bmw_connected_drive/const.py b/homeassistant/components/bmw_connected_drive/const.py index 98d4acbfc91..750289e9d0a 100644 --- a/homeassistant/components/bmw_connected_drive/const.py +++ b/homeassistant/components/bmw_connected_drive/const.py @@ -8,10 +8,15 @@ ATTR_DIRECTION = "direction" ATTR_VIN = "vin" CONF_ALLOWED_REGIONS = ["china", "north_america", "rest_of_world"] +CONF_CAPTCHA_REGIONS = ["north_america", "rest_of_world"] CONF_READ_ONLY = "read_only" CONF_ACCOUNT = "account" CONF_REFRESH_TOKEN = "refresh_token" CONF_GCID = "gcid" +CONF_CAPTCHA_TOKEN = "captcha_token" +CONF_CAPTCHA_URL = ( + "https://bimmer-connected.readthedocs.io/en/stable/captcha/{region}.html" +) DATA_HASS_CONFIG = "hass_config" diff --git a/homeassistant/components/bmw_connected_drive/coordinator.py b/homeassistant/components/bmw_connected_drive/coordinator.py index d38b7ffacc2..4f560d16f9c 100644 --- a/homeassistant/components/bmw_connected_drive/coordinator.py +++ b/homeassistant/components/bmw_connected_drive/coordinator.py @@ -84,11 +84,6 @@ class BMWDataUpdateCoordinator(DataUpdateCoordinator[None]): if self.account.refresh_token != old_refresh_token: self._update_config_entry_refresh_token(self.account.refresh_token) - _LOGGER.debug( - "bimmer_connected: refresh token %s > %s", - old_refresh_token, - self.account.refresh_token, - ) def _update_config_entry_refresh_token(self, refresh_token: str | None) -> None: """Update or delete the refresh_token in the Config Entry.""" diff --git a/homeassistant/components/bmw_connected_drive/strings.json b/homeassistant/components/bmw_connected_drive/strings.json index 0e7a4a32ef4..8078971acd1 100644 --- a/homeassistant/components/bmw_connected_drive/strings.json +++ b/homeassistant/components/bmw_connected_drive/strings.json @@ -7,6 +7,16 @@ "password": "[%key:common::config_flow::data::password%]", "region": "ConnectedDrive Region" } + }, + "captcha": { + "title": "Are you a robot?", + "description": "A captcha is required for BMW login. Visit the external website to complete the challenge and submit the form. Copy the resulting token into the field below.\n\n{captcha_url}\n\nNo data will be exposed outside of your Home Assistant instance.", + "data": { + "captcha_token": "Captcha token" + }, + "data_description": { + "captcha_token": "One-time token retrieved from the captcha challenge." + } } }, "error": { diff --git a/tests/components/bmw_connected_drive/__init__.py b/tests/components/bmw_connected_drive/__init__.py index 4d280a1d0e5..f490b854749 100644 --- a/tests/components/bmw_connected_drive/__init__.py +++ b/tests/components/bmw_connected_drive/__init__.py @@ -9,6 +9,7 @@ import respx from homeassistant import config_entries from homeassistant.components.bmw_connected_drive.const import ( + CONF_CAPTCHA_TOKEN, CONF_GCID, CONF_READ_ONLY, CONF_REFRESH_TOKEN, @@ -24,8 +25,12 @@ FIXTURE_USER_INPUT = { CONF_PASSWORD: "p4ssw0rd", CONF_REGION: "rest_of_world", } -FIXTURE_REFRESH_TOKEN = "SOME_REFRESH_TOKEN" -FIXTURE_GCID = "SOME_GCID" +FIXTURE_CAPTCHA_INPUT = { + CONF_CAPTCHA_TOKEN: "captcha_token", +} +FIXTURE_USER_INPUT_W_CAPTCHA = FIXTURE_USER_INPUT | FIXTURE_CAPTCHA_INPUT +FIXTURE_REFRESH_TOKEN = "another_token_string" +FIXTURE_GCID = "DUMMY" FIXTURE_CONFIG_ENTRY = { "entry_id": "1", diff --git a/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr b/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr index 81ef1220069..b87da22a332 100644 --- a/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr +++ b/tests/components/bmw_connected_drive/snapshots/test_diagnostics.ambr @@ -4833,7 +4833,7 @@ }), ]), 'info': dict({ - 'gcid': 'SOME_GCID', + 'gcid': 'DUMMY', 'password': '**REDACTED**', 'refresh_token': '**REDACTED**', 'region': 'rest_of_world', @@ -7202,7 +7202,7 @@ }), ]), 'info': dict({ - 'gcid': 'SOME_GCID', + 'gcid': 'DUMMY', 'password': '**REDACTED**', 'refresh_token': '**REDACTED**', 'region': 'rest_of_world', @@ -8925,7 +8925,7 @@ }), ]), 'info': dict({ - 'gcid': 'SOME_GCID', + 'gcid': 'DUMMY', 'password': '**REDACTED**', 'refresh_token': '**REDACTED**', 'region': 'rest_of_world', diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index f57f1a304ac..8fa9d9be22b 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -4,17 +4,14 @@ from copy import deepcopy from unittest.mock import patch from bimmer_connected.api.authentication import MyBMWAuthentication -from bimmer_connected.models import ( - MyBMWAPIError, - MyBMWAuthError, - MyBMWCaptchaMissingError, -) +from bimmer_connected.models import MyBMWAPIError, MyBMWAuthError from httpx import RequestError import pytest from homeassistant import config_entries from homeassistant.components.bmw_connected_drive.config_flow import DOMAIN from homeassistant.components.bmw_connected_drive.const import ( + CONF_CAPTCHA_TOKEN, CONF_READ_ONLY, CONF_REFRESH_TOKEN, ) @@ -23,10 +20,12 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from . import ( + FIXTURE_CAPTCHA_INPUT, FIXTURE_CONFIG_ENTRY, FIXTURE_GCID, FIXTURE_REFRESH_TOKEN, FIXTURE_USER_INPUT, + FIXTURE_USER_INPUT_W_CAPTCHA, ) from tests.common import MockConfigEntry @@ -61,7 +60,7 @@ async def test_authentication_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, - data=FIXTURE_USER_INPUT, + data=deepcopy(FIXTURE_USER_INPUT_W_CAPTCHA), ) assert result["type"] is FlowResultType.FORM @@ -79,7 +78,7 @@ async def test_connection_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, - data=FIXTURE_USER_INPUT, + data=deepcopy(FIXTURE_USER_INPUT_W_CAPTCHA), ) assert result["type"] is FlowResultType.FORM @@ -97,7 +96,7 @@ async def test_api_error(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, - data=deepcopy(FIXTURE_USER_INPUT), + data=deepcopy(FIXTURE_USER_INPUT_W_CAPTCHA), ) assert result["type"] is FlowResultType.FORM @@ -105,6 +104,28 @@ async def test_api_error(hass: HomeAssistant) -> None: assert result["errors"] == {"base": "cannot_connect"} +@pytest.mark.usefixtures("bmw_fixture") +async def test_captcha_flow_missing_error(hass: HomeAssistant) -> None: + """Test the external flow with captcha failing once and succeeding the second time.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=deepcopy(FIXTURE_USER_INPUT), + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "captcha" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_CAPTCHA_TOKEN: " "} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "missing_captcha"} + + async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: """Test registering an integration and finishing flow works.""" with ( @@ -118,14 +139,22 @@ async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: return_value=True, ) as mock_setup_entry, ): - result2 = await hass.config_entries.flow.async_init( + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=deepcopy(FIXTURE_USER_INPUT), ) - assert result2["type"] is FlowResultType.CREATE_ENTRY - assert result2["title"] == FIXTURE_COMPLETE_ENTRY[CONF_USERNAME] - assert result2["data"] == FIXTURE_COMPLETE_ENTRY + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "captcha" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_CAPTCHA_INPUT + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert result["title"] == FIXTURE_COMPLETE_ENTRY[CONF_USERNAME] + assert result["data"] == FIXTURE_COMPLETE_ENTRY assert len(mock_setup_entry.mock_calls) == 1 @@ -206,13 +235,20 @@ async def test_reauth(hass: HomeAssistant) -> None: assert suggested_values[CONF_PASSWORD] == wrong_password assert suggested_values[CONF_REGION] == FIXTURE_USER_INPUT[CONF_REGION] - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], FIXTURE_USER_INPUT + result = await hass.config_entries.flow.async_configure( + result["flow_id"], deepcopy(FIXTURE_USER_INPUT) ) await hass.async_block_till_done() - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "reauth_successful" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "captcha" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_CAPTCHA_INPUT + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" assert config_entry.data == FIXTURE_COMPLETE_ENTRY assert len(mock_setup_entry.mock_calls) == 2 @@ -243,13 +279,13 @@ async def test_reauth_unique_id_abort(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {**FIXTURE_USER_INPUT, CONF_REGION: "north_america"} ) await hass.async_block_till_done() - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "account_mismatch" + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "account_mismatch" assert config_entry.data == config_entry_with_wrong_password["data"] @@ -279,13 +315,20 @@ async def test_reconfigure(hass: HomeAssistant) -> None: assert suggested_values[CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] assert suggested_values[CONF_REGION] == FIXTURE_USER_INPUT[CONF_REGION] - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], FIXTURE_USER_INPUT ) await hass.async_block_till_done() - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "reconfigure_successful" + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "captcha" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_CAPTCHA_INPUT + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" assert config_entry.data == FIXTURE_COMPLETE_ENTRY @@ -307,40 +350,12 @@ async def test_reconfigure_unique_id_abort(hass: HomeAssistant) -> None: assert result["step_id"] == "user" assert result["errors"] == {} - result2 = await hass.config_entries.flow.async_configure( + result = await hass.config_entries.flow.async_configure( result["flow_id"], {**FIXTURE_USER_INPUT, CONF_USERNAME: "somebody@email.com"}, ) await hass.async_block_till_done() - assert result2["type"] is FlowResultType.ABORT - assert result2["reason"] == "account_mismatch" + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "account_mismatch" assert config_entry.data == FIXTURE_COMPLETE_ENTRY - - -@pytest.mark.usefixtures("bmw_fixture") -async def test_captcha_flow_not_set(hass: HomeAssistant) -> None: - """Test the external flow with captcha failing once and succeeding the second time.""" - - TEST_REGION = "north_america" - - # Start flow and open form - # Start flow and open form - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] is FlowResultType.FORM - assert result["step_id"] == "user" - - # Add login data - with patch( - "bimmer_connected.api.authentication.MyBMWAuthentication._login_row_na", - side_effect=MyBMWCaptchaMissingError( - "Missing hCaptcha token for North America login" - ), - ): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={**FIXTURE_USER_INPUT, CONF_REGION: TEST_REGION}, - ) - assert result["errors"]["base"] == "missing_captcha" From 06838c028001b8a79f04aaaea40819c12b2cf827 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Nov 2024 21:02:37 +0100 Subject: [PATCH 031/103] Bump version to 2024.12.0b2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index bf84dbb6ff9..1e49ea64c07 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 419d04fdf25..f57013e7dd9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.12.0b1" +version = "2024.12.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 4326689f5225143b722af6c8175c8fe2153499f1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Nov 2024 21:07:51 -0600 Subject: [PATCH 032/103] Bump SQLAlchemy to 2.0.36 (#126683) * Bump SQLAlchemy to 2.0.35 changelog: https://docs.sqlalchemy.org/en/20/changelog/changelog_20.html#change-2.0.35 * fix mocking * adjust to .36 * remove ignored as these are now typed * fix SQLAlchemy --- .github/workflows/wheels.yml | 2 +- .../components/recorder/db_schema.py | 6 +-- .../components/recorder/manifest.json | 2 +- homeassistant/components/sql/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/sql/test_config_flow.py | 52 ++++++++----------- tests/components/sql/test_sensor.py | 47 ++++++++++------- 11 files changed, 62 insertions(+), 59 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index b9f54bba081..e0a850fa340 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -143,7 +143,7 @@ jobs: wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-dev" - skip-binary: aiohttp;multidict;yarl + skip-binary: aiohttp;multidict;yarl;SQLAlchemy constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements.txt" diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 7e8343321c3..dbe2b775297 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -162,14 +162,14 @@ class Unused(CHAR): """An unused column type that behaves like a string.""" -@compiles(UnusedDateTime, "mysql", "mariadb", "sqlite") # type: ignore[misc,no-untyped-call] -@compiles(Unused, "mysql", "mariadb", "sqlite") # type: ignore[misc,no-untyped-call] +@compiles(UnusedDateTime, "mysql", "mariadb", "sqlite") +@compiles(Unused, "mysql", "mariadb", "sqlite") def compile_char_zero(type_: TypeDecorator, compiler: Any, **kw: Any) -> str: """Compile UnusedDateTime and Unused as CHAR(0) on mysql, mariadb, and sqlite.""" return "CHAR(0)" # Uses 1 byte on MySQL (no change on sqlite) -@compiles(Unused, "postgresql") # type: ignore[misc,no-untyped-call] +@compiles(Unused, "postgresql") def compile_char_one(type_: TypeDecorator, compiler: Any, **kw: Any) -> str: """Compile Unused as CHAR(1) on postgresql.""" return "CHAR(1)" # Uses 1 byte diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 2be4b6862ba..93ffb12d18c 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_push", "quality_scale": "internal", "requirements": [ - "SQLAlchemy==2.0.31", + "SQLAlchemy==2.0.36", "fnv-hash-fast==1.0.2", "psutil-home-assistant==0.0.1" ] diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index dcb5f47829c..01c95d6c5e4 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sql", "iot_class": "local_polling", - "requirements": ["SQLAlchemy==2.0.31", "sqlparse==0.5.0"] + "requirements": ["SQLAlchemy==2.0.36", "sqlparse==0.5.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cb3f51476c8..cb7aa1219ab 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -59,7 +59,7 @@ pyudev==0.24.1 PyYAML==6.0.2 requests==2.32.3 securetar==2024.11.0 -SQLAlchemy==2.0.31 +SQLAlchemy==2.0.36 standard-aifc==3.13.0;python_version>='3.13' standard-telnetlib==3.13.0;python_version>='3.13' typing-extensions>=4.12.2,<5.0 diff --git a/pyproject.toml b/pyproject.toml index f57013e7dd9..10e169f7116 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -66,7 +66,7 @@ dependencies = [ "PyYAML==6.0.2", "requests==2.32.3", "securetar==2024.11.0", - "SQLAlchemy==2.0.31", + "SQLAlchemy==2.0.36", "standard-aifc==3.13.0;python_version>='3.13'", "standard-telnetlib==3.13.0;python_version>='3.13'", "typing-extensions>=4.12.2,<5.0", diff --git a/requirements.txt b/requirements.txt index 2cbdeb14b98..1fa82f175bb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -37,7 +37,7 @@ python-slugify==8.0.4 PyYAML==6.0.2 requests==2.32.3 securetar==2024.11.0 -SQLAlchemy==2.0.31 +SQLAlchemy==2.0.36 standard-aifc==3.13.0;python_version>='3.13' standard-telnetlib==3.13.0;python_version>='3.13' typing-extensions>=4.12.2,<5.0 diff --git a/requirements_all.txt b/requirements_all.txt index 0226fa8d924..421c833964d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -116,7 +116,7 @@ RtmAPI==0.7.2 # homeassistant.components.recorder # homeassistant.components.sql -SQLAlchemy==2.0.31 +SQLAlchemy==2.0.36 # homeassistant.components.tami4 Tami4EdgeAPI==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac180f8c650..5b7a91d4176 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -110,7 +110,7 @@ RtmAPI==0.7.2 # homeassistant.components.recorder # homeassistant.components.sql -SQLAlchemy==2.0.31 +SQLAlchemy==2.0.36 # homeassistant.components.tami4 Tami4EdgeAPI==3.0 diff --git a/tests/components/sql/test_config_flow.py b/tests/components/sql/test_config_flow.py index cb990e454b7..3f2400c0a32 100644 --- a/tests/components/sql/test_config_flow.py +++ b/tests/components/sql/test_config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +from pathlib import Path from unittest.mock import patch from sqlalchemy.exc import SQLAlchemyError @@ -597,9 +598,6 @@ async def test_options_flow_db_url_empty( "homeassistant.components.sql.async_setup_entry", return_value=True, ), - patch( - "homeassistant.components.sql.config_flow.sqlalchemy.create_engine", - ), ): result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -621,7 +619,9 @@ async def test_options_flow_db_url_empty( async def test_full_flow_not_recorder_db( - recorder_mock: Recorder, hass: HomeAssistant + recorder_mock: Recorder, + hass: HomeAssistant, + tmp_path: Path, ) -> None: """Test full config flow with not using recorder db.""" result = await hass.config_entries.flow.async_init( @@ -629,20 +629,19 @@ async def test_full_flow_not_recorder_db( ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {} + db_path = tmp_path / "db.db" + db_path_str = f"sqlite:///{db_path}" with ( patch( "homeassistant.components.sql.async_setup_entry", return_value=True, ), - patch( - "homeassistant.components.sql.config_flow.sqlalchemy.create_engine", - ), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { - "db_url": "sqlite://path/to/db.db", + "db_url": db_path_str, "name": "Get Value", "query": "SELECT 5 as value", "column": "value", @@ -654,7 +653,7 @@ async def test_full_flow_not_recorder_db( assert result2["title"] == "Get Value" assert result2["options"] == { "name": "Get Value", - "db_url": "sqlite://path/to/db.db", + "db_url": db_path_str, "query": "SELECT 5 as value", "column": "value", } @@ -671,15 +670,12 @@ async def test_full_flow_not_recorder_db( "homeassistant.components.sql.async_setup_entry", return_value=True, ), - patch( - "homeassistant.components.sql.config_flow.sqlalchemy.create_engine", - ), ): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ "query": "SELECT 5 as value", - "db_url": "sqlite://path/to/db.db", + "db_url": db_path_str, "column": "value", "unit_of_measurement": "MiB", }, @@ -689,7 +685,7 @@ async def test_full_flow_not_recorder_db( assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "Get Value", - "db_url": "sqlite://path/to/db.db", + "db_url": db_path_str, "query": "SELECT 5 as value", "column": "value", "unit_of_measurement": "MiB", @@ -697,24 +693,22 @@ async def test_full_flow_not_recorder_db( # Need to test same again to mitigate issue with db_url removal result = await hass.config_entries.options.async_init(entry.entry_id) - with patch( - "homeassistant.components.sql.config_flow.sqlalchemy.create_engine", - ): - result = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={ - "query": "SELECT 5 as value", - "db_url": "sqlite://path/to/db.db", - "column": "value", - "unit_of_measurement": "MB", - }, - ) - await hass.async_block_till_done() + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "query": "SELECT 5 as value", + "db_url": db_path_str, + "column": "value", + "unit_of_measurement": "MB", + }, + ) + await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == { "name": "Get Value", - "db_url": "sqlite://path/to/db.db", + "db_url": db_path_str, "query": "SELECT 5 as value", "column": "value", "unit_of_measurement": "MB", @@ -722,7 +716,7 @@ async def test_full_flow_not_recorder_db( assert entry.options == { "name": "Get Value", - "db_url": "sqlite://path/to/db.db", + "db_url": db_path_str, "query": "SELECT 5 as value", "column": "value", "unit_of_measurement": "MB", diff --git a/tests/components/sql/test_sensor.py b/tests/components/sql/test_sensor.py index b219ad47f3a..6b4032323d0 100644 --- a/tests/components/sql/test_sensor.py +++ b/tests/components/sql/test_sensor.py @@ -3,12 +3,13 @@ from __future__ import annotations from datetime import timedelta +from pathlib import Path +import sqlite3 from typing import Any from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest -from sqlalchemy import text as sql_text from sqlalchemy.exc import SQLAlchemyError from homeassistant.components.recorder import Recorder @@ -143,29 +144,37 @@ async def test_query_no_value( assert text in caplog.text -async def test_query_mssql_no_result( - recorder_mock: Recorder, hass: HomeAssistant, caplog: pytest.LogCaptureFixture +async def test_query_on_disk_sqlite_no_result( + recorder_mock: Recorder, + hass: HomeAssistant, + caplog: pytest.LogCaptureFixture, + tmp_path: Path, ) -> None: """Test the SQL sensor with a query that returns no value.""" - config = { - "db_url": "mssql://", - "query": "SELECT 5 as value where 1=2", - "column": "value", - "name": "count_tables", - } - with ( - patch("homeassistant.components.sql.sensor.sqlalchemy"), - patch( - "homeassistant.components.sql.sensor.sqlalchemy.text", - return_value=sql_text("SELECT TOP 1 5 as value where 1=2"), - ), - ): - await init_integration(hass, config) + db_path = tmp_path / "test.db" + db_path_str = f"sqlite:///{db_path}" - state = hass.states.get("sensor.count_tables") + def make_test_db(): + """Create a test database.""" + conn = sqlite3.connect(db_path) + conn.execute("CREATE TABLE users (value INTEGER)") + conn.commit() + conn.close() + + await hass.async_add_executor_job(make_test_db) + + config = { + "db_url": db_path_str, + "query": "SELECT value from users", + "column": "value", + "name": "count_users", + } + await init_integration(hass, config) + + state = hass.states.get("sensor.count_users") assert state.state == STATE_UNKNOWN - text = "SELECT TOP 1 5 AS VALUE WHERE 1=2 returned no results" + text = "SELECT value from users LIMIT 1; returned no results" assert text in caplog.text From 8eb52edabf4013b638abf6f607fdfba841de4105 Mon Sep 17 00:00:00 2001 From: "Glenn Vandeuren (aka Iondependent)" Date: Sat, 30 Nov 2024 09:30:24 +0100 Subject: [PATCH 033/103] Fix modbus state not dumped on restart (#131319) * Fix modbus state not dumped on restart * Update test_init.py * Set event back to stop * Update test_init.py --------- Co-authored-by: VandeurenGlenn <8685280+VandeurenGlenn@users.noreply.github.com> --- homeassistant/components/modbus/modbus.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/modbus/modbus.py b/homeassistant/components/modbus/modbus.py index d85b4e0e67f..18d91f8dd3b 100644 --- a/homeassistant/components/modbus/modbus.py +++ b/homeassistant/components/modbus/modbus.py @@ -158,8 +158,6 @@ async def async_modbus_setup( async def async_stop_modbus(event: Event) -> None: """Stop Modbus service.""" - - async_dispatcher_send(hass, SIGNAL_STOP_ENTITY) for client in hub_collect.values(): await client.async_close() From 5bf972ff16c3df16658115970552cc2c1152bed9 Mon Sep 17 00:00:00 2001 From: karwosts <32912880+karwosts@users.noreply.github.com> Date: Fri, 29 Nov 2024 21:43:31 -0800 Subject: [PATCH 034/103] Fix history stats count update immediately after change (#131856) * Fix history stats count update immediately after change * rerun CI --- homeassistant/components/history_stats/data.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index 40cf351fd9e..f9b79d74cb4 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -4,6 +4,8 @@ from __future__ import annotations from dataclasses import dataclass import datetime +import logging +import math from homeassistant.components.recorder import get_instance, history from homeassistant.core import Event, EventStateChangedData, HomeAssistant, State @@ -14,6 +16,8 @@ from .helpers import async_calculate_period, floored_timestamp MIN_TIME_UTC = datetime.datetime.min.replace(tzinfo=dt_util.UTC) +_LOGGER = logging.getLogger(__name__) + @dataclass class HistoryStatsState: @@ -186,8 +190,13 @@ class HistoryStats: current_state_matches = history_state.state in self._entity_states state_change_timestamp = history_state.last_changed - if state_change_timestamp > now_timestamp: + if math.floor(state_change_timestamp) > now_timestamp: # Shouldn't count states that are in the future + _LOGGER.debug( + "Skipping future timestamp %s (now %s)", + state_change_timestamp, + now_timestamp, + ) continue if previous_state_matches: From aaf3f61675c8cc73009374027f906650a84afc84 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Sat, 30 Nov 2024 04:31:56 +0100 Subject: [PATCH 035/103] Guard against hostname change in lamarzocco discovery (#131873) * Guard against hostname change in lamarzocco discovery * switch to abort_entries_match --- .../components/lamarzocco/config_flow.py | 1 + .../components/lamarzocco/test_config_flow.py | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/homeassistant/components/lamarzocco/config_flow.py b/homeassistant/components/lamarzocco/config_flow.py index 0f288e22c4a..a727e3fe357 100644 --- a/homeassistant/components/lamarzocco/config_flow.py +++ b/homeassistant/components/lamarzocco/config_flow.py @@ -291,6 +291,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): CONF_ADDRESS: discovery_info.macaddress, } ) + self._async_abort_entries_match({CONF_ADDRESS: discovery_info.macaddress}) _LOGGER.debug( "Discovered La Marzocco machine %s through DHCP at address %s", diff --git a/tests/components/lamarzocco/test_config_flow.py b/tests/components/lamarzocco/test_config_flow.py index f8103ac3054..b206b7b68a3 100644 --- a/tests/components/lamarzocco/test_config_flow.py +++ b/tests/components/lamarzocco/test_config_flow.py @@ -493,6 +493,27 @@ async def test_dhcp_discovery( } +async def test_dhcp_discovery_abort_on_hostname_changed( + hass: HomeAssistant, + mock_lamarzocco: MagicMock, + mock_cloud_client: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test dhcp discovery aborts when hostname was changed manually.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=DhcpServiceInfo( + ip="192.168.1.42", + hostname="custom_name", + macaddress="00:00:00:00:00:00", + ), + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "already_configured" + + async def test_dhcp_already_configured_and_update( hass: HomeAssistant, mock_lamarzocco: MagicMock, From b60b2fdd7c945219642037fb9420223629d4d18d Mon Sep 17 00:00:00 2001 From: Oliver <10700296+ol-iver@users.noreply.github.com> Date: Sat, 30 Nov 2024 17:27:31 +0100 Subject: [PATCH 036/103] Bump denonavr to v1.0.1 (#131882) --- homeassistant/components/denonavr/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index eff70b94a18..328ab504bd1 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/denonavr", "iot_class": "local_push", "loggers": ["denonavr"], - "requirements": ["denonavr==1.0.0"], + "requirements": ["denonavr==1.0.1"], "ssdp": [ { "manufacturer": "Denon", diff --git a/requirements_all.txt b/requirements_all.txt index 421c833964d..00cce2bd575 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -752,7 +752,7 @@ deluge-client==1.10.2 demetriek==0.4.0 # homeassistant.components.denonavr -denonavr==1.0.0 +denonavr==1.0.1 # homeassistant.components.devialet devialet==1.4.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5b7a91d4176..0f66554467c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -642,7 +642,7 @@ deluge-client==1.10.2 demetriek==0.4.0 # homeassistant.components.denonavr -denonavr==1.0.0 +denonavr==1.0.1 # homeassistant.components.devialet devialet==1.4.5 From 29e80e56c61e0ee57861d68798bbec2791e8580e Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Sat, 30 Nov 2024 04:32:20 +0100 Subject: [PATCH 037/103] Bump aioacaia to 0.1.10 (#131906) --- homeassistant/components/acaia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/acaia/manifest.json b/homeassistant/components/acaia/manifest.json index 49b3489cf9a..3f3e1c14d58 100644 --- a/homeassistant/components/acaia/manifest.json +++ b/homeassistant/components/acaia/manifest.json @@ -25,5 +25,5 @@ "integration_type": "device", "iot_class": "local_push", "loggers": ["aioacaia"], - "requirements": ["aioacaia==0.1.9"] + "requirements": ["aioacaia==0.1.10"] } diff --git a/requirements_all.txt b/requirements_all.txt index 00cce2bd575..84c41569da5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -173,7 +173,7 @@ aio-geojson-usgs-earthquakes==0.3 aio-georss-gdacs==0.10 # homeassistant.components.acaia -aioacaia==0.1.9 +aioacaia==0.1.10 # homeassistant.components.airq aioairq==0.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f66554467c..342b8592486 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -161,7 +161,7 @@ aio-geojson-usgs-earthquakes==0.3 aio-georss-gdacs==0.10 # homeassistant.components.acaia -aioacaia==0.1.9 +aioacaia==0.1.10 # homeassistant.components.airq aioairq==0.4.3 From 572347025b378310bac8ab6d2be252e9948dc321 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Sat, 30 Nov 2024 04:11:57 +0100 Subject: [PATCH 038/103] Fix media player join action for Music Assistant integration (#131910) * Fix media player join action for Music Assistant integration * Add tests for join/unjoin * add one more test --- .../music_assistant/media_player.py | 10 +-- .../music_assistant/test_media_player.py | 68 +++++++++++++++++++ 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/music_assistant/media_player.py b/homeassistant/components/music_assistant/media_player.py index d1d707c92e1..fdf3a0c0c48 100644 --- a/homeassistant/components/music_assistant/media_player.py +++ b/homeassistant/components/music_assistant/media_player.py @@ -400,13 +400,13 @@ class MusicAssistantPlayer(MusicAssistantEntity, MediaPlayerEntity): async def async_join_players(self, group_members: list[str]) -> None: """Join `group_members` as a player group with the current player.""" player_ids: list[str] = [] + entity_registry = er.async_get(self.hass) for child_entity_id in group_members: # resolve HA entity_id to MA player_id - if (hass_state := self.hass.states.get(child_entity_id)) is None: - continue - if (mass_player_id := hass_state.attributes.get("mass_player_id")) is None: - continue - player_ids.append(mass_player_id) + if not (entity_reg_entry := entity_registry.async_get(child_entity_id)): + raise HomeAssistantError(f"Entity {child_entity_id} not found") + # unique id is the MA player_id + player_ids.append(entity_reg_entry.unique_id) await self.mass.players.player_command_group_many(self.player_id, player_ids) @catch_musicassistant_error diff --git a/tests/components/music_assistant/test_media_player.py b/tests/components/music_assistant/test_media_player.py index 26ed5d1e538..13716b6a479 100644 --- a/tests/components/music_assistant/test_media_player.py +++ b/tests/components/music_assistant/test_media_player.py @@ -8,6 +8,7 @@ import pytest from syrupy import SnapshotAssertion from homeassistant.components.media_player import ( + ATTR_GROUP_MEMBERS, ATTR_MEDIA_ENQUEUE, ATTR_MEDIA_REPEAT, ATTR_MEDIA_SEEK_POSITION, @@ -16,6 +17,8 @@ from homeassistant.components.media_player import ( ATTR_MEDIA_VOLUME_MUTED, DOMAIN as MEDIA_PLAYER_DOMAIN, SERVICE_CLEAR_PLAYLIST, + SERVICE_JOIN, + SERVICE_UNJOIN, ) from homeassistant.components.music_assistant.const import DOMAIN as MASS_DOMAIN from homeassistant.components.music_assistant.media_player import ( @@ -269,6 +272,71 @@ async def test_media_player_repeat_set_action( ) +async def test_media_player_join_players_action( + hass: HomeAssistant, + music_assistant_client: MagicMock, +) -> None: + """Test media_player entity join_players action.""" + await setup_integration_from_fixtures(hass, music_assistant_client) + entity_id = "media_player.test_player_1" + mass_player_id = "00:00:00:00:00:01" + state = hass.states.get(entity_id) + assert state + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_JOIN, + { + ATTR_ENTITY_ID: entity_id, + ATTR_GROUP_MEMBERS: ["media_player.my_super_test_player_2"], + }, + blocking=True, + ) + assert music_assistant_client.send_command.call_count == 1 + assert music_assistant_client.send_command.call_args == call( + "players/cmd/group_many", + target_player=mass_player_id, + child_player_ids=["00:00:00:00:00:02"], + ) + # test again with invalid source player + music_assistant_client.send_command.reset_mock() + with pytest.raises( + HomeAssistantError, match="Entity media_player.blah_blah not found" + ): + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_JOIN, + { + ATTR_ENTITY_ID: entity_id, + ATTR_GROUP_MEMBERS: ["media_player.blah_blah"], + }, + blocking=True, + ) + + +async def test_media_player_unjoin_player_action( + hass: HomeAssistant, + music_assistant_client: MagicMock, +) -> None: + """Test media_player entity unjoin player action.""" + await setup_integration_from_fixtures(hass, music_assistant_client) + entity_id = "media_player.test_player_1" + mass_player_id = "00:00:00:00:00:01" + state = hass.states.get(entity_id) + assert state + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, + SERVICE_UNJOIN, + { + ATTR_ENTITY_ID: entity_id, + }, + blocking=True, + ) + assert music_assistant_client.send_command.call_count == 1 + assert music_assistant_client.send_command.call_args == call( + "players/cmd/ungroup", player_id=mass_player_id + ) + + async def test_media_player_clear_playlist_action( hass: HomeAssistant, music_assistant_client: MagicMock, From e9b34eaad09b9b05306cf635c81a19cddd697b13 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 29 Nov 2024 17:29:10 +0000 Subject: [PATCH 039/103] Bump aiohomekit to 3.2.7 (#131924) --- 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 cddd61a12c1..b7c82b9fd51 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.2.6"], + "requirements": ["aiohomekit==3.2.7"], "zeroconf": ["_hap._tcp.local.", "_hap._udp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 84c41569da5..e0330ce0cd1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -265,7 +265,7 @@ aioharmony==0.2.10 aiohasupervisor==0.2.1 # homeassistant.components.homekit_controller -aiohomekit==3.2.6 +aiohomekit==3.2.7 # homeassistant.components.hue aiohue==4.7.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 342b8592486..87b4f9b6ccd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -250,7 +250,7 @@ aioharmony==0.2.10 aiohasupervisor==0.2.1 # homeassistant.components.homekit_controller -aiohomekit==3.2.6 +aiohomekit==3.2.7 # homeassistant.components.hue aiohue==4.7.3 From bb847b346d8dad6dd65a5d33de6fdb5409ef3cb4 Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Fri, 29 Nov 2024 19:47:33 +0100 Subject: [PATCH 040/103] Bump uiprotect to 6.6.4 (#131931) --- 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 9a76ba6f984..9730c1e3741 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==6.6.3", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==6.6.4", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index e0330ce0cd1..87c4bc43c6a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2897,7 +2897,7 @@ typedmonarchmoney==0.3.1 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==6.6.3 +uiprotect==6.6.4 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 87b4f9b6ccd..169c57a587c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2310,7 +2310,7 @@ typedmonarchmoney==0.3.1 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==6.6.3 +uiprotect==6.6.4 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 787a1613ecbd80b875f9608375128ba72a25f388 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 30 Nov 2024 01:13:52 +0100 Subject: [PATCH 041/103] Fix KNX IP Secure tunnelling endpoint selection with keyfile (#131941) --- homeassistant/components/knx/__init__.py | 3 +++ homeassistant/components/knx/const.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 9180e287618..ea654c358e7 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -54,6 +54,7 @@ from .const import ( CONF_KNX_SECURE_USER_PASSWORD, CONF_KNX_STATE_UPDATER, CONF_KNX_TELEGRAM_LOG_SIZE, + CONF_KNX_TUNNEL_ENDPOINT_IA, CONF_KNX_TUNNELING, CONF_KNX_TUNNELING_TCP, CONF_KNX_TUNNELING_TCP_SECURE, @@ -352,6 +353,7 @@ class KNXModule: if _conn_type == CONF_KNX_TUNNELING_TCP: return ConnectionConfig( connection_type=ConnectionType.TUNNELING_TCP, + individual_address=self.entry.data.get(CONF_KNX_TUNNEL_ENDPOINT_IA), gateway_ip=self.entry.data[CONF_HOST], gateway_port=self.entry.data[CONF_PORT], auto_reconnect=True, @@ -364,6 +366,7 @@ class KNXModule: if _conn_type == CONF_KNX_TUNNELING_TCP_SECURE: return ConnectionConfig( connection_type=ConnectionType.TUNNELING_TCP_SECURE, + individual_address=self.entry.data.get(CONF_KNX_TUNNEL_ENDPOINT_IA), gateway_ip=self.entry.data[CONF_HOST], gateway_port=self.entry.data[CONF_PORT], secure_config=SecureConfig( diff --git a/homeassistant/components/knx/const.py b/homeassistant/components/knx/const.py index 7a9dfc34546..a946ded0359 100644 --- a/homeassistant/components/knx/const.py +++ b/homeassistant/components/knx/const.py @@ -104,7 +104,7 @@ class KNXConfigEntryData(TypedDict, total=False): route_back: bool # not required host: str # only required for tunnelling port: int # only required for tunnelling - tunnel_endpoint_ia: str | None + tunnel_endpoint_ia: str | None # tunnelling only - not required (use get()) # KNX secure user_id: int | None # not required user_password: str | None # not required From e48be5c406c5b3fcc7e2a7253c577823da09e45d Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Sat, 30 Nov 2024 11:15:19 +0000 Subject: [PATCH 042/103] Bump aiomealie to 0.9.4 (#131951) --- homeassistant/components/mealie/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../mealie/snapshots/test_diagnostics.ambr | 66 +++++++++---------- .../mealie/snapshots/test_services.ambr | 36 +++++----- 5 files changed, 54 insertions(+), 54 deletions(-) diff --git a/homeassistant/components/mealie/manifest.json b/homeassistant/components/mealie/manifest.json index f594f1398e3..c555fcbc3d6 100644 --- a/homeassistant/components/mealie/manifest.json +++ b/homeassistant/components/mealie/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/mealie", "integration_type": "service", "iot_class": "local_polling", - "requirements": ["aiomealie==0.9.3"] + "requirements": ["aiomealie==0.9.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 87c4bc43c6a..cba073a3150 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -298,7 +298,7 @@ aiolookin==1.0.0 aiolyric==2.0.1 # homeassistant.components.mealie -aiomealie==0.9.3 +aiomealie==0.9.4 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 169c57a587c..856d4cc5c46 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -280,7 +280,7 @@ aiolookin==1.0.0 aiolyric==2.0.1 # homeassistant.components.mealie -aiomealie==0.9.3 +aiomealie==0.9.4 # homeassistant.components.modern_forms aiomodernforms==0.1.8 diff --git a/tests/components/mealie/snapshots/test_diagnostics.ambr b/tests/components/mealie/snapshots/test_diagnostics.ambr index ecb5d1d6cd1..a694c72fcf6 100644 --- a/tests/components/mealie/snapshots/test_diagnostics.ambr +++ b/tests/components/mealie/snapshots/test_diagnostics.ambr @@ -15,7 +15,7 @@ '__type': "", 'isoformat': '2024-01-23', }), - 'mealplan_id': '229', + 'mealplan_id': 229, 'recipe': dict({ 'description': 'The BEST Roast Chicken recipe is simple, budget friendly, and gives you a tender, mouth-watering chicken full of flavor! Served with roasted vegetables, this recipe is simple enough for any cook!', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -42,7 +42,7 @@ '__type': "", 'isoformat': '2024-01-22', }), - 'mealplan_id': '230', + 'mealplan_id': 230, 'recipe': dict({ 'description': "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -67,7 +67,7 @@ '__type': "", 'isoformat': '2024-01-23', }), - 'mealplan_id': '222', + 'mealplan_id': 222, 'recipe': dict({ 'description': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο από τον Άκη Πετρετζίκη. Φτιάξτε την πιο εύκολη μακαρονάδα με κεφτεδάκια σε μόνο ένα σκεύος.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -92,7 +92,7 @@ '__type': "", 'isoformat': '2024-01-23', }), - 'mealplan_id': '221', + 'mealplan_id': 221, 'recipe': dict({ 'description': 'Delicious Greek turkey meatballs with lemon orzo, tender veggies, and a creamy feta yogurt sauce. These healthy baked Greek turkey meatballs are filled with tons of wonderful herbs and make the perfect protein-packed weeknight meal!', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -117,7 +117,7 @@ '__type': "", 'isoformat': '2024-01-23', }), - 'mealplan_id': '219', + 'mealplan_id': 219, 'recipe': dict({ 'description': 'This is a modified Pampered Chef recipe. You can use a trifle bowl or large glass punch/salad bowl to show it off. It is really easy to make and I never have any leftovers. Cook time includes chill time.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -142,7 +142,7 @@ '__type': "", 'isoformat': '2024-01-22', }), - 'mealplan_id': '217', + 'mealplan_id': 217, 'recipe': dict({ 'description': 'Cheeseburger Sliders are juicy, cheesy and beefy - everything we love about classic burgers! These sliders are quick and easy plus they are make-ahead and reheat really well.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -167,7 +167,7 @@ '__type': "", 'isoformat': '2024-01-23', }), - 'mealplan_id': '212', + 'mealplan_id': 212, 'recipe': dict({ 'description': 'This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -192,7 +192,7 @@ '__type': "", 'isoformat': '2024-01-22', }), - 'mealplan_id': '211', + 'mealplan_id': 211, 'recipe': dict({ 'description': 'Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -217,7 +217,7 @@ '__type': "", 'isoformat': '2024-01-23', }), - 'mealplan_id': '196', + 'mealplan_id': 196, 'recipe': dict({ 'description': 'Simple to prepare and ready in 25 minutes, this vegetarian miso noodle recipe can be eaten on its own or served as a side.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -242,7 +242,7 @@ '__type': "", 'isoformat': '2024-01-22', }), - 'mealplan_id': '195', + 'mealplan_id': 195, 'recipe': dict({ 'description': 'Avis aux nostalgiques des années 1980, la mousse de saumon est de retour dans une présentation adaptée au goût du jour. On utilise une technique sans faille : un saumon frais cuit au micro-ondes et mélangé au robot avec du fromage à la crème et de la crème sure. On obtient ainsi une texture onctueuse à tartiner, qui n’a rien à envier aux préparations gélatineuses d’antan !', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -267,7 +267,7 @@ '__type': "", 'isoformat': '2024-01-21', }), - 'mealplan_id': '1', + 'mealplan_id': 1, 'recipe': None, 'title': 'Aquavite', 'user_id': '6caa6e4d-521f-4ef4-9ed7-388bdd63f47d', @@ -283,7 +283,7 @@ '__type': "", 'isoformat': '2024-01-23', }), - 'mealplan_id': '226', + 'mealplan_id': 226, 'recipe': dict({ 'description': 'Te explicamos paso a paso, de manera sencilla, la elaboración de la receta de pollo al curry con leche de coco en 10 minutos. Ingredientes, tiempo de...', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -308,7 +308,7 @@ '__type': "", 'isoformat': '2024-01-23', }), - 'mealplan_id': '224', + 'mealplan_id': 224, 'recipe': dict({ 'description': 'bourguignon, oignon, carotte, bouquet garni, vin rouge, beurre, sel, poivre', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -333,7 +333,7 @@ '__type': "", 'isoformat': '2024-01-22', }), - 'mealplan_id': '216', + 'mealplan_id': 216, 'recipe': dict({ 'description': 'This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -360,7 +360,7 @@ '__type': "", 'isoformat': '2024-01-23', }), - 'mealplan_id': '220', + 'mealplan_id': 220, 'recipe': dict({ 'description': 'Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -385,15 +385,15 @@ 'checked': False, 'disable_amount': True, 'display': '2 Apples', - 'food_id': 'None', + 'food_id': None, 'is_food': False, 'item_id': 'f45430f7-3edf-45a9-a50f-73bb375090be', - 'label_id': 'None', + 'label_id': None, 'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e', 'note': 'Apples', 'position': 0, 'quantity': 2.0, - 'unit_id': 'None', + 'unit_id': None, }), dict({ 'checked': False, @@ -402,7 +402,7 @@ 'food_id': '09322430-d24c-4b1a-abb6-22b6ed3a88f5', 'is_food': True, 'item_id': '84d8fd74-8eb0-402e-84b6-71f251bfb7cc', - 'label_id': 'None', + 'label_id': None, 'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e', 'note': '', 'position': 1, @@ -416,12 +416,12 @@ 'food_id': '96801494-4e26-4148-849a-8155deb76327', 'is_food': True, 'item_id': '69913b9a-7c75-4935-abec-297cf7483f88', - 'label_id': 'None', + 'label_id': None, 'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e', 'note': '', 'position': 2, 'quantity': 0.0, - 'unit_id': 'None', + 'unit_id': None, }), ]), 'shopping_list': dict({ @@ -435,15 +435,15 @@ 'checked': False, 'disable_amount': True, 'display': '2 Apples', - 'food_id': 'None', + 'food_id': None, 'is_food': False, 'item_id': 'f45430f7-3edf-45a9-a50f-73bb375090be', - 'label_id': 'None', + 'label_id': None, 'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e', 'note': 'Apples', 'position': 0, 'quantity': 2.0, - 'unit_id': 'None', + 'unit_id': None, }), dict({ 'checked': False, @@ -452,7 +452,7 @@ 'food_id': '09322430-d24c-4b1a-abb6-22b6ed3a88f5', 'is_food': True, 'item_id': '84d8fd74-8eb0-402e-84b6-71f251bfb7cc', - 'label_id': 'None', + 'label_id': None, 'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e', 'note': '', 'position': 1, @@ -466,12 +466,12 @@ 'food_id': '96801494-4e26-4148-849a-8155deb76327', 'is_food': True, 'item_id': '69913b9a-7c75-4935-abec-297cf7483f88', - 'label_id': 'None', + 'label_id': None, 'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e', 'note': '', 'position': 2, 'quantity': 0.0, - 'unit_id': 'None', + 'unit_id': None, }), ]), 'shopping_list': dict({ @@ -485,15 +485,15 @@ 'checked': False, 'disable_amount': True, 'display': '2 Apples', - 'food_id': 'None', + 'food_id': None, 'is_food': False, 'item_id': 'f45430f7-3edf-45a9-a50f-73bb375090be', - 'label_id': 'None', + 'label_id': None, 'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e', 'note': 'Apples', 'position': 0, 'quantity': 2.0, - 'unit_id': 'None', + 'unit_id': None, }), dict({ 'checked': False, @@ -502,7 +502,7 @@ 'food_id': '09322430-d24c-4b1a-abb6-22b6ed3a88f5', 'is_food': True, 'item_id': '84d8fd74-8eb0-402e-84b6-71f251bfb7cc', - 'label_id': 'None', + 'label_id': None, 'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e', 'note': '', 'position': 1, @@ -516,12 +516,12 @@ 'food_id': '96801494-4e26-4148-849a-8155deb76327', 'is_food': True, 'item_id': '69913b9a-7c75-4935-abec-297cf7483f88', - 'label_id': 'None', + 'label_id': None, 'list_id': '9ce096fe-ded2-4077-877d-78ba450ab13e', 'note': '', 'position': 2, 'quantity': 0.0, - 'unit_id': 'None', + 'unit_id': None, }), ]), 'shopping_list': dict({ diff --git a/tests/components/mealie/snapshots/test_services.ambr b/tests/components/mealie/snapshots/test_services.ambr index 93b5f2cad1d..4f9ee6a5c09 100644 --- a/tests/components/mealie/snapshots/test_services.ambr +++ b/tests/components/mealie/snapshots/test_services.ambr @@ -199,7 +199,7 @@ 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 22), - 'mealplan_id': '230', + 'mealplan_id': 230, 'recipe': dict({ 'description': "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -221,7 +221,7 @@ 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 23), - 'mealplan_id': '229', + 'mealplan_id': 229, 'recipe': dict({ 'description': 'The BEST Roast Chicken recipe is simple, budget friendly, and gives you a tender, mouth-watering chicken full of flavor! Served with roasted vegetables, this recipe is simple enough for any cook!', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -243,7 +243,7 @@ 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 23), - 'mealplan_id': '226', + 'mealplan_id': 226, 'recipe': dict({ 'description': 'Te explicamos paso a paso, de manera sencilla, la elaboración de la receta de pollo al curry con leche de coco en 10 minutos. Ingredientes, tiempo de...', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -265,7 +265,7 @@ 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 23), - 'mealplan_id': '224', + 'mealplan_id': 224, 'recipe': dict({ 'description': 'bourguignon, oignon, carotte, bouquet garni, vin rouge, beurre, sel, poivre', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -287,7 +287,7 @@ 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 23), - 'mealplan_id': '222', + 'mealplan_id': 222, 'recipe': dict({ 'description': 'Εύκολη μακαρονάδα με κεφτεδάκια στον φούρνο από τον Άκη Πετρετζίκη. Φτιάξτε την πιο εύκολη μακαρονάδα με κεφτεδάκια σε μόνο ένα σκεύος.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -309,7 +309,7 @@ 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 23), - 'mealplan_id': '221', + 'mealplan_id': 221, 'recipe': dict({ 'description': 'Delicious Greek turkey meatballs with lemon orzo, tender veggies, and a creamy feta yogurt sauce. These healthy baked Greek turkey meatballs are filled with tons of wonderful herbs and make the perfect protein-packed weeknight meal!', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -331,7 +331,7 @@ 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 23), - 'mealplan_id': '220', + 'mealplan_id': 220, 'recipe': dict({ 'description': 'Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -353,7 +353,7 @@ 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 23), - 'mealplan_id': '219', + 'mealplan_id': 219, 'recipe': dict({ 'description': 'This is a modified Pampered Chef recipe. You can use a trifle bowl or large glass punch/salad bowl to show it off. It is really easy to make and I never have any leftovers. Cook time includes chill time.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -375,7 +375,7 @@ 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 22), - 'mealplan_id': '217', + 'mealplan_id': 217, 'recipe': dict({ 'description': 'Cheeseburger Sliders are juicy, cheesy and beefy - everything we love about classic burgers! These sliders are quick and easy plus they are make-ahead and reheat really well.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -397,7 +397,7 @@ 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 22), - 'mealplan_id': '216', + 'mealplan_id': 216, 'recipe': dict({ 'description': 'This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -419,7 +419,7 @@ 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 23), - 'mealplan_id': '212', + 'mealplan_id': 212, 'recipe': dict({ 'description': 'This All-American beef stew recipe includes tender beef coated in a rich, intense sauce and vegetables that bring complementary texture and flavor.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -441,7 +441,7 @@ 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 22), - 'mealplan_id': '211', + 'mealplan_id': 211, 'recipe': dict({ 'description': 'Einfacher Nudelauflauf mit Brokkoli, Sahnesauce und extra Käse. Dieses vegetarische 5 Zutaten Rezept ist super schnell gemacht und SO gut!', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -463,7 +463,7 @@ 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 23), - 'mealplan_id': '196', + 'mealplan_id': 196, 'recipe': dict({ 'description': 'Simple to prepare and ready in 25 minutes, this vegetarian miso noodle recipe can be eaten on its own or served as a side.', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -485,7 +485,7 @@ 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 22), - 'mealplan_id': '195', + 'mealplan_id': 195, 'recipe': dict({ 'description': 'Avis aux nostalgiques des années 1980, la mousse de saumon est de retour dans une présentation adaptée au goût du jour. On utilise une technique sans faille : un saumon frais cuit au micro-ondes et mélangé au robot avec du fromage à la crème et de la crème sure. On obtient ainsi une texture onctueuse à tartiner, qui n’a rien à envier aux préparations gélatineuses d’antan !', 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -507,7 +507,7 @@ 'group_id': '3931df86-0679-4579-8c63-4bedc9ca9a85', 'household_id': None, 'mealplan_date': FakeDate(2024, 1, 21), - 'mealplan_id': '1', + 'mealplan_id': 1, 'recipe': None, 'title': 'Aquavite', 'user_id': '6caa6e4d-521f-4ef4-9ed7-388bdd63f47d', @@ -714,7 +714,7 @@ 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', 'household_id': None, 'mealplan_date': datetime.date(2024, 1, 22), - 'mealplan_id': '230', + 'mealplan_id': 230, 'recipe': dict({ 'description': "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -740,7 +740,7 @@ 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', 'household_id': None, 'mealplan_date': datetime.date(2024, 1, 22), - 'mealplan_id': '230', + 'mealplan_id': 230, 'recipe': dict({ 'description': "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', @@ -766,7 +766,7 @@ 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', 'household_id': None, 'mealplan_date': datetime.date(2024, 1, 22), - 'mealplan_id': '230', + 'mealplan_id': 230, 'recipe': dict({ 'description': "Een traybake is eigenlijk altijd een goed idee. Deze zoete aardappel curry traybake dus ook. Waarom? Omdat je alleen maar wat groenten - en in dit geval kip - op een bakplaat (traybake dus) legt, hier wat kruiden aan toevoegt en deze in de oven schuift. Ideaal dus als je geen zin hebt om lang in de keuken te staan. Maar gewoon lekker op de bank wil ploffen om te wachten tot de oven klaar is. Joe! That\\'s what we like. Deze zoete aardappel curry traybake bevat behalve zoete aardappel en curry ook kikkererwten, kippendijfilet en bloemkoolroosjes. Je gebruikt yoghurt en limoen als een soort dressing. En je serveert deze heerlijke traybake met naanbrood. Je kunt natuurljk ook voor deze traybake met chipolataworstjes gaan. Wil je graag meer ovengerechten? Dan moet je eigenlijk even kijken naar onze Ovenbijbel. Onmisbaar in je keuken! We willen je deze zoete aardappelstamppot met prei ook niet onthouden. Megalekker bordje comfortfood als je \\'t ons vraagt.", 'group_id': '0bf60b2e-ca89-42a9-94d4-8f67ca72b157', From 0d155c416a661c1a84757fba157eb27c53187bbd Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Sat, 30 Nov 2024 17:32:53 +0100 Subject: [PATCH 043/103] Bump reolink_aio to 0.11.4 (#131957) --- homeassistant/components/reolink/host.py | 2 ++ homeassistant/components/reolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index d2b2bba6276..a8e1de07642 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -536,6 +536,8 @@ class ReolinkHost: async def renew(self) -> None: """Renew the subscription of motion events (lease time is 15 minutes).""" + await self._api.baichuan.check_subscribe_events() + if self._api.baichuan.events_active and self._api.subscribed(SubType.push): # TCP push active, unsubscribe from ONVIF push because not needed self.unregister_webhook() diff --git a/homeassistant/components/reolink/manifest.json b/homeassistant/components/reolink/manifest.json index 4846ec8cb94..913864a92fa 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.11.3"] + "requirements": ["reolink-aio==0.11.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index cba073a3150..5427fd61718 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2556,7 +2556,7 @@ renault-api==0.2.7 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.11.3 +reolink-aio==0.11.4 # homeassistant.components.idteck_prox rfk101py==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 856d4cc5c46..abb6578d832 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2047,7 +2047,7 @@ renault-api==0.2.7 renson-endura-delta==1.7.1 # homeassistant.components.reolink -reolink-aio==0.11.3 +reolink-aio==0.11.4 # homeassistant.components.rflink rflink==0.0.66 From e8ef990e72325d3cf36451e6c572154fa07ce153 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Nov 2024 14:47:40 -0600 Subject: [PATCH 044/103] Strip trailing spaces from HomeKit names (#131971) --- homeassistant/components/homekit/util.py | 2 +- tests/components/homekit/test_accessories.py | 4 ++-- tests/components/homekit/test_util.py | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index ae7e35030be..8fc2a039304 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -114,7 +114,7 @@ _LOGGER = logging.getLogger(__name__) NUMBERS_ONLY_RE = re.compile(r"[^\d.]+") VERSION_RE = re.compile(r"([0-9]+)(\.[0-9]+)?(\.[0-9]+)?") -INVALID_END_CHARS = "-_" +INVALID_END_CHARS = "-_ " MAX_VERSION_PART = 2**32 - 1 diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index c37cac84b8a..00cf42bb916 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -121,7 +121,7 @@ async def test_home_accessory(hass: HomeAssistant, hk_driver) -> None: serv = acc3.services[0] # SERV_ACCESSORY_INFO assert ( serv.get_characteristic(CHAR_NAME).value - == "Home Accessory that exceeds the maximum maximum maximum maximum " + == "Home Accessory that exceeds the maximum maximum maximum maximum" ) assert ( serv.get_characteristic(CHAR_MANUFACTURER).value @@ -154,7 +154,7 @@ async def test_home_accessory(hass: HomeAssistant, hk_driver) -> None: serv = acc4.services[0] # SERV_ACCESSORY_INFO assert ( serv.get_characteristic(CHAR_NAME).value - == "Home Accessory that exceeds the maximum maximum maximum maximum " + == "Home Accessory that exceeds the maximum maximum maximum maximum" ) assert ( serv.get_characteristic(CHAR_MANUFACTURER).value diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 7f7e3ee0ce0..20e536baf81 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -256,6 +256,7 @@ def test_cleanup_name_for_homekit() -> None: """Ensure name sanitize works as expected.""" assert cleanup_name_for_homekit("abc") == "abc" + assert cleanup_name_for_homekit("abc ") == "abc" assert cleanup_name_for_homekit("a b c") == "a b c" assert cleanup_name_for_homekit("ab_c") == "ab c" assert ( From 673bdcc556bc6a39c9f5b7c54d82b0d02c38cdeb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Nov 2024 16:09:37 -0600 Subject: [PATCH 045/103] Reduce precision loss when converting HomeKit temperature (#131973) --- homeassistant/components/homekit/util.py | 12 ++---------- tests/components/homekit/test_type_thermostats.py | 10 +++++----- tests/components/homekit/test_util.py | 8 +++++--- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 8fc2a039304..8395c1a8c9a 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -424,20 +424,12 @@ def cleanup_name_for_homekit(name: str | None) -> str: def temperature_to_homekit(temperature: float, unit: str) -> float: """Convert temperature to Celsius for HomeKit.""" - return round( - TemperatureConverter.convert(temperature, unit, UnitOfTemperature.CELSIUS), 1 - ) + return TemperatureConverter.convert(temperature, unit, UnitOfTemperature.CELSIUS) def temperature_to_states(temperature: float, unit: str) -> float: """Convert temperature back from Celsius to Home Assistant unit.""" - return ( - round( - TemperatureConverter.convert(temperature, UnitOfTemperature.CELSIUS, unit) - * 2 - ) - / 2 - ) + return TemperatureConverter.convert(temperature, UnitOfTemperature.CELSIUS, unit) def density_to_air_quality(density: float) -> int: diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 8454610566b..e99db8f6234 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -921,8 +921,8 @@ async def test_thermostat_fahrenheit( await hass.async_block_till_done() assert call_set_temperature[0] assert call_set_temperature[0].data[ATTR_ENTITY_ID] == entity_id - assert call_set_temperature[0].data[ATTR_TARGET_TEMP_HIGH] == 73.5 - assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 68 + assert call_set_temperature[0].data[ATTR_TARGET_TEMP_HIGH] == 73.4 + assert call_set_temperature[0].data[ATTR_TARGET_TEMP_LOW] == 68.18 assert len(events) == 1 assert events[-1].data[ATTR_VALUE] == "CoolingThresholdTemperature to 23°C" @@ -942,8 +942,8 @@ async def test_thermostat_fahrenheit( await hass.async_block_till_done() assert call_set_temperature[1] assert call_set_temperature[1].data[ATTR_ENTITY_ID] == entity_id - assert call_set_temperature[1].data[ATTR_TARGET_TEMP_HIGH] == 73.5 - assert call_set_temperature[1].data[ATTR_TARGET_TEMP_LOW] == 71.5 + assert call_set_temperature[1].data[ATTR_TARGET_TEMP_HIGH] == 73.4 + assert call_set_temperature[1].data[ATTR_TARGET_TEMP_LOW] == 71.6 assert len(events) == 2 assert events[-1].data[ATTR_VALUE] == "HeatingThresholdTemperature to 22°C" @@ -962,7 +962,7 @@ async def test_thermostat_fahrenheit( await hass.async_block_till_done() assert call_set_temperature[2] assert call_set_temperature[2].data[ATTR_ENTITY_ID] == entity_id - assert call_set_temperature[2].data[ATTR_TEMPERATURE] == 75.0 + assert call_set_temperature[2].data[ATTR_TEMPERATURE] == 75.2 assert len(events) == 3 assert events[-1].data[ATTR_VALUE] == "TargetTemperature to 24.0°C" diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 20e536baf81..30efd7fcc5c 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -268,14 +268,16 @@ def test_cleanup_name_for_homekit() -> None: def test_temperature_to_homekit() -> None: """Test temperature conversion from HA to HomeKit.""" - assert temperature_to_homekit(20.46, UnitOfTemperature.CELSIUS) == 20.5 - assert temperature_to_homekit(92.1, UnitOfTemperature.FAHRENHEIT) == 33.4 + assert temperature_to_homekit(20.46, UnitOfTemperature.CELSIUS) == 20.46 + assert temperature_to_homekit(92.1, UnitOfTemperature.FAHRENHEIT) == pytest.approx( + 33.388888888888886 + ) def test_temperature_to_states() -> None: """Test temperature conversion from HomeKit to HA.""" assert temperature_to_states(20, UnitOfTemperature.CELSIUS) == 20.0 - assert temperature_to_states(20.2, UnitOfTemperature.FAHRENHEIT) == 68.5 + assert temperature_to_states(20.2, UnitOfTemperature.FAHRENHEIT) == 68.36 def test_density_to_air_quality() -> None: From d7428786cdcac337e1e5f383e69832b1e7bdab44 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 1 Dec 2024 03:14:16 +0000 Subject: [PATCH 046/103] Bump version to 2024.12.0b3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1e49ea64c07..b91d0cd53be 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 10e169f7116..469abb4aca0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.12.0b2" +version = "2024.12.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From e2073d7762f3e639b5fa5ec6533d1a6327c41f43 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sun, 1 Dec 2024 22:01:19 +0100 Subject: [PATCH 047/103] Bugfix for Plugwise, small code optimization (#131990) --- homeassistant/components/plugwise/climate.py | 51 +++++++++++--------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/plugwise/climate.py b/homeassistant/components/plugwise/climate.py index f1f54aa6647..242b0944782 100644 --- a/homeassistant/components/plugwise/climate.py +++ b/homeassistant/components/plugwise/climate.py @@ -78,19 +78,18 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): self._attr_extra_state_attributes = {} self._attr_unique_id = f"{device_id}-climate" + self._devices = coordinator.data.devices + self._gateway = coordinator.data.gateway + gateway_id: str = self._gateway["gateway_id"] + self._gateway_data = self._devices[gateway_id] + self._location = device_id if (location := self.device.get("location")) is not None: self._location = location - self.cdr_gateway = coordinator.data.gateway - gateway_id: str = coordinator.data.gateway["gateway_id"] - self.gateway_data = coordinator.data.devices[gateway_id] # Determine supported features self._attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE - if ( - self.cdr_gateway["cooling_present"] - and self.cdr_gateway["smile_name"] != "Adam" - ): + if self._gateway["cooling_present"] and self._gateway["smile_name"] != "Adam": self._attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE_RANGE ) @@ -116,10 +115,10 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): """ # When no cooling available, _previous_mode is always heating if ( - "regulation_modes" in self.gateway_data - and "cooling" in self.gateway_data["regulation_modes"] + "regulation_modes" in self._gateway_data + and "cooling" in self._gateway_data["regulation_modes"] ): - mode = self.gateway_data["select_regulation_mode"] + mode = self._gateway_data["select_regulation_mode"] if mode in ("cooling", "heating"): self._previous_mode = mode @@ -166,17 +165,17 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): def hvac_modes(self) -> list[HVACMode]: """Return a list of available HVACModes.""" hvac_modes: list[HVACMode] = [] - if "regulation_modes" in self.gateway_data: + if "regulation_modes" in self._gateway_data: hvac_modes.append(HVACMode.OFF) if "available_schedules" in self.device: hvac_modes.append(HVACMode.AUTO) - if self.cdr_gateway["cooling_present"]: - if "regulation_modes" in self.gateway_data: - if self.gateway_data["select_regulation_mode"] == "cooling": + if self._gateway["cooling_present"]: + if "regulation_modes" in self._gateway_data: + if self._gateway_data["select_regulation_mode"] == "cooling": hvac_modes.append(HVACMode.COOL) - if self.gateway_data["select_regulation_mode"] == "heating": + if self._gateway_data["select_regulation_mode"] == "heating": hvac_modes.append(HVACMode.HEAT) else: hvac_modes.append(HVACMode.HEAT_COOL) @@ -192,17 +191,21 @@ class PlugwiseClimateEntity(PlugwiseEntity, ClimateEntity): self._previous_action_mode(self.coordinator) # Adam provides the hvac_action for each thermostat - if (control_state := self.device.get("control_state")) == "cooling": - return HVACAction.COOLING - if control_state == "heating": - return HVACAction.HEATING - if control_state == "preheating": - return HVACAction.PREHEATING - if control_state == "off": + if self._gateway["smile_name"] == "Adam": + if (control_state := self.device.get("control_state")) == "cooling": + return HVACAction.COOLING + if control_state == "heating": + return HVACAction.HEATING + if control_state == "preheating": + return HVACAction.PREHEATING + if control_state == "off": + return HVACAction.IDLE + return HVACAction.IDLE - heater: str = self.coordinator.data.gateway["heater_id"] - heater_data = self.coordinator.data.devices[heater] + # Anna + heater: str = self._gateway["heater_id"] + heater_data = self._devices[heater] if heater_data["binary_sensors"]["heating_state"]: return HVACAction.HEATING if heater_data["binary_sensors"].get("cooling_state", False): From b6dec11487b011c1060c37a8c6a63162f627d2ab Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 1 Dec 2024 16:17:55 +0100 Subject: [PATCH 048/103] Freeze integration setup timeout for recorder during non-live data migration (#131998) --- homeassistant/components/recorder/core.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 8c2e1c9e006..0c61f8a955e 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -740,7 +740,7 @@ class Recorder(threading.Thread): self.schema_version = schema_status.current_version # Do non-live data migration - migration.migrate_data_non_live(self, self.get_session, schema_status) + self._migrate_data_offline(schema_status) # Non-live migration is now completed, remaining steps are live self.migration_is_live = True @@ -916,6 +916,13 @@ class Recorder(threading.Thread): return False + def _migrate_data_offline( + self, schema_status: migration.SchemaValidationStatus + ) -> None: + """Migrate data.""" + with self.hass.timeout.freeze(DOMAIN): + migration.migrate_data_non_live(self, self.get_session, schema_status) + def _migrate_schema_offline( self, schema_status: migration.SchemaValidationStatus ) -> tuple[bool, migration.SchemaValidationStatus]: From 79c919f62d18acdb0a55f528e07e91bf7053d732 Mon Sep 17 00:00:00 2001 From: Richard Kroegel <42204099+rikroe@users.noreply.github.com> Date: Sun, 1 Dec 2024 21:45:31 +0100 Subject: [PATCH 049/103] Bump bimmer_connected to 0.17.2 (#132005) --- homeassistant/components/bmw_connected_drive/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bmw_connected_drive/manifest.json b/homeassistant/components/bmw_connected_drive/manifest.json index d1ca735ce55..81928a59a52 100644 --- a/homeassistant/components/bmw_connected_drive/manifest.json +++ b/homeassistant/components/bmw_connected_drive/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/bmw_connected_drive", "iot_class": "cloud_polling", "loggers": ["bimmer_connected"], - "requirements": ["bimmer-connected[china]==0.17.0"] + "requirements": ["bimmer-connected[china]==0.17.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5427fd61718..c72ecc77647 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -582,7 +582,7 @@ beautifulsoup4==4.12.3 # beewi-smartclim==0.0.10 # homeassistant.components.bmw_connected_drive -bimmer-connected[china]==0.17.0 +bimmer-connected[china]==0.17.2 # homeassistant.components.bizkaibus bizkaibus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index abb6578d832..079f82089ae 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -516,7 +516,7 @@ base36==0.1.1 beautifulsoup4==4.12.3 # homeassistant.components.bmw_connected_drive -bimmer-connected[china]==0.17.0 +bimmer-connected[china]==0.17.2 # homeassistant.components.eq3btsmart # homeassistant.components.esphome From 4e0cdb0537496a557dbc11b475fd8236c4acb111 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Dec 2024 14:37:03 -0600 Subject: [PATCH 050/103] Bump propcache to 0.2.1 (#132022) --- .github/workflows/wheels.yml | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index e0a850fa340..749f95fa922 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -143,7 +143,7 @@ jobs: wheels-key: ${{ secrets.WHEELS_KEY }} env-file: true apk: "libffi-dev;openssl-dev;yaml-dev;nasm;zlib-dev" - skip-binary: aiohttp;multidict;yarl;SQLAlchemy + skip-binary: aiohttp;multidict;propcache;yarl;SQLAlchemy constraints: "homeassistant/package_constraints.txt" requirements-diff: "requirements_diff.txt" requirements: "requirements.txt" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cb7aa1219ab..5c0db0659d6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -45,7 +45,7 @@ orjson==3.10.12 packaging>=23.1 paho-mqtt==1.6.1 Pillow==11.0.0 -propcache==0.2.0 +propcache==0.2.1 psutil-home-assistant==0.0.1 PyJWT==2.10.0 pymicro-vad==1.0.1 diff --git a/pyproject.toml b/pyproject.toml index 469abb4aca0..aee114d01bf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ dependencies = [ # PyJWT has loose dependency. We want the latest one. "cryptography==43.0.1", "Pillow==11.0.0", - "propcache==0.2.0", + "propcache==0.2.1", "pyOpenSSL==24.2.1", "orjson==3.10.12", "packaging>=23.1", diff --git a/requirements.txt b/requirements.txt index 1fa82f175bb..514ab132bc8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ lru-dict==1.3.0 PyJWT==2.10.0 cryptography==43.0.1 Pillow==11.0.0 -propcache==0.2.0 +propcache==0.2.1 pyOpenSSL==24.2.1 orjson==3.10.12 packaging>=23.1 From f2bafee84a3f64f8ad497d8c3015feedf907a40e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Dec 2024 20:17:36 -0600 Subject: [PATCH 051/103] Bump yarl to 1.18.3 (#132025) changelog: https://github.com/aio-libs/yarl/compare/v1.18.0...v1.18.3 --- 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 5c0db0659d6..a07536160a9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -70,7 +70,7 @@ voluptuous-openapi==0.0.5 voluptuous-serialize==2.6.0 voluptuous==0.15.2 webrtc-models==0.3.0 -yarl==1.18.0 +yarl==1.18.3 zeroconf==0.136.2 # Constrain pycryptodome to avoid vulnerability diff --git a/pyproject.toml b/pyproject.toml index aee114d01bf..5f72c2bf99b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,7 @@ dependencies = [ "voluptuous==0.15.2", "voluptuous-serialize==2.6.0", "voluptuous-openapi==0.0.5", - "yarl==1.18.0", + "yarl==1.18.3", "webrtc-models==0.3.0", ] diff --git a/requirements.txt b/requirements.txt index 514ab132bc8..40a372856b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -47,5 +47,5 @@ uv==0.5.4 voluptuous==0.15.2 voluptuous-serialize==2.6.0 voluptuous-openapi==0.0.5 -yarl==1.18.0 +yarl==1.18.3 webrtc-models==0.3.0 From 6b6fc6bbebca5944ca467de5a67df77b099488c0 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 1 Dec 2024 22:39:26 +0100 Subject: [PATCH 052/103] Bump yt-dlp to 2024.11.18 (#132026) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index ebfa79d7190..866215839bf 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp[default]==2024.11.04"], + "requirements": ["yt-dlp[default]==2024.11.18"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index c72ecc77647..79cb785b521 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3066,7 +3066,7 @@ youless-api==2.1.2 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp[default]==2024.11.04 +yt-dlp[default]==2024.11.18 # homeassistant.components.zamg zamg==0.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 079f82089ae..80f1da80096 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2452,7 +2452,7 @@ youless-api==2.1.2 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp[default]==2024.11.04 +yt-dlp[default]==2024.11.18 # homeassistant.components.zamg zamg==0.3.6 From e4d19541f5ffc0bbde35483d1e30c62e41bdb0bc Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Sun, 1 Dec 2024 23:05:34 +0100 Subject: [PATCH 053/103] Bump spotifyaio to 0.8.11 (#132032) --- homeassistant/components/spotify/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index 6c5b7382bbb..27b8da7cecf 100644 --- a/homeassistant/components/spotify/manifest.json +++ b/homeassistant/components/spotify/manifest.json @@ -8,6 +8,6 @@ "integration_type": "service", "iot_class": "cloud_polling", "loggers": ["spotifyaio"], - "requirements": ["spotifyaio==0.8.10"], + "requirements": ["spotifyaio==0.8.11"], "zeroconf": ["_spotify-connect._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 79cb785b521..7a6252c2102 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2719,7 +2719,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.3 # homeassistant.components.spotify -spotifyaio==0.8.10 +spotifyaio==0.8.11 # homeassistant.components.sql sqlparse==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 80f1da80096..3b98cc097aa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2171,7 +2171,7 @@ speak2mary==1.4.0 speedtest-cli==2.1.3 # homeassistant.components.spotify -spotifyaio==0.8.10 +spotifyaio==0.8.11 # homeassistant.components.sql sqlparse==0.5.0 From fab35f227d6bf308a70397bca2475cc8906dff4d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 2 Dec 2024 03:17:07 +0100 Subject: [PATCH 054/103] Handle not found playlists in Spotify (#132033) * Handle not found playlists * Handle not found playlists * Handle not found playlists * Handle not found playlists * Handle not found playlists * Update homeassistant/components/spotify/coordinator.py --------- Co-authored-by: Paulus Schoutsen --- .../components/spotify/coordinator.py | 25 ++++- tests/components/spotify/test_media_player.py | 93 +++++++++++++++++++ 2 files changed, 114 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/spotify/coordinator.py b/homeassistant/components/spotify/coordinator.py index a7c95e31245..099b1cb3ca8 100644 --- a/homeassistant/components/spotify/coordinator.py +++ b/homeassistant/components/spotify/coordinator.py @@ -11,6 +11,7 @@ from spotifyaio import ( Playlist, SpotifyClient, SpotifyConnectionError, + SpotifyNotFoundError, UserProfile, ) @@ -62,6 +63,7 @@ class SpotifyCoordinator(DataUpdateCoordinator[SpotifyCoordinatorData]): ) self.client = client self._playlist: Playlist | None = None + self._checked_playlist_id: str | None = None async def _async_setup(self) -> None: """Set up the coordinator.""" @@ -87,15 +89,29 @@ class SpotifyCoordinator(DataUpdateCoordinator[SpotifyCoordinatorData]): dj_playlist = False if (context := current.context) is not None: - if self._playlist is None or self._playlist.uri != context.uri: + dj_playlist = context.uri == SPOTIFY_DJ_PLAYLIST_URI + if not ( + context.uri + in ( + self._checked_playlist_id, + SPOTIFY_DJ_PLAYLIST_URI, + ) + or (self._playlist is None and context.uri == self._checked_playlist_id) + ): + self._checked_playlist_id = context.uri self._playlist = None - if context.uri == SPOTIFY_DJ_PLAYLIST_URI: - dj_playlist = True - elif context.context_type == ContextType.PLAYLIST: + if context.context_type == ContextType.PLAYLIST: # Make sure any playlist lookups don't break the current # playback state update try: self._playlist = await self.client.get_playlist(context.uri) + except SpotifyNotFoundError: + _LOGGER.debug( + "Spotify playlist '%s' not found. " + "Most likely a Spotify-created playlist", + context.uri, + ) + self._playlist = None except SpotifyConnectionError: _LOGGER.debug( "Unable to load spotify playlist '%s'. " @@ -103,6 +119,7 @@ class SpotifyCoordinator(DataUpdateCoordinator[SpotifyCoordinatorData]): context.uri, ) self._playlist = None + self._checked_playlist_id = None return SpotifyCoordinatorData( current_playback=current, position_updated_at=position_updated_at, diff --git a/tests/components/spotify/test_media_player.py b/tests/components/spotify/test_media_player.py index b03424f8459..55e0ea8f1d8 100644 --- a/tests/components/spotify/test_media_player.py +++ b/tests/components/spotify/test_media_player.py @@ -10,6 +10,7 @@ from spotifyaio import ( ProductType, RepeatMode as SpotifyRepeatMode, SpotifyConnectionError, + SpotifyNotFoundError, ) from syrupy import SnapshotAssertion @@ -142,6 +143,7 @@ async def test_spotify_dj_list( hass: HomeAssistant, mock_spotify: MagicMock, mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, ) -> None: """Test the Spotify entities with a Spotify DJ playlist.""" mock_spotify.return_value.get_playback.return_value.context.uri = ( @@ -152,12 +154,67 @@ async def test_spotify_dj_list( assert state assert state.attributes["media_playlist"] == "DJ" + mock_spotify.return_value.get_playlist.assert_not_called() + + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("media_player.spotify_spotify_1") + assert state + assert state.attributes["media_playlist"] == "DJ" + + mock_spotify.return_value.get_playlist.assert_not_called() + + +@pytest.mark.usefixtures("setup_credentials") +async def test_normal_playlist( + hass: HomeAssistant, + mock_spotify: MagicMock, + freezer: FrozenDateTimeFactory, + mock_config_entry: MockConfigEntry, +) -> None: + """Test normal playlist switching.""" + await setup_integration(hass, mock_config_entry) + state = hass.states.get("media_player.spotify_spotify_1") + assert state + assert state.attributes["media_playlist"] == "Spotify Web API Testing playlist" + + mock_spotify.return_value.get_playlist.assert_called_once_with( + "spotify:user:rushofficial:playlist:2r35vbe6hHl6yDSMfjKgmm" + ) + + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("media_player.spotify_spotify_1") + assert state + assert state.attributes["media_playlist"] == "Spotify Web API Testing playlist" + + mock_spotify.return_value.get_playlist.assert_called_once_with( + "spotify:user:rushofficial:playlist:2r35vbe6hHl6yDSMfjKgmm" + ) + + mock_spotify.return_value.get_playback.return_value.context.uri = ( + "spotify:playlist:123123123123123" + ) + + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + mock_spotify.return_value.get_playlist.assert_called_with( + "spotify:playlist:123123123123123" + ) + @pytest.mark.usefixtures("setup_credentials") async def test_fetching_playlist_does_not_fail( hass: HomeAssistant, mock_spotify: MagicMock, mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, ) -> None: """Test failing fetching playlist does not fail update.""" mock_spotify.return_value.get_playlist.side_effect = SpotifyConnectionError @@ -166,6 +223,42 @@ async def test_fetching_playlist_does_not_fail( assert state assert "media_playlist" not in state.attributes + mock_spotify.return_value.get_playlist.assert_called_once() + + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert mock_spotify.return_value.get_playlist.call_count == 2 + + +@pytest.mark.usefixtures("setup_credentials") +async def test_fetching_playlist_once( + hass: HomeAssistant, + mock_spotify: MagicMock, + mock_config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, +) -> None: + """Test that not being able to find a playlist doesn't retry.""" + mock_spotify.return_value.get_playlist.side_effect = SpotifyNotFoundError + await setup_integration(hass, mock_config_entry) + + state = hass.states.get("media_player.spotify_spotify_1") + assert state + assert "media_playlist" not in state.attributes + + mock_spotify.return_value.get_playlist.assert_called_once() + + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + state = hass.states.get("media_player.spotify_spotify_1") + assert state + assert "media_playlist" not in state.attributes + + mock_spotify.return_value.get_playlist.assert_called_once() + @pytest.mark.usefixtures("setup_credentials") async def test_idle( From 8ff8cd8b657f1e6d42c8f5885b1332b7399c71b2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 1 Dec 2024 20:05:45 -0600 Subject: [PATCH 055/103] Bump aiohttp to 3.11.9 (#132036) changelog: https://github.com/aio-libs/aiohttp/compare/v3.11.8...v3.11.9 --- 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 a07536160a9..4b6777417e9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -5,7 +5,7 @@ aiodiscover==2.1.0 aiodns==3.2.0 aiohasupervisor==0.2.1 aiohttp-fast-zlib==0.2.0 -aiohttp==3.11.8 +aiohttp==3.11.9 aiohttp_cors==0.7.0 aiozoneinfo==0.2.1 astral==2.2 diff --git a/pyproject.toml b/pyproject.toml index 5f72c2bf99b..98a9778e8dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ dependencies = [ # change behavior based on presence of supervisor. Deprecated with #127228 # Lib can be removed with 2025.11 "aiohasupervisor==0.2.1", - "aiohttp==3.11.8", + "aiohttp==3.11.9", "aiohttp_cors==0.7.0", "aiohttp-fast-zlib==0.2.0", "aiozoneinfo==0.2.1", diff --git a/requirements.txt b/requirements.txt index 40a372856b6..0f5047a0bbb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ # Home Assistant Core aiodns==3.2.0 aiohasupervisor==0.2.1 -aiohttp==3.11.8 +aiohttp==3.11.9 aiohttp_cors==0.7.0 aiohttp-fast-zlib==0.2.0 aiozoneinfo==0.2.1 From d956e4b11d5780cb26a574169d1d8b90a43fa942 Mon Sep 17 00:00:00 2001 From: TimL Date: Mon, 2 Dec 2024 19:24:49 +1100 Subject: [PATCH 056/103] Bump psymlight v0.1.4 (#132045) --- homeassistant/components/smlight/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/smlight/manifest.json b/homeassistant/components/smlight/manifest.json index c1eca45871b..cb791ac111b 100644 --- a/homeassistant/components/smlight/manifest.json +++ b/homeassistant/components/smlight/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/smlight", "integration_type": "device", "iot_class": "local_push", - "requirements": ["pysmlight==0.1.3"], + "requirements": ["pysmlight==0.1.4"], "zeroconf": [ { "type": "_slzb-06._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 7a6252c2102..eae98405012 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2269,7 +2269,7 @@ pysmarty2==0.10.1 pysml==0.0.12 # homeassistant.components.smlight -pysmlight==0.1.3 +pysmlight==0.1.4 # homeassistant.components.snmp pysnmp==6.2.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3b98cc097aa..2a14c769ce5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1829,7 +1829,7 @@ pysmarty2==0.10.1 pysml==0.0.12 # homeassistant.components.smlight -pysmlight==0.1.3 +pysmlight==0.1.4 # homeassistant.components.snmp pysnmp==6.2.6 From 1e5a5925e6cf6155dee7b7556d923ec4cf8c339c Mon Sep 17 00:00:00 2001 From: ashionky <35916938+ashionky@users.noreply.github.com> Date: Mon, 2 Dec 2024 18:05:09 +0800 Subject: [PATCH 057/103] Bump refoss to v1.2.5 (#132051) --- homeassistant/components/refoss/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/refoss/manifest.json b/homeassistant/components/refoss/manifest.json index bf046e954d1..da7050433f3 100644 --- a/homeassistant/components/refoss/manifest.json +++ b/homeassistant/components/refoss/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/refoss", "iot_class": "local_polling", - "requirements": ["refoss-ha==1.2.4"] + "requirements": ["refoss-ha==1.2.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index eae98405012..0100b67d785 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2544,7 +2544,7 @@ rapt-ble==0.1.2 raspyrfm-client==1.2.8 # homeassistant.components.refoss -refoss-ha==1.2.4 +refoss-ha==1.2.5 # homeassistant.components.rainmachine regenmaschine==2024.03.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2a14c769ce5..c938702efb7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2035,7 +2035,7 @@ radiotherm==2.1.0 rapt-ble==0.1.2 # homeassistant.components.refoss -refoss-ha==1.2.4 +refoss-ha==1.2.5 # homeassistant.components.rainmachine regenmaschine==2024.03.0 From c3c500955ac9d3dc18a5316cc4222af30b9017c9 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:38:39 +0100 Subject: [PATCH 058/103] Use format_mac correctly for acaia (#132062) --- homeassistant/components/acaia/config_flow.py | 10 +++++----- homeassistant/components/acaia/entity.py | 7 ++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/acaia/config_flow.py b/homeassistant/components/acaia/config_flow.py index 36727059c8a..fb2639fc886 100644 --- a/homeassistant/components/acaia/config_flow.py +++ b/homeassistant/components/acaia/config_flow.py @@ -42,7 +42,7 @@ class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: - mac = format_mac(user_input[CONF_ADDRESS]) + mac = user_input[CONF_ADDRESS] try: is_new_style_scale = await is_new_scale(mac) except AcaiaDeviceNotFound: @@ -53,12 +53,12 @@ class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN): except AcaiaUnknownDevice: return self.async_abort(reason="unsupported_device") else: - await self.async_set_unique_id(mac) + await self.async_set_unique_id(format_mac(mac)) self._abort_if_unique_id_configured() if not errors: return self.async_create_entry( - title=self._discovered_devices[user_input[CONF_ADDRESS]], + title=self._discovered_devices[mac], data={ CONF_ADDRESS: mac, CONF_IS_NEW_STYLE_SCALE: is_new_style_scale, @@ -99,10 +99,10 @@ class AcaiaConfigFlow(ConfigFlow, domain=DOMAIN): ) -> ConfigFlowResult: """Handle a discovered Bluetooth device.""" - self._discovered[CONF_ADDRESS] = mac = format_mac(discovery_info.address) + self._discovered[CONF_ADDRESS] = discovery_info.address self._discovered[CONF_NAME] = discovery_info.name - await self.async_set_unique_id(mac) + await self.async_set_unique_id(format_mac(discovery_info.address)) self._abort_if_unique_id_configured() try: diff --git a/homeassistant/components/acaia/entity.py b/homeassistant/components/acaia/entity.py index 8a2108d2687..db01b414b99 100644 --- a/homeassistant/components/acaia/entity.py +++ b/homeassistant/components/acaia/entity.py @@ -2,7 +2,7 @@ from dataclasses import dataclass -from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.device_registry import DeviceInfo, format_mac from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -25,10 +25,11 @@ class AcaiaEntity(CoordinatorEntity[AcaiaCoordinator]): super().__init__(coordinator) self.entity_description = entity_description self._scale = coordinator.scale - self._attr_unique_id = f"{self._scale.mac}_{entity_description.key}" + formatted_mac = format_mac(self._scale.mac) + self._attr_unique_id = f"{formatted_mac}_{entity_description.key}" self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, self._scale.mac)}, + identifiers={(DOMAIN, formatted_mac)}, manufacturer="Acaia", model=self._scale.model, suggested_area="Kitchen", From be40db3dff0a54edb33da7045b897e00cc82f588 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 2 Dec 2024 13:02:23 +0100 Subject: [PATCH 059/103] Bump version to 2024.12.0b4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b91d0cd53be..5617ab1d22a 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 98a9778e8dd..2f86ff4a6c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.12.0b3" +version = "2024.12.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 110935461e9a0d256878dbeca5451f32c18b3470 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Tue, 3 Dec 2024 13:06:18 +0100 Subject: [PATCH 060/103] Add support for features changing at runtime in Matter integration (#129426) --- homeassistant/components/matter/adapter.py | 18 ++++--- .../components/matter/binary_sensor.py | 1 + homeassistant/components/matter/button.py | 1 + homeassistant/components/matter/const.py | 2 + homeassistant/components/matter/discovery.py | 24 ++++++++-- homeassistant/components/matter/entity.py | 39 ++++++++++++++- homeassistant/components/matter/lock.py | 1 - homeassistant/components/matter/models.py | 7 +++ .../matter/fixtures/nodes/door_lock.json | 2 +- .../matter/snapshots/test_binary_sensor.ambr | 47 ------------------- tests/components/matter/test_binary_sensor.py | 32 +++++++++++++ 11 files changed, 113 insertions(+), 61 deletions(-) diff --git a/homeassistant/components/matter/adapter.py b/homeassistant/components/matter/adapter.py index 475e4a44538..0ccd3e065ff 100644 --- a/homeassistant/components/matter/adapter.py +++ b/homeassistant/components/matter/adapter.py @@ -45,6 +45,7 @@ class MatterAdapter: self.hass = hass self.config_entry = config_entry self.platform_handlers: dict[Platform, AddEntitiesCallback] = {} + self.discovered_entities: set[str] = set() def register_platform_handler( self, platform: Platform, add_entities: AddEntitiesCallback @@ -54,23 +55,19 @@ class MatterAdapter: async def setup_nodes(self) -> None: """Set up all existing nodes and subscribe to new nodes.""" - initialized_nodes: set[int] = set() for node in self.matter_client.get_nodes(): - initialized_nodes.add(node.node_id) self._setup_node(node) def node_added_callback(event: EventType, node: MatterNode) -> None: """Handle node added event.""" - initialized_nodes.add(node.node_id) self._setup_node(node) def node_updated_callback(event: EventType, node: MatterNode) -> None: """Handle node updated event.""" - if node.node_id in initialized_nodes: - return if not node.available: return - initialized_nodes.add(node.node_id) + # We always run the discovery logic again, + # because the firmware version could have been changed or features added. self._setup_node(node) def endpoint_added_callback(event: EventType, data: dict[str, int]) -> None: @@ -237,11 +234,20 @@ class MatterAdapter: self._create_device_registry(endpoint) # run platform discovery from device type instances for entity_info in async_discover_entities(endpoint): + discovery_key = ( + f"{entity_info.platform}_{endpoint.node.node_id}_{endpoint.endpoint_id}_" + f"{entity_info.primary_attribute.cluster_id}_" + f"{entity_info.primary_attribute.attribute_id}_" + f"{entity_info.entity_description.key}" + ) + if discovery_key in self.discovered_entities: + continue LOGGER.debug( "Creating %s entity for %s", entity_info.platform, entity_info.primary_attribute, ) + self.discovered_entities.add(discovery_key) new_entity = entity_info.entity_class( self.matter_client, endpoint, entity_info ) diff --git a/homeassistant/components/matter/binary_sensor.py b/homeassistant/components/matter/binary_sensor.py index 875b063dc88..6882078a712 100644 --- a/homeassistant/components/matter/binary_sensor.py +++ b/homeassistant/components/matter/binary_sensor.py @@ -159,6 +159,7 @@ DISCOVERY_SCHEMAS = [ ), entity_class=MatterBinarySensor, required_attributes=(clusters.DoorLock.Attributes.DoorState,), + featuremap_contains=clusters.DoorLock.Bitmaps.Feature.kDoorPositionSensor, ), MatterDiscoverySchema( platform=Platform.BINARY_SENSOR, diff --git a/homeassistant/components/matter/button.py b/homeassistant/components/matter/button.py index 918b334061b..153124a4f7e 100644 --- a/homeassistant/components/matter/button.py +++ b/homeassistant/components/matter/button.py @@ -69,6 +69,7 @@ DISCOVERY_SCHEMAS = [ entity_class=MatterCommandButton, required_attributes=(clusters.Identify.Attributes.AcceptedCommandList,), value_contains=clusters.Identify.Commands.Identify.command_id, + allow_multi=True, ), MatterDiscoverySchema( platform=Platform.BUTTON, diff --git a/homeassistant/components/matter/const.py b/homeassistant/components/matter/const.py index a0e160a6c01..8018d5e09ed 100644 --- a/homeassistant/components/matter/const.py +++ b/homeassistant/components/matter/const.py @@ -13,3 +13,5 @@ LOGGER = logging.getLogger(__package__) # prefixes to identify device identifier id types ID_TYPE_DEVICE_ID = "deviceid" ID_TYPE_SERIAL = "serial" + +FEATUREMAP_ATTRIBUTE_ID = 65532 diff --git a/homeassistant/components/matter/discovery.py b/homeassistant/components/matter/discovery.py index 5b07f9a069f..3b9fb0b8a94 100644 --- a/homeassistant/components/matter/discovery.py +++ b/homeassistant/components/matter/discovery.py @@ -13,6 +13,7 @@ from homeassistant.core import callback from .binary_sensor import DISCOVERY_SCHEMAS as BINARY_SENSOR_SCHEMAS from .button import DISCOVERY_SCHEMAS as BUTTON_SCHEMAS from .climate import DISCOVERY_SCHEMAS as CLIMATE_SENSOR_SCHEMAS +from .const import FEATUREMAP_ATTRIBUTE_ID from .cover import DISCOVERY_SCHEMAS as COVER_SCHEMAS from .event import DISCOVERY_SCHEMAS as EVENT_SCHEMAS from .fan import DISCOVERY_SCHEMAS as FAN_SCHEMAS @@ -121,12 +122,24 @@ def async_discover_entities( continue # check for required value in (primary) attribute + primary_attribute = schema.required_attributes[0] + primary_value = endpoint.get_attribute_value(None, primary_attribute) if schema.value_contains is not None and ( - (primary_attribute := next((x for x in schema.required_attributes), None)) - is None - or (value := endpoint.get_attribute_value(None, primary_attribute)) is None - or not isinstance(value, list) - or schema.value_contains not in value + isinstance(primary_value, list) + and schema.value_contains not in primary_value + ): + continue + + # check for required value in cluster featuremap + if schema.featuremap_contains is not None and ( + not bool( + int( + endpoint.get_attribute_value( + primary_attribute.cluster_id, FEATUREMAP_ATTRIBUTE_ID + ) + ) + & schema.featuremap_contains + ) ): continue @@ -147,6 +160,7 @@ def async_discover_entities( attributes_to_watch=attributes_to_watch, entity_description=schema.entity_description, entity_class=schema.entity_class, + discovery_schema=schema, ) # prevent re-discovery of the primary attribute if not allowed diff --git a/homeassistant/components/matter/entity.py b/homeassistant/components/matter/entity.py index 7c378fe465e..50a0f2b1fee 100644 --- a/homeassistant/components/matter/entity.py +++ b/homeassistant/components/matter/entity.py @@ -16,9 +16,10 @@ from propcache import cached_property from homeassistant.core import callback from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity, EntityDescription +import homeassistant.helpers.entity_registry as er from homeassistant.helpers.typing import UndefinedType -from .const import DOMAIN, ID_TYPE_DEVICE_ID +from .const import DOMAIN, FEATUREMAP_ATTRIBUTE_ID, ID_TYPE_DEVICE_ID from .helpers import get_device_id if TYPE_CHECKING: @@ -140,6 +141,19 @@ class MatterEntity(Entity): node_filter=self._endpoint.node.node_id, ) ) + # subscribe to FeatureMap attribute (as that can dynamically change) + self._unsubscribes.append( + self.matter_client.subscribe_events( + callback=self._on_featuremap_update, + event_filter=EventType.ATTRIBUTE_UPDATED, + node_filter=self._endpoint.node.node_id, + attr_path_filter=create_attribute_path( + endpoint=self._endpoint.endpoint_id, + cluster_id=self._entity_info.primary_attribute.cluster_id, + attribute_id=FEATUREMAP_ATTRIBUTE_ID, + ), + ) + ) @cached_property def name(self) -> str | UndefinedType | None: @@ -159,6 +173,29 @@ class MatterEntity(Entity): self._update_from_device() self.async_write_ha_state() + @callback + def _on_featuremap_update( + self, event: EventType, data: tuple[int, str, int] | None + ) -> None: + """Handle FeatureMap attribute updates.""" + if data is None: + return + new_value = data[2] + # handle edge case where a Feature is removed from a cluster + if ( + self._entity_info.discovery_schema.featuremap_contains is not None + and not bool( + new_value & self._entity_info.discovery_schema.featuremap_contains + ) + ): + # this entity is no longer supported by the device + ent_reg = er.async_get(self.hass) + ent_reg.async_remove(self.entity_id) + + return + # all other cases, just update the entity + self._on_matter_event(event, data) + @callback def _update_from_device(self) -> None: """Update data from Matter device.""" diff --git a/homeassistant/components/matter/lock.py b/homeassistant/components/matter/lock.py index c5e10554fe7..d69d0fd3dab 100644 --- a/homeassistant/components/matter/lock.py +++ b/homeassistant/components/matter/lock.py @@ -206,6 +206,5 @@ DISCOVERY_SCHEMAS = [ ), entity_class=MatterLock, required_attributes=(clusters.DoorLock.Attributes.LockState,), - optional_attributes=(clusters.DoorLock.Attributes.DoorState,), ), ] diff --git a/homeassistant/components/matter/models.py b/homeassistant/components/matter/models.py index f04c0f7e107..a00963c825a 100644 --- a/homeassistant/components/matter/models.py +++ b/homeassistant/components/matter/models.py @@ -51,6 +51,9 @@ class MatterEntityInfo: # entity class to use to instantiate the entity entity_class: type + # the original discovery schema used to create this entity + discovery_schema: MatterDiscoverySchema + @property def primary_attribute(self) -> type[ClusterAttributeDescriptor]: """Return Primary Attribute belonging to the entity.""" @@ -113,6 +116,10 @@ class MatterDiscoverySchema: # NOTE: only works for list values value_contains: Any | None = None + # [optional] the primary attribute's cluster featuremap must contain this value + # for example for the DoorSensor on a DoorLock Cluster + featuremap_contains: int | None = None + # [optional] bool to specify if this primary value may be discovered # by multiple platforms allow_multi: bool = False diff --git a/tests/components/matter/fixtures/nodes/door_lock.json b/tests/components/matter/fixtures/nodes/door_lock.json index b6231e04af4..acd327ac56c 100644 --- a/tests/components/matter/fixtures/nodes/door_lock.json +++ b/tests/components/matter/fixtures/nodes/door_lock.json @@ -495,7 +495,7 @@ "1/257/48": 3, "1/257/49": 10, "1/257/51": false, - "1/257/65532": 3507, + "1/257/65532": 0, "1/257/65533": 6, "1/257/65528": [12, 15, 18, 28, 35, 37], "1/257/65529": [ diff --git a/tests/components/matter/snapshots/test_binary_sensor.ambr b/tests/components/matter/snapshots/test_binary_sensor.ambr index 2e3367121e9..82dcc166f13 100644 --- a/tests/components/matter/snapshots/test_binary_sensor.ambr +++ b/tests/components/matter/snapshots/test_binary_sensor.ambr @@ -46,53 +46,6 @@ 'state': 'off', }) # --- -# name: test_binary_sensors[door_lock][binary_sensor.mock_door_lock_door-entry] - EntityRegistryEntrySnapshot({ - 'aliases': set({ - }), - 'area_id': None, - 'capabilities': None, - 'config_entry_id': , - 'device_class': None, - 'device_id': , - 'disabled_by': None, - 'domain': 'binary_sensor', - 'entity_category': None, - 'entity_id': 'binary_sensor.mock_door_lock_door', - 'has_entity_name': True, - 'hidden_by': None, - 'icon': None, - 'id': , - 'labels': set({ - }), - 'name': None, - 'options': dict({ - }), - 'original_device_class': , - 'original_icon': None, - 'original_name': 'Door', - 'platform': 'matter', - 'previous_unique_id': None, - 'supported_features': 0, - 'translation_key': None, - 'unique_id': '00000000000004D2-0000000000000001-MatterNodeDevice-1-LockDoorStateSensor-257-3', - 'unit_of_measurement': None, - }) -# --- -# name: test_binary_sensors[door_lock][binary_sensor.mock_door_lock_door-state] - StateSnapshot({ - 'attributes': ReadOnlyDict({ - 'device_class': 'door', - 'friendly_name': 'Mock Door Lock Door', - }), - 'context': , - 'entity_id': 'binary_sensor.mock_door_lock_door', - 'last_changed': , - 'last_reported': , - 'last_updated': , - 'state': 'off', - }) -# --- # name: test_binary_sensors[door_lock_with_unbolt][binary_sensor.mock_door_lock_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/matter/test_binary_sensor.py b/tests/components/matter/test_binary_sensor.py index 7ae483162bf..cddee975ac8 100644 --- a/tests/components/matter/test_binary_sensor.py +++ b/tests/components/matter/test_binary_sensor.py @@ -4,6 +4,7 @@ from collections.abc import Generator from unittest.mock import MagicMock, patch from matter_server.client.models.node import MatterNode +from matter_server.common.models import EventType import pytest from syrupy import SnapshotAssertion @@ -115,3 +116,34 @@ async def test_battery_sensor( state = hass.states.get(entity_id) assert state assert state.state == "on" + + +@pytest.mark.parametrize("node_fixture", ["door_lock"]) +async def test_optional_sensor_from_featuremap( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + matter_client: MagicMock, + matter_node: MatterNode, +) -> None: + """Test discovery of optional doorsensor in doorlock featuremap.""" + entity_id = "binary_sensor.mock_door_lock_door" + state = hass.states.get(entity_id) + assert state is None + + # update the feature map to include the optional door sensor feature + # and fire a node updated event + set_node_attribute(matter_node, 1, 257, 65532, 32) + await trigger_subscription_callback( + hass, matter_client, event=EventType.NODE_UPDATED, data=matter_node + ) + # this should result in a new binary sensor entity being discovered + state = hass.states.get(entity_id) + assert state + assert state.state == "off" + # now test the reverse, by removing the feature from the feature map + set_node_attribute(matter_node, 1, 257, 65532, 0) + await trigger_subscription_callback( + hass, matter_client, data=(matter_node.node_id, "1/257/65532", 0) + ) + state = hass.states.get(entity_id) + assert state is None From c3499e52943356761a0b2bed58490e45c29037b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Mon, 2 Dec 2024 12:52:59 +0000 Subject: [PATCH 061/103] Update buienradar sensors only after being added to HA (#131830) * Update buienradar sensors only after being added to HA * Move check to util * Check for platform in sensor state property * Move check to unit translation key property * Add test for sensor check * Properly handle added_to_hass * Remove redundant comment --- homeassistant/components/buienradar/sensor.py | 21 ++++++++-- homeassistant/components/sensor/__init__.py | 5 +++ tests/components/sensor/test_init.py | 39 +++++++++++++++++++ 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/buienradar/sensor.py b/homeassistant/components/buienradar/sensor.py index afce293402e..712f765237e 100644 --- a/homeassistant/components/buienradar/sensor.py +++ b/homeassistant/components/buienradar/sensor.py @@ -742,6 +742,7 @@ class BrSensor(SensorEntity): ) -> None: """Initialize the sensor.""" self.entity_description = description + self._data: BrData | None = None self._measured = None self._attr_unique_id = ( f"{coordinates[CONF_LATITUDE]:2.6f}{coordinates[CONF_LONGITUDE]:2.6f}" @@ -756,17 +757,29 @@ class BrSensor(SensorEntity): if description.key.startswith(PRECIPITATION_FORECAST): self._timeframe = None + async def async_added_to_hass(self) -> None: + """Handle entity being added to hass.""" + if self._data is None: + return + self._update() + @callback def data_updated(self, data: BrData): - """Update data.""" - if self._load_data(data.data) and self.hass: + """Handle data update.""" + self._data = data + if not self.hass: + return + self._update() + + def _update(self): + """Update sensor data.""" + _LOGGER.debug("Updating sensor %s", self.entity_id) + if self._load_data(self._data.data): self.async_write_ha_state() @callback def _load_data(self, data): # noqa: C901 """Load the sensor with relevant data.""" - # Find sensor - # Check if we have a new measurement, # otherwise we do not have to update the sensor if self._measured == data.get(MEASURED): diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index f1864458ce8..1e3b5d10c98 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -509,6 +509,11 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): """Return translation key for unit of measurement.""" if self.translation_key is None: return None + if self.platform is None: + raise ValueError( + f"Sensor {type(self)} cannot have a translation key for " + "unit of measurement before being added to the entity platform" + ) platform = self.platform return ( f"component.{platform.platform_name}.entity.{platform.domain}" diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 19c25d819b6..d53818e77b3 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -548,6 +548,45 @@ async def test_translated_unit_with_native_unit_raises( assert entity0.entity_id is None +async def test_unit_translation_key_without_platform_raises( + hass: HomeAssistant, +) -> None: + """Test that unit translation key property raises if the entity has no platform yet.""" + + with patch( + "homeassistant.helpers.service.translation.async_get_translations", + return_value={ + "component.test.entity.sensor.test_translation_key.unit_of_measurement": "Tests" + }, + ): + entity0 = MockSensor( + name="Test", + native_value="123", + unique_id="very_unique", + ) + entity0.entity_description = SensorEntityDescription( + "test", + translation_key="test_translation_key", + ) + with pytest.raises( + ValueError, + match="cannot have a translation key for unit of measurement before " + "being added to the entity platform", + ): + unit = entity0.unit_of_measurement # noqa: F841 + + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) + + assert await async_setup_component( + hass, "sensor", {"sensor": {"platform": "test"}} + ) + await hass.async_block_till_done() + + # Should not raise after being added to the platform + unit = entity0.unit_of_measurement # noqa: F841 + assert unit == "Tests" + + @pytest.mark.parametrize( ( "device_class", From 97a725c2c6ddd052ef44d001beb6304a6d5c035c Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Mon, 2 Dec 2024 09:54:37 +0000 Subject: [PATCH 062/103] Add translated native unit of measurement - squeezebox (#131912) --- homeassistant/components/squeezebox/sensor.py | 6 ------ .../components/squeezebox/strings.json | 18 ++++++++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/squeezebox/sensor.py b/homeassistant/components/squeezebox/sensor.py index ff9f86ccf1f..0ca33179f9f 100644 --- a/homeassistant/components/squeezebox/sensor.py +++ b/homeassistant/components/squeezebox/sensor.py @@ -33,12 +33,10 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=STATUS_SENSOR_INFO_TOTAL_ALBUMS, state_class=SensorStateClass.TOTAL, - native_unit_of_measurement="albums", ), SensorEntityDescription( key=STATUS_SENSOR_INFO_TOTAL_ARTISTS, state_class=SensorStateClass.TOTAL, - native_unit_of_measurement="artists", ), SensorEntityDescription( key=STATUS_SENSOR_INFO_TOTAL_DURATION, @@ -49,12 +47,10 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=STATUS_SENSOR_INFO_TOTAL_GENRES, state_class=SensorStateClass.TOTAL, - native_unit_of_measurement="genres", ), SensorEntityDescription( key=STATUS_SENSOR_INFO_TOTAL_SONGS, state_class=SensorStateClass.TOTAL, - native_unit_of_measurement="songs", ), SensorEntityDescription( key=STATUS_SENSOR_LASTSCAN, @@ -63,13 +59,11 @@ SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=STATUS_SENSOR_PLAYER_COUNT, state_class=SensorStateClass.TOTAL, - native_unit_of_measurement="players", ), SensorEntityDescription( key=STATUS_SENSOR_OTHER_PLAYER_COUNT, state_class=SensorStateClass.TOTAL, entity_registry_visible_default=False, - native_unit_of_measurement="players", ), ) diff --git a/homeassistant/components/squeezebox/strings.json b/homeassistant/components/squeezebox/strings.json index b1b71cd8c1d..406c7243a1a 100644 --- a/homeassistant/components/squeezebox/strings.json +++ b/homeassistant/components/squeezebox/strings.json @@ -76,25 +76,31 @@ "name": "Last scan" }, "info_total_albums": { - "name": "Total albums" + "name": "Total albums", + "unit_of_measurement": "albums" }, "info_total_artists": { - "name": "Total artists" + "name": "Total artists", + "unit_of_measurement": "artists" }, "info_total_duration": { "name": "Total duration" }, "info_total_genres": { - "name": "Total genres" + "name": "Total genres", + "unit_of_measurement": "genres" }, "info_total_songs": { - "name": "Total songs" + "name": "Total songs", + "unit_of_measurement": "songs" }, "player_count": { - "name": "Player count" + "name": "Player count", + "unit_of_measurement": "players" }, "other_player_count": { - "name": "Player count off service" + "name": "Player count off service", + "unit_of_measurement": "[%key:component::squeezebox::entity::sensor::player_count::unit_of_measurement%]" } } } From 42c46a15b40c8f8df3fd3715c9c44859f5f987e0 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Mon, 2 Dec 2024 09:51:32 +0000 Subject: [PATCH 063/103] Add translated native unit of measurement - Transmission (#131913) --- homeassistant/components/transmission/sensor.py | 5 ----- .../components/transmission/strings.json | 15 ++++++++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index 737520adb5f..652f5d51fbb 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -83,7 +83,6 @@ SENSOR_TYPES: tuple[TransmissionSensorEntityDescription, ...] = ( TransmissionSensorEntityDescription( key="active_torrents", translation_key="active_torrents", - native_unit_of_measurement="torrents", val_func=lambda coordinator: coordinator.data.active_torrent_count, extra_state_attr_func=lambda coordinator: _torrents_info_attr( coordinator=coordinator, key="active_torrents" @@ -92,7 +91,6 @@ SENSOR_TYPES: tuple[TransmissionSensorEntityDescription, ...] = ( TransmissionSensorEntityDescription( key="paused_torrents", translation_key="paused_torrents", - native_unit_of_measurement="torrents", val_func=lambda coordinator: coordinator.data.paused_torrent_count, extra_state_attr_func=lambda coordinator: _torrents_info_attr( coordinator=coordinator, key="paused_torrents" @@ -101,7 +99,6 @@ SENSOR_TYPES: tuple[TransmissionSensorEntityDescription, ...] = ( TransmissionSensorEntityDescription( key="total_torrents", translation_key="total_torrents", - native_unit_of_measurement="torrents", val_func=lambda coordinator: coordinator.data.torrent_count, extra_state_attr_func=lambda coordinator: _torrents_info_attr( coordinator=coordinator, key="total_torrents" @@ -110,7 +107,6 @@ SENSOR_TYPES: tuple[TransmissionSensorEntityDescription, ...] = ( TransmissionSensorEntityDescription( key="completed_torrents", translation_key="completed_torrents", - native_unit_of_measurement="torrents", val_func=lambda coordinator: len( _filter_torrents(coordinator.torrents, MODES["completed_torrents"]) ), @@ -121,7 +117,6 @@ SENSOR_TYPES: tuple[TransmissionSensorEntityDescription, ...] = ( TransmissionSensorEntityDescription( key="started_torrents", translation_key="started_torrents", - native_unit_of_measurement="torrents", val_func=lambda coordinator: len( _filter_torrents(coordinator.torrents, MODES["started_torrents"]) ), diff --git a/homeassistant/components/transmission/strings.json b/homeassistant/components/transmission/strings.json index 20ae6ca723d..578bc262589 100644 --- a/homeassistant/components/transmission/strings.json +++ b/homeassistant/components/transmission/strings.json @@ -60,19 +60,24 @@ } }, "active_torrents": { - "name": "Active torrents" + "name": "Active torrents", + "unit_of_measurement": "torrents" }, "paused_torrents": { - "name": "Paused torrents" + "name": "Paused torrents", + "unit_of_measurement": "[%key:component::transmission::entity::sensor::active_torrents::unit_of_measurement%]" }, "total_torrents": { - "name": "Total torrents" + "name": "Total torrents", + "unit_of_measurement": "[%key:component::transmission::entity::sensor::active_torrents::unit_of_measurement%]" }, "completed_torrents": { - "name": "Completed torrents" + "name": "Completed torrents", + "unit_of_measurement": "[%key:component::transmission::entity::sensor::active_torrents::unit_of_measurement%]" }, "started_torrents": { - "name": "Started torrents" + "name": "Started torrents", + "unit_of_measurement": "[%key:component::transmission::entity::sensor::active_torrents::unit_of_measurement%]" } }, "switch": { From 3dc0ca7e1e627453a10fc36133695634c8f22837 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Mon, 2 Dec 2024 09:51:50 +0000 Subject: [PATCH 064/103] Add translated native unit of measurement - PiHole (#131915) --- homeassistant/components/pi_hole/sensor.py | 29 ++++--------------- homeassistant/components/pi_hole/strings.json | 24 ++++++++++----- 2 files changed, 21 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/pi_hole/sensor.py b/homeassistant/components/pi_hole/sensor.py index 503883e9326..4cf5133e700 100644 --- a/homeassistant/components/pi_hole/sensor.py +++ b/homeassistant/components/pi_hole/sensor.py @@ -18,7 +18,6 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="ads_blocked_today", translation_key="ads_blocked_today", - native_unit_of_measurement="ads", ), SensorEntityDescription( key="ads_percentage_today", @@ -28,38 +27,20 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key="clients_ever_seen", translation_key="clients_ever_seen", - native_unit_of_measurement="clients", ), SensorEntityDescription( - key="dns_queries_today", - translation_key="dns_queries_today", - native_unit_of_measurement="queries", + key="dns_queries_today", translation_key="dns_queries_today" ), SensorEntityDescription( key="domains_being_blocked", translation_key="domains_being_blocked", - native_unit_of_measurement="domains", ), + SensorEntityDescription(key="queries_cached", translation_key="queries_cached"), SensorEntityDescription( - key="queries_cached", - translation_key="queries_cached", - native_unit_of_measurement="queries", - ), - SensorEntityDescription( - key="queries_forwarded", - translation_key="queries_forwarded", - native_unit_of_measurement="queries", - ), - SensorEntityDescription( - key="unique_clients", - translation_key="unique_clients", - native_unit_of_measurement="clients", - ), - SensorEntityDescription( - key="unique_domains", - translation_key="unique_domains", - native_unit_of_measurement="domains", + key="queries_forwarded", translation_key="queries_forwarded" ), + SensorEntityDescription(key="unique_clients", translation_key="unique_clients"), + SensorEntityDescription(key="unique_domains", translation_key="unique_domains"), ) diff --git a/homeassistant/components/pi_hole/strings.json b/homeassistant/components/pi_hole/strings.json index b76b61f1903..9e1d5948a09 100644 --- a/homeassistant/components/pi_hole/strings.json +++ b/homeassistant/components/pi_hole/strings.json @@ -41,31 +41,39 @@ }, "sensor": { "ads_blocked_today": { - "name": "Ads blocked today" + "name": "Ads blocked today", + "unit_of_measurement": "ads" }, "ads_percentage_today": { "name": "Ads percentage blocked today" }, "clients_ever_seen": { - "name": "Seen clients" + "name": "Seen clients", + "unit_of_measurement": "clients" }, "dns_queries_today": { - "name": "DNS queries today" + "name": "DNS queries today", + "unit_of_measurement": "queries" }, "domains_being_blocked": { - "name": "Domains blocked" + "name": "Domains blocked", + "unit_of_measurement": "domains" }, "queries_cached": { - "name": "DNS queries cached" + "name": "DNS queries cached", + "unit_of_measurement": "[%key:component::pi_hole::entity::sensor::dns_queries_today::unit_of_measurement%]" }, "queries_forwarded": { - "name": "DNS queries forwarded" + "name": "DNS queries forwarded", + "unit_of_measurement": "[%key:component::pi_hole::entity::sensor::dns_queries_today::unit_of_measurement%]" }, "unique_clients": { - "name": "DNS unique clients" + "name": "DNS unique clients", + "unit_of_measurement": "[%key:component::pi_hole::entity::sensor::clients_ever_seen::unit_of_measurement%]" }, "unique_domains": { - "name": "DNS unique domains" + "name": "DNS unique domains", + "unit_of_measurement": "[%key:component::pi_hole::entity::sensor::domains_being_blocked::unit_of_measurement%]" } }, "update": { From b5e7da426241325161a7d18bd6ae1ab9b3970ffe Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Mon, 2 Dec 2024 09:50:49 +0000 Subject: [PATCH 065/103] Add translated native unit of measurement - QBitTorrent (#131918) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/qbittorrent/sensor.py | 4 ---- homeassistant/components/qbittorrent/strings.json | 12 ++++++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index abc23f39975..67eb856bb83 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -100,13 +100,11 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = ( QBittorrentSensorEntityDescription( key=SENSOR_TYPE_ALL_TORRENTS, translation_key="all_torrents", - native_unit_of_measurement="torrents", value_fn=lambda coordinator: count_torrents_in_states(coordinator, []), ), QBittorrentSensorEntityDescription( key=SENSOR_TYPE_ACTIVE_TORRENTS, translation_key="active_torrents", - native_unit_of_measurement="torrents", value_fn=lambda coordinator: count_torrents_in_states( coordinator, ["downloading", "uploading"] ), @@ -114,7 +112,6 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = ( QBittorrentSensorEntityDescription( key=SENSOR_TYPE_INACTIVE_TORRENTS, translation_key="inactive_torrents", - native_unit_of_measurement="torrents", value_fn=lambda coordinator: count_torrents_in_states( coordinator, ["stalledDL", "stalledUP"] ), @@ -122,7 +119,6 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = ( QBittorrentSensorEntityDescription( key=SENSOR_TYPE_PAUSED_TORRENTS, translation_key="paused_torrents", - native_unit_of_measurement="torrents", value_fn=lambda coordinator: count_torrents_in_states( coordinator, ["pausedDL", "pausedUP"] ), diff --git a/homeassistant/components/qbittorrent/strings.json b/homeassistant/components/qbittorrent/strings.json index 88015dad5c3..9c9ee371737 100644 --- a/homeassistant/components/qbittorrent/strings.json +++ b/homeassistant/components/qbittorrent/strings.json @@ -36,16 +36,20 @@ } }, "active_torrents": { - "name": "Active torrents" + "name": "Active torrents", + "unit_of_measurement": "torrents" }, "inactive_torrents": { - "name": "Inactive torrents" + "name": "Inactive torrents", + "unit_of_measurement": "[%key:component::qbittorrent::entity::sensor::active_torrents::unit_of_measurement%]" }, "paused_torrents": { - "name": "Paused torrents" + "name": "Paused torrents", + "unit_of_measurement": "[%key:component::qbittorrent::entity::sensor::active_torrents::unit_of_measurement%]" }, "all_torrents": { - "name": "All torrents" + "name": "All torrents", + "unit_of_measurement": "[%key:component::qbittorrent::entity::sensor::active_torrents::unit_of_measurement%]" } }, "switch": { From 43899b6f28a1fa7113c7aec411da56a487a8db9a Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:11:15 +0100 Subject: [PATCH 066/103] Catch InverterReturnedError in APSystems (#131930) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- .../components/apsystems/coordinator.py | 18 ++++++++++--- .../components/apsystems/strings.json | 5 ++++ tests/components/apsystems/test_init.py | 25 +++++++++++++++++++ 3 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 tests/components/apsystems/test_init.py diff --git a/homeassistant/components/apsystems/coordinator.py b/homeassistant/components/apsystems/coordinator.py index b6e951343f7..e56cb826840 100644 --- a/homeassistant/components/apsystems/coordinator.py +++ b/homeassistant/components/apsystems/coordinator.py @@ -5,12 +5,17 @@ from __future__ import annotations from dataclasses import dataclass from datetime import timedelta -from APsystemsEZ1 import APsystemsEZ1M, ReturnAlarmInfo, ReturnOutputData +from APsystemsEZ1 import ( + APsystemsEZ1M, + InverterReturnedError, + ReturnAlarmInfo, + ReturnOutputData, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import LOGGER +from .const import DOMAIN, LOGGER @dataclass @@ -43,6 +48,11 @@ class ApSystemsDataCoordinator(DataUpdateCoordinator[ApSystemsSensorData]): self.api.min_power = device_info.minPower async def _async_update_data(self) -> ApSystemsSensorData: - output_data = await self.api.get_output_data() - alarm_info = await self.api.get_alarm_info() + try: + output_data = await self.api.get_output_data() + alarm_info = await self.api.get_alarm_info() + except InverterReturnedError: + raise UpdateFailed( + translation_domain=DOMAIN, translation_key="inverter_error" + ) from None return ApSystemsSensorData(output_data=output_data, alarm_info=alarm_info) diff --git a/homeassistant/components/apsystems/strings.json b/homeassistant/components/apsystems/strings.json index e02f86c2730..b3a10ca49a7 100644 --- a/homeassistant/components/apsystems/strings.json +++ b/homeassistant/components/apsystems/strings.json @@ -72,5 +72,10 @@ "name": "Inverter status" } } + }, + "exceptions": { + "inverter_error": { + "message": "Inverter returned an error" + } } } diff --git a/tests/components/apsystems/test_init.py b/tests/components/apsystems/test_init.py new file mode 100644 index 00000000000..c85c4094e30 --- /dev/null +++ b/tests/components/apsystems/test_init.py @@ -0,0 +1,25 @@ +"""Test the APSystem setup.""" + +from unittest.mock import AsyncMock + +from APsystemsEZ1 import InverterReturnedError + +from homeassistant.components.apsystems.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.common import MockConfigEntry + + +async def test_update_failed( + hass: HomeAssistant, + mock_apsystems: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test update failed.""" + mock_apsystems.get_output_data.side_effect = InverterReturnedError + await setup_integration(hass, mock_config_entry) + entry = hass.config_entries.async_entries(DOMAIN)[0] + assert entry.state is ConfigEntryState.SETUP_RETRY From 905769f0e878111b7c8a8a0d115abdb21216bcfe Mon Sep 17 00:00:00 2001 From: starkillerOG Date: Mon, 2 Dec 2024 22:18:24 +0100 Subject: [PATCH 067/103] Fix Reolink dispatcher ID for onvif fallback (#131953) --- .../components/reolink/binary_sensor.py | 4 ++-- homeassistant/components/reolink/host.py | 10 ++++---- tests/components/reolink/test_host.py | 23 +++++++++++++++---- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/reolink/binary_sensor.py b/homeassistant/components/reolink/binary_sensor.py index c59c1e7785f..c168c97e809 100644 --- a/homeassistant/components/reolink/binary_sensor.py +++ b/homeassistant/components/reolink/binary_sensor.py @@ -176,14 +176,14 @@ class ReolinkPushBinarySensorEntity(ReolinkBinarySensorEntity): self.async_on_remove( async_dispatcher_connect( self.hass, - f"{self._host.webhook_id}_{self._channel}", + f"{self._host.unique_id}_{self._channel}", self._async_handle_event, ) ) self.async_on_remove( async_dispatcher_connect( self.hass, - f"{self._host.webhook_id}_all", + f"{self._host.unique_id}_all", self._async_handle_event, ) ) diff --git a/homeassistant/components/reolink/host.py b/homeassistant/components/reolink/host.py index a8e1de07642..97d888c0323 100644 --- a/homeassistant/components/reolink/host.py +++ b/homeassistant/components/reolink/host.py @@ -723,7 +723,7 @@ class ReolinkHost: self._hass, POLL_INTERVAL_NO_PUSH, self._poll_job ) - self._signal_write_ha_state(None) + self._signal_write_ha_state() async def handle_webhook( self, hass: HomeAssistant, webhook_id: str, request: Request @@ -782,7 +782,7 @@ class ReolinkHost: "Could not poll motion state after losing connection during receiving ONVIF event" ) return - async_dispatcher_send(hass, f"{webhook_id}_all", {}) + self._signal_write_ha_state() return message = data.decode("utf-8") @@ -795,14 +795,14 @@ class ReolinkHost: self._signal_write_ha_state(channels) - def _signal_write_ha_state(self, channels: list[int] | None) -> None: + def _signal_write_ha_state(self, channels: list[int] | None = None) -> None: """Update the binary sensors with async_write_ha_state.""" if channels is None: - async_dispatcher_send(self._hass, f"{self.webhook_id}_all", {}) + async_dispatcher_send(self._hass, f"{self.unique_id}_all", {}) return for channel in channels: - async_dispatcher_send(self._hass, f"{self.webhook_id}_{channel}", {}) + async_dispatcher_send(self._hass, f"{self.unique_id}_{channel}", {}) @property def event_connection(self) -> str: diff --git a/tests/components/reolink/test_host.py b/tests/components/reolink/test_host.py index 2286ca5d266..c777e4064f0 100644 --- a/tests/components/reolink/test_host.py +++ b/tests/components/reolink/test_host.py @@ -21,13 +21,15 @@ from homeassistant.components.reolink.host import ( ) from homeassistant.components.webhook import async_handle_webhook from homeassistant.config_entries import ConfigEntryState -from homeassistant.const import Platform +from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.network import NoURLAvailableError from homeassistant.util.aiohttp import MockRequest +from .conftest import TEST_NVR_NAME + from tests.common import MockConfigEntry, async_fire_time_changed from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator @@ -92,23 +94,32 @@ async def test_webhook_callback( entity_registry: er.EntityRegistry, ) -> None: """Test webhook callback with motion sensor.""" - assert await hass.config_entries.async_setup(config_entry.entry_id) + reolink_connect.motion_detected.return_value = False + + with patch("homeassistant.components.reolink.PLATFORMS", [Platform.BINARY_SENSOR]): + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.LOADED + entity_id = f"{Platform.BINARY_SENSOR}.{TEST_NVR_NAME}_motion" webhook_id = config_entry.runtime_data.host.webhook_id + unique_id = config_entry.runtime_data.host.unique_id signal_all = MagicMock() signal_ch = MagicMock() - async_dispatcher_connect(hass, f"{webhook_id}_all", signal_all) - async_dispatcher_connect(hass, f"{webhook_id}_0", signal_ch) + async_dispatcher_connect(hass, f"{unique_id}_all", signal_all) + async_dispatcher_connect(hass, f"{unique_id}_0", signal_ch) client = await hass_client_no_auth() + assert hass.states.get(entity_id).state == STATE_OFF + # test webhook callback success all channels + reolink_connect.motion_detected.return_value = True reolink_connect.ONVIF_event_callback.return_value = None await client.post(f"/api/webhook/{webhook_id}") signal_all.assert_called_once() + assert hass.states.get(entity_id).state == STATE_ON freezer.tick(timedelta(seconds=FIRST_ONVIF_TIMEOUT)) async_fire_time_changed(hass) @@ -120,10 +131,14 @@ async def test_webhook_callback( await client.post(f"/api/webhook/{webhook_id}") signal_all.assert_not_called() + assert hass.states.get(entity_id).state == STATE_ON + # test webhook callback success single channel + reolink_connect.motion_detected.return_value = False reolink_connect.ONVIF_event_callback.return_value = [0] await client.post(f"/api/webhook/{webhook_id}", data="test_data") signal_ch.assert_called_once() + assert hass.states.get(entity_id).state == STATE_OFF # test webhook callback single channel with error in event callback signal_ch.reset_mock() From f1ebda7c6f6b0ad9e5533b4b99916cc12442d6fb Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:10:58 +0100 Subject: [PATCH 068/103] Instantiate new httpx client for lamarzocco (#132016) --- homeassistant/components/lamarzocco/__init__.py | 8 ++++---- homeassistant/components/lamarzocco/config_flow.py | 9 +++++++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lamarzocco/__init__.py b/homeassistant/components/lamarzocco/__init__.py index da513bc8cff..5de9a2eeed4 100644 --- a/homeassistant/components/lamarzocco/__init__.py +++ b/homeassistant/components/lamarzocco/__init__.py @@ -23,7 +23,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant from homeassistant.helpers import issue_registry as ir -from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.helpers.httpx_client import create_async_httpx_client from .const import CONF_USE_BLUETOOTH, DOMAIN from .coordinator import LaMarzoccoConfigEntry, LaMarzoccoUpdateCoordinator @@ -47,11 +47,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) - assert entry.unique_id serial = entry.unique_id - + client = create_async_httpx_client(hass) cloud_client = LaMarzoccoCloudClient( username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], - client=get_async_client(hass), + client=client, ) # initialize local API @@ -61,7 +61,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: LaMarzoccoConfigEntry) - local_client = LaMarzoccoLocalClient( host=host, local_bearer=entry.data[CONF_TOKEN], - client=get_async_client(hass), + client=client, ) # initialize Bluetooth diff --git a/homeassistant/components/lamarzocco/config_flow.py b/homeassistant/components/lamarzocco/config_flow.py index a727e3fe357..c01b55fb885 100644 --- a/homeassistant/components/lamarzocco/config_flow.py +++ b/homeassistant/components/lamarzocco/config_flow.py @@ -6,6 +6,7 @@ from collections.abc import Mapping import logging from typing import Any +from httpx import AsyncClient from pylamarzocco.client_cloud import LaMarzoccoCloudClient from pylamarzocco.client_local import LaMarzoccoLocalClient from pylamarzocco.exceptions import AuthFail, RequestNotSuccessful @@ -37,7 +38,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.httpx_client import get_async_client +from homeassistant.helpers.httpx_client import create_async_httpx_client from homeassistant.helpers.selector import ( SelectOptionDict, SelectSelector, @@ -57,6 +58,8 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 2 + _client: AsyncClient + def __init__(self) -> None: """Initialize the config flow.""" self._config: dict[str, Any] = {} @@ -79,10 +82,12 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): **user_input, **self._discovered, } + self._client = create_async_httpx_client(self.hass) cloud_client = LaMarzoccoCloudClient( username=data[CONF_USERNAME], password=data[CONF_PASSWORD], + client=self._client, ) try: self._fleet = await cloud_client.get_customer_fleet() @@ -163,7 +168,7 @@ class LmConfigFlow(ConfigFlow, domain=DOMAIN): # validate local connection if host is provided if user_input.get(CONF_HOST): if not await LaMarzoccoLocalClient.validate_connection( - client=get_async_client(self.hass), + client=self._client, host=user_input[CONF_HOST], token=selected_device.communication_key, ): From f44103ac7f258662cd6df7b5ff0ec43bd4a7c033 Mon Sep 17 00:00:00 2001 From: Jan Rieger <271149+jrieger@users.noreply.github.com> Date: Mon, 2 Dec 2024 13:28:54 +0100 Subject: [PATCH 069/103] Add translated native unit of measurement to Jellyfin (#132055) --- homeassistant/components/jellyfin/sensor.py | 1 - homeassistant/components/jellyfin/strings.json | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/jellyfin/sensor.py b/homeassistant/components/jellyfin/sensor.py index 24aeecab7e5..5c519f661ee 100644 --- a/homeassistant/components/jellyfin/sensor.py +++ b/homeassistant/components/jellyfin/sensor.py @@ -36,7 +36,6 @@ SENSOR_TYPES: tuple[JellyfinSensorEntityDescription, ...] = ( key="watching", translation_key="watching", value_fn=_count_now_playing, - native_unit_of_measurement="clients", ), ) diff --git a/homeassistant/components/jellyfin/strings.json b/homeassistant/components/jellyfin/strings.json index f2afa0c8ad5..a9816b1fb78 100644 --- a/homeassistant/components/jellyfin/strings.json +++ b/homeassistant/components/jellyfin/strings.json @@ -29,7 +29,8 @@ "entity": { "sensor": { "watching": { - "name": "Active clients" + "name": "Active clients", + "unit_of_measurement": "clients" } } }, From d3a577ad894b445f066e243a14e9150fa5d5d7f5 Mon Sep 17 00:00:00 2001 From: Simone Rescio Date: Mon, 2 Dec 2024 13:18:53 +0100 Subject: [PATCH 070/103] Bump pyezviz to 0.2.2.3 (#132060) --- homeassistant/components/ezviz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ezviz/manifest.json b/homeassistant/components/ezviz/manifest.json index 53976bf3002..7c796c74ef7 100644 --- a/homeassistant/components/ezviz/manifest.json +++ b/homeassistant/components/ezviz/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/ezviz", "iot_class": "cloud_polling", "loggers": ["paho_mqtt", "pyezviz"], - "requirements": ["pyezviz==0.2.1.2"] + "requirements": ["pyezviz==0.2.2.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0100b67d785..21611e914a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1907,7 +1907,7 @@ pyeverlights==0.1.0 pyevilgenius==2.0.0 # homeassistant.components.ezviz -pyezviz==0.2.1.2 +pyezviz==0.2.2.3 # homeassistant.components.fibaro pyfibaro==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c938702efb7..647f309cbbb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1536,7 +1536,7 @@ pyeverlights==0.1.0 pyevilgenius==2.0.0 # homeassistant.components.ezviz -pyezviz==0.2.1.2 +pyezviz==0.2.2.3 # homeassistant.components.fibaro pyfibaro==0.8.0 From 3f1286b3383832eb96d6145ae140178ad0be9796 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Mon, 2 Dec 2024 15:27:44 +0100 Subject: [PATCH 071/103] Set connections on device for acaia (#132064) --- homeassistant/components/acaia/entity.py | 7 ++++++- tests/components/acaia/snapshots/test_init.ambr | 4 ++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/acaia/entity.py b/homeassistant/components/acaia/entity.py index db01b414b99..bef1ac313ca 100644 --- a/homeassistant/components/acaia/entity.py +++ b/homeassistant/components/acaia/entity.py @@ -2,7 +2,11 @@ from dataclasses import dataclass -from homeassistant.helpers.device_registry import DeviceInfo, format_mac +from homeassistant.helpers.device_registry import ( + CONNECTION_BLUETOOTH, + DeviceInfo, + format_mac, +) from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -33,6 +37,7 @@ class AcaiaEntity(CoordinatorEntity[AcaiaCoordinator]): manufacturer="Acaia", model=self._scale.model, suggested_area="Kitchen", + connections={(CONNECTION_BLUETOOTH, self._scale.mac)}, ) @property diff --git a/tests/components/acaia/snapshots/test_init.ambr b/tests/components/acaia/snapshots/test_init.ambr index 1cc3d8dbbc0..7011b20f68c 100644 --- a/tests/components/acaia/snapshots/test_init.ambr +++ b/tests/components/acaia/snapshots/test_init.ambr @@ -5,6 +5,10 @@ 'config_entries': , 'configuration_url': None, 'connections': set({ + tuple( + 'bluetooth', + 'aa:bb:cc:dd:ee:ff', + ), }), 'disabled_by': None, 'entry_type': None, From 895ffbabf734ae9f386ddcfd1375a669317d9850 Mon Sep 17 00:00:00 2001 From: Duco Sebel <74970928+DCSBL@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:04:39 +0100 Subject: [PATCH 072/103] Round status light brightness number in HomeWizard (#132069) --- homeassistant/components/homewizard/number.py | 2 +- tests/components/homewizard/snapshots/test_number.ambr | 4 ++-- tests/components/homewizard/test_number.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homewizard/number.py b/homeassistant/components/homewizard/number.py index 1b4a0643dbe..1ed4c642f6b 100644 --- a/homeassistant/components/homewizard/number.py +++ b/homeassistant/components/homewizard/number.py @@ -64,4 +64,4 @@ class HWEnergyNumberEntity(HomeWizardEntity, NumberEntity): or (brightness := self.coordinator.data.state.brightness) is None ): return None - return brightness_to_value((0, 100), brightness) + return round(brightness_to_value((0, 100), brightness)) diff --git a/tests/components/homewizard/snapshots/test_number.ambr b/tests/components/homewizard/snapshots/test_number.ambr index 49f23cf8e2f..b14028cd97c 100644 --- a/tests/components/homewizard/snapshots/test_number.ambr +++ b/tests/components/homewizard/snapshots/test_number.ambr @@ -14,7 +14,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '100.0', + 'state': '100', }) # --- # name: test_number_entities[HWE-SKT-11].1 @@ -106,7 +106,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '100.0', + 'state': '100', }) # --- # name: test_number_entities[HWE-SKT-21].1 diff --git a/tests/components/homewizard/test_number.py b/tests/components/homewizard/test_number.py index ddadf09bb6e..623ba018dee 100644 --- a/tests/components/homewizard/test_number.py +++ b/tests/components/homewizard/test_number.py @@ -42,7 +42,7 @@ async def test_number_entities( assert snapshot == device_entry # Test unknown handling - assert state.state == "100.0" + assert state.state == "100" mock_homewizardenergy.state.return_value.brightness = None From c6468aca2b1bfc20b4f843f7f28ae721088c6428 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Mon, 2 Dec 2024 21:54:57 +0100 Subject: [PATCH 073/103] Mark trend sensor unavailable when source entity is unknown/unavailable (#132080) --- .../components/trend/binary_sensor.py | 9 +++- tests/components/trend/test_binary_sensor.py | 44 ++++++++++++++++++- 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/trend/binary_sensor.py b/homeassistant/components/trend/binary_sensor.py index 681680f180f..9691ecf0744 100644 --- a/homeassistant/components/trend/binary_sensor.py +++ b/homeassistant/components/trend/binary_sensor.py @@ -227,10 +227,15 @@ class SensorTrend(BinarySensorEntity, RestoreEntity): state = new_state.attributes.get(self._attribute) else: state = new_state.state - if state not in (STATE_UNKNOWN, STATE_UNAVAILABLE): + + if state in (STATE_UNKNOWN, STATE_UNAVAILABLE): + self._attr_available = False + else: + self._attr_available = True sample = (new_state.last_updated.timestamp(), float(state)) # type: ignore[arg-type] self.samples.append(sample) - self.async_schedule_update_ha_state(True) + + self.async_schedule_update_ha_state(True) except (ValueError, TypeError) as ex: _LOGGER.error(ex) diff --git a/tests/components/trend/test_binary_sensor.py b/tests/components/trend/test_binary_sensor.py index ad85f65a9fc..4a829bb86d2 100644 --- a/tests/components/trend/test_binary_sensor.py +++ b/tests/components/trend/test_binary_sensor.py @@ -9,7 +9,7 @@ import pytest from homeassistant import setup from homeassistant.components.trend.const import DOMAIN -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNKNOWN +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant, State from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component @@ -395,3 +395,45 @@ async def test_device_id( trend_entity = entity_registry.async_get("binary_sensor.trend") assert trend_entity is not None assert trend_entity.device_id == source_entity.device_id + + +@pytest.mark.parametrize( + "error_state", + [ + STATE_UNKNOWN, + STATE_UNAVAILABLE, + ], +) +async def test_unavailable_source( + hass: HomeAssistant, + config_entry: MockConfigEntry, + freezer: FrozenDateTimeFactory, + setup_component: ComponentSetup, + error_state: str, +) -> None: + """Test for unavailable source.""" + await setup_component( + { + "sample_duration": 10000, + "min_gradient": 1, + "max_samples": 25, + "min_samples": 5, + }, + ) + + for val in (10, 20, 30, 40, 50, 60): + freezer.tick(timedelta(seconds=2)) + hass.states.async_set("sensor.test_state", val) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test_trend_sensor").state == "on" + + hass.states.async_set("sensor.test_state", error_state) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test_trend_sensor").state == STATE_UNAVAILABLE + + hass.states.async_set("sensor.test_state", 50) + await hass.async_block_till_done() + + assert hass.states.get("binary_sensor.test_trend_sensor").state == "on" From ab5165fdfa31a683ca22b73b1e2ad06f64e99188 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 3 Dec 2024 13:06:54 +0100 Subject: [PATCH 074/103] Fix imap sensor in case of alternative empty search response (#132081) --- homeassistant/components/imap/coordinator.py | 12 +++++++++++- tests/components/imap/const.py | 2 ++ tests/components/imap/test_init.py | 13 +++++++++++-- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index a9d0fdfbd48..2726b47a679 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -332,7 +332,17 @@ class ImapDataUpdateCoordinator(DataUpdateCoordinator[int | None]): raise UpdateFailed( f"Invalid response for search '{self.config_entry.data[CONF_SEARCH]}': {result} / {lines[0]}" ) - if not (count := len(message_ids := lines[0].split())): + # Check we do have returned items. + # + # In rare cases, when no UID's are returned, + # only the status line is returned, and not an empty line. + # See: https://github.com/home-assistant/core/issues/132042 + # + # Strictly the RfC notes that 0 or more numbers should be returned + # delimited by a space. + # + # See: https://datatracker.ietf.org/doc/html/rfc3501#section-7.2.5 + if len(lines) == 1 or not (count := len(message_ids := lines[0].split())): self._last_message_uid = None return 0 last_message_uid = ( diff --git a/tests/components/imap/const.py b/tests/components/imap/const.py index 037960c9e5d..8f6761bd795 100644 --- a/tests/components/imap/const.py +++ b/tests/components/imap/const.py @@ -141,6 +141,8 @@ TEST_CONTENT_MULTIPART_BASE64_INVALID = ( ) EMPTY_SEARCH_RESPONSE = ("OK", [b"", b"Search completed (0.0001 + 0.000 secs)."]) +EMPTY_SEARCH_RESPONSE_ALT = ("OK", [b"Search completed (0.0001 + 0.000 secs)."]) + BAD_RESPONSE = ("BAD", [b"", b"Unexpected error"]) TEST_SEARCH_RESPONSE = ("OK", [b"1", b"Search completed (0.0001 + 0.000 secs)."]) diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index 7bdfc44571a..d4281b9e513 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -20,6 +20,7 @@ from homeassistant.util.dt import utcnow from .const import ( BAD_RESPONSE, EMPTY_SEARCH_RESPONSE, + EMPTY_SEARCH_RESPONSE_ALT, TEST_BADLY_ENCODED_CONTENT, TEST_FETCH_RESPONSE_BINARY, TEST_FETCH_RESPONSE_HTML, @@ -517,6 +518,11 @@ async def test_fetch_number_of_messages( assert state.state == STATE_UNAVAILABLE +@pytest.mark.parametrize( + "empty_search_reponse", + [EMPTY_SEARCH_RESPONSE, EMPTY_SEARCH_RESPONSE_ALT], + ids=["regular_empty_search_response", "alt_empty_search_response"], +) @pytest.mark.parametrize("imap_search", [TEST_SEARCH_RESPONSE]) @pytest.mark.parametrize( ("imap_fetch", "valid_date"), @@ -525,7 +531,10 @@ async def test_fetch_number_of_messages( ) @pytest.mark.parametrize("imap_has_capability", [True, False], ids=["push", "poll"]) async def test_reset_last_message( - hass: HomeAssistant, mock_imap_protocol: MagicMock, valid_date: bool + hass: HomeAssistant, + mock_imap_protocol: MagicMock, + valid_date: bool, + empty_search_reponse: tuple[str, list[bytes]], ) -> None: """Test receiving a message successfully.""" event = asyncio.Event() # needed for pushed coordinator to make a new loop @@ -580,7 +589,7 @@ async def test_reset_last_message( ) # Simulate an update where no messages are found (needed for pushed coordinator) - mock_imap_protocol.search.return_value = Response(*EMPTY_SEARCH_RESPONSE) + mock_imap_protocol.search.return_value = Response(*empty_search_reponse) # Make sure we have an update async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) From 2aea7380320d259325aad07a062a15a8ae2a9768 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Mon, 2 Dec 2024 13:09:35 -0600 Subject: [PATCH 075/103] Bump hassil and intents (#132092) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- script/hassfest/docker/Dockerfile | 2 +- tests/components/conversation/snapshots/test_http.ambr | 4 ++-- tests/testing_config/custom_sentences/en/beer.yaml | 4 ++-- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 26265a37cce..2d2f2f58a3a 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==2.0.4", "home-assistant-intents==2024.11.27"] + "requirements": ["hassil==2.0.5", "home-assistant-intents==2024.12.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4b6777417e9..1e43a098712 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -32,10 +32,10 @@ go2rtc-client==0.1.1 ha-ffmpeg==3.2.2 habluetooth==3.6.0 hass-nabucasa==0.85.0 -hassil==2.0.4 +hassil==2.0.5 home-assistant-bluetooth==1.13.0 home-assistant-frontend==20241127.1 -home-assistant-intents==2024.11.27 +home-assistant-intents==2024.12.2 httpx==0.27.2 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index 21611e914a0..60d4ac61701 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1096,7 +1096,7 @@ hass-nabucasa==0.85.0 hass-splunk==0.1.1 # homeassistant.components.conversation -hassil==2.0.4 +hassil==2.0.5 # homeassistant.components.jewish_calendar hdate==0.11.1 @@ -1133,7 +1133,7 @@ holidays==0.61 home-assistant-frontend==20241127.1 # homeassistant.components.conversation -home-assistant-intents==2024.11.27 +home-assistant-intents==2024.12.2 # homeassistant.components.home_connect homeconnect==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 647f309cbbb..cd2504a6d68 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -931,7 +931,7 @@ habluetooth==3.6.0 hass-nabucasa==0.85.0 # homeassistant.components.conversation -hassil==2.0.4 +hassil==2.0.5 # homeassistant.components.jewish_calendar hdate==0.11.1 @@ -959,7 +959,7 @@ holidays==0.61 home-assistant-frontend==20241127.1 # homeassistant.components.conversation -home-assistant-intents==2024.11.27 +home-assistant-intents==2024.12.2 # homeassistant.components.home_connect homeconnect==0.8.0 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index e11ffca025d..044635d2d58 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -23,7 +23,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.5.4,source=/uv,target=/bin/uv \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ stdlib-list==0.10.0 pipdeptree==2.23.4 tqdm==4.66.5 ruff==0.8.0 \ - PyTurboJPEG==1.7.5 go2rtc-client==0.1.1 ha-ffmpeg==3.2.2 hassil==2.0.4 home-assistant-intents==2024.11.27 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 + PyTurboJPEG==1.7.5 go2rtc-client==0.1.1 ha-ffmpeg==3.2.2 hassil==2.0.4 home-assistant-intents==2024.12.2 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " diff --git a/tests/components/conversation/snapshots/test_http.ambr b/tests/components/conversation/snapshots/test_http.ambr index 966abd63d78..a3edd4fa51c 100644 --- a/tests/components/conversation/snapshots/test_http.ambr +++ b/tests/components/conversation/snapshots/test_http.ambr @@ -535,7 +535,7 @@ 'name': 'HassTurnOn', }), 'match': True, - 'sentence_template': ' on [all] in ', + 'sentence_template': ' on [] ', 'slots': dict({ 'area': 'kitchen', 'domain': 'light', @@ -606,7 +606,7 @@ 'name': 'OrderBeer', }), 'match': True, - 'sentence_template': "I'd like to order a {beer_style} [please]", + 'sentence_template': "[I'd like to ]order a {beer_style} [please]", 'slots': dict({ 'beer_style': 'lager', }), diff --git a/tests/testing_config/custom_sentences/en/beer.yaml b/tests/testing_config/custom_sentences/en/beer.yaml index f318e0221b2..7222ffcb0ca 100644 --- a/tests/testing_config/custom_sentences/en/beer.yaml +++ b/tests/testing_config/custom_sentences/en/beer.yaml @@ -3,11 +3,11 @@ intents: OrderBeer: data: - sentences: - - "I'd like to order a {beer_style} [please]" + - "[I'd like to ]order a {beer_style} [please]" OrderFood: data: - sentences: - - "I'd like to order {food_name:name} [please]" + - "[I'd like to ]order {food_name:name} [please]" lists: beer_style: values: From f480cc3396c3370c3476dc78ad2cb2bd4ea701b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ab=C3=ADlio=20Costa?= Date: Tue, 3 Dec 2024 16:08:09 +0000 Subject: [PATCH 076/103] Use translations on NumberEntity unit_of_measurement property (#132095) Co-authored-by: Martin Hjelmare --- homeassistant/components/number/__init__.py | 12 ++++ homeassistant/components/sensor/__init__.py | 16 ----- homeassistant/helpers/entity.py | 16 +++++ tests/components/number/test_init.py | 65 ++++++++++++++++++++- 4 files changed, 92 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/number/__init__.py b/homeassistant/components/number/__init__.py index dc169fcb348..9f4aef08aa9 100644 --- a/homeassistant/components/number/__init__.py +++ b/homeassistant/components/number/__init__.py @@ -384,6 +384,18 @@ class NumberEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): ): return self.hass.config.units.temperature_unit + if (translation_key := self._unit_of_measurement_translation_key) and ( + unit_of_measurement + := self.platform.default_language_platform_translations.get(translation_key) + ): + if native_unit_of_measurement is not None: + raise ValueError( + f"Number entity {type(self)} from integration '{self.platform.platform_name}' " + f"has a translation key for unit_of_measurement '{unit_of_measurement}', " + f"but also has a native_unit_of_measurement '{native_unit_of_measurement}'" + ) + return unit_of_measurement + return native_unit_of_measurement @cached_property diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 1e3b5d10c98..a0220c23d9d 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -504,22 +504,6 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_): return self.entity_description.suggested_unit_of_measurement return None - @cached_property - def _unit_of_measurement_translation_key(self) -> str | None: - """Return translation key for unit of measurement.""" - if self.translation_key is None: - return None - if self.platform is None: - raise ValueError( - f"Sensor {type(self)} cannot have a translation key for " - "unit of measurement before being added to the entity platform" - ) - platform = self.platform - return ( - f"component.{platform.platform_name}.entity.{platform.domain}" - f".{self.translation_key}.unit_of_measurement" - ) - @final @property @override diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 1f77dd3f95c..19076c4edc0 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -647,6 +647,22 @@ class Entity( f".{self.translation_key}.name" ) + @cached_property + def _unit_of_measurement_translation_key(self) -> str | None: + """Return translation key for unit of measurement.""" + if self.translation_key is None: + return None + if self.platform is None: + raise ValueError( + f"Entity {type(self)} cannot have a translation key for " + "unit of measurement before being added to the entity platform" + ) + platform = self.platform + return ( + f"component.{platform.platform_name}.entity.{platform.domain}" + f".{self.translation_key}.unit_of_measurement" + ) + def _substitute_name_placeholders(self, name: str) -> str: """Substitute placeholders in entity name.""" try: diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 721b531e8cd..31d99dc55d7 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -2,7 +2,7 @@ from collections.abc import Generator from typing import Any -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import pytest @@ -836,6 +836,69 @@ async def test_custom_unit_change( assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == default_unit +async def test_translated_unit( + hass: HomeAssistant, +) -> None: + """Test translated unit.""" + + with patch( + "homeassistant.helpers.service.translation.async_get_translations", + return_value={ + "component.test.entity.number.test_translation_key.unit_of_measurement": "Tests" + }, + ): + entity0 = common.MockNumberEntity( + name="Test", + native_value=123, + unique_id="very_unique", + ) + entity0.entity_description = NumberEntityDescription( + "test", + translation_key="test_translation_key", + ) + setup_test_component_platform(hass, DOMAIN, [entity0]) + + assert await async_setup_component( + hass, "number", {"number": {"platform": "test"}} + ) + await hass.async_block_till_done() + + entity_id = entity0.entity_id + state = hass.states.get(entity_id) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "Tests" + + +async def test_translated_unit_with_native_unit_raises( + hass: HomeAssistant, +) -> None: + """Test that translated unit.""" + + with patch( + "homeassistant.helpers.service.translation.async_get_translations", + return_value={ + "component.test.entity.number.test_translation_key.unit_of_measurement": "Tests" + }, + ): + entity0 = common.MockNumberEntity( + name="Test", + native_value=123, + unique_id="very_unique", + ) + entity0.entity_description = NumberEntityDescription( + "test", + translation_key="test_translation_key", + native_unit_of_measurement="bad_unit", + ) + setup_test_component_platform(hass, DOMAIN, [entity0]) + + assert await async_setup_component( + hass, "number", {"number": {"platform": "test"}} + ) + await hass.async_block_till_done() + # Setup fails so entity_id is None + assert entity0.entity_id is None + + def test_device_classes_aligned() -> None: """Make sure all sensor device classes are also available in NumberDeviceClass.""" From 54ec41f25d101401c22d745ba84a681c55d43c1d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 3 Dec 2024 09:03:43 -0600 Subject: [PATCH 077/103] Bump PyJWT to 2.10.1 (#132100) --- 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 1e43a098712..7d8a116e794 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -47,7 +47,7 @@ paho-mqtt==1.6.1 Pillow==11.0.0 propcache==0.2.1 psutil-home-assistant==0.0.1 -PyJWT==2.10.0 +PyJWT==2.10.1 pymicro-vad==1.0.1 PyNaCl==1.5.0 pyOpenSSL==24.2.1 diff --git a/pyproject.toml b/pyproject.toml index 2f86ff4a6c2..64795d19f69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ dependencies = [ "ifaddr==0.2.0", "Jinja2==3.1.4", "lru-dict==1.3.0", - "PyJWT==2.10.0", + "PyJWT==2.10.1", # PyJWT has loose dependency. We want the latest one. "cryptography==43.0.1", "Pillow==11.0.0", diff --git a/requirements.txt b/requirements.txt index 0f5047a0bbb..7aadd55c024 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,7 @@ home-assistant-bluetooth==1.13.0 ifaddr==0.2.0 Jinja2==3.1.4 lru-dict==1.3.0 -PyJWT==2.10.0 +PyJWT==2.10.1 cryptography==43.0.1 Pillow==11.0.0 propcache==0.2.1 From 155fafb735d4322437a878e1fe6f4a361674b9dd Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 3 Dec 2024 14:08:31 +0100 Subject: [PATCH 078/103] Update frontend to 20241127.2 (#132109) Co-authored-by: Franck Nijhof --- 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 7bd500f17ea..f59ca05ba55 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==20241127.1"] + "requirements": ["home-assistant-frontend==20241127.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7d8a116e794..4839c71327e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ habluetooth==3.6.0 hass-nabucasa==0.85.0 hassil==2.0.5 home-assistant-bluetooth==1.13.0 -home-assistant-frontend==20241127.1 +home-assistant-frontend==20241127.2 home-assistant-intents==2024.12.2 httpx==0.27.2 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 60d4ac61701..e7408b51686 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1130,7 +1130,7 @@ hole==0.8.0 holidays==0.61 # homeassistant.components.frontend -home-assistant-frontend==20241127.1 +home-assistant-frontend==20241127.2 # homeassistant.components.conversation home-assistant-intents==2024.12.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd2504a6d68..2b79b7423ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -956,7 +956,7 @@ hole==0.8.0 holidays==0.61 # homeassistant.components.frontend -home-assistant-frontend==20241127.1 +home-assistant-frontend==20241127.2 # homeassistant.components.conversation home-assistant-intents==2024.12.2 From 0a38af7e48a9eebad8868015eea45a0d28a29835 Mon Sep 17 00:00:00 2001 From: Tobias Perschon Date: Tue, 3 Dec 2024 13:33:47 +0100 Subject: [PATCH 079/103] Bump unifi_ap to 0.0.2 (#132125) --- homeassistant/components/unifi_direct/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifi_direct/manifest.json b/homeassistant/components/unifi_direct/manifest.json index 775279c64e2..aa696985dbe 100644 --- a/homeassistant/components/unifi_direct/manifest.json +++ b/homeassistant/components/unifi_direct/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_polling", "loggers": ["unifi_ap"], "quality_scale": "legacy", - "requirements": ["unifi_ap==0.0.1"] + "requirements": ["unifi_ap==0.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index e7408b51686..a57451f892c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2906,7 +2906,7 @@ ultraheat-api==0.5.7 unifi-discovery==1.2.0 # homeassistant.components.unifi_direct -unifi_ap==0.0.1 +unifi_ap==0.0.2 # homeassistant.components.unifiled unifiled==0.11 From 07196b0fdaa5feabf4db7573e598b66906565512 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 3 Dec 2024 00:08:51 -0500 Subject: [PATCH 080/103] Fix bad hassil tests on CI (#132132) * Fix CI * Fix whitespace --------- Co-authored-by: Michael Hansen --- .../conversation/snapshots/test_default_agent.ambr | 6 +++--- tests/components/conversation/test_default_agent.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/components/conversation/snapshots/test_default_agent.ambr b/tests/components/conversation/snapshots/test_default_agent.ambr index b1f2ea0db75..f1e220b10b2 100644 --- a/tests/components/conversation/snapshots/test_default_agent.ambr +++ b/tests/components/conversation/snapshots/test_default_agent.ambr @@ -308,7 +308,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called late added light', + 'speech': 'Sorry, I am not aware of any area called late added', }), }), }), @@ -378,7 +378,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called kitchen light', + 'speech': 'Sorry, I am not aware of any area called kitchen', }), }), }), @@ -428,7 +428,7 @@ 'speech': dict({ 'plain': dict({ 'extra_data': None, - 'speech': 'Sorry, I am not aware of any device called renamed light', + 'speech': 'Sorry, I am not aware of any area called renamed', }), }), }), diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 6990ffe7717..00c47b42629 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -2930,7 +2930,7 @@ async def test_intent_cache_fuzzy(hass: HomeAssistant) -> None: ) result = await agent.async_recognize_intent(user_input) assert result is not None - assert result.unmatched_entities["name"].text == "test light" + assert result.unmatched_entities["area"].text == "test " # Mark this result so we know it is from cache next time mark = "_from_cache" From 8a310cbbf8ffd4620acea8c27079ba0cab1126b9 Mon Sep 17 00:00:00 2001 From: Tobias Perschon Date: Tue, 3 Dec 2024 13:34:13 +0100 Subject: [PATCH 081/103] Improve error logging for unifi-ap (#132141) --- homeassistant/components/unifi_direct/device_tracker.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/unifi_direct/device_tracker.py b/homeassistant/components/unifi_direct/device_tracker.py index 144cbd4dec7..d5e2e926114 100644 --- a/homeassistant/components/unifi_direct/device_tracker.py +++ b/homeassistant/components/unifi_direct/device_tracker.py @@ -67,11 +67,11 @@ class UnifiDeviceScanner(DeviceScanner): """Update the client info from AP.""" try: self.clients = self.ap.get_clients() - except UniFiAPConnectionException: - _LOGGER.error("Failed to connect to accesspoint") + except UniFiAPConnectionException as e: + _LOGGER.error("Failed to connect to accesspoint: %s", str(e)) return False - except UniFiAPDataException: - _LOGGER.error("Failed to get proper response from accesspoint") + except UniFiAPDataException as e: + _LOGGER.error("Failed to get proper response from accesspoint: %s", str(e)) return False return True From b7038d4eb7691163b539a42631e9b6c1f1ffa4b9 Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Tue, 3 Dec 2024 17:03:43 +0100 Subject: [PATCH 082/103] Bump uiprotect to 6.6.5 (#132147) --- 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 9730c1e3741..e8a8c062800 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -40,7 +40,7 @@ "integration_type": "hub", "iot_class": "local_push", "loggers": ["uiprotect", "unifi_discovery"], - "requirements": ["uiprotect==6.6.4", "unifi-discovery==1.2.0"], + "requirements": ["uiprotect==6.6.5", "unifi-discovery==1.2.0"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index a57451f892c..ede3290e322 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2897,7 +2897,7 @@ typedmonarchmoney==0.3.1 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==6.6.4 +uiprotect==6.6.5 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2b79b7423ec..6209f1e71a6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2310,7 +2310,7 @@ typedmonarchmoney==0.3.1 uasiren==0.0.1 # homeassistant.components.unifiprotect -uiprotect==6.6.4 +uiprotect==6.6.5 # homeassistant.components.landisgyr_heat_meter ultraheat-api==0.5.7 From 79352ea0f068a3ea6487c7330c2d7876acaf2bc9 Mon Sep 17 00:00:00 2001 From: Jon Seager Date: Tue, 3 Dec 2024 12:34:50 +0000 Subject: [PATCH 083/103] Bump pytouchlinesl to 0.3.0 (#132157) --- homeassistant/components/touchline_sl/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/touchline_sl/manifest.json b/homeassistant/components/touchline_sl/manifest.json index ca3136f55c0..ab07ae770fd 100644 --- a/homeassistant/components/touchline_sl/manifest.json +++ b/homeassistant/components/touchline_sl/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/touchline_sl", "integration_type": "hub", "iot_class": "cloud_polling", - "requirements": ["pytouchlinesl==0.2.0"] + "requirements": ["pytouchlinesl==0.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index ede3290e322..2cca98afc4b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2435,7 +2435,7 @@ pytomorrowio==0.3.6 pytouchline==0.7 # homeassistant.components.touchline_sl -pytouchlinesl==0.2.0 +pytouchlinesl==0.3.0 # homeassistant.components.traccar # homeassistant.components.traccar_server diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6209f1e71a6..528b9c25068 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1947,7 +1947,7 @@ pytile==2023.12.0 pytomorrowio==0.3.6 # homeassistant.components.touchline_sl -pytouchlinesl==0.2.0 +pytouchlinesl==0.3.0 # homeassistant.components.traccar # homeassistant.components.traccar_server From 08773cefb7aa08496d83460f08fdef9abfd1a99b Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Tue, 3 Dec 2024 15:01:35 +0100 Subject: [PATCH 084/103] Pin rpds-py to 0.21.0 to fix CI (#132170) * Pin rpds-py==0.21.0 to fix CI * Add carriage return --- homeassistant/package_constraints.txt | 5 +++++ script/gen_requirements_all.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4839c71327e..46df2892653 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -205,3 +205,8 @@ async-timeout==4.0.3 # https://github.com/home-assistant/core/issues/122508 # https://github.com/home-assistant/core/issues/118004 aiofiles>=24.1.0 + +# 0.22.0 causes CI failures on Python 3.13 +# python3 -X dev -m pytest tests/components/matrix +# python3 -X dev -m pytest tests/components/zha +rpds-py==0.21.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 97ffcac79a4..450469096ea 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -238,6 +238,11 @@ async-timeout==4.0.3 # https://github.com/home-assistant/core/issues/122508 # https://github.com/home-assistant/core/issues/118004 aiofiles>=24.1.0 + +# 0.22.0 causes CI failures on Python 3.13 +# python3 -X dev -m pytest tests/components/matrix +# python3 -X dev -m pytest tests/components/zha +rpds-py==0.21.0 """ GENERATED_MESSAGE = ( From ebffcb455fa750dc211aa07c7bd7a9a8972ce0e9 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 3 Dec 2024 16:13:15 +0100 Subject: [PATCH 085/103] Update frontend to 20241127.3 (#132176) --- 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 f59ca05ba55..264f0756b82 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==20241127.2"] + "requirements": ["home-assistant-frontend==20241127.3"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 46df2892653..af91964994e 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ habluetooth==3.6.0 hass-nabucasa==0.85.0 hassil==2.0.5 home-assistant-bluetooth==1.13.0 -home-assistant-frontend==20241127.2 +home-assistant-frontend==20241127.3 home-assistant-intents==2024.12.2 httpx==0.27.2 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2cca98afc4b..aeb999eef66 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1130,7 +1130,7 @@ hole==0.8.0 holidays==0.61 # homeassistant.components.frontend -home-assistant-frontend==20241127.2 +home-assistant-frontend==20241127.3 # homeassistant.components.conversation home-assistant-intents==2024.12.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 528b9c25068..af4688b7e3e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -956,7 +956,7 @@ hole==0.8.0 holidays==0.61 # homeassistant.components.frontend -home-assistant-frontend==20241127.2 +home-assistant-frontend==20241127.3 # homeassistant.components.conversation home-assistant-intents==2024.12.2 From 759a2b84f5c78522b915e4cfe8e51aae352f1167 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 3 Dec 2024 18:03:36 +0100 Subject: [PATCH 086/103] Bump version to 2024.12.0b5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 5617ab1d22a..075262346b4 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 64795d19f69..57523be4e68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.12.0b4" +version = "2024.12.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 33633f885d913e5a3191b9b1aae34673a8894ee2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 4 Dec 2024 09:59:28 +0100 Subject: [PATCH 087/103] Ran hassfest --- script/hassfest/docker/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 044635d2d58..26ca6475af7 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -23,7 +23,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.5.4,source=/uv,target=/bin/uv \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ stdlib-list==0.10.0 pipdeptree==2.23.4 tqdm==4.66.5 ruff==0.8.0 \ - PyTurboJPEG==1.7.5 go2rtc-client==0.1.1 ha-ffmpeg==3.2.2 hassil==2.0.4 home-assistant-intents==2024.12.2 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 + PyTurboJPEG==1.7.5 go2rtc-client==0.1.1 ha-ffmpeg==3.2.2 hassil==2.0.5 home-assistant-intents==2024.12.2 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " From 139b42471789454116bd08af8db8b00a08f094c5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 4 Dec 2024 14:56:42 +0100 Subject: [PATCH 088/103] Bump knocki to 0.4.2 (#129261) --- homeassistant/components/knocki/__init__.py | 5 ++--- homeassistant/components/knocki/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/knocki/__init__.py b/homeassistant/components/knocki/__init__.py index 42c3956bd68..dfdf060e3b5 100644 --- a/homeassistant/components/knocki/__init__.py +++ b/homeassistant/components/knocki/__init__.py @@ -41,13 +41,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: KnockiConfigEntry) -> bo await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - entry.async_create_background_task( - hass, client.start_websocket(), "knocki-websocket" - ) + await client.start_websocket() return True async def async_unload_entry(hass: HomeAssistant, entry: KnockiConfigEntry) -> bool: """Unload a config entry.""" + await entry.runtime_data.client.close() return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/knocki/manifest.json b/homeassistant/components/knocki/manifest.json index d9a45b18f0e..a91119ca831 100644 --- a/homeassistant/components/knocki/manifest.json +++ b/homeassistant/components/knocki/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["knocki"], - "requirements": ["knocki==0.3.5"] + "requirements": ["knocki==0.4.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index aeb999eef66..527b979ba51 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1250,7 +1250,7 @@ kegtron-ble==0.4.0 kiwiki-client==0.1.1 # homeassistant.components.knocki -knocki==0.3.5 +knocki==0.4.2 # homeassistant.components.knx knx-frontend==2024.11.16.205004 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af4688b7e3e..0260108e7c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1049,7 +1049,7 @@ justnimbus==0.7.4 kegtron-ble==0.4.0 # homeassistant.components.knocki -knocki==0.3.5 +knocki==0.4.2 # homeassistant.components.knx knx-frontend==2024.11.16.205004 From 66e3ffffa796a9d9a3432f8163b2ac350e3e7dd6 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 3 Dec 2024 22:24:39 +0100 Subject: [PATCH 089/103] Bump holidays to 0.62 (#132108) --- homeassistant/components/holiday/manifest.json | 2 +- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index a3c0a4514d3..7edc140da11 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.61", "babel==2.15.0"] + "requirements": ["holidays==0.62", "babel==2.15.0"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index ea08bfe1717..842c6f1f1ad 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.61"] + "requirements": ["holidays==0.62"] } diff --git a/requirements_all.txt b/requirements_all.txt index 527b979ba51..25111274e82 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1127,7 +1127,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.61 +holidays==0.62 # homeassistant.components.frontend home-assistant-frontend==20241127.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0260108e7c8..e3f573dc56c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -953,7 +953,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.61 +holidays==0.62 # homeassistant.components.frontend home-assistant-frontend==20241127.3 From 629c7a53ce8303f4f81c1e9f253f29899eadcc86 Mon Sep 17 00:00:00 2001 From: LG-ThinQ-Integration Date: Wed, 4 Dec 2024 09:46:36 +0900 Subject: [PATCH 090/103] Bump thinqconnect to 1.0.2 (#132131) Co-authored-by: yunseon.park --- homeassistant/components/lg_thinq/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/lg_thinq/manifest.json b/homeassistant/components/lg_thinq/manifest.json index daab1353098..6dd60909c66 100644 --- a/homeassistant/components/lg_thinq/manifest.json +++ b/homeassistant/components/lg_thinq/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/lg_thinq", "iot_class": "cloud_push", "loggers": ["thinqconnect"], - "requirements": ["thinqconnect==1.0.1"] + "requirements": ["thinqconnect==1.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 25111274e82..8f1a4ede8c1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2837,7 +2837,7 @@ thermopro-ble==0.10.0 thingspeak==1.0.0 # homeassistant.components.lg_thinq -thinqconnect==1.0.1 +thinqconnect==1.0.2 # homeassistant.components.tikteck tikteck==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3f573dc56c..afd21bede3c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2259,7 +2259,7 @@ thermobeacon-ble==0.7.0 thermopro-ble==0.10.0 # homeassistant.components.lg_thinq -thinqconnect==1.0.1 +thinqconnect==1.0.2 # homeassistant.components.tilt_ble tilt-ble==0.2.3 From 49c40cd9023d0979575be15d7586e75d8cfc8d57 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 4 Dec 2024 09:52:15 +0100 Subject: [PATCH 091/103] Track if intent was processed locally (#132166) --- .../components/assist_pipeline/pipeline.py | 8 +++++++- .../assist_pipeline/snapshots/test_init.ambr | 8 ++++++++ .../snapshots/test_websocket.ambr | 16 ++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/assist_pipeline/pipeline.py b/homeassistant/components/assist_pipeline/pipeline.py index 5bbc81adb86..9e9e84fb5d6 100644 --- a/homeassistant/components/assist_pipeline/pipeline.py +++ b/homeassistant/components/assist_pipeline/pipeline.py @@ -1018,6 +1018,7 @@ class PipelineRun: "intent_input": intent_input, "conversation_id": conversation_id, "device_id": device_id, + "prefer_local_intents": self.pipeline.prefer_local_intents, }, ) ) @@ -1031,6 +1032,7 @@ class PipelineRun: language=self.pipeline.language, agent_id=self.intent_agent, ) + processed_locally = self.intent_agent == conversation.HOME_ASSISTANT_AGENT conversation_result: conversation.ConversationResult | None = None if user_input.agent_id != conversation.HOME_ASSISTANT_AGENT: @@ -1061,6 +1063,7 @@ class PipelineRun: response=intent_response, conversation_id=user_input.conversation_id, ) + processed_locally = True if conversation_result is None: # Fall back to pipeline conversation agent @@ -1085,7 +1088,10 @@ class PipelineRun: self.process_event( PipelineEvent( PipelineEventType.INTENT_END, - {"intent_output": conversation_result.as_dict()}, + { + "processed_locally": processed_locally, + "intent_output": conversation_result.as_dict(), + }, ) ) diff --git a/tests/components/assist_pipeline/snapshots/test_init.ambr b/tests/components/assist_pipeline/snapshots/test_init.ambr index c70d3944f88..3b829e0e14a 100644 --- a/tests/components/assist_pipeline/snapshots/test_init.ambr +++ b/tests/components/assist_pipeline/snapshots/test_init.ambr @@ -37,6 +37,7 @@ 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en', + 'prefer_local_intents': False, }), 'type': , }), @@ -60,6 +61,7 @@ }), }), }), + 'processed_locally': True, }), 'type': , }), @@ -126,6 +128,7 @@ 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en-US', + 'prefer_local_intents': False, }), 'type': , }), @@ -149,6 +152,7 @@ }), }), }), + 'processed_locally': True, }), 'type': , }), @@ -215,6 +219,7 @@ 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en-US', + 'prefer_local_intents': False, }), 'type': , }), @@ -238,6 +243,7 @@ }), }), }), + 'processed_locally': True, }), 'type': , }), @@ -328,6 +334,7 @@ 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en', + 'prefer_local_intents': False, }), 'type': , }), @@ -351,6 +358,7 @@ }), }), }), + 'processed_locally': True, }), 'type': , }), diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index 566fb129959..41747a50eb6 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -36,6 +36,7 @@ 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en', + 'prefer_local_intents': False, }) # --- # name: test_audio_pipeline.4 @@ -58,6 +59,7 @@ }), }), }), + 'processed_locally': True, }) # --- # name: test_audio_pipeline.5 @@ -117,6 +119,7 @@ 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en', + 'prefer_local_intents': False, }) # --- # name: test_audio_pipeline_debug.4 @@ -139,6 +142,7 @@ }), }), }), + 'processed_locally': True, }) # --- # name: test_audio_pipeline_debug.5 @@ -210,6 +214,7 @@ 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en', + 'prefer_local_intents': False, }) # --- # name: test_audio_pipeline_with_enhancements.4 @@ -232,6 +237,7 @@ }), }), }), + 'processed_locally': True, }) # --- # name: test_audio_pipeline_with_enhancements.5 @@ -313,6 +319,7 @@ 'engine': 'conversation.home_assistant', 'intent_input': 'test transcript', 'language': 'en', + 'prefer_local_intents': False, }) # --- # name: test_audio_pipeline_with_wake_word_no_timeout.6 @@ -335,6 +342,7 @@ }), }), }), + 'processed_locally': True, }) # --- # name: test_audio_pipeline_with_wake_word_no_timeout.7 @@ -519,6 +527,7 @@ 'engine': 'conversation.home_assistant', 'intent_input': 'Are the lights on?', 'language': 'en', + 'prefer_local_intents': False, }) # --- # name: test_intent_failed.2 @@ -541,6 +550,7 @@ 'engine': 'conversation.home_assistant', 'intent_input': 'Are the lights on?', 'language': 'en', + 'prefer_local_intents': False, }) # --- # name: test_intent_timeout.2 @@ -569,6 +579,7 @@ 'engine': 'conversation.home_assistant', 'intent_input': 'never mind', 'language': 'en', + 'prefer_local_intents': False, }) # --- # name: test_pipeline_empty_tts_output.2 @@ -592,6 +603,7 @@ }), }), }), + 'processed_locally': True, }) # --- # name: test_pipeline_empty_tts_output.3 @@ -680,6 +692,7 @@ 'engine': 'conversation.home_assistant', 'intent_input': 'Are the lights on?', 'language': 'en', + 'prefer_local_intents': False, }) # --- # name: test_text_only_pipeline[extra_msg0].2 @@ -702,6 +715,7 @@ }), }), }), + 'processed_locally': True, }) # --- # name: test_text_only_pipeline[extra_msg0].3 @@ -724,6 +738,7 @@ 'engine': 'conversation.home_assistant', 'intent_input': 'Are the lights on?', 'language': 'en', + 'prefer_local_intents': False, }) # --- # name: test_text_only_pipeline[extra_msg1].2 @@ -746,6 +761,7 @@ }), }), }), + 'processed_locally': True, }) # --- # name: test_text_only_pipeline[extra_msg1].3 From 22b353f7d55d4322ebf4eb278e1d4028f4c95459 Mon Sep 17 00:00:00 2001 From: Pete Date: Wed, 4 Dec 2024 13:21:10 +0100 Subject: [PATCH 092/103] Fix recorder "year" period in leap year (#132167) * FIX: make "year" period work in leap year * Add test * Set second and microsecond to non-zero in test start times * FIX: better fix for leap year problem * Revert "FIX: better fix for leap year problem" This reverts commit 06aba46ec6a0a1e944c88fe99d9bc6181a73cc1c. --------- Co-authored-by: Erik --- homeassistant/components/recorder/util.py | 2 +- tests/components/recorder/test_util.py | 92 ++++++++++++++++------- 2 files changed, 67 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index a59519ef38d..125b354211e 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -902,7 +902,7 @@ def resolve_period( start_time = (start_time + timedelta(days=cal_offset * 366)).replace( month=1, day=1 ) - end_time = (start_time + timedelta(days=365)).replace(day=1) + end_time = (start_time + timedelta(days=366)).replace(day=1) start_time = dt_util.as_utc(start_time) end_time = dt_util.as_utc(end_time) diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 4904bdecc4d..7b8eef6b16f 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -9,6 +9,7 @@ import threading from typing import Any from unittest.mock import MagicMock, Mock, patch +from freezegun.api import FrozenDateTimeFactory import pytest from sqlalchemy import lambda_stmt, text from sqlalchemy.engine.result import ChunkedIteratorResult @@ -1052,55 +1053,94 @@ async def test_execute_stmt_lambda_element( assert rows == ["mock_row"] -@pytest.mark.freeze_time(datetime(2022, 10, 21, 7, 25, tzinfo=UTC)) -async def test_resolve_period(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("start_time", "periods"), + [ + ( + # Test 00:25 local time, during DST + datetime(2022, 10, 21, 7, 25, 50, 123, tzinfo=UTC), + { + "hour": ["2022-10-21T07:00:00+00:00", "2022-10-21T08:00:00+00:00"], + "hour-1": ["2022-10-21T06:00:00+00:00", "2022-10-21T07:00:00+00:00"], + "day": ["2022-10-21T07:00:00+00:00", "2022-10-22T07:00:00+00:00"], + "day-1": ["2022-10-20T07:00:00+00:00", "2022-10-21T07:00:00+00:00"], + "week": ["2022-10-17T07:00:00+00:00", "2022-10-24T07:00:00+00:00"], + "week-1": ["2022-10-10T07:00:00+00:00", "2022-10-17T07:00:00+00:00"], + "month": ["2022-10-01T07:00:00+00:00", "2022-11-01T07:00:00+00:00"], + "month-1": ["2022-09-01T07:00:00+00:00", "2022-10-01T07:00:00+00:00"], + "year": ["2022-01-01T08:00:00+00:00", "2023-01-01T08:00:00+00:00"], + "year-1": ["2021-01-01T08:00:00+00:00", "2022-01-01T08:00:00+00:00"], + }, + ), + ( + # Test 00:25 local time, standard time, February 28th a leap year + datetime(2024, 2, 28, 8, 25, 50, 123, tzinfo=UTC), + { + "hour": ["2024-02-28T08:00:00+00:00", "2024-02-28T09:00:00+00:00"], + "hour-1": ["2024-02-28T07:00:00+00:00", "2024-02-28T08:00:00+00:00"], + "day": ["2024-02-28T08:00:00+00:00", "2024-02-29T08:00:00+00:00"], + "day-1": ["2024-02-27T08:00:00+00:00", "2024-02-28T08:00:00+00:00"], + "week": ["2024-02-26T08:00:00+00:00", "2024-03-04T08:00:00+00:00"], + "week-1": ["2024-02-19T08:00:00+00:00", "2024-02-26T08:00:00+00:00"], + "month": ["2024-02-01T08:00:00+00:00", "2024-03-01T08:00:00+00:00"], + "month-1": ["2024-01-01T08:00:00+00:00", "2024-02-01T08:00:00+00:00"], + "year": ["2024-01-01T08:00:00+00:00", "2025-01-01T08:00:00+00:00"], + "year-1": ["2023-01-01T08:00:00+00:00", "2024-01-01T08:00:00+00:00"], + }, + ), + ], +) +async def test_resolve_period( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + start_time: datetime, + periods: dict[str, tuple[str, str]], +) -> None: """Test statistic_during_period.""" + assert hass.config.time_zone == "US/Pacific" + freezer.move_to(start_time) now = dt_util.utcnow() start_t, end_t = resolve_period({"calendar": {"period": "hour"}}) - assert start_t.isoformat() == "2022-10-21T07:00:00+00:00" - assert end_t.isoformat() == "2022-10-21T08:00:00+00:00" - - start_t, end_t = resolve_period({"calendar": {"period": "hour"}}) - assert start_t.isoformat() == "2022-10-21T07:00:00+00:00" - assert end_t.isoformat() == "2022-10-21T08:00:00+00:00" + assert start_t.isoformat() == periods["hour"][0] + assert end_t.isoformat() == periods["hour"][1] start_t, end_t = resolve_period({"calendar": {"period": "hour", "offset": -1}}) - assert start_t.isoformat() == "2022-10-21T06:00:00+00:00" - assert end_t.isoformat() == "2022-10-21T07:00:00+00:00" + assert start_t.isoformat() == periods["hour-1"][0] + assert end_t.isoformat() == periods["hour-1"][1] start_t, end_t = resolve_period({"calendar": {"period": "day"}}) - assert start_t.isoformat() == "2022-10-21T07:00:00+00:00" - assert end_t.isoformat() == "2022-10-22T07:00:00+00:00" + assert start_t.isoformat() == periods["day"][0] + assert end_t.isoformat() == periods["day"][1] start_t, end_t = resolve_period({"calendar": {"period": "day", "offset": -1}}) - assert start_t.isoformat() == "2022-10-20T07:00:00+00:00" - assert end_t.isoformat() == "2022-10-21T07:00:00+00:00" + assert start_t.isoformat() == periods["day-1"][0] + assert end_t.isoformat() == periods["day-1"][1] start_t, end_t = resolve_period({"calendar": {"period": "week"}}) - assert start_t.isoformat() == "2022-10-17T07:00:00+00:00" - assert end_t.isoformat() == "2022-10-24T07:00:00+00:00" + assert start_t.isoformat() == periods["week"][0] + assert end_t.isoformat() == periods["week"][1] start_t, end_t = resolve_period({"calendar": {"period": "week", "offset": -1}}) - assert start_t.isoformat() == "2022-10-10T07:00:00+00:00" - assert end_t.isoformat() == "2022-10-17T07:00:00+00:00" + assert start_t.isoformat() == periods["week-1"][0] + assert end_t.isoformat() == periods["week-1"][1] start_t, end_t = resolve_period({"calendar": {"period": "month"}}) - assert start_t.isoformat() == "2022-10-01T07:00:00+00:00" - assert end_t.isoformat() == "2022-11-01T07:00:00+00:00" + assert start_t.isoformat() == periods["month"][0] + assert end_t.isoformat() == periods["month"][1] start_t, end_t = resolve_period({"calendar": {"period": "month", "offset": -1}}) - assert start_t.isoformat() == "2022-09-01T07:00:00+00:00" - assert end_t.isoformat() == "2022-10-01T07:00:00+00:00" + assert start_t.isoformat() == periods["month-1"][0] + assert end_t.isoformat() == periods["month-1"][1] start_t, end_t = resolve_period({"calendar": {"period": "year"}}) - assert start_t.isoformat() == "2022-01-01T08:00:00+00:00" - assert end_t.isoformat() == "2023-01-01T08:00:00+00:00" + assert start_t.isoformat() == periods["year"][0] + assert end_t.isoformat() == periods["year"][1] start_t, end_t = resolve_period({"calendar": {"period": "year", "offset": -1}}) - assert start_t.isoformat() == "2021-01-01T08:00:00+00:00" - assert end_t.isoformat() == "2022-01-01T08:00:00+00:00" + assert start_t.isoformat() == periods["year-1"][0] + assert end_t.isoformat() == periods["year-1"][1] # Fixed period assert resolve_period({}) == (None, None) From 512ac7d572871992facd3adccec78ead68156852 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 3 Dec 2024 12:37:05 -0600 Subject: [PATCH 093/103] Ensure entity names are not hassil templates (#132184) --- .../components/conversation/default_agent.py | 2 +- .../conversation/test_default_agent.py | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index c1256a1507b..1194091fd46 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -711,7 +711,7 @@ class DefaultAgent(ConversationEntity): for name_tuple in self._get_entity_name_tuples(exposed=False): self._unexposed_names_trie.insert( name_tuple[0].lower(), - TextSlotValue.from_tuple(name_tuple), + TextSlotValue.from_tuple(name_tuple, allow_template=False), ) # Build filtered slot list diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index 00c47b42629..39ecdb7f422 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -3013,3 +3013,39 @@ async def test_entities_filtered_by_input(hass: HomeAssistant) -> None: assert len(name_list.values) == 2 assert name_list.values[0].text_in.text == "test light" assert name_list.values[1].text_in.text == "test light" + + +@pytest.mark.usefixtures("init_components") +async def test_entities_names_are_not_templates(hass: HomeAssistant) -> None: + """Test that entities names are not treated as hassil templates.""" + # Contains hassil template characters + hass.states.async_set( + "light.test_light", "off", attributes={ATTR_FRIENDLY_NAME: " Date: Tue, 3 Dec 2024 19:31:28 +0100 Subject: [PATCH 094/103] Fix typo in exception message in google_photos integration (#132194) --- homeassistant/components/google_photos/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/google_photos/strings.json b/homeassistant/components/google_photos/strings.json index bd565a6122d..fa3f4669dac 100644 --- a/homeassistant/components/google_photos/strings.json +++ b/homeassistant/components/google_photos/strings.json @@ -48,7 +48,7 @@ "message": "`{filename}` is not an image" }, "missing_upload_permission": { - "message": "Home Assistnt was not granted permission to upload to Google Photos" + "message": "Home Assistant was not granted permission to upload to Google Photos" }, "upload_error": { "message": "Failed to upload content: {message}" From d40a9bd9ef011372775062f64f4aeb2870a76c6f Mon Sep 17 00:00:00 2001 From: cnico Date: Wed, 4 Dec 2024 09:53:29 +0100 Subject: [PATCH 095/103] Fix blocking call in netdata (#132209) Co-authored-by: G Johansson --- homeassistant/components/netdata/manifest.json | 2 +- homeassistant/components/netdata/sensor.py | 5 ++++- requirements_all.txt | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/netdata/manifest.json b/homeassistant/components/netdata/manifest.json index 199073298ab..8901a271de2 100644 --- a/homeassistant/components/netdata/manifest.json +++ b/homeassistant/components/netdata/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_polling", "loggers": ["netdata"], "quality_scale": "legacy", - "requirements": ["netdata==1.1.0"] + "requirements": ["netdata==1.3.0"] } diff --git a/homeassistant/components/netdata/sensor.py b/homeassistant/components/netdata/sensor.py index b77a4392ef4..f33349c56ce 100644 --- a/homeassistant/components/netdata/sensor.py +++ b/homeassistant/components/netdata/sensor.py @@ -24,6 +24,7 @@ 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.httpx_client import get_async_client from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType _LOGGER = logging.getLogger(__name__) @@ -70,7 +71,9 @@ async def async_setup_platform( port = config[CONF_PORT] resources = config[CONF_RESOURCES] - netdata = NetdataData(Netdata(host, port=port, timeout=20.0)) + netdata = NetdataData( + Netdata(host, port=port, timeout=20.0, httpx_client=get_async_client(hass)) + ) await netdata.async_update() if netdata.api.metrics is None: diff --git a/requirements_all.txt b/requirements_all.txt index 8f1a4ede8c1..5d4acff1ae3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1433,7 +1433,7 @@ ndms2-client==0.1.2 nessclient==1.1.2 # homeassistant.components.netdata -netdata==1.1.0 +netdata==1.3.0 # homeassistant.components.nmap_tracker netmap==0.7.0.2 From f28579357efd92644ab1b54da44bfb6b91c9e9a2 Mon Sep 17 00:00:00 2001 From: Raphael Hehl <7577984+RaHehl@users.noreply.github.com> Date: Wed, 4 Dec 2024 06:03:31 +0100 Subject: [PATCH 096/103] fix: unifiprotect prevent RTSP repair for third-party cameras (#132212) Co-authored-by: J. Nick Koston --- .../components/unifiprotect/camera.py | 2 +- tests/components/unifiprotect/test_repairs.py | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/unifiprotect/camera.py b/homeassistant/components/unifiprotect/camera.py index a40939be917..0b1c03b8dd6 100644 --- a/homeassistant/components/unifiprotect/camera.py +++ b/homeassistant/components/unifiprotect/camera.py @@ -90,7 +90,7 @@ def _get_camera_channels( is_default = False # no RTSP enabled use first channel with no stream - if is_default: + if is_default and not camera.is_third_party_camera: _create_rtsp_repair(hass, entry, data, camera) yield camera, camera.channels[0], True else: diff --git a/tests/components/unifiprotect/test_repairs.py b/tests/components/unifiprotect/test_repairs.py index adb9555e6ea..1117038bbd0 100644 --- a/tests/components/unifiprotect/test_repairs.py +++ b/tests/components/unifiprotect/test_repairs.py @@ -363,3 +363,30 @@ async def test_rtsp_writable_fix_when_not_setup( ufp.api.update_device.assert_called_with( ModelType.CAMERA, doorbell.id, {"channels": channels} ) + + +async def test_rtsp_no_fix_if_third_party( + hass: HomeAssistant, + ufp: MockUFPFixture, + doorbell: Camera, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test no RTSP disabled warning if camera is third-party.""" + + for channel in doorbell.channels: + channel.is_rtsp_enabled = False + for user in ufp.api.bootstrap.users.values(): + user.all_permissions = [] + + ufp.api.get_camera = AsyncMock(return_value=doorbell) + doorbell.is_third_party_camera = True + + await init_entry(hass, ufp, [doorbell]) + await async_process_repairs_platforms(hass) + ws_client = await hass_ws_client(hass) + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert not msg["result"]["issues"] From e463d5d16f7e9c3514e0f71daa23941a16455b14 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 4 Dec 2024 09:35:53 +0100 Subject: [PATCH 097/103] Bump yt-dlp to 2024.12.03 (#132220) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 866215839bf..f85f1561bb9 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -8,6 +8,6 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp[default]==2024.11.18"], + "requirements": ["yt-dlp[default]==2024.12.03"], "single_config_entry": true } diff --git a/requirements_all.txt b/requirements_all.txt index 5d4acff1ae3..799da7d791c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3066,7 +3066,7 @@ youless-api==2.1.2 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp[default]==2024.11.18 +yt-dlp[default]==2024.12.03 # homeassistant.components.zamg zamg==0.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index afd21bede3c..04f2cfb48ee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2452,7 +2452,7 @@ youless-api==2.1.2 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp[default]==2024.11.18 +yt-dlp[default]==2024.12.03 # homeassistant.components.zamg zamg==0.3.6 From 7e96666dc53dfc48dbbc36f4973c59b11b74c8f9 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 4 Dec 2024 11:50:55 +0100 Subject: [PATCH 098/103] Bump deebot-client to 9.1.0 (#132253) --- homeassistant/components/ecovacs/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecovacs/manifest.json b/homeassistant/components/ecovacs/manifest.json index 4a43489ff24..546aba01d90 100644 --- a/homeassistant/components/ecovacs/manifest.json +++ b/homeassistant/components/ecovacs/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/ecovacs", "iot_class": "cloud_push", "loggers": ["sleekxmppfs", "sucks", "deebot_client"], - "requirements": ["py-sucks==0.9.10", "deebot-client==9.0.0"] + "requirements": ["py-sucks==0.9.10", "deebot-client==9.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 799da7d791c..c3bc9c0942d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -738,7 +738,7 @@ debugpy==1.8.6 # decora==0.6 # homeassistant.components.ecovacs -deebot-client==9.0.0 +deebot-client==9.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 04f2cfb48ee..4476faa48f8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -628,7 +628,7 @@ dbus-fast==2.24.3 debugpy==1.8.6 # homeassistant.components.ecovacs -deebot-client==9.0.0 +deebot-client==9.1.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns From 4fd4ba781366357c50b19e85a8bfe1fed6d49246 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 4 Dec 2024 14:57:25 +0100 Subject: [PATCH 099/103] Update frontend to 20241127.4 (#132268) --- 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 264f0756b82..97a67cbc082 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==20241127.3"] + "requirements": ["home-assistant-frontend==20241127.4"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index af91964994e..7e4958a7a00 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -34,7 +34,7 @@ habluetooth==3.6.0 hass-nabucasa==0.85.0 hassil==2.0.5 home-assistant-bluetooth==1.13.0 -home-assistant-frontend==20241127.3 +home-assistant-frontend==20241127.4 home-assistant-intents==2024.12.2 httpx==0.27.2 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index c3bc9c0942d..22c5ffe3f30 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1130,7 +1130,7 @@ hole==0.8.0 holidays==0.62 # homeassistant.components.frontend -home-assistant-frontend==20241127.3 +home-assistant-frontend==20241127.4 # homeassistant.components.conversation home-assistant-intents==2024.12.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4476faa48f8..267bb9dfa69 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -956,7 +956,7 @@ hole==0.8.0 holidays==0.62 # homeassistant.components.frontend -home-assistant-frontend==20241127.3 +home-assistant-frontend==20241127.4 # homeassistant.components.conversation home-assistant-intents==2024.12.2 From 333ada767045dd9c57ce10cb5ef716ba835b2c60 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Wed, 4 Dec 2024 15:18:04 +0100 Subject: [PATCH 100/103] Ensure MQTT subscriptions can be made when the broker is disconnected (#132270) --- homeassistant/components/mqtt/client.py | 2 +- tests/components/mqtt/test_client.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index a626e0e5b28..ee6f02912b2 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -227,7 +227,7 @@ def async_subscribe_internal( translation_placeholders={"topic": topic}, ) from exc client = mqtt_data.client - if not client.connected and not mqtt_config_entry_enabled(hass): + if not mqtt_config_entry_enabled(hass): raise HomeAssistantError( f"Cannot subscribe to topic '{topic}', MQTT is not enabled", translation_key="mqtt_not_setup_cannot_subscribe", diff --git a/tests/components/mqtt/test_client.py b/tests/components/mqtt/test_client.py index 164c164cdfc..4bfcde752ae 100644 --- a/tests/components/mqtt/test_client.py +++ b/tests/components/mqtt/test_client.py @@ -1045,10 +1045,17 @@ async def test_restore_subscriptions_on_reconnect( mqtt_client_mock.reset_mock() mqtt_client_mock.on_disconnect(None, None, 0) + # Test to subscribe orther topic while the client is not connected + await mqtt.async_subscribe(hass, "test/other", record_calls) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=3)) # cooldown + assert ("test/other", 0) not in help_all_subscribe_calls(mqtt_client_mock) + mock_debouncer.clear() mqtt_client_mock.on_connect(None, None, None, 0) await mock_debouncer.wait() + # Assert all subscriptions are performed at the broker assert ("test/state", 0) in help_all_subscribe_calls(mqtt_client_mock) + assert ("test/other", 0) in help_all_subscribe_calls(mqtt_client_mock) @pytest.mark.parametrize( From 4c3ae395a4d7cff46ed36e1afa36e9a01c88a9d8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 4 Dec 2024 15:33:47 +0100 Subject: [PATCH 101/103] Bump version to 2024.12.0b6 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 075262346b4..255b19e6667 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 57523be4e68..a2f0659f3df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.12.0b5" +version = "2024.12.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From dcdf033fa93eb888886a6bfed9ad1121bcc37dbb Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 4 Dec 2024 10:02:00 -0600 Subject: [PATCH 102/103] Bump intents to 2024.12.4 (#132274) --- homeassistant/components/conversation/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- script/hassfest/docker/Dockerfile | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 2d2f2f58a3a..72e1cebf462 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==2.0.5", "home-assistant-intents==2024.12.2"] + "requirements": ["hassil==2.0.5", "home-assistant-intents==2024.12.4"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 7e4958a7a00..ed7e995408f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -35,7 +35,7 @@ hass-nabucasa==0.85.0 hassil==2.0.5 home-assistant-bluetooth==1.13.0 home-assistant-frontend==20241127.4 -home-assistant-intents==2024.12.2 +home-assistant-intents==2024.12.4 httpx==0.27.2 ifaddr==0.2.0 Jinja2==3.1.4 diff --git a/requirements_all.txt b/requirements_all.txt index 22c5ffe3f30..20f105b7f07 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1133,7 +1133,7 @@ holidays==0.62 home-assistant-frontend==20241127.4 # homeassistant.components.conversation -home-assistant-intents==2024.12.2 +home-assistant-intents==2024.12.4 # homeassistant.components.home_connect homeconnect==0.8.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 267bb9dfa69..38440ddcf52 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -959,7 +959,7 @@ holidays==0.62 home-assistant-frontend==20241127.4 # homeassistant.components.conversation -home-assistant-intents==2024.12.2 +home-assistant-intents==2024.12.4 # homeassistant.components.home_connect homeconnect==0.8.0 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 26ca6475af7..100be4fdec9 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -23,7 +23,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.5.4,source=/uv,target=/bin/uv \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ stdlib-list==0.10.0 pipdeptree==2.23.4 tqdm==4.66.5 ruff==0.8.0 \ - PyTurboJPEG==1.7.5 go2rtc-client==0.1.1 ha-ffmpeg==3.2.2 hassil==2.0.5 home-assistant-intents==2024.12.2 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 + PyTurboJPEG==1.7.5 go2rtc-client==0.1.1 ha-ffmpeg==3.2.2 hassil==2.0.5 home-assistant-intents==2024.12.4 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " From 9b90df74a6d19682b8b3f49ff9f58bf895ee60e9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 4 Dec 2024 19:18:48 +0100 Subject: [PATCH 103/103] Bump version to 2024.12.0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 255b19e6667..c41ab6ec382 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,7 +25,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 12 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index a2f0659f3df..2ceb074cc48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.12.0b6" +version = "2024.12.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"