From 9dc66404e78e4460ebae18a0664bc5adad0b8df5 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Fri, 24 May 2024 04:42:45 -0400 Subject: [PATCH 01/24] Fix Sonos album artwork performance (#116391) --- .../components/sonos/media_browser.py | 14 +- tests/components/sonos/conftest.py | 43 +++++- .../sonos/fixtures/music_library_albums.json | 23 +++ .../fixtures/music_library_categories.json | 44 ++++++ .../sonos/fixtures/music_library_tracks.json | 14 ++ .../sonos/snapshots/test_media_browser.ambr | 133 ++++++++++++++++++ tests/components/sonos/test_media_browser.py | 82 +++++++++++ 7 files changed, 344 insertions(+), 9 deletions(-) create mode 100644 tests/components/sonos/fixtures/music_library_albums.json create mode 100644 tests/components/sonos/fixtures/music_library_categories.json create mode 100644 tests/components/sonos/fixtures/music_library_tracks.json create mode 100644 tests/components/sonos/snapshots/test_media_browser.ambr diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index eeadd7db232..008c539581b 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -53,14 +53,16 @@ def get_thumbnail_url_full( media_content_type: str, media_content_id: str, media_image_id: str | None = None, + item: MusicServiceItem | None = None, ) -> str | None: """Get thumbnail URL.""" if is_internal: - item = get_media( - media.library, - media_content_id, - media_content_type, - ) + if not item: + item = get_media( + media.library, + media_content_id, + media_content_type, + ) return urllib.parse.unquote(getattr(item, "album_art_uri", "")) return urllib.parse.unquote( @@ -255,7 +257,7 @@ def item_payload(item: DidlObject, get_thumbnail_url=None) -> BrowseMedia: content_id = get_content_id(item) thumbnail = None if getattr(item, "album_art_uri", None): - thumbnail = get_thumbnail_url(media_class, content_id) + thumbnail = get_thumbnail_url(media_class, content_id, item=item) return BrowseMedia( title=item.title, diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 15f371f272c..a7062b24e88 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -316,12 +316,35 @@ def sonos_favorites_fixture() -> SearchResult: class MockMusicServiceItem: """Mocks a Soco MusicServiceItem.""" - def __init__(self, title: str, item_id: str, parent_id: str, item_class: str): + def __init__( + self, + title: str, + item_id: str, + parent_id: str, + item_class: str, + album_art_uri: None | str = None, + ): """Initialize the mock item.""" self.title = title self.item_id = item_id self.item_class = item_class self.parent_id = parent_id + self.album_art_uri: None | str = album_art_uri + + +def list_from_json_fixture(file_name: str) -> list[MockMusicServiceItem]: + """Create a list of music service items from a json fixture file.""" + item_list = load_json_value_fixture(file_name, "sonos") + return [ + MockMusicServiceItem( + item.get("title"), + item.get("item_id"), + item.get("parent_id"), + item.get("item_class"), + item.get("album_art_uri"), + ) + for item in item_list + ] def mock_browse_by_idstring( @@ -398,6 +421,10 @@ def mock_browse_by_idstring( "object.container.album.musicAlbum", ), ] + if search_type == "tracks": + return list_from_json_fixture("music_library_tracks.json") + if search_type == "albums" and idstring == "A:ALBUM": + return list_from_json_fixture("music_library_albums.json") return [] @@ -416,13 +443,23 @@ def mock_get_music_library_information( ] +@pytest.fixture(name="music_library_browse_categories") +def music_library_browse_categories() -> list[MockMusicServiceItem]: + """Create fixture for top-level music library categories.""" + return list_from_json_fixture("music_library_categories.json") + + @pytest.fixture(name="music_library") -def music_library_fixture(sonos_favorites: SearchResult) -> Mock: +def music_library_fixture( + sonos_favorites: SearchResult, + music_library_browse_categories: list[MockMusicServiceItem], +) -> Mock: """Create music_library fixture.""" music_library = MagicMock() music_library.get_sonos_favorites.return_value = sonos_favorites - music_library.browse_by_idstring = mock_browse_by_idstring + music_library.browse_by_idstring = Mock(side_effect=mock_browse_by_idstring) music_library.get_music_library_information = mock_get_music_library_information + music_library.browse = Mock(return_value=music_library_browse_categories) return music_library diff --git a/tests/components/sonos/fixtures/music_library_albums.json b/tests/components/sonos/fixtures/music_library_albums.json new file mode 100644 index 00000000000..4941abe8ba7 --- /dev/null +++ b/tests/components/sonos/fixtures/music_library_albums.json @@ -0,0 +1,23 @@ +[ + { + "title": "A Hard Day's Night", + "item_id": "A:ALBUM/A%20Hard%20Day's%20Night", + "parent_id": "A:ALBUM", + "item_class": "object.container.album.musicAlbum", + "album_art_uri": "http://192.168.42.2:1400/getaa?u=x-file-cifs%3a%2f%2f192.168.42.100%2fmusic%2fThe%2520Beatles%2fA%2520Hard%2520Day's%2520Night%2f01%2520A%2520Hard%2520Day's%2520Night%25201.m4a&v=53" + }, + { + "title": "Abbey Road", + "item_id": "A:ALBUM/Abbey%20Road", + "parent_id": "A:ALBUM", + "item_class": "object.container.album.musicAlbum", + "album_art_uri": "http://192.168.42.2:1400/getaa?u=x-file-cifs%3a%2f%2f192.168.42.100%2fmusic%2fThe%2520Beatles%2fAbbeyA%2520Road%2f01%2520Come%2520Together.m4a&v=53" + }, + { + "title": "Between Good And Evil", + "item_id": "A:ALBUM/Between%20Good%20And%20Evil", + "parent_id": "A:ALBUM", + "item_class": "object.container.album.musicAlbum", + "album_art_uri": "http://192.168.42.2:1400/getaa?u=x-file-cifs%3a%2f%2f192.168.42.100%2fmusic%2fSantana%2fA%2520Between%2520Good%2520And%2520Evil%2f02%2520A%2520Persuasion.m4a&v=53" + } +] diff --git a/tests/components/sonos/fixtures/music_library_categories.json b/tests/components/sonos/fixtures/music_library_categories.json new file mode 100644 index 00000000000..b6d6d3bf2dd --- /dev/null +++ b/tests/components/sonos/fixtures/music_library_categories.json @@ -0,0 +1,44 @@ +[ + { + "title": "Contributing Artists", + "item_id": "A:ARTIST", + "parent_id": "A:", + "item_class": "object.container" + }, + { + "title": "Artists", + "item_id": "A:ALBUMARTIST", + "parent_id": "A:", + "item_class": "object.container" + }, + { + "title": "Albums", + "item_id": "A:ALBUM", + "parent_id": "A:", + "item_class": "object.container" + }, + { + "title": "Genres", + "item_id": "A:GENRE", + "parent_id": "A:", + "item_class": "object.container" + }, + { + "title": "Composers", + "item_id": "A:COMPOSER", + "parent_id": "A:", + "item_class": "object.container" + }, + { + "title": "Tracks", + "item_id": "A:TRACKS", + "parent_id": "A:", + "item_class": "object.container.playlistContainer" + }, + { + "title": "Playlists", + "item_id": "A:PLAYLISTS", + "parent_id": "A:", + "item_class": "object.container" + } +] diff --git a/tests/components/sonos/fixtures/music_library_tracks.json b/tests/components/sonos/fixtures/music_library_tracks.json new file mode 100644 index 00000000000..1f1fcdbc21c --- /dev/null +++ b/tests/components/sonos/fixtures/music_library_tracks.json @@ -0,0 +1,14 @@ +[ + { + "title": "A Hard Day's Night", + "item_id": "S://192.168.42.100/music/iTunes/The%20Beatles/A%20Hard%20Day%2fs%20Night/A%20Hard%20Day%2fs%20Night.mp3", + "parent_id": "A:ALBUMARTIST/Beatles/A%20Hard%20Day's%20Night", + "item_class": "object.container.album.musicTrack" + }, + { + "title": "I Should Have Known Better", + "item_id": "S://192.168.42.100/music/iTunes/The%20Beatles/A%20Hard%20Day%2fs%I%20Should%20Have%20Known%20Better.mp3", + "parent_id": "A:ALBUMARTIST/Beatles/A%20Hard%20Day's%20Night", + "item_class": "object.container.album.musicTrack" + } +] diff --git a/tests/components/sonos/snapshots/test_media_browser.ambr b/tests/components/sonos/snapshots/test_media_browser.ambr new file mode 100644 index 00000000000..b4388b148e5 --- /dev/null +++ b/tests/components/sonos/snapshots/test_media_browser.ambr @@ -0,0 +1,133 @@ +# serializer version: 1 +# name: test_browse_media_library + list([ + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': None, + 'media_class': 'contributing_artist', + 'media_content_id': 'A:ARTIST', + 'media_content_type': 'contributing_artist', + 'thumbnail': None, + 'title': 'Contributing Artists', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': None, + 'media_class': 'artist', + 'media_content_id': 'A:ALBUMARTIST', + 'media_content_type': 'artist', + 'thumbnail': None, + 'title': 'Artists', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': None, + 'media_class': 'album', + 'media_content_id': 'A:ALBUM', + 'media_content_type': 'album', + 'thumbnail': None, + 'title': 'Albums', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': None, + 'media_class': 'genre', + 'media_content_id': 'A:GENRE', + 'media_content_type': 'genre', + 'thumbnail': None, + 'title': 'Genres', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': None, + 'media_class': 'composer', + 'media_content_id': 'A:COMPOSER', + 'media_content_type': 'composer', + 'thumbnail': None, + 'title': 'Composers', + }), + dict({ + 'can_expand': True, + 'can_play': True, + 'children_media_class': None, + 'media_class': 'track', + 'media_content_id': 'A:TRACKS', + 'media_content_type': 'track', + 'thumbnail': None, + 'title': 'Tracks', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': None, + 'media_class': 'playlist', + 'media_content_id': 'A:PLAYLISTS', + 'media_content_type': 'playlist', + 'thumbnail': None, + 'title': 'Playlists', + }), + ]) +# --- +# name: test_browse_media_library_albums + list([ + dict({ + 'can_expand': True, + 'can_play': True, + 'children_media_class': None, + 'media_class': 'album', + 'media_content_id': "A:ALBUM/A%20Hard%20Day's%20Night", + 'media_content_type': 'album', + 'thumbnail': "http://192.168.42.2:1400/getaa?u=x-file-cifs://192.168.42.100/music/The%20Beatles/A%20Hard%20Day's%20Night/01%20A%20Hard%20Day's%20Night%201.m4a&v=53", + 'title': "A Hard Day's Night", + }), + dict({ + 'can_expand': True, + 'can_play': True, + 'children_media_class': None, + 'media_class': 'album', + 'media_content_id': 'A:ALBUM/Abbey%20Road', + 'media_content_type': 'album', + 'thumbnail': 'http://192.168.42.2:1400/getaa?u=x-file-cifs://192.168.42.100/music/The%20Beatles/AbbeyA%20Road/01%20Come%20Together.m4a&v=53', + 'title': 'Abbey Road', + }), + dict({ + 'can_expand': True, + 'can_play': True, + 'children_media_class': None, + 'media_class': 'album', + 'media_content_id': 'A:ALBUM/Between%20Good%20And%20Evil', + 'media_content_type': 'album', + 'thumbnail': 'http://192.168.42.2:1400/getaa?u=x-file-cifs://192.168.42.100/music/Santana/A%20Between%20Good%20And%20Evil/02%20A%20Persuasion.m4a&v=53', + 'title': 'Between Good And Evil', + }), + ]) +# --- +# name: test_browse_media_root + list([ + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': None, + 'media_class': 'directory', + 'media_content_id': '', + 'media_content_type': 'favorites', + 'thumbnail': 'https://brands.home-assistant.io/_/sonos/logo.png', + 'title': 'Favorites', + }), + dict({ + 'can_expand': True, + 'can_play': False, + 'children_media_class': None, + 'media_class': 'directory', + 'media_content_id': '', + 'media_content_type': 'library', + 'thumbnail': 'https://brands.home-assistant.io/_/sonos/logo.png', + 'title': 'Music Library', + }), + ]) +# --- diff --git a/tests/components/sonos/test_media_browser.py b/tests/components/sonos/test_media_browser.py index d8d0e1c3a07..4f6c2f53d8b 100644 --- a/tests/components/sonos/test_media_browser.py +++ b/tests/components/sonos/test_media_browser.py @@ -2,6 +2,8 @@ from functools import partial +from syrupy import SnapshotAssertion + from homeassistant.components.media_player.browse_media import BrowseMedia from homeassistant.components.media_player.const import MediaClass, MediaType from homeassistant.components.sonos.media_browser import ( @@ -12,6 +14,8 @@ from homeassistant.core import HomeAssistant from .conftest import SoCoMockFactory +from tests.typing import WebSocketGenerator + class MockMusicServiceItem: """Mocks a Soco MusicServiceItem.""" @@ -95,3 +99,81 @@ async def test_build_item_response( browse_item.children[1].media_content_id == "x-file-cifs://192.168.42.10/music/The%20Beatles/Abbey%20Road/03%20Something.mp3" ) + + +async def test_browse_media_root( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + soco, + discover, + hass_ws_client: WebSocketGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test the async_browse_media method.""" + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": "media_player.zone_a", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"]["children"] == snapshot + + +async def test_browse_media_library( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + soco, + discover, + hass_ws_client: WebSocketGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test the async_browse_media method.""" + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": "media_player.zone_a", + "media_content_id": "", + "media_content_type": "library", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"]["children"] == snapshot + + +async def test_browse_media_library_albums( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + soco, + discover, + hass_ws_client: WebSocketGenerator, + snapshot: SnapshotAssertion, +) -> None: + """Test the async_browse_media method.""" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "media_player/browse_media", + "entity_id": "media_player.zone_a", + "media_content_id": "A:ALBUM", + "media_content_type": "album", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"]["children"] == snapshot + assert soco_mock.music_library.browse_by_idstring.call_count == 1 From 85f0fffa5a9538eb135875ae4809aa2ff366873c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 18 May 2024 14:45:42 +0300 Subject: [PATCH 02/24] Filter out HTML greater/less than entities from huawei_lte sensor values (#117209) --- homeassistant/components/huawei_lte/sensor.py | 2 +- tests/components/huawei_lte/test_sensor.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/huawei_lte/sensor.py b/homeassistant/components/huawei_lte/sensor.py index cef5bc5030e..5c5f7fc8b8e 100644 --- a/homeassistant/components/huawei_lte/sensor.py +++ b/homeassistant/components/huawei_lte/sensor.py @@ -54,7 +54,7 @@ def format_default(value: StateType) -> tuple[StateType, str | None]: if value is not None: # Clean up value and infer unit, e.g. -71dBm, 15 dB if match := re.match( - r"([>=<]*)(?P.+?)\s*(?P[a-zA-Z]+)\s*$", str(value) + r"((&[gl]t;|[><])=?)?(?P.+?)\s*(?P[a-zA-Z]+)\s*$", str(value) ): try: value = float(match.group("value")) diff --git a/tests/components/huawei_lte/test_sensor.py b/tests/components/huawei_lte/test_sensor.py index 4d5acaf2d31..75cdc7be1c2 100644 --- a/tests/components/huawei_lte/test_sensor.py +++ b/tests/components/huawei_lte/test_sensor.py @@ -15,6 +15,8 @@ from homeassistant.const import ( ("-71 dBm", (-71, SIGNAL_STRENGTH_DECIBELS_MILLIWATT)), ("15dB", (15, SIGNAL_STRENGTH_DECIBELS)), (">=-51dBm", (-51, SIGNAL_STRENGTH_DECIBELS_MILLIWATT)), + ("<-20dB", (-20, SIGNAL_STRENGTH_DECIBELS)), + (">=30dB", (30, SIGNAL_STRENGTH_DECIBELS)), ], ) def test_format_default(value, expected) -> None: From c6a9388aeac0e4ae493dad9c51d2a36377c90616 Mon Sep 17 00:00:00 2001 From: Bouwe Westerdijk <11290930+bouwew@users.noreply.github.com> Date: Sat, 18 May 2024 12:39:58 +0200 Subject: [PATCH 03/24] Add options-property to Plugwise Select (#117655) --- homeassistant/components/plugwise/select.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plugwise/select.py b/homeassistant/components/plugwise/select.py index 10718a818ff..a3e2a567e85 100644 --- a/homeassistant/components/plugwise/select.py +++ b/homeassistant/components/plugwise/select.py @@ -91,13 +91,17 @@ class PlugwiseSelectEntity(PlugwiseEntity, SelectEntity): super().__init__(coordinator, device_id) self.entity_description = entity_description self._attr_unique_id = f"{device_id}-{entity_description.key}" - self._attr_options = self.device[entity_description.options_key] @property def current_option(self) -> str: """Return the selected entity option to represent the entity state.""" return self.device[self.entity_description.key] + @property + def options(self) -> list[str]: + """Return the available select-options.""" + return self.device[self.entity_description.options_key] + async def async_select_option(self, option: str) -> None: """Change to the selected entity option.""" await self.entity_description.command( From ecb587c4ca4451eee116929efd613382e2462c35 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 May 2024 14:09:21 -1000 Subject: [PATCH 04/24] Fix setting MQTT socket buffer size with WebsocketWrapper (#117672) --- homeassistant/components/mqtt/client.py | 8 ++++++ tests/components/mqtt/test_init.py | 37 +++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index 8245363fd85..e6e4bb52049 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -542,6 +542,14 @@ class MQTT: def _increase_socket_buffer_size(self, sock: SocketType) -> None: """Increase the socket buffer size.""" + if not hasattr(sock, "setsockopt") and hasattr(sock, "_socket"): + # The WebsocketWrapper does not wrap setsockopt + # so we need to get the underlying socket + # Remove this once + # https://github.com/eclipse/paho.mqtt.python/pull/843 + # is available. + sock = sock._socket # noqa: SLF001 + new_buffer_size = PREFERRED_BUFFER_SIZE while True: try: diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 448d41c59cc..6ead70e4150 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -4410,6 +4410,43 @@ async def test_server_sock_buffer_size( assert "Unable to increase the socket buffer size" in caplog.text +@patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) +@patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) +async def test_server_sock_buffer_size_with_websocket( + hass: HomeAssistant, + mqtt_client_mock: MqttMockPahoClient, + mqtt_mock_entry: MqttMockHAClientGenerator, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test handling the socket buffer size fails.""" + mqtt_mock = await mqtt_mock_entry() + await hass.async_block_till_done() + assert mqtt_mock.connected is True + + mqtt_client_mock.loop_misc.return_value = paho_mqtt.MQTT_ERR_SUCCESS + + client, server = socket.socketpair( + family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0 + ) + client.setblocking(False) + server.setblocking(False) + + class FakeWebsocket(paho_mqtt.WebsocketWrapper): + def _do_handshake(self, *args, **kwargs): + pass + + wrapped_socket = FakeWebsocket(client, "127.0.01", 1, False, "/", None) + + with patch.object(client, "setsockopt", side_effect=OSError("foo")): + mqtt_client_mock.on_socket_open(mqtt_client_mock, None, wrapped_socket) + mqtt_client_mock.on_socket_register_write( + mqtt_client_mock, None, wrapped_socket + ) + await hass.async_block_till_done() + assert "Unable to increase the socket buffer size" in caplog.text + + @patch("homeassistant.components.mqtt.client.INITIAL_SUBSCRIBE_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.DISCOVERY_COOLDOWN", 0.0) @patch("homeassistant.components.mqtt.client.SUBSCRIBE_COOLDOWN", 0.0) From 66fccb72967992fa7ce3c39274b3e84b498f3e16 Mon Sep 17 00:00:00 2001 From: On Freund Date: Sat, 18 May 2024 12:37:24 +0300 Subject: [PATCH 05/24] Bump pyrisco to 0.6.2 (#117682) --- homeassistant/components/risco/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index 22e73a10d6d..25520d1f96e 100644 --- a/homeassistant/components/risco/manifest.json +++ b/homeassistant/components/risco/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_push", "loggers": ["pyrisco"], "quality_scale": "platinum", - "requirements": ["pyrisco==0.6.1"] + "requirements": ["pyrisco==0.6.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 675b01a31b9..b5ffb75bc2b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2093,7 +2093,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.6.1 +pyrisco==0.6.2 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c313ef952a3..3f91c7fe76d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1635,7 +1635,7 @@ pyqwikswitch==0.93 pyrainbird==4.0.2 # homeassistant.components.risco -pyrisco==0.6.1 +pyrisco==0.6.2 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 From 66c52e144e8e1e07ebbd7837c48ba141df85fabf Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sat, 18 May 2024 16:38:22 +0200 Subject: [PATCH 06/24] Consider only active config entries as media source in Synology DSM (#117691) consider only active config entries as media source --- homeassistant/components/synology_dsm/media_source.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/synology_dsm/media_source.py b/homeassistant/components/synology_dsm/media_source.py index 4699a1a5c20..4b0c19b2b55 100644 --- a/homeassistant/components/synology_dsm/media_source.py +++ b/homeassistant/components/synology_dsm/media_source.py @@ -27,7 +27,9 @@ from .models import SynologyDSMData async def async_get_media_source(hass: HomeAssistant) -> MediaSource: """Set up Synology media source.""" - entries = hass.config_entries.async_entries(DOMAIN) + entries = hass.config_entries.async_entries( + DOMAIN, include_disabled=False, include_ignore=False + ) hass.http.register_view(SynologyDsmMediaView(hass)) return SynologyPhotosMediaSource(hass, entries) From b44821b805ab9fdb1c1d5b1004dd880afa0fdb73 Mon Sep 17 00:00:00 2001 From: Anrijs Date: Sun, 19 May 2024 21:08:39 +0300 Subject: [PATCH 07/24] Bump aranet4 to 2.3.4 (#117738) bump aranet4 lib version to 2.3.4 --- homeassistant/components/aranet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/aranet/manifest.json b/homeassistant/components/aranet/manifest.json index 152c56e80f3..f7f831df05c 100644 --- a/homeassistant/components/aranet/manifest.json +++ b/homeassistant/components/aranet/manifest.json @@ -19,5 +19,5 @@ "documentation": "https://www.home-assistant.io/integrations/aranet", "integration_type": "device", "iot_class": "local_push", - "requirements": ["aranet4==2.3.3"] + "requirements": ["aranet4==2.3.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index b5ffb75bc2b..1aa74face86 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -461,7 +461,7 @@ aprslib==0.7.2 aqualogic==2.6 # homeassistant.components.aranet -aranet4==2.3.3 +aranet4==2.3.4 # homeassistant.components.arcam_fmj arcam-fmj==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3f91c7fe76d..e8004593afb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -422,7 +422,7 @@ apprise==1.7.4 aprslib==0.7.2 # homeassistant.components.aranet -aranet4==2.3.3 +aranet4==2.3.4 # homeassistant.components.arcam_fmj arcam-fmj==1.4.0 From 8d24f68f55c0492d48ab1190e30d4828608fbef4 Mon Sep 17 00:00:00 2001 From: Ricardo Steijn <61013287+RicArch97@users.noreply.github.com> Date: Mon, 20 May 2024 07:18:28 +0200 Subject: [PATCH 08/24] Bump crownstone-sse to 2.0.5, crownstone-cloud to 1.4.11 (#117748) --- homeassistant/components/crownstone/manifest.json | 4 ++-- requirements_all.txt | 4 ++-- requirements_test_all.txt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/crownstone/manifest.json b/homeassistant/components/crownstone/manifest.json index 532fd859b4e..6168d483ab5 100644 --- a/homeassistant/components/crownstone/manifest.json +++ b/homeassistant/components/crownstone/manifest.json @@ -13,8 +13,8 @@ "crownstone_uart" ], "requirements": [ - "crownstone-cloud==1.4.9", - "crownstone-sse==2.0.4", + "crownstone-cloud==1.4.11", + "crownstone-sse==2.0.5", "crownstone-uart==2.1.0", "pyserial==3.5" ] diff --git a/requirements_all.txt b/requirements_all.txt index 1aa74face86..46d2b49461a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -670,10 +670,10 @@ construct==2.10.68 croniter==2.0.2 # homeassistant.components.crownstone -crownstone-cloud==1.4.9 +crownstone-cloud==1.4.11 # homeassistant.components.crownstone -crownstone-sse==2.0.4 +crownstone-sse==2.0.5 # homeassistant.components.crownstone crownstone-uart==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e8004593afb..f8d3da0fc2e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -554,10 +554,10 @@ construct==2.10.68 croniter==2.0.2 # homeassistant.components.crownstone -crownstone-cloud==1.4.9 +crownstone-cloud==1.4.11 # homeassistant.components.crownstone -crownstone-sse==2.0.4 +crownstone-sse==2.0.5 # homeassistant.components.crownstone crownstone-uart==2.1.0 From 56b55a0df5217ae6b3dbde733b0749f3dd6bd364 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 May 2024 21:45:52 -1000 Subject: [PATCH 09/24] Block older versions of custom integration mydolphin_plus since they cause crashes (#117751) --- homeassistant/loader.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 89c3442be6a..b65d6f34f7b 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -96,6 +96,11 @@ BLOCKED_CUSTOM_INTEGRATIONS: dict[str, BlockedIntegration] = { "dreame_vacuum": BlockedIntegration( AwesomeVersion("1.0.4"), "crashes Home Assistant" ), + # Added in 2024.5.5 because of + # https://github.com/sh00t2kill/dolphin-robot/issues/185 + "mydolphin_plus": BlockedIntegration( + AwesomeVersion("1.0.13"), "crashes Home Assistant" + ), } DATA_COMPONENTS = "components" From dae4d316ae9db4813b6ab52e69551f09f2b75940 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 19 May 2024 21:47:47 -1000 Subject: [PATCH 10/24] Fix race in config entry setup (#117756) --- homeassistant/config_entries.py | 11 +++++ tests/test_config_entries.py | 83 +++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index f982f63b948..9635d5cba48 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -709,6 +709,17 @@ class ConfigEntry: ) -> None: """Set up while holding the setup lock.""" async with self.setup_lock: + if self.state is ConfigEntryState.LOADED: + # If something loaded the config entry while + # we were waiting for the lock, we should not + # set it up again. + _LOGGER.debug( + "Not setting up %s (%s %s) again, already loaded", + self.title, + self.domain, + self.entry_id, + ) + return await self.async_setup(hass, integration=integration) @callback diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 8d7efad8918..9c491987d79 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -90,6 +90,89 @@ async def manager(hass: HomeAssistant) -> config_entries.ConfigEntries: return manager +async def test_setup_race_only_setup_once(hass: HomeAssistant) -> None: + """Test ensure that config entries are only setup once.""" + attempts = 0 + slow_config_entry_setup_future = hass.loop.create_future() + fast_config_entry_setup_future = hass.loop.create_future() + slow_setup_future = hass.loop.create_future() + + async def async_setup(hass, config): + """Mock setup.""" + await slow_setup_future + return True + + async def async_setup_entry(hass, entry): + """Mock setup entry.""" + slow = entry.data["slow"] + if slow: + await slow_config_entry_setup_future + return True + nonlocal attempts + attempts += 1 + if attempts == 1: + raise ConfigEntryNotReady + await fast_config_entry_setup_future + return True + + async def async_unload_entry(hass, entry): + """Mock unload entry.""" + return True + + mock_integration( + hass, + MockModule( + "comp", + async_setup=async_setup, + async_setup_entry=async_setup_entry, + async_unload_entry=async_unload_entry, + ), + ) + mock_platform(hass, "comp.config_flow", None) + + entry = MockConfigEntry(domain="comp", data={"slow": False}) + entry.add_to_hass(hass) + + entry2 = MockConfigEntry(domain="comp", data={"slow": True}) + entry2.add_to_hass(hass) + await entry2.setup_lock.acquire() + + async def _async_reload_entry(entry: MockConfigEntry): + async with entry.setup_lock: + await entry.async_unload(hass) + await entry.async_setup(hass) + + hass.async_create_task(_async_reload_entry(entry2)) + + setup_task = hass.async_create_task(async_setup_component(hass, "comp", {})) + entry2.setup_lock.release() + + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED + assert entry2.state is config_entries.ConfigEntryState.NOT_LOADED + + assert "comp" not in hass.config.components + slow_setup_future.set_result(None) + await asyncio.sleep(0) + assert "comp" in hass.config.components + + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY + assert entry2.state is config_entries.ConfigEntryState.SETUP_IN_PROGRESS + + fast_config_entry_setup_future.set_result(None) + # Make sure setup retry is started + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) + slow_config_entry_setup_future.set_result(None) + await hass.async_block_till_done() + + assert entry.state is config_entries.ConfigEntryState.LOADED + await hass.async_block_till_done() + + assert attempts == 2 + await hass.async_block_till_done() + assert setup_task.done() + assert entry2.state is config_entries.ConfigEntryState.LOADED + + async def test_call_setup_entry(hass: HomeAssistant) -> None: """Test we call .setup_entry.""" entry = MockConfigEntry(domain="comp") From db73074185fa3d69dd3392852bb19ddbffc5e0f8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 20 May 2024 13:49:52 +0200 Subject: [PATCH 11/24] Update wled to 0.18.0 (#117790) --- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index fd15d8ef171..a01bbcabdd6 100644 --- a/homeassistant/components/wled/manifest.json +++ b/homeassistant/components/wled/manifest.json @@ -7,6 +7,6 @@ "integration_type": "device", "iot_class": "local_push", "quality_scale": "platinum", - "requirements": ["wled==0.17.1"], + "requirements": ["wled==0.18.0"], "zeroconf": ["_wled._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 46d2b49461a..fc909232f2f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2866,7 +2866,7 @@ wiffi==1.1.2 wirelesstagpy==0.8.1 # homeassistant.components.wled -wled==0.17.1 +wled==0.18.0 # homeassistant.components.wolflink wolf-comm==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8d3da0fc2e..fc12be7e292 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2222,7 +2222,7 @@ whois==0.9.27 wiffi==1.1.2 # homeassistant.components.wled -wled==0.17.1 +wled==0.18.0 # homeassistant.components.wolflink wolf-comm==0.0.7 From 6956d0d65a6bc9d3140b99012cd0f59831446561 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Mon, 20 May 2024 21:35:57 -0400 Subject: [PATCH 12/24] Account for disabled ZHA discovery config entries when migrating SkyConnect integration (#117800) * Properly handle disabled ZHA discovery config entries * Update tests/components/homeassistant_sky_connect/test_util.py Co-authored-by: TheJulianJES --------- Co-authored-by: TheJulianJES --- .../homeassistant_sky_connect/util.py | 18 ++++++++++-------- .../homeassistant_sky_connect/test_util.py | 12 ++++++++++++ 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/homeassistant_sky_connect/util.py b/homeassistant/components/homeassistant_sky_connect/util.py index f242416fa9a..864d6bfd9dc 100644 --- a/homeassistant/components/homeassistant_sky_connect/util.py +++ b/homeassistant/components/homeassistant_sky_connect/util.py @@ -50,9 +50,9 @@ def get_hardware_variant(config_entry: ConfigEntry) -> HardwareVariant: return HardwareVariant.from_usb_product_name(config_entry.data["product"]) -def get_zha_device_path(config_entry: ConfigEntry) -> str: +def get_zha_device_path(config_entry: ConfigEntry) -> str | None: """Get the device path from a ZHA config entry.""" - return cast(str, config_entry.data["device"]["path"]) + return cast(str | None, config_entry.data.get("device", {}).get("path", None)) @singleton(OTBR_ADDON_MANAGER_DATA) @@ -94,13 +94,15 @@ async def guess_firmware_type(hass: HomeAssistant, device_path: str) -> Firmware for zha_config_entry in hass.config_entries.async_entries(ZHA_DOMAIN): zha_path = get_zha_device_path(zha_config_entry) - device_guesses[zha_path].append( - FirmwareGuess( - is_running=(zha_config_entry.state == ConfigEntryState.LOADED), - firmware_type=ApplicationType.EZSP, - source="zha", + + if zha_path is not None: + device_guesses[zha_path].append( + FirmwareGuess( + is_running=(zha_config_entry.state == ConfigEntryState.LOADED), + firmware_type=ApplicationType.EZSP, + source="zha", + ) ) - ) if is_hassio(hass): otbr_addon_manager = get_otbr_addon_manager(hass) diff --git a/tests/components/homeassistant_sky_connect/test_util.py b/tests/components/homeassistant_sky_connect/test_util.py index 12ba352eb16..b560acc65b7 100644 --- a/tests/components/homeassistant_sky_connect/test_util.py +++ b/tests/components/homeassistant_sky_connect/test_util.py @@ -94,6 +94,18 @@ def test_get_zha_device_path() -> None: ) +def test_get_zha_device_path_ignored_discovery() -> None: + """Test extracting the ZHA device path from an ignored ZHA discovery.""" + config_entry = MockConfigEntry( + domain="zha", + unique_id="some_unique_id", + data={}, + version=4, + ) + + assert get_zha_device_path(config_entry) is None + + async def test_guess_firmware_type_unknown(hass: HomeAssistant) -> None: """Test guessing the firmware type.""" From 0fb5aaf0f827e631c467345eb6a5d239f290d552 Mon Sep 17 00:00:00 2001 From: Bernardus Jansen Date: Tue, 21 May 2024 10:00:29 +0200 Subject: [PATCH 13/24] Tesla Wall Connector fix spelling error/typo (#117841) --- homeassistant/components/tesla_wall_connector/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/tesla_wall_connector/strings.json b/homeassistant/components/tesla_wall_connector/strings.json index ed1878caecb..e8f73f22d20 100644 --- a/homeassistant/components/tesla_wall_connector/strings.json +++ b/homeassistant/components/tesla_wall_connector/strings.json @@ -37,7 +37,7 @@ "not_connected": "Vehicle not connected", "connected": "Vehicle connected", "ready": "Ready to charge", - "negociating": "Negociating connection", + "negotiating": "Negotiating connection", "error": "Error", "charging_finished": "Charging finished", "waiting_car": "Waiting for car", From 7d5f9b1adf52c89645a8ab7b906a6dd3bef65fac Mon Sep 17 00:00:00 2001 From: Peter Date: Wed, 22 May 2024 22:36:03 +0200 Subject: [PATCH 14/24] Prevent time pattern reschedule if cancelled during job execution (#117879) Co-authored-by: J. Nick Koston --- homeassistant/helpers/event.py | 2 +- tests/helpers/test_event.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 5c026064c28..67b057463dd 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -1766,7 +1766,6 @@ class _TrackUTCTimeChange: # time when the timer was scheduled utc_now = time_tracker_utcnow() localized_now = dt_util.as_local(utc_now) if self.local else utc_now - hass.async_run_hass_job(self.job, localized_now, background=True) if TYPE_CHECKING: assert self._pattern_time_change_listener_job is not None self._cancel_callback = async_track_point_in_utc_time( @@ -1774,6 +1773,7 @@ class _TrackUTCTimeChange: self._pattern_time_change_listener_job, self._calculate_next(utc_now + timedelta(seconds=1)), ) + hass.async_run_hass_job(self.job, localized_now, background=True) @callback def async_cancel(self) -> None: diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index a6fad968eac..7fb02024170 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -4589,6 +4589,40 @@ async def test_async_track_point_in_time_cancel(hass: HomeAssistant) -> None: assert "US/Hawaii" in str(times[0].tzinfo) +async def test_async_track_point_in_time_cancel_in_job( + hass: HomeAssistant, freezer: FrozenDateTimeFactory +) -> None: + """Test cancel of async track point in time during job execution.""" + + now = dt_util.utcnow() + times = [] + + time_that_will_not_match_right_away = datetime( + now.year + 1, 5, 24, 11, 59, 55, tzinfo=dt_util.UTC + ) + freezer.move_to(time_that_will_not_match_right_away) + + @callback + def action(x: datetime): + nonlocal times + times.append(x) + unsub() + + unsub = async_track_utc_time_change(hass, action, minute=0, second="*") + + async_fire_time_changed( + hass, datetime(now.year + 1, 5, 24, 12, 0, 0, 999999, tzinfo=dt_util.UTC) + ) + await hass.async_block_till_done() + assert len(times) == 1 + + async_fire_time_changed( + hass, datetime(now.year + 1, 5, 24, 13, 0, 0, 999999, tzinfo=dt_util.UTC) + ) + await hass.async_block_till_done() + assert len(times) == 1 + + async def test_async_track_entity_registry_updated_event(hass: HomeAssistant) -> None: """Test tracking entity registry updates for an entity_id.""" From 7e18527dfb6bb31717e4ce17db5364fc2f0d56ad Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Wed, 22 May 2024 00:11:10 +0200 Subject: [PATCH 15/24] Update philips_js to 3.2.1 (#117881) * Update philips_js to 3.2.0 * Update to 3.2.1 --- homeassistant/components/philips_js/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/philips_js/manifest.json b/homeassistant/components/philips_js/manifest.json index 4751e85d378..b4ca9b931a7 100644 --- a/homeassistant/components/philips_js/manifest.json +++ b/homeassistant/components/philips_js/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/philips_js", "iot_class": "local_polling", "loggers": ["haphilipsjs"], - "requirements": ["ha-philipsjs==3.1.1"] + "requirements": ["ha-philipsjs==3.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index fc909232f2f..3b7b854a2b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1029,7 +1029,7 @@ ha-ffmpeg==3.2.0 ha-iotawattpy==0.1.2 # homeassistant.components.philips_js -ha-philipsjs==3.1.1 +ha-philipsjs==3.2.1 # homeassistant.components.habitica habitipy==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fc12be7e292..307c3b39d3e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -843,7 +843,7 @@ ha-ffmpeg==3.2.0 ha-iotawattpy==0.1.2 # homeassistant.components.philips_js -ha-philipsjs==3.1.1 +ha-philipsjs==3.2.1 # homeassistant.components.habitica habitipy==0.2.0 From ac97f25d6ce3433c0ef807ff6f66b3c9e2aa7911 Mon Sep 17 00:00:00 2001 From: On Freund Date: Wed, 22 May 2024 19:14:04 +0300 Subject: [PATCH 16/24] Bump pyrympro to 0.0.8 (#117919) --- homeassistant/components/rympro/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rympro/manifest.json b/homeassistant/components/rympro/manifest.json index e14ac9af71f..046e778f05b 100644 --- a/homeassistant/components/rympro/manifest.json +++ b/homeassistant/components/rympro/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/rympro", "iot_class": "cloud_polling", - "requirements": ["pyrympro==0.0.7"] + "requirements": ["pyrympro==0.0.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3b7b854a2b0..4ef01b9b52e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2102,7 +2102,7 @@ pyrituals==0.0.6 pyroute2==0.7.5 # homeassistant.components.rympro -pyrympro==0.0.7 +pyrympro==0.0.8 # homeassistant.components.sabnzbd pysabnzbd==1.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 307c3b39d3e..9fd712fbab3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1644,7 +1644,7 @@ pyrituals==0.0.6 pyroute2==0.7.5 # homeassistant.components.rympro -pyrympro==0.0.7 +pyrympro==0.0.8 # homeassistant.components.sabnzbd pysabnzbd==1.1.1 From 09779b5f6ee8b56f38961f63bb65f58f8105ec44 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Thu, 23 May 2024 06:15:15 +0300 Subject: [PATCH 17/24] Add Shelly debug logging for async_reconnect_soon (#117945) --- homeassistant/components/shelly/config_flow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index 46cea4e49a4..ccc86c564d5 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -256,6 +256,7 @@ class ShellyConfigFlow(ConfigFlow, domain=DOMAIN): if ( current_entry := await self.async_set_unique_id(mac) ) and current_entry.data.get(CONF_HOST) == host: + LOGGER.debug("async_reconnect_soon: host: %s, mac: %s", host, mac) await async_reconnect_soon(self.hass, current_entry) if host == INTERNAL_WIFI_AP_IP: # If the device is broadcasting the internal wifi ap ip From f4b653a7678a15e5d22945eca41f404786388184 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 May 2024 17:59:44 -1000 Subject: [PATCH 18/24] Update pySwitchbot to 0.46.0 to fix lock key retrieval (#118005) * Update pySwitchbot to 0.46.0 to fix lock key retrieval needs https://github.com/Danielhiversen/pySwitchbot/pull/236 * bump * fixes --- homeassistant/components/switchbot/config_flow.py | 15 +++++++++++---- homeassistant/components/switchbot/manifest.json | 2 +- homeassistant/components/switchbot/strings.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/switchbot/test_config_flow.py | 11 ++++++----- 6 files changed, 21 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/switchbot/config_flow.py b/homeassistant/components/switchbot/config_flow.py index 06b95c6f8aa..bb69da52239 100644 --- a/homeassistant/components/switchbot/config_flow.py +++ b/homeassistant/components/switchbot/config_flow.py @@ -8,6 +8,7 @@ from typing import Any from switchbot import ( SwitchbotAccountConnectionError, SwitchBotAdvertisement, + SwitchbotApiError, SwitchbotAuthenticationError, SwitchbotLock, SwitchbotModel, @@ -33,6 +34,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.data_entry_flow import AbortFlow +from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( CONF_ENCRYPTION_KEY, @@ -175,14 +177,19 @@ class SwitchbotConfigFlow(ConfigFlow, domain=DOMAIN): description_placeholders = {} if user_input is not None: try: - key_details = await self.hass.async_add_executor_job( - SwitchbotLock.retrieve_encryption_key, + key_details = await SwitchbotLock.async_retrieve_encryption_key( + async_get_clientsession(self.hass), self._discovered_adv.address, user_input[CONF_USERNAME], user_input[CONF_PASSWORD], ) - except SwitchbotAccountConnectionError as ex: - raise AbortFlow("cannot_connect") from ex + except (SwitchbotApiError, SwitchbotAccountConnectionError) as ex: + _LOGGER.debug( + "Failed to connect to SwitchBot API: %s", ex, exc_info=True + ) + raise AbortFlow( + "api_error", description_placeholders={"error_detail": str(ex)} + ) from ex except SwitchbotAuthenticationError as ex: _LOGGER.debug("Authentication failed: %s", ex, exc_info=True) errors = {"base": "auth_failed"} diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 401d85e7376..ba4782c8b63 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -39,5 +39,5 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot", "iot_class": "local_push", "loggers": ["switchbot"], - "requirements": ["PySwitchbot==0.45.0"] + "requirements": ["PySwitchbot==0.46.0"] } diff --git a/homeassistant/components/switchbot/strings.json b/homeassistant/components/switchbot/strings.json index 8eab1ec6f1a..a20b4939f8f 100644 --- a/homeassistant/components/switchbot/strings.json +++ b/homeassistant/components/switchbot/strings.json @@ -46,7 +46,7 @@ "already_configured_device": "[%key:common::config_flow::abort::already_configured_device%]", "no_devices_found": "No supported SwitchBot devices found in range; If the device is in range, ensure the scanner has active scanning enabled, as SwitchBot devices cannot be discovered with passive scans. Active scans can be disabled once the device is configured. If you need clarification on whether the device is in-range, download the diagnostics for the integration that provides your Bluetooth adapter or proxy and check if the MAC address of the SwitchBot device is present.", "unknown": "[%key:common::config_flow::error::unknown%]", - "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "api_error": "Error while communicating with SwitchBot API: {error_detail}", "switchbot_unsupported_type": "Unsupported Switchbot Type." } }, diff --git a/requirements_all.txt b/requirements_all.txt index 4ef01b9b52e..34cce0f7dc1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -93,7 +93,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.45.0 +PySwitchbot==0.46.0 # homeassistant.components.switchmate PySwitchmate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9fd712fbab3..26e704781e7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.45.0 +PySwitchbot==0.46.0 # homeassistant.components.syncthru PySyncThru==0.7.10 diff --git a/tests/components/switchbot/test_config_flow.py b/tests/components/switchbot/test_config_flow.py index a62a100f55a..182e9457f22 100644 --- a/tests/components/switchbot/test_config_flow.py +++ b/tests/components/switchbot/test_config_flow.py @@ -487,7 +487,7 @@ async def test_user_setup_wolock_auth(hass: HomeAssistant) -> None: assert result["errors"] == {} with patch( - "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key", + "homeassistant.components.switchbot.config_flow.SwitchbotLock.async_retrieve_encryption_key", side_effect=SwitchbotAuthenticationError("error from api"), ): result = await hass.config_entries.flow.async_configure( @@ -510,7 +510,7 @@ async def test_user_setup_wolock_auth(hass: HomeAssistant) -> None: return_value=True, ), patch( - "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key", + "homeassistant.components.switchbot.config_flow.SwitchbotLock.async_retrieve_encryption_key", return_value={ CONF_KEY_ID: "ff", CONF_ENCRYPTION_KEY: "ffffffffffffffffffffffffffffffff", @@ -560,8 +560,8 @@ async def test_user_setup_wolock_auth_switchbot_api_down(hass: HomeAssistant) -> assert result["errors"] == {} with patch( - "homeassistant.components.switchbot.config_flow.SwitchbotLock.retrieve_encryption_key", - side_effect=SwitchbotAccountConnectionError, + "homeassistant.components.switchbot.config_flow.SwitchbotLock.async_retrieve_encryption_key", + side_effect=SwitchbotAccountConnectionError("Switchbot API down"), ): result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -572,7 +572,8 @@ async def test_user_setup_wolock_auth_switchbot_api_down(hass: HomeAssistant) -> ) await hass.async_block_till_done() assert result["type"] is FlowResultType.ABORT - assert result["reason"] == "cannot_connect" + assert result["reason"] == "api_error" + assert result["description_placeholders"] == {"error_detail": "Switchbot API down"} async def test_user_setup_wolock_or_bot(hass: HomeAssistant) -> None: From 3238bc83b8711ce1a72915d179ef658dc20a31c0 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 24 May 2024 09:55:05 +0200 Subject: [PATCH 19/24] Improve async_get_issue_tracker for custom integrations (#118016) --- homeassistant/bootstrap.py | 3 +++ homeassistant/loader.py | 8 ++++++++ tests/test_loader.py | 35 +++++++++++++++++++++++++++++++++-- 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index fc5eedffc39..f733c6f9ff1 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -414,6 +414,9 @@ async def async_from_config_dict( start = monotonic() hass.config_entries = config_entries.ConfigEntries(hass, config) + # Prime custom component cache early so we know if registry entries are tied + # to a custom integration + await loader.async_get_custom_components(hass) await async_load_base_functionality(hass) # Set up core. diff --git a/homeassistant/loader.py b/homeassistant/loader.py index b65d6f34f7b..d8b32b053db 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -1674,6 +1674,14 @@ def async_get_issue_tracker( # If we know nothing about the entity, suggest opening an issue on HA core return issue_tracker + if ( + not integration + and (hass and integration_domain) + and (comps_or_future := hass.data.get(DATA_CUSTOM_COMPONENTS)) + and not isinstance(comps_or_future, asyncio.Future) + ): + integration = comps_or_future.get(integration_domain) + if not integration and (hass and integration_domain): with suppress(IntegrationNotLoaded): integration = async_get_loaded_integration(hass, integration_domain) diff --git a/tests/test_loader.py b/tests/test_loader.py index 404858200bc..09afdf1504b 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -2,6 +2,7 @@ import asyncio import os +import pathlib import sys import threading from typing import Any @@ -1108,14 +1109,18 @@ CUSTOM_ISSUE_TRACKER = "https://blablabla.com" # Integration domain is not currently deduced from module (None, "homeassistant.components.hue.sensor", CORE_ISSUE_TRACKER), ("hue", "homeassistant.components.mqtt.sensor", CORE_ISSUE_TRACKER_HUE), - # Custom integration with known issue tracker + # Loaded custom integration with known issue tracker ("bla_custom", "custom_components.bla_custom.sensor", CUSTOM_ISSUE_TRACKER), ("bla_custom", None, CUSTOM_ISSUE_TRACKER), - # Custom integration without known issue tracker + # Loaded custom integration without known issue tracker (None, "custom_components.bla.sensor", None), ("bla_custom_no_tracker", "custom_components.bla_custom.sensor", None), ("bla_custom_no_tracker", None, None), ("hue", "custom_components.bla.sensor", None), + # Unloaded custom integration with known issue tracker + ("bla_custom_not_loaded", None, CUSTOM_ISSUE_TRACKER), + # Unloaded custom integration without known issue tracker + ("bla_custom_not_loaded_no_tracker", None, None), # Integration domain has priority over module ("bla_custom_no_tracker", "homeassistant.components.bla_custom.sensor", None), ], @@ -1133,6 +1138,32 @@ async def test_async_get_issue_tracker( built_in=False, ) mock_integration(hass, MockModule("bla_custom_no_tracker"), built_in=False) + + cust_unloaded_module = MockModule( + "bla_custom_not_loaded", + partial_manifest={"issue_tracker": CUSTOM_ISSUE_TRACKER}, + ) + cust_unloaded = loader.Integration( + hass, + f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{cust_unloaded_module.DOMAIN}", + pathlib.Path(""), + cust_unloaded_module.mock_manifest(), + set(), + ) + + cust_unloaded_no_tracker_module = MockModule("bla_custom_not_loaded_no_tracker") + cust_unloaded_no_tracker = loader.Integration( + hass, + f"{loader.PACKAGE_CUSTOM_COMPONENTS}.{cust_unloaded_no_tracker_module.DOMAIN}", + pathlib.Path(""), + cust_unloaded_no_tracker_module.mock_manifest(), + set(), + ) + hass.data["custom_components"] = { + "bla_custom_not_loaded": cust_unloaded, + "bla_custom_not_loaded_no_tracker": cust_unloaded_no_tracker, + } + assert ( loader.async_get_issue_tracker(hass, integration_domain=domain, module=module) == issue_tracker From f5c20b3528c0c5c1cfca2260711680d3c319892e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 23 May 2024 22:37:10 -1000 Subject: [PATCH 20/24] Bump pySwitchbot to 0.46.1 (#118025) --- homeassistant/components/switchbot/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index ba4782c8b63..2388e5a98b3 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -39,5 +39,5 @@ "documentation": "https://www.home-assistant.io/integrations/switchbot", "iot_class": "local_push", "loggers": ["switchbot"], - "requirements": ["PySwitchbot==0.46.0"] + "requirements": ["PySwitchbot==0.46.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 34cce0f7dc1..bd747808819 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -93,7 +93,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.46.0 +PySwitchbot==0.46.1 # homeassistant.components.switchmate PySwitchmate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 26e704781e7..716abc3edd2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -81,7 +81,7 @@ PyQRCode==1.2.1 PyRMVtransport==0.3.3 # homeassistant.components.switchbot -PySwitchbot==0.46.0 +PySwitchbot==0.46.1 # homeassistant.components.syncthru PySyncThru==0.7.10 From 81bf31bbb16ad570604aafaecadda6d36c9f55b0 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Fri, 24 May 2024 13:50:10 +0200 Subject: [PATCH 21/24] Extend the blocklist for Matter transitions with more models (#118038) --- homeassistant/components/matter/light.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/matter/light.py b/homeassistant/components/matter/light.py index da72798dda1..acd85884875 100644 --- a/homeassistant/components/matter/light.py +++ b/homeassistant/components/matter/light.py @@ -52,7 +52,10 @@ DEFAULT_TRANSITION = 0.2 # sw version (attributeKey 0/40/10) TRANSITION_BLOCKLIST = ( (4488, 514, "1.0", "1.0.0"), + (4488, 260, "1.0", "1.0.0"), (5010, 769, "3.0", "1.0.0"), + (4999, 25057, "1.0", "27.0"), + (4448, 36866, "V1", "V1.0.0.5"), ) From 3f7e57dde224587b6559f67fbc011885ec1578b5 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 24 May 2024 16:13:44 +0200 Subject: [PATCH 22/24] Bump version to 2024.5.5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 278050b69e1..e0832f7cc85 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -23,7 +23,7 @@ if TYPE_CHECKING: APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 5 -PATCH_VERSION: Final = "4" +PATCH_VERSION: Final = "5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 1805545235f..b84159eb457 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.5.4" +version = "2024.5.5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 8128449879c557e72161584c2e383608e48219a2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 24 May 2024 18:41:46 +0200 Subject: [PATCH 23/24] Fix rc pylint warning in MQTT (#118050) --- homeassistant/components/mqtt/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/mqtt/client.py b/homeassistant/components/mqtt/client.py index e6e4bb52049..0261512fe99 100644 --- a/homeassistant/components/mqtt/client.py +++ b/homeassistant/components/mqtt/client.py @@ -548,7 +548,7 @@ class MQTT: # Remove this once # https://github.com/eclipse/paho.mqtt.python/pull/843 # is available. - sock = sock._socket # noqa: SLF001 + sock = sock._socket # pylint: disable=protected-access new_buffer_size = PREFERRED_BUFFER_SIZE while True: From 750ec261be3891d5fde345455317b7855e2cb5ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 10 May 2024 17:09:28 -0500 Subject: [PATCH 24/24] Add state check to config entry setup to ensure it cannot be setup twice (#117193) --- homeassistant/config_entries.py | 9 +++++ tests/components/upnp/test_config_flow.py | 8 ++-- tests/test_config_entries.py | 46 +++++++++++++++++++++-- 3 files changed, 56 insertions(+), 7 deletions(-) diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 9635d5cba48..252f7be8b7e 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -514,6 +514,15 @@ class ConfigEntry: # Only store setup result as state if it was not forwarded. if domain_is_integration := self.domain == integration.domain: + if self.state in ( + ConfigEntryState.LOADED, + ConfigEntryState.SETUP_IN_PROGRESS, + ): + raise OperationNotAllowed( + f"The config entry {self.title} ({self.domain}) with entry_id" + f" {self.entry_id} cannot be setup because is already loaded in the" + f" {self.state} state" + ) self._async_set_state(hass, ConfigEntryState.SETUP_IN_PROGRESS, None) if self.supports_unload is None: diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index a3d2b97f3ed..a4598346a51 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -196,7 +196,7 @@ async def test_flow_ssdp_discovery_changed_udn_match_mac(hass: HomeAssistant) -> CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, }, source=config_entries.SOURCE_SSDP, - state=config_entries.ConfigEntryState.LOADED, + state=config_entries.ConfigEntryState.NOT_LOADED, ) entry.add_to_hass(hass) @@ -228,7 +228,7 @@ async def test_flow_ssdp_discovery_changed_udn_match_host(hass: HomeAssistant) - CONFIG_ENTRY_HOST: TEST_HOST, }, source=config_entries.SOURCE_SSDP, - state=config_entries.ConfigEntryState.LOADED, + state=config_entries.ConfigEntryState.NOT_LOADED, ) entry.add_to_hass(hass) @@ -266,7 +266,7 @@ async def test_flow_ssdp_discovery_changed_udn_but_st_differs( CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, }, source=config_entries.SOURCE_SSDP, - state=config_entries.ConfigEntryState.LOADED, + state=config_entries.ConfigEntryState.NOT_LOADED, ) entry.add_to_hass(hass) @@ -320,7 +320,7 @@ async def test_flow_ssdp_discovery_changed_location(hass: HomeAssistant) -> None CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, }, source=config_entries.SOURCE_SSDP, - state=config_entries.ConfigEntryState.LOADED, + state=config_entries.ConfigEntryState.NOT_LOADED, ) entry.add_to_hass(hass) diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 9c491987d79..1394ca1e435 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -469,7 +469,7 @@ async def test_remove_entry( ] # Setup entry - await entry.async_setup(hass) + await manager.async_setup(entry.entry_id) await hass.async_block_till_done() # Check entity state got added @@ -1696,7 +1696,9 @@ async def test_entry_reload_succeed( hass: HomeAssistant, manager: config_entries.ConfigEntries ) -> None: """Test that we can reload an entry.""" - entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED) + entry = MockConfigEntry( + domain="comp", state=config_entries.ConfigEntryState.NOT_LOADED + ) entry.add_to_hass(hass) async_setup = AsyncMock(return_value=True) @@ -1720,6 +1722,42 @@ async def test_entry_reload_succeed( assert entry.state is config_entries.ConfigEntryState.LOADED +@pytest.mark.parametrize( + "state", + [ + config_entries.ConfigEntryState.LOADED, + config_entries.ConfigEntryState.SETUP_IN_PROGRESS, + ], +) +async def test_entry_cannot_be_loaded_twice( + hass: HomeAssistant, state: config_entries.ConfigEntryState +) -> None: + """Test that a config entry cannot be loaded twice.""" + entry = MockConfigEntry(domain="comp", state=state) + entry.add_to_hass(hass) + + async_setup = AsyncMock(return_value=True) + async_setup_entry = AsyncMock(return_value=True) + async_unload_entry = AsyncMock(return_value=True) + + mock_integration( + hass, + MockModule( + "comp", + async_setup=async_setup, + async_setup_entry=async_setup_entry, + async_unload_entry=async_unload_entry, + ), + ) + mock_platform(hass, "comp.config_flow", None) + + with pytest.raises(config_entries.OperationNotAllowed, match=str(state)): + await entry.async_setup(hass) + assert len(async_setup.mock_calls) == 0 + assert len(async_setup_entry.mock_calls) == 0 + assert entry.state is state + + @pytest.mark.parametrize( "state", [ @@ -4088,7 +4126,9 @@ async def test_entry_reload_concurrency_not_setup_setup( hass: HomeAssistant, manager: config_entries.ConfigEntries ) -> None: """Test multiple reload calls do not cause a reload race.""" - entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED) + entry = MockConfigEntry( + domain="comp", state=config_entries.ConfigEntryState.NOT_LOADED + ) entry.add_to_hass(hass) async_setup = AsyncMock(return_value=True)