From 649dd433d5e2bccaf5162d6339c8a1bc69c06a6e Mon Sep 17 00:00:00 2001 From: Jan Stienstra <65826735+j-stienstra@users.noreply.github.com> Date: Mon, 11 Mar 2024 19:09:18 +0100 Subject: [PATCH 01/99] Fix optional Jellyfin RunTimeTicks (#108254) --- homeassistant/components/jellyfin/media_player.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/jellyfin/media_player.py b/homeassistant/components/jellyfin/media_player.py index 76343818702..0f4b58b17e8 100644 --- a/homeassistant/components/jellyfin/media_player.py +++ b/homeassistant/components/jellyfin/media_player.py @@ -149,7 +149,9 @@ class JellyfinMediaPlayer(JellyfinEntity, MediaPlayerEntity): media_content_type = CONTENT_TYPE_MAP.get(self.now_playing["Type"], None) media_content_id = self.now_playing["Id"] media_title = self.now_playing["Name"] - media_duration = int(self.now_playing["RunTimeTicks"] / 10000000) + + if "RunTimeTicks" in self.now_playing: + media_duration = int(self.now_playing["RunTimeTicks"] / 10000000) if media_content_type == MediaType.EPISODE: media_content_type = MediaType.TVSHOW From 6f6f37ca240c7034b48cf12c6ba0e01413e6b2a0 Mon Sep 17 00:00:00 2001 From: mattmccormack Date: Wed, 13 Mar 2024 06:55:26 +1000 Subject: [PATCH 02/99] Add auto fan mode icon (#110185) --- homeassistant/components/climate/icons.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/climate/icons.json b/homeassistant/components/climate/icons.json index 4317698b257..5afce637ed5 100644 --- a/homeassistant/components/climate/icons.json +++ b/homeassistant/components/climate/icons.json @@ -6,6 +6,7 @@ "fan_mode": { "default": "mdi:circle-medium", "state": { + "auto": "mdi:fan-auto", "diffuse": "mdi:weather-windy", "focus": "mdi:target", "high": "mdi:speedometer", From 3c4bdebcda92b4b1fed0f2ed586033d56617f2ed Mon Sep 17 00:00:00 2001 From: FieldofClay <7278759+FieldofClay@users.noreply.github.com> Date: Thu, 14 Mar 2024 03:51:40 +1100 Subject: [PATCH 03/99] Ignore AussieBroadband services that don't support usage information (#110253) --- homeassistant/components/aussie_broadband/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/aussie_broadband/__init__.py b/homeassistant/components/aussie_broadband/__init__.py index 093480afd7d..e65abaebb98 100644 --- a/homeassistant/components/aussie_broadband/__init__.py +++ b/homeassistant/components/aussie_broadband/__init__.py @@ -31,9 +31,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async_get_clientsession(hass), ) + # Ignore services that don't support usage data + ignore_types = FETCH_TYPES + ["Hardware"] + try: await client.login() - services = await client.get_services(drop_types=FETCH_TYPES) + services = await client.get_services(drop_types=ignore_types) except AuthenticationException as exc: raise ConfigEntryAuthFailed() from exc except ClientError as exc: From e087ea5345d61080d0b857c2c5086d206ba148bb Mon Sep 17 00:00:00 2001 From: On Freund Date: Tue, 12 Mar 2024 22:20:14 +0200 Subject: [PATCH 04/99] Use friendly name for camera media source (#110882) --- .../components/camera/media_source.py | 16 ++++++--- tests/components/camera/conftest.py | 35 +++++++++++++++++++ tests/components/camera/test_media_source.py | 21 +++++++++++ 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/camera/media_source.py b/homeassistant/components/camera/media_source.py index 3c9a386f958..a49ce11413d 100644 --- a/homeassistant/components/camera/media_source.py +++ b/homeassistant/components/camera/media_source.py @@ -12,6 +12,7 @@ from homeassistant.components.media_source.models import ( PlayMedia, ) from homeassistant.components.stream import FORMAT_CONTENT_TYPE, HLS_PROVIDER +from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_component import EntityComponent @@ -25,13 +26,20 @@ async def async_get_media_source(hass: HomeAssistant) -> CameraMediaSource: return CameraMediaSource(hass) -def _media_source_for_camera(camera: Camera, content_type: str) -> BrowseMediaSource: +def _media_source_for_camera( + hass: HomeAssistant, camera: Camera, content_type: str +) -> BrowseMediaSource: + camera_state = hass.states.get(camera.entity_id) + title = camera.name + if camera_state: + title = camera_state.attributes.get(ATTR_FRIENDLY_NAME, camera.name) + return BrowseMediaSource( domain=DOMAIN, identifier=camera.entity_id, media_class=MediaClass.VIDEO, media_content_type=content_type, - title=camera.name, + title=title, thumbnail=f"/api/camera_proxy/{camera.entity_id}", can_play=True, can_expand=False, @@ -89,7 +97,7 @@ class CameraMediaSource(MediaSource): async def _filter_browsable_camera(camera: Camera) -> BrowseMediaSource | None: stream_type = camera.frontend_stream_type if stream_type is None: - return _media_source_for_camera(camera, camera.content_type) + return _media_source_for_camera(self.hass, camera, camera.content_type) if not can_stream_hls: return None @@ -97,7 +105,7 @@ class CameraMediaSource(MediaSource): if stream_type != StreamType.HLS and not (await camera.stream_source()): return None - return _media_source_for_camera(camera, content_type) + return _media_source_for_camera(self.hass, camera, content_type) component: EntityComponent[Camera] = self.hass.data[DOMAIN] results = await asyncio.gather( diff --git a/tests/components/camera/conftest.py b/tests/components/camera/conftest.py index 21737323671..bb5812680f0 100644 --- a/tests/components/camera/conftest.py +++ b/tests/components/camera/conftest.py @@ -7,6 +7,7 @@ from homeassistant.components import camera from homeassistant.components.camera.const import StreamType from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.setup import async_setup_component from .common import WEBRTC_ANSWER @@ -69,3 +70,37 @@ async def mock_camera_web_rtc_fixture(hass): return_value=WEBRTC_ANSWER, ): yield + + +@pytest.fixture(name="mock_camera_with_device") +async def mock_camera_with_device_fixture(): + """Initialize a demo camera platform with a device.""" + dev_info = DeviceInfo( + identifiers={("camera", "test_unique_id")}, + name="Test Camera Device", + ) + + class UniqueIdMock(PropertyMock): + def __get__(self, obj, obj_type=None): + return obj.name + + with patch( + "homeassistant.components.camera.Camera.has_entity_name", + new_callable=PropertyMock(return_value=True), + ), patch( + "homeassistant.components.camera.Camera.unique_id", new=UniqueIdMock() + ), patch( + "homeassistant.components.camera.Camera.device_info", + new_callable=PropertyMock(return_value=dev_info), + ): + yield + + +@pytest.fixture(name="mock_camera_with_no_name") +async def mock_camera_with_no_name_fixture(mock_camera_with_device): + """Initialize a demo camera platform with a device and no name.""" + with patch( + "homeassistant.components.camera.Camera._attr_name", + new_callable=PropertyMock(return_value=None), + ): + yield diff --git a/tests/components/camera/test_media_source.py b/tests/components/camera/test_media_source.py index f965bdadb09..a70bb262103 100644 --- a/tests/components/camera/test_media_source.py +++ b/tests/components/camera/test_media_source.py @@ -16,6 +16,26 @@ async def setup_media_source(hass): assert await async_setup_component(hass, "media_source", {}) +async def test_device_with_device( + hass: HomeAssistant, mock_camera_with_device, mock_camera +) -> None: + """Test browsing when camera has a device and a name.""" + item = await media_source.async_browse_media(hass, "media-source://camera") + assert item.not_shown == 2 + assert len(item.children) == 1 + assert item.children[0].title == "Test Camera Device Demo camera without stream" + + +async def test_device_with_no_name( + hass: HomeAssistant, mock_camera_with_no_name, mock_camera +) -> None: + """Test browsing when camera has device and name == None.""" + item = await media_source.async_browse_media(hass, "media-source://camera") + assert item.not_shown == 2 + assert len(item.children) == 1 + assert item.children[0].title == "Test Camera Device Demo camera without stream" + + async def test_browsing_hls(hass: HomeAssistant, mock_camera_hls) -> None: """Test browsing HLS camera media source.""" item = await media_source.async_browse_media(hass, "media-source://camera") @@ -41,6 +61,7 @@ async def test_browsing_mjpeg(hass: HomeAssistant, mock_camera) -> None: assert len(item.children) == 1 assert item.not_shown == 2 assert item.children[0].media_content_type == "image/jpg" + assert item.children[0].title == "Demo camera without stream" async def test_browsing_web_rtc(hass: HomeAssistant, mock_camera_web_rtc) -> None: From 57c8d47ff33cd6d44699053b878921c0e6a4c2f5 Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Tue, 12 Mar 2024 17:38:09 +0100 Subject: [PATCH 05/99] Improve discovering upnp/igd device by always using the SSDP-discovery for the Unique Device Name (#111487) * Always use the UDN found in the SSDP discovery, instead of the device description * Ensure existing DeviceEntries are still matched --- homeassistant/components/upnp/__init__.py | 5 +- homeassistant/components/upnp/config_flow.py | 15 +++--- tests/components/upnp/test_config_flow.py | 41 ++++++++++++++- tests/components/upnp/test_init.py | 54 +++++++++++++++++++- 4 files changed, 103 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 2e546f8893f..905a3de75f0 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -79,6 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Create device. assert discovery_info is not None + assert discovery_info.ssdp_udn assert discovery_info.ssdp_all_locations location = get_preferred_location(discovery_info.ssdp_all_locations) try: @@ -117,7 +118,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if device.serial_number: identifiers.add((IDENTIFIER_SERIAL_NUMBER, device.serial_number)) - connections = {(dr.CONNECTION_UPNP, device.udn)} + connections = {(dr.CONNECTION_UPNP, discovery_info.ssdp_udn)} + if discovery_info.ssdp_udn != device.udn: + connections.add((dr.CONNECTION_UPNP, device.udn)) if device_mac_address: connections.add((dr.CONNECTION_NETWORK_MAC, device_mac_address)) diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index fa33d4b29d3..c7882285b9c 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -42,7 +42,7 @@ def _friendly_name_from_discovery(discovery_info: ssdp.SsdpServiceInfo) -> str: def _is_complete_discovery(discovery_info: ssdp.SsdpServiceInfo) -> bool: """Test if discovery is complete and usable.""" return bool( - ssdp.ATTR_UPNP_UDN in discovery_info.upnp + discovery_info.ssdp_udn and discovery_info.ssdp_st and discovery_info.ssdp_all_locations and discovery_info.ssdp_usn @@ -80,9 +80,8 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 # Paths: - # - ssdp(discovery_info) --> ssdp_confirm(None) - # --> ssdp_confirm({}) --> create_entry() - # - user(None): scan --> user({...}) --> create_entry() + # 1: ssdp(discovery_info) --> ssdp_confirm(None) --> ssdp_confirm({}) --> create_entry() + # 2: user(None): scan --> user({...}) --> create_entry() @property def _discoveries(self) -> dict[str, SsdpServiceInfo]: @@ -241,9 +240,9 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): discovery = self._remove_discovery(usn) mac_address = await _async_mac_address_from_discovery(self.hass, discovery) data = { - CONFIG_ENTRY_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN], + CONFIG_ENTRY_UDN: discovery.ssdp_udn, CONFIG_ENTRY_ST: discovery.ssdp_st, - CONFIG_ENTRY_ORIGINAL_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN], + CONFIG_ENTRY_ORIGINAL_UDN: discovery.ssdp_udn, CONFIG_ENTRY_MAC_ADDRESS: mac_address, CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"], CONFIG_ENTRY_LOCATION: get_preferred_location(discovery.ssdp_all_locations), @@ -265,9 +264,9 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): title = _friendly_name_from_discovery(discovery) mac_address = await _async_mac_address_from_discovery(self.hass, discovery) data = { - CONFIG_ENTRY_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN], + CONFIG_ENTRY_UDN: discovery.ssdp_udn, CONFIG_ENTRY_ST: discovery.ssdp_st, - CONFIG_ENTRY_ORIGINAL_UDN: discovery.upnp[ssdp.ATTR_UPNP_UDN], + CONFIG_ENTRY_ORIGINAL_UDN: discovery.ssdp_udn, CONFIG_ENTRY_LOCATION: get_preferred_location(discovery.ssdp_all_locations), CONFIG_ENTRY_MAC_ADDRESS: mac_address, CONFIG_ENTRY_HOST: discovery.ssdp_headers["_host"], diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index 7c542e33c9d..67b4e5b10e6 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -1,5 +1,6 @@ """Test UPnP/IGD config flow.""" +import copy from copy import deepcopy from unittest.mock import patch @@ -111,6 +112,7 @@ async def test_flow_ssdp_incomplete_discovery(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=ssdp.SsdpServiceInfo( ssdp_usn=TEST_USN, + # ssdp_udn=TEST_UDN, # Not provided. ssdp_st=TEST_ST, ssdp_location=TEST_LOCATION, upnp={ @@ -132,12 +134,12 @@ async def test_flow_ssdp_non_igd_device(hass: HomeAssistant) -> None: context={"source": config_entries.SOURCE_SSDP}, data=ssdp.SsdpServiceInfo( ssdp_usn=TEST_USN, + ssdp_udn=TEST_UDN, ssdp_st=TEST_ST, ssdp_location=TEST_LOCATION, ssdp_all_locations=[TEST_LOCATION], upnp={ ssdp.ATTR_UPNP_DEVICE_TYPE: "urn:schemas-upnp-org:device:WFADevice:1", # Non-IGD - ssdp.ATTR_UPNP_UDN: TEST_UDN, }, ), ) @@ -442,3 +444,40 @@ async def test_flow_user_no_discovery(hass: HomeAssistant) -> None: ) assert result["type"] == data_entry_flow.FlowResultType.ABORT assert result["reason"] == "no_devices_found" + + +@pytest.mark.usefixtures( + "ssdp_instant_discovery", + "mock_setup_entry", + "mock_get_source_ip", + "mock_mac_address_from_host", +) +async def test_flow_ssdp_with_mismatched_udn(hass: HomeAssistant) -> None: + """Test config flow: discovered + configured through ssdp, where the UDN differs in the SSDP-discovery vs device description.""" + # Discovered via step ssdp. + test_discovery = copy.deepcopy(TEST_DISCOVERY) + test_discovery.upnp[ssdp.ATTR_UPNP_UDN] = "uuid:another_udn" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=test_discovery, + ) + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "ssdp_confirm" + + # Confirm via step ssdp_confirm. + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={}, + ) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["title"] == TEST_FRIENDLY_NAME + assert result["data"] == { + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, + CONFIG_ENTRY_LOCATION: TEST_LOCATION, + CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, + CONFIG_ENTRY_HOST: TEST_HOST, + } diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index d1d3dfa6c35..aeb228a1433 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -1,10 +1,12 @@ """Test UPnP/IGD setup process.""" from __future__ import annotations -from unittest.mock import AsyncMock +import copy +from unittest.mock import AsyncMock, MagicMock, patch import pytest +from homeassistant.components import ssdp from homeassistant.components.upnp.const import ( CONFIG_ENTRY_LOCATION, CONFIG_ENTRY_MAC_ADDRESS, @@ -15,7 +17,14 @@ from homeassistant.components.upnp.const import ( ) from homeassistant.core import HomeAssistant -from .conftest import TEST_LOCATION, TEST_MAC_ADDRESS, TEST_ST, TEST_UDN, TEST_USN +from .conftest import ( + TEST_DISCOVERY, + TEST_LOCATION, + TEST_MAC_ADDRESS, + TEST_ST, + TEST_UDN, + TEST_USN, +) from tests.common import MockConfigEntry @@ -94,3 +103,44 @@ async def test_async_setup_entry_multi_location( # Ensure that the IPv4 location is used. mock_async_create_device.assert_called_once_with(TEST_LOCATION) + + +@pytest.mark.usefixtures("mock_get_source_ip", "mock_mac_address_from_host") +async def test_async_setup_udn_mismatch( + hass: HomeAssistant, mock_async_create_device: AsyncMock +) -> None: + """Test async_setup_entry for a device which reports a different UDN from SSDP-discovery and device description.""" + test_discovery = copy.deepcopy(TEST_DISCOVERY) + test_discovery.upnp[ssdp.ATTR_UPNP_UDN] = "uuid:another_udn" + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_USN, + data={ + CONFIG_ENTRY_ST: TEST_ST, + CONFIG_ENTRY_UDN: TEST_UDN, + CONFIG_ENTRY_ORIGINAL_UDN: TEST_UDN, + CONFIG_ENTRY_LOCATION: TEST_LOCATION, + CONFIG_ENTRY_MAC_ADDRESS: TEST_MAC_ADDRESS, + }, + ) + + # Set up device discovery callback. + async def register_callback(hass, callback, match_dict): + """Immediately do callback.""" + await callback(test_discovery, ssdp.SsdpChange.ALIVE) + return MagicMock() + + with patch( + "homeassistant.components.ssdp.async_register_callback", + side_effect=register_callback, + ), patch( + "homeassistant.components.ssdp.async_get_discovery_info_by_st", + return_value=[test_discovery], + ): + # Load config_entry. + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) is True + + # Ensure that the IPv4 location is used. + mock_async_create_device.assert_called_once_with(TEST_LOCATION) From c60f203aab389d0e89d01a872f4611dfe8d1c8e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henning=20Cla=C3=9Fen?= Date: Sun, 10 Mar 2024 11:02:25 +0100 Subject: [PATCH 06/99] Update the numato-gpio dependency of the numato integration to v0.12.0 (#112272) * Update the numato-gpio dependency of the numato integration to v0.12.0 * Augment numato integration manifest with integration_type Fulfills a requirement in the PR checklist. --- homeassistant/components/numato/manifest.json | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/numato/manifest.json b/homeassistant/components/numato/manifest.json index 7dcbeae1f21..e6efcea5315 100644 --- a/homeassistant/components/numato/manifest.json +++ b/homeassistant/components/numato/manifest.json @@ -3,7 +3,8 @@ "name": "Numato USB GPIO Expander", "codeowners": ["@clssn"], "documentation": "https://www.home-assistant.io/integrations/numato", + "integration_type": "hub", "iot_class": "local_push", "loggers": ["numato_gpio"], - "requirements": ["numato-gpio==0.10.0"] + "requirements": ["numato-gpio==0.12.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 62d5b75af2d..835e9dd7c7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1409,7 +1409,7 @@ nsw-fuel-api-client==1.1.0 nuheat==1.0.1 # homeassistant.components.numato -numato-gpio==0.10.0 +numato-gpio==0.12.0 # homeassistant.components.compensation # homeassistant.components.iqvia diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 552e9074daf..4b7f0752cd8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1127,7 +1127,7 @@ nsw-fuel-api-client==1.1.0 nuheat==1.0.1 # homeassistant.components.numato -numato-gpio==0.10.0 +numato-gpio==0.12.0 # homeassistant.components.compensation # homeassistant.components.iqvia From 095aab5f9df10aead579894e3fb08caeb33e02f2 Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Fri, 8 Mar 2024 14:33:33 -0500 Subject: [PATCH 07/99] Disable updating ZHA coordinator path from discovery info (#112415) * Never update the device path from config flows * Bring coverage up to 100% * Update tests/components/zha/test_config_flow.py Co-authored-by: TheJulianJES --------- Co-authored-by: TheJulianJES --- homeassistant/components/zha/config_flow.py | 30 ++++++++------ tests/components/zha/test_config_flow.py | 45 ++------------------- 2 files changed, 20 insertions(+), 55 deletions(-) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 60cf917d9f6..e544b2a415a 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -491,23 +491,27 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN VERSION = 4 - async def _set_unique_id_or_update_path( + async def _set_unique_id_and_update_ignored_flow( self, unique_id: str, device_path: str ) -> None: - """Set the flow's unique ID and update the device path if it isn't unique.""" + """Set the flow's unique ID and update the device path in an ignored flow.""" current_entry = await self.async_set_unique_id(unique_id) if not current_entry: return - self._abort_if_unique_id_configured( - updates={ - CONF_DEVICE: { - **current_entry.data.get(CONF_DEVICE, {}), - CONF_DEVICE_PATH: device_path, - }, - } - ) + if current_entry.source != SOURCE_IGNORE: + self._abort_if_unique_id_configured() + else: + # Only update the current entry if it is an ignored discovery + self._abort_if_unique_id_configured( + updates={ + CONF_DEVICE: { + **current_entry.data.get(CONF_DEVICE, {}), + CONF_DEVICE_PATH: device_path, + }, + } + ) @staticmethod @callback @@ -575,7 +579,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN description = discovery_info.description dev_path = discovery_info.device - await self._set_unique_id_or_update_path( + await self._set_unique_id_and_update_ignored_flow( unique_id=f"{vid}:{pid}_{serial_number}_{manufacturer}_{description}", device_path=dev_path, ) @@ -625,7 +629,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN node_name = local_name.removesuffix(".local") device_path = f"socket://{discovery_info.host}:{port}" - await self._set_unique_id_or_update_path( + await self._set_unique_id_and_update_ignored_flow( unique_id=node_name, device_path=device_path, ) @@ -650,7 +654,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN device_settings = discovery_data["port"] device_path = device_settings[CONF_DEVICE_PATH] - await self._set_unique_id_or_update_path( + await self._set_unique_id_and_update_ignored_flow( unique_id=f"{name}_{radio_type.name}_{device_path}", device_path=device_path, ) diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 3cd20771e6e..0972918a648 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -293,45 +293,6 @@ async def test_efr32_via_zeroconf(hass: HomeAssistant) -> None: } -@patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) -@patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) -async def test_discovery_via_zeroconf_ip_change(hass: HomeAssistant) -> None: - """Test zeroconf flow -- radio detected.""" - entry = MockConfigEntry( - domain=DOMAIN, - unique_id="tube_zb_gw_cc2652p2_poe", - data={ - CONF_DEVICE: { - CONF_DEVICE_PATH: "socket://192.168.1.5:6638", - CONF_BAUDRATE: 115200, - CONF_FLOW_CONTROL: None, - } - }, - ) - entry.add_to_hass(hass) - - service_info = zeroconf.ZeroconfServiceInfo( - ip_address=ip_address("192.168.1.22"), - ip_addresses=[ip_address("192.168.1.22")], - hostname="tube_zb_gw_cc2652p2_poe.local.", - name="mock_name", - port=6053, - properties={"address": "tube_zb_gw_cc2652p2_poe.local"}, - type="mock_type", - ) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF}, data=service_info - ) - - assert result["type"] == FlowResultType.ABORT - assert result["reason"] == "already_configured" - assert entry.data[CONF_DEVICE] == { - CONF_DEVICE_PATH: "socket://192.168.1.22:6638", - CONF_BAUDRATE: 115200, - CONF_FLOW_CONTROL: None, - } - - @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) @patch(f"zigpy_znp.{PROBE_FUNCTION_PATH}", AsyncMock(return_value=True)) async def test_discovery_via_zeroconf_ip_change_ignored(hass: HomeAssistant) -> None: @@ -547,8 +508,8 @@ async def test_discovery_via_usb_already_setup(hass: HomeAssistant) -> None: @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) -async def test_discovery_via_usb_path_changes(hass: HomeAssistant) -> None: - """Test usb flow already setup and the path changes.""" +async def test_discovery_via_usb_path_does_not_change(hass: HomeAssistant) -> None: + """Test usb flow already set up and the path does not change.""" entry = MockConfigEntry( domain=DOMAIN, @@ -579,7 +540,7 @@ async def test_discovery_via_usb_path_changes(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" assert entry.data[CONF_DEVICE] == { - CONF_DEVICE_PATH: "/dev/ttyZIGBEE", + CONF_DEVICE_PATH: "/dev/ttyUSB1", CONF_BAUDRATE: 115200, CONF_FLOW_CONTROL: None, } From fba6e5f065be075a70b2c2ec5589cbcbd91e938c Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 6 Mar 2024 13:47:58 -0600 Subject: [PATCH 08/99] Bump intents to 2024.3.6 (#112515) --- homeassistant/components/conversation/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/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 6f484941a3d..4e3339d227b 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.2.28"] + "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.3.6"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 03a08a442a8..d9737e3d124 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240306.0 -home-assistant-intents==2024.2.28 +home-assistant-intents==2024.3.6 httpx==0.27.0 ifaddr==0.2.0 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 835e9dd7c7c..4f0b4eed0ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1077,7 +1077,7 @@ holidays==0.44 home-assistant-frontend==20240306.0 # homeassistant.components.conversation -home-assistant-intents==2024.2.28 +home-assistant-intents==2024.3.6 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b7f0752cd8..e97fb4ff829 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,7 +876,7 @@ holidays==0.44 home-assistant-frontend==20240306.0 # homeassistant.components.conversation -home-assistant-intents==2024.2.28 +home-assistant-intents==2024.3.6 # homeassistant.components.home_connect homeconnect==0.7.2 From 4b387b5d770a4267a42f2dc58bebe433587833a9 Mon Sep 17 00:00:00 2001 From: Jeef Date: Mon, 4 Mar 2024 11:35:50 -0700 Subject: [PATCH 09/99] Weatherflow_cloud backing lib bump (#112262) Backing lib bump --- homeassistant/components/weatherflow_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/weatherflow_cloud/manifest.json b/homeassistant/components/weatherflow_cloud/manifest.json index 6abbeef02df..15943dde32a 100644 --- a/homeassistant/components/weatherflow_cloud/manifest.json +++ b/homeassistant/components/weatherflow_cloud/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/weatherflow_cloud", "iot_class": "cloud_polling", - "requirements": ["weatherflow4py==0.1.12"] + "requirements": ["weatherflow4py==0.1.13"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4f0b4eed0ab..3096c6c467b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2836,7 +2836,7 @@ watchdog==2.3.1 waterfurnace==1.1.0 # homeassistant.components.weatherflow_cloud -weatherflow4py==0.1.12 +weatherflow4py==0.1.13 # homeassistant.components.webmin webmin-xmlrpc==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e97fb4ff829..72c163781c8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2174,7 +2174,7 @@ wallbox==0.6.0 watchdog==2.3.1 # homeassistant.components.weatherflow_cloud -weatherflow4py==0.1.12 +weatherflow4py==0.1.13 # homeassistant.components.webmin webmin-xmlrpc==0.0.1 From 85b63c16e90770804ff28ef5a2d1ef6361485a9b Mon Sep 17 00:00:00 2001 From: Jeef Date: Thu, 7 Mar 2024 04:39:38 -0700 Subject: [PATCH 10/99] Bump weatherflow4py to 0.1.14 (#112554) adding missing rain states --- homeassistant/components/weatherflow_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/weatherflow_cloud/manifest.json b/homeassistant/components/weatherflow_cloud/manifest.json index 15943dde32a..4cb0f5296e5 100644 --- a/homeassistant/components/weatherflow_cloud/manifest.json +++ b/homeassistant/components/weatherflow_cloud/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/weatherflow_cloud", "iot_class": "cloud_polling", - "requirements": ["weatherflow4py==0.1.13"] + "requirements": ["weatherflow4py==0.1.14"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3096c6c467b..3480929b9a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2836,7 +2836,7 @@ watchdog==2.3.1 waterfurnace==1.1.0 # homeassistant.components.weatherflow_cloud -weatherflow4py==0.1.13 +weatherflow4py==0.1.14 # homeassistant.components.webmin webmin-xmlrpc==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 72c163781c8..84194256772 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2174,7 +2174,7 @@ wallbox==0.6.0 watchdog==2.3.1 # homeassistant.components.weatherflow_cloud -weatherflow4py==0.1.13 +weatherflow4py==0.1.14 # homeassistant.components.webmin webmin-xmlrpc==0.0.1 From 2689f78925fc15167d7ce7caf0a862193fbd7201 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 7 Mar 2024 16:41:54 +0100 Subject: [PATCH 11/99] Restore the juicenet integration (#112578) --- .coveragerc | 6 + CODEOWNERS | 2 + homeassistant/components/juicenet/__init__.py | 112 +++++++++++++--- .../components/juicenet/config_flow.py | 70 +++++++++- homeassistant/components/juicenet/const.py | 6 + homeassistant/components/juicenet/device.py | 19 +++ homeassistant/components/juicenet/entity.py | 34 +++++ .../components/juicenet/manifest.json | 7 +- homeassistant/components/juicenet/number.py | 99 ++++++++++++++ homeassistant/components/juicenet/sensor.py | 116 ++++++++++++++++ .../components/juicenet/strings.json | 41 +++++- homeassistant/components/juicenet/switch.py | 49 +++++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + script/hassfest/requirements.py | 1 + tests/components/juicenet/test_config_flow.py | 124 ++++++++++++++++++ tests/components/juicenet/test_init.py | 50 ------- 19 files changed, 669 insertions(+), 80 deletions(-) create mode 100644 homeassistant/components/juicenet/const.py create mode 100644 homeassistant/components/juicenet/device.py create mode 100644 homeassistant/components/juicenet/entity.py create mode 100644 homeassistant/components/juicenet/number.py create mode 100644 homeassistant/components/juicenet/sensor.py create mode 100644 homeassistant/components/juicenet/switch.py create mode 100644 tests/components/juicenet/test_config_flow.py delete mode 100644 tests/components/juicenet/test_init.py diff --git a/.coveragerc b/.coveragerc index 378532dfd88..0ca73662a84 100644 --- a/.coveragerc +++ b/.coveragerc @@ -639,6 +639,12 @@ omit = homeassistant/components/izone/climate.py homeassistant/components/izone/discovery.py homeassistant/components/joaoapps_join/* + homeassistant/components/juicenet/__init__.py + homeassistant/components/juicenet/device.py + homeassistant/components/juicenet/entity.py + homeassistant/components/juicenet/number.py + homeassistant/components/juicenet/sensor.py + homeassistant/components/juicenet/switch.py homeassistant/components/justnimbus/coordinator.py homeassistant/components/justnimbus/entity.py homeassistant/components/justnimbus/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 1424469a94b..759d3cd84d3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -669,6 +669,8 @@ build.json @home-assistant/supervisor /tests/components/jellyfin/ @j-stienstra @ctalkington /homeassistant/components/jewish_calendar/ @tsvi /tests/components/jewish_calendar/ @tsvi +/homeassistant/components/juicenet/ @jesserockz +/tests/components/juicenet/ @jesserockz /homeassistant/components/justnimbus/ @kvanzuijlen /tests/components/justnimbus/ @kvanzuijlen /homeassistant/components/jvc_projector/ @SteveEasley @msavazzi diff --git a/homeassistant/components/juicenet/__init__.py b/homeassistant/components/juicenet/__init__.py index 820f0d1fcc0..bcefe763e15 100644 --- a/homeassistant/components/juicenet/__init__.py +++ b/homeassistant/components/juicenet/__init__.py @@ -1,37 +1,107 @@ """The JuiceNet integration.""" -from __future__ import annotations +from datetime import timedelta +import logging -from homeassistant.config_entries import ConfigEntry, ConfigEntryState +import aiohttp +from pyjuicenet import Api, TokenError +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -DOMAIN = "juicenet" +from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR +from .device import JuiceNetApi + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = [Platform.NUMBER, Platform.SENSOR, Platform.SWITCH] + +CONFIG_SCHEMA = vol.Schema( + vol.All( + cv.deprecated(DOMAIN), + {DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})}, + ), + extra=vol.ALLOW_EXTRA, +) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the JuiceNet component.""" + conf = config.get(DOMAIN) + hass.data.setdefault(DOMAIN, {}) + + if not conf: + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) + return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up JuiceNet from a config entry.""" - ir.async_create_issue( + + config = entry.data + + session = async_get_clientsession(hass) + + access_token = config[CONF_ACCESS_TOKEN] + api = Api(access_token, session) + + juicenet = JuiceNetApi(api) + + try: + await juicenet.setup() + except TokenError as error: + _LOGGER.error("JuiceNet Error %s", error) + return False + except aiohttp.ClientError as error: + _LOGGER.error("Could not reach the JuiceNet API %s", error) + raise ConfigEntryNotReady from error + + if not juicenet.devices: + _LOGGER.error("No JuiceNet devices found for this account") + return False + _LOGGER.info("%d JuiceNet device(s) found", len(juicenet.devices)) + + async def async_update_data(): + """Update all device states from the JuiceNet API.""" + for device in juicenet.devices: + await device.update_state(True) + return True + + coordinator = DataUpdateCoordinator( hass, - DOMAIN, - DOMAIN, - is_fixable=False, - severity=ir.IssueSeverity.ERROR, - translation_key="integration_removed", - translation_placeholders={ - "entries": "/config/integrations/integration/juicenet", - }, + _LOGGER, + name="JuiceNet", + update_method=async_update_data, + update_interval=timedelta(seconds=30), ) + await coordinator.async_config_entry_first_refresh() + + hass.data[DOMAIN][entry.entry_id] = { + JUICENET_API: juicenet, + JUICENET_COORDINATOR: coordinator, + } + + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" - if all( - config_entry.state is ConfigEntryState.NOT_LOADED - for config_entry in hass.config_entries.async_entries(DOMAIN) - if config_entry.entry_id != entry.entry_id - ): - ir.async_delete_issue(hass, DOMAIN, DOMAIN) - - return True + unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/juicenet/config_flow.py b/homeassistant/components/juicenet/config_flow.py index 7fdc024df47..35c1853b974 100644 --- a/homeassistant/components/juicenet/config_flow.py +++ b/homeassistant/components/juicenet/config_flow.py @@ -1,11 +1,77 @@ """Config flow for JuiceNet integration.""" +import logging -from homeassistant import config_entries +import aiohttp +from pyjuicenet import Api, TokenError +import voluptuous as vol -from . import DOMAIN +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + session = async_get_clientsession(hass) + juicenet = Api(data[CONF_ACCESS_TOKEN], session) + + try: + await juicenet.get_devices() + except TokenError as error: + _LOGGER.error("Token Error %s", error) + raise InvalidAuth from error + except aiohttp.ClientError as error: + _LOGGER.error("Error connecting %s", error) + raise CannotConnect from error + + # Return info that you want to store in the config entry. + return {"title": "JuiceNet"} class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for JuiceNet.""" VERSION = 1 + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + await self.async_set_unique_id(user_input[CONF_ACCESS_TOKEN]) + self._abort_if_unique_id_configured() + + try: + info = await validate_input(self.hass, user_input) + return self.async_create_entry(title=info["title"], data=user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, user_input): + """Handle import.""" + return await self.async_step_user(user_input) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/juicenet/const.py b/homeassistant/components/juicenet/const.py new file mode 100644 index 00000000000..5dc3e5c3e27 --- /dev/null +++ b/homeassistant/components/juicenet/const.py @@ -0,0 +1,6 @@ +"""Constants used by the JuiceNet component.""" + +DOMAIN = "juicenet" + +JUICENET_API = "juicenet_api" +JUICENET_COORDINATOR = "juicenet_coordinator" diff --git a/homeassistant/components/juicenet/device.py b/homeassistant/components/juicenet/device.py new file mode 100644 index 00000000000..86e1c92e4da --- /dev/null +++ b/homeassistant/components/juicenet/device.py @@ -0,0 +1,19 @@ +"""Adapter to wrap the pyjuicenet api for home assistant.""" + + +class JuiceNetApi: + """Represent a connection to JuiceNet.""" + + def __init__(self, api): + """Create an object from the provided API instance.""" + self.api = api + self._devices = [] + + async def setup(self): + """JuiceNet device setup.""" # noqa: D403 + self._devices = await self.api.get_devices() + + @property + def devices(self) -> list: + """Get a list of devices managed by this account.""" + return self._devices diff --git a/homeassistant/components/juicenet/entity.py b/homeassistant/components/juicenet/entity.py new file mode 100644 index 00000000000..b3433948582 --- /dev/null +++ b/homeassistant/components/juicenet/entity.py @@ -0,0 +1,34 @@ +"""Adapter to wrap the pyjuicenet api for home assistant.""" + +from pyjuicenet import Charger + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DOMAIN + + +class JuiceNetDevice(CoordinatorEntity): + """Represent a base JuiceNet device.""" + + _attr_has_entity_name = True + + def __init__( + self, device: Charger, key: str, coordinator: DataUpdateCoordinator + ) -> None: + """Initialise the sensor.""" + super().__init__(coordinator) + self.device = device + self.key = key + self._attr_unique_id = f"{device.id}-{key}" + self._attr_device_info = DeviceInfo( + configuration_url=( + f"https://home.juice.net/Portal/Details?unitID={device.id}" + ), + identifiers={(DOMAIN, device.id)}, + manufacturer="JuiceNet", + name=device.name, + ) diff --git a/homeassistant/components/juicenet/manifest.json b/homeassistant/components/juicenet/manifest.json index 5bdad83ac1e..979e540af01 100644 --- a/homeassistant/components/juicenet/manifest.json +++ b/homeassistant/components/juicenet/manifest.json @@ -1,9 +1,10 @@ { "domain": "juicenet", "name": "JuiceNet", - "codeowners": [], + "codeowners": ["@jesserockz"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/juicenet", - "integration_type": "system", "iot_class": "cloud_polling", - "requirements": [] + "loggers": ["pyjuicenet"], + "requirements": ["python-juicenet==1.1.0"] } diff --git a/homeassistant/components/juicenet/number.py b/homeassistant/components/juicenet/number.py new file mode 100644 index 00000000000..fd2535c5bf3 --- /dev/null +++ b/homeassistant/components/juicenet/number.py @@ -0,0 +1,99 @@ +"""Support for controlling juicenet/juicepoint/juicebox based EVSE numbers.""" +from __future__ import annotations + +from dataclasses import dataclass + +from pyjuicenet import Api, Charger + +from homeassistant.components.number import ( + DEFAULT_MAX_VALUE, + NumberEntity, + NumberEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR +from .entity import JuiceNetDevice + + +@dataclass(frozen=True) +class JuiceNetNumberEntityDescriptionMixin: + """Mixin for required keys.""" + + setter_key: str + + +@dataclass(frozen=True) +class JuiceNetNumberEntityDescription( + NumberEntityDescription, JuiceNetNumberEntityDescriptionMixin +): + """An entity description for a JuiceNetNumber.""" + + native_max_value_key: str | None = None + + +NUMBER_TYPES: tuple[JuiceNetNumberEntityDescription, ...] = ( + JuiceNetNumberEntityDescription( + translation_key="amperage_limit", + key="current_charging_amperage_limit", + native_min_value=6, + native_max_value_key="max_charging_amperage", + native_step=1, + setter_key="set_charging_amperage_limit", + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the JuiceNet Numbers.""" + juicenet_data = hass.data[DOMAIN][config_entry.entry_id] + api: Api = juicenet_data[JUICENET_API] + coordinator = juicenet_data[JUICENET_COORDINATOR] + + entities = [ + JuiceNetNumber(device, description, coordinator) + for device in api.devices + for description in NUMBER_TYPES + ] + async_add_entities(entities) + + +class JuiceNetNumber(JuiceNetDevice, NumberEntity): + """Implementation of a JuiceNet number.""" + + entity_description: JuiceNetNumberEntityDescription + + def __init__( + self, + device: Charger, + description: JuiceNetNumberEntityDescription, + coordinator: DataUpdateCoordinator, + ) -> None: + """Initialise the number.""" + super().__init__(device, description.key, coordinator) + self.entity_description = description + + @property + def native_value(self) -> float | None: + """Return the value of the entity.""" + return getattr(self.device, self.entity_description.key, None) + + @property + def native_max_value(self) -> float: + """Return the maximum value.""" + if self.entity_description.native_max_value_key is not None: + return getattr(self.device, self.entity_description.native_max_value_key) + if self.entity_description.native_max_value is not None: + return self.entity_description.native_max_value + return DEFAULT_MAX_VALUE + + async def async_set_native_value(self, value: float) -> None: + """Update the current value.""" + await getattr(self.device, self.entity_description.setter_key)(value) diff --git a/homeassistant/components/juicenet/sensor.py b/homeassistant/components/juicenet/sensor.py new file mode 100644 index 00000000000..5f71e066b9c --- /dev/null +++ b/homeassistant/components/juicenet/sensor.py @@ -0,0 +1,116 @@ +"""Support for monitoring juicenet/juicepoint/juicebox based EVSE sensors.""" +from __future__ import annotations + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, + SensorStateClass, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + UnitOfElectricCurrent, + UnitOfElectricPotential, + UnitOfEnergy, + UnitOfPower, + UnitOfTemperature, + UnitOfTime, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR +from .entity import JuiceNetDevice + +SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( + SensorEntityDescription( + key="status", + name="Charging Status", + ), + SensorEntityDescription( + key="temperature", + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="voltage", + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + device_class=SensorDeviceClass.VOLTAGE, + ), + SensorEntityDescription( + key="amps", + native_unit_of_measurement=UnitOfElectricCurrent.AMPERE, + device_class=SensorDeviceClass.CURRENT, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="watts", + native_unit_of_measurement=UnitOfPower.WATT, + device_class=SensorDeviceClass.POWER, + state_class=SensorStateClass.MEASUREMENT, + ), + SensorEntityDescription( + key="charge_time", + translation_key="charge_time", + native_unit_of_measurement=UnitOfTime.SECONDS, + icon="mdi:timer-outline", + ), + SensorEntityDescription( + key="energy_added", + translation_key="energy_added", + native_unit_of_measurement=UnitOfEnergy.WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the JuiceNet Sensors.""" + juicenet_data = hass.data[DOMAIN][config_entry.entry_id] + api = juicenet_data[JUICENET_API] + coordinator = juicenet_data[JUICENET_COORDINATOR] + + entities = [ + JuiceNetSensorDevice(device, coordinator, description) + for device in api.devices + for description in SENSOR_TYPES + ] + async_add_entities(entities) + + +class JuiceNetSensorDevice(JuiceNetDevice, SensorEntity): + """Implementation of a JuiceNet sensor.""" + + def __init__( + self, device, coordinator, description: SensorEntityDescription + ) -> None: + """Initialise the sensor.""" + super().__init__(device, description.key, coordinator) + self.entity_description = description + + @property + def icon(self): + """Return the icon of the sensor.""" + icon = None + if self.entity_description.key == "status": + status = self.device.status + if status == "standby": + icon = "mdi:power-plug-off" + elif status == "plugged": + icon = "mdi:power-plug" + elif status == "charging": + icon = "mdi:battery-positive" + else: + icon = self.entity_description.icon + return icon + + @property + def native_value(self): + """Return the state.""" + return getattr(self.device, self.entity_description.key, None) diff --git a/homeassistant/components/juicenet/strings.json b/homeassistant/components/juicenet/strings.json index 6e25130955b..0e3732c66d2 100644 --- a/homeassistant/components/juicenet/strings.json +++ b/homeassistant/components/juicenet/strings.json @@ -1,8 +1,41 @@ { - "issues": { - "integration_removed": { - "title": "The JuiceNet integration has been removed", - "description": "Enel X has dropped support for JuiceNet in favor of JuicePass, and the JuiceNet integration has been removed from Home Assistant as it was no longer working.\n\nTo resolve this issue, please remove the (now defunct) integration entries from your Home Assistant setup. [Click here to see your existing JuiceNet integration entries]({entries})." + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "step": { + "user": { + "data": { + "api_token": "[%key:common::config_flow::data::api_token%]" + }, + "description": "You will need the API Token from https://home.juice.net/Manage.", + "title": "Connect to JuiceNet" + } + } + }, + "entity": { + "number": { + "amperage_limit": { + "name": "Amperage limit" + } + }, + "sensor": { + "charge_time": { + "name": "Charge time" + }, + "energy_added": { + "name": "Energy added" + } + }, + "switch": { + "charge_now": { + "name": "Charge now" + } } } } diff --git a/homeassistant/components/juicenet/switch.py b/homeassistant/components/juicenet/switch.py new file mode 100644 index 00000000000..7c373eeeb24 --- /dev/null +++ b/homeassistant/components/juicenet/switch.py @@ -0,0 +1,49 @@ +"""Support for monitoring juicenet/juicepoint/juicebox based EVSE switches.""" +from typing import Any + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .const import DOMAIN, JUICENET_API, JUICENET_COORDINATOR +from .entity import JuiceNetDevice + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the JuiceNet switches.""" + entities = [] + juicenet_data = hass.data[DOMAIN][config_entry.entry_id] + api = juicenet_data[JUICENET_API] + coordinator = juicenet_data[JUICENET_COORDINATOR] + + for device in api.devices: + entities.append(JuiceNetChargeNowSwitch(device, coordinator)) + async_add_entities(entities) + + +class JuiceNetChargeNowSwitch(JuiceNetDevice, SwitchEntity): + """Implementation of a JuiceNet switch.""" + + _attr_translation_key = "charge_now" + + def __init__(self, device, coordinator): + """Initialise the switch.""" + super().__init__(device, "charge_now", coordinator) + + @property + def is_on(self): + """Return true if switch is on.""" + return self.device.override_time != 0 + + async def async_turn_on(self, **kwargs: Any) -> None: + """Charge now.""" + await self.device.set_override(True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Don't charge now.""" + await self.device.set_override(False) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 55d77e26336..3dc07c4b287 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -255,6 +255,7 @@ FLOWS = { "isy994", "izone", "jellyfin", + "juicenet", "justnimbus", "jvc_projector", "kaleidescape", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 6b6c41e412c..11aab652967 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2911,6 +2911,12 @@ "config_flow": false, "iot_class": "cloud_push" }, + "juicenet": { + "name": "JuiceNet", + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling" + }, "justnimbus": { "name": "JustNimbus", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 3480929b9a0..67a2962c296 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2244,6 +2244,9 @@ python-izone==1.2.9 # homeassistant.components.joaoapps_join python-join-api==0.0.9 +# homeassistant.components.juicenet +python-juicenet==1.1.0 + # homeassistant.components.tplink python-kasa[speedups]==0.6.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 84194256772..5cbad6d4366 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1723,6 +1723,9 @@ python-homewizard-energy==4.3.1 # homeassistant.components.izone python-izone==1.2.9 +# homeassistant.components.juicenet +python-juicenet==1.1.0 + # homeassistant.components.tplink python-kasa[speedups]==0.6.2.1 diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 9ad9c3676b8..8b9f73336fe 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -33,6 +33,7 @@ IGNORE_VIOLATIONS = { "blink", "ezviz", "hdmi_cec", + "juicenet", "lupusec", "rainbird", "slide", diff --git a/tests/components/juicenet/test_config_flow.py b/tests/components/juicenet/test_config_flow.py new file mode 100644 index 00000000000..6adc841862e --- /dev/null +++ b/tests/components/juicenet/test_config_flow.py @@ -0,0 +1,124 @@ +"""Test the JuiceNet config flow.""" +from unittest.mock import MagicMock, patch + +import aiohttp +from pyjuicenet import TokenError + +from homeassistant import config_entries +from homeassistant.components.juicenet.const import DOMAIN +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.core import HomeAssistant + + +def _mock_juicenet_return_value(get_devices=None): + juicenet_mock = MagicMock() + type(juicenet_mock).get_devices = MagicMock(return_value=get_devices) + return juicenet_mock + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.juicenet.config_flow.Api.get_devices", + return_value=MagicMock(), + ), patch( + "homeassistant.components.juicenet.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.juicenet.async_setup_entry", return_value=True + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"} + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "JuiceNet" + assert result2["data"] == {CONF_ACCESS_TOKEN: "access_token"} + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.juicenet.config_flow.Api.get_devices", + side_effect=TokenError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"} + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.juicenet.config_flow.Api.get_devices", + side_effect=aiohttp.ClientError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"} + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_catch_unknown_errors(hass: HomeAssistant) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.juicenet.config_flow.Api.get_devices", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_ACCESS_TOKEN: "access_token"} + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_import(hass: HomeAssistant) -> None: + """Test that import works as expected.""" + + with patch( + "homeassistant.components.juicenet.config_flow.Api.get_devices", + return_value=MagicMock(), + ), patch( + "homeassistant.components.juicenet.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.juicenet.async_setup_entry", return_value=True + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_ACCESS_TOKEN: "access_token"}, + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == "JuiceNet" + assert result["data"] == {CONF_ACCESS_TOKEN: "access_token"} + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/juicenet/test_init.py b/tests/components/juicenet/test_init.py deleted file mode 100644 index 8896798abe3..00000000000 --- a/tests/components/juicenet/test_init.py +++ /dev/null @@ -1,50 +0,0 @@ -"""Tests for the JuiceNet component.""" - -from homeassistant.components.juicenet import DOMAIN -from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant -from homeassistant.helpers import issue_registry as ir - -from tests.common import MockConfigEntry - - -async def test_juicenet_repair_issue( - hass: HomeAssistant, issue_registry: ir.IssueRegistry -) -> None: - """Test the JuiceNet configuration entry loading/unloading handles the repair.""" - config_entry_1 = MockConfigEntry( - title="Example 1", - domain=DOMAIN, - ) - config_entry_1.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry_1.entry_id) - await hass.async_block_till_done() - assert config_entry_1.state is ConfigEntryState.LOADED - - # Add a second one - config_entry_2 = MockConfigEntry( - title="Example 2", - domain=DOMAIN, - ) - config_entry_2.add_to_hass(hass) - await hass.config_entries.async_setup(config_entry_2.entry_id) - await hass.async_block_till_done() - - assert config_entry_2.state is ConfigEntryState.LOADED - assert issue_registry.async_get_issue(DOMAIN, DOMAIN) - - # Remove the first one - await hass.config_entries.async_remove(config_entry_1.entry_id) - await hass.async_block_till_done() - - assert config_entry_1.state is ConfigEntryState.NOT_LOADED - assert config_entry_2.state is ConfigEntryState.LOADED - assert issue_registry.async_get_issue(DOMAIN, DOMAIN) - - # Remove the second one - await hass.config_entries.async_remove(config_entry_2.entry_id) - await hass.async_block_till_done() - - assert config_entry_1.state is ConfigEntryState.NOT_LOADED - assert config_entry_2.state is ConfigEntryState.NOT_LOADED - assert issue_registry.async_get_issue(DOMAIN, DOMAIN) is None From 4514f08a42556efd21825aa42f5d1b690450264d Mon Sep 17 00:00:00 2001 From: "Mr. Bubbles" Date: Fri, 8 Mar 2024 11:55:18 +0100 Subject: [PATCH 12/99] Fix incorrect filtering of unsupported locales in bring-api (#112589) --- homeassistant/components/bring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bring/manifest.json b/homeassistant/components/bring/manifest.json index d8bfc6c7ebd..6b905a61b7d 100644 --- a/homeassistant/components/bring/manifest.json +++ b/homeassistant/components/bring/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/bring", "integration_type": "service", "iot_class": "cloud_polling", - "requirements": ["bring-api==0.5.5"] + "requirements": ["bring-api==0.5.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index 67a2962c296..f8dcaeb3927 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -603,7 +603,7 @@ boschshcpy==0.2.75 boto3==1.33.13 # homeassistant.components.bring -bring-api==0.5.5 +bring-api==0.5.6 # homeassistant.components.broadlink broadlink==0.18.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5cbad6d4366..45f919f906a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -514,7 +514,7 @@ bond-async==0.2.1 boschshcpy==0.2.75 # homeassistant.components.bring -bring-api==0.5.5 +bring-api==0.5.6 # homeassistant.components.broadlink broadlink==0.18.3 From b9a14d5eb570b7782d4ed25bdf5baa608a3dc3ff Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Tue, 12 Mar 2024 09:37:48 +0100 Subject: [PATCH 13/99] Include pytedee_async logger in tedee integration (#112590) add pytedee logger --- homeassistant/components/tedee/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/tedee/manifest.json b/homeassistant/components/tedee/manifest.json index a3e29e1b40f..6291a585009 100644 --- a/homeassistant/components/tedee/manifest.json +++ b/homeassistant/components/tedee/manifest.json @@ -6,5 +6,6 @@ "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/tedee", "iot_class": "local_push", + "loggers": ["pytedee_async"], "requirements": ["pytedee-async==0.2.15"] } From 84d14cad7f97317a6edb274768a88163edd606ad Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sat, 9 Mar 2024 08:49:11 +0100 Subject: [PATCH 14/99] Issue warning modbus configuration when modbus configuration is empty (#112618) --- homeassistant/components/modbus/validators.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 765ce4d8be3..90af203d208 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -368,12 +368,14 @@ def check_config(config: dict) -> dict: if not validate_modbus(hub, hub_name_inx): del config[hub_inx] continue + minimum_scan_interval = 9999 + no_entities = True for _component, conf_key in PLATFORMS: if conf_key not in hub: continue + no_entities = False entity_inx = 0 entities = hub[conf_key] - minimum_scan_interval = 9999 while entity_inx < len(entities): if not validate_entity( hub[CONF_NAME], @@ -385,7 +387,11 @@ def check_config(config: dict) -> dict: del entities[entity_inx] else: entity_inx += 1 - + if no_entities: + err = f"Modbus {hub[CONF_NAME]} contain no entities, this will cause instability, please add at least one entity!" + _LOGGER.warning(err) + # Ensure timeout is not started/handled. + hub[CONF_TIMEOUT] = -1 if hub[CONF_TIMEOUT] >= minimum_scan_interval: hub[CONF_TIMEOUT] = minimum_scan_interval - 1 _LOGGER.warning( From aa374944a19f25b2ea1cfdfb72c2c10f30904d9c Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 7 Mar 2024 17:55:06 +0100 Subject: [PATCH 15/99] modbus scan_interval: 0 is correct configuration (#112619) --- homeassistant/components/modbus/validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 90af203d208..202425bca48 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -310,7 +310,7 @@ def check_config(config: dict) -> dict: name = entity[CONF_NAME] addr = f"{hub_name}{entity[CONF_ADDRESS]}" scan_interval = entity.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) - if scan_interval < 5: + if 0 < scan_interval < 5: _LOGGER.warning( ( "%s %s scan_interval(%d) is lower than 5 seconds, " From 2985ab39223c61242f19df84696cd0a49e95bf90 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 7 Mar 2024 16:58:49 +0100 Subject: [PATCH 16/99] Update frontend to 20240307.0 (#112620) --- 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 cea376fa8ff..f49a632cae2 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==20240306.0"] + "requirements": ["home-assistant-frontend==20240307.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d9737e3d124..5d9889d2855 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240306.0 +home-assistant-frontend==20240307.0 home-assistant-intents==2024.3.6 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index f8dcaeb3927..61d24aab678 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1074,7 +1074,7 @@ hole==0.8.0 holidays==0.44 # homeassistant.components.frontend -home-assistant-frontend==20240306.0 +home-assistant-frontend==20240307.0 # homeassistant.components.conversation home-assistant-intents==2024.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45f919f906a..d34858770ef 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -873,7 +873,7 @@ hole==0.8.0 holidays==0.44 # homeassistant.components.frontend -home-assistant-frontend==20240306.0 +home-assistant-frontend==20240307.0 # homeassistant.components.conversation home-assistant-intents==2024.3.6 From aebbee681c38eae1adbd58ce926568ddf8d7d67d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 7 Mar 2024 17:11:45 +0100 Subject: [PATCH 17/99] Make hass-nabucasa a core requirement (#112623) --- pyproject.toml | 3 +++ requirements.txt | 1 + 2 files changed, 4 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index ba2360adc2b..b99dac7d54e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,9 @@ dependencies = [ "bcrypt==4.1.2", "certifi>=2021.5.30", "ciso8601==2.3.1", + # hass-nabucasa is imported by helpers which don't depend on the cloud + # integration + "hass-nabucasa==0.78.0", # When bumping httpx, please check the version pins of # httpcore, anyio, and h11 in gen_requirements_all "httpx==0.27.0", diff --git a/requirements.txt b/requirements.txt index 8ded95427c2..23ab0b08567 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,7 @@ awesomeversion==24.2.0 bcrypt==4.1.2 certifi>=2021.5.30 ciso8601==2.3.1 +hass-nabucasa==0.78.0 httpx==0.27.0 home-assistant-bluetooth==1.12.0 ifaddr==0.2.0 From 4db36d5ea99873600adc333241f93299a604987f Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Sun, 10 Mar 2024 22:10:27 +0100 Subject: [PATCH 18/99] Bump aioautomower to 2024.3.0 (#112627) Fix error in Husqvarna automower in Zones dataclass --- .../husqvarna_automower/config_flow.py | 4 +-- .../husqvarna_automower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../husqvarna_automower/fixtures/mower.json | 30 ++++++++++++++++--- 5 files changed, 31 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/config_flow.py b/homeassistant/components/husqvarna_automower/config_flow.py index cafe942a894..e306be7137c 100644 --- a/homeassistant/components/husqvarna_automower/config_flow.py +++ b/homeassistant/components/husqvarna_automower/config_flow.py @@ -2,7 +2,7 @@ import logging from typing import Any -from aioautomower.utils import async_structure_token +from aioautomower.utils import structure_token from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN from homeassistant.data_entry_flow import FlowResult @@ -27,7 +27,7 @@ class HusqvarnaConfigFlowHandler( """Create an entry for the flow.""" token = data[CONF_TOKEN] user_id = token[CONF_USER_ID] - structured_token = await async_structure_token(token[CONF_ACCESS_TOKEN]) + structured_token = structure_token(token[CONF_ACCESS_TOKEN]) first_name = structured_token.user.first_name last_name = structured_token.user.last_name await self.async_set_unique_id(user_id) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index dc40116f31e..525f057c1ff 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", "iot_class": "cloud_push", - "requirements": ["aioautomower==2024.2.10"] + "requirements": ["aioautomower==2024.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 61d24aab678..5f320d2f4e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -206,7 +206,7 @@ aioaseko==0.0.2 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.2.10 +aioautomower==2024.3.0 # homeassistant.components.azure_devops aioazuredevops==1.3.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d34858770ef..bae14cd3868 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -185,7 +185,7 @@ aioaseko==0.0.2 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.2.10 +aioautomower==2024.3.0 # homeassistant.components.azure_devops aioazuredevops==1.3.5 diff --git a/tests/components/husqvarna_automower/fixtures/mower.json b/tests/components/husqvarna_automower/fixtures/mower.json index 0af4926c561..1e608e654a6 100644 --- a/tests/components/husqvarna_automower/fixtures/mower.json +++ b/tests/components/husqvarna_automower/fixtures/mower.json @@ -62,6 +62,18 @@ "connected": true, "statusTimestamp": 1697669932683 }, + "workAreas": [ + { + "workAreaId": 123456, + "name": "Front lawn", + "cuttingHeight": 50 + }, + { + "workAreaId": 0, + "name": "", + "cuttingHeight": 50 + } + ], "positions": [ { "latitude": 35.5402913, @@ -120,10 +132,6 @@ "longitude": -82.5520054 } ], - "cuttingHeight": 4, - "headlight": { - "mode": "EVENING_ONLY" - }, "statistics": { "cuttingBladeUsageTime": 123, "numberOfChargingCycles": 1380, @@ -133,6 +141,20 @@ "totalDriveDistance": 1780272, "totalRunningTime": 4564800, "totalSearchingTime": 370800 + }, + "stayOutZones": { + "dirty": false, + "zones": [ + { + "id": "81C6EEA2-D139-4FEA-B134-F22A6B3EA403", + "name": "Springflowers", + "enabled": true + } + ] + }, + "cuttingHeight": 4, + "headlight": { + "mode": "EVENING_ONLY" } } } From e348c7b04362749233d2d8b3ff0423ec94f0ca7f Mon Sep 17 00:00:00 2001 From: jan iversen Date: Thu, 7 Mar 2024 22:38:51 +0100 Subject: [PATCH 19/99] Bump pymodbus to v3.6.5 (#112629) --- homeassistant/components/modbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index b90f5663643..6b072457144 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_polling", "loggers": ["pymodbus"], "quality_scale": "gold", - "requirements": ["pymodbus==3.6.4"] + "requirements": ["pymodbus==3.6.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5f320d2f4e4..9a44517141a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1971,7 +1971,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.6.4 +pymodbus==3.6.5 # homeassistant.components.monoprice pymonoprice==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bae14cd3868..64260cb5c6e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1525,7 +1525,7 @@ pymeteoclimatic==0.1.0 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.6.4 +pymodbus==3.6.5 # homeassistant.components.monoprice pymonoprice==0.4 From 403013b7bd3052b8ba31aa19849b8d19a724a736 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 8 Mar 2024 07:42:37 +0100 Subject: [PATCH 20/99] Bump axis to v52 (#112632) * Bump axis to v51 * Bump to v52 --- homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 5311d18f991..9bfe37da9da 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -26,7 +26,7 @@ "iot_class": "local_push", "loggers": ["axis"], "quality_scale": "platinum", - "requirements": ["axis==50"], + "requirements": ["axis==52"], "ssdp": [ { "manufacturer": "AXIS" diff --git a/requirements_all.txt b/requirements_all.txt index 9a44517141a..a6126d67fc3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==50 +axis==52 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 64260cb5c6e..1df4a18f447 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -454,7 +454,7 @@ auroranoaa==0.0.3 aurorapy==0.2.7 # homeassistant.components.axis -axis==50 +axis==52 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 From 1f9e369b73a64c5ba872ac63870500f20a1fe6a9 Mon Sep 17 00:00:00 2001 From: Mike Woudenberg Date: Mon, 11 Mar 2024 22:20:06 +0100 Subject: [PATCH 21/99] Update Loqed helper for more logging (#112646) Updates Loqed helper for more logging --- homeassistant/components/loqed/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/loqed/manifest.json b/homeassistant/components/loqed/manifest.json index 7c682b3189d..46ac84b1768 100644 --- a/homeassistant/components/loqed/manifest.json +++ b/homeassistant/components/loqed/manifest.json @@ -7,7 +7,7 @@ "dependencies": ["webhook"], "documentation": "https://www.home-assistant.io/integrations/loqed", "iot_class": "local_push", - "requirements": ["loqedAPI==2.1.8"], + "requirements": ["loqedAPI==2.1.10"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index a6126d67fc3..111307315a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1253,7 +1253,7 @@ logi-circle==0.2.3 london-tube-status==0.5 # homeassistant.components.loqed -loqedAPI==2.1.8 +loqedAPI==2.1.10 # homeassistant.components.luftdaten luftdaten==0.7.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1df4a18f447..1ab761f53ab 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1001,7 +1001,7 @@ logi-circle==0.2.3 london-tube-status==0.5 # homeassistant.components.loqed -loqedAPI==2.1.8 +loqedAPI==2.1.10 # homeassistant.components.luftdaten luftdaten==0.7.4 From 23fee438a9b3606a7cfe371cacb63cd9271cdaab Mon Sep 17 00:00:00 2001 From: Jeef Date: Tue, 12 Mar 2024 14:13:30 -0600 Subject: [PATCH 22/99] Bump weatherflow4py to 0.1.17 (#112661) --- homeassistant/components/weatherflow_cloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/weatherflow_cloud/manifest.json b/homeassistant/components/weatherflow_cloud/manifest.json index 4cb0f5296e5..72a49c0cf19 100644 --- a/homeassistant/components/weatherflow_cloud/manifest.json +++ b/homeassistant/components/weatherflow_cloud/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/weatherflow_cloud", "iot_class": "cloud_polling", - "requirements": ["weatherflow4py==0.1.14"] + "requirements": ["weatherflow4py==0.1.17"] } diff --git a/requirements_all.txt b/requirements_all.txt index 111307315a9..74290285697 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2839,7 +2839,7 @@ watchdog==2.3.1 waterfurnace==1.1.0 # homeassistant.components.weatherflow_cloud -weatherflow4py==0.1.14 +weatherflow4py==0.1.17 # homeassistant.components.webmin webmin-xmlrpc==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1ab761f53ab..d2266b38e74 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2177,7 +2177,7 @@ wallbox==0.6.0 watchdog==2.3.1 # homeassistant.components.weatherflow_cloud -weatherflow4py==0.1.14 +weatherflow4py==0.1.17 # homeassistant.components.webmin webmin-xmlrpc==0.0.1 From 503fbfc0383a9c0c1a94f4f40cddd79231976f7b Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 8 Mar 2024 03:51:05 -0700 Subject: [PATCH 23/99] Bump `aionotion` to 2024.03.0 (#112675) --- homeassistant/components/notion/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/notion/manifest.json b/homeassistant/components/notion/manifest.json index 5fc94b5e646..d4d2cb579a3 100644 --- a/homeassistant/components/notion/manifest.json +++ b/homeassistant/components/notion/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["aionotion"], - "requirements": ["aionotion==2024.02.2"] + "requirements": ["aionotion==2024.03.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 74290285697..a1227ce6eed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -315,7 +315,7 @@ aiomusiccast==0.14.8 aionanoleaf==0.2.1 # homeassistant.components.notion -aionotion==2024.02.2 +aionotion==2024.03.0 # homeassistant.components.oncue aiooncue==0.3.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d2266b38e74..d89d9675ce4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -288,7 +288,7 @@ aiomusiccast==0.14.8 aionanoleaf==0.2.1 # homeassistant.components.notion -aionotion==2024.02.2 +aionotion==2024.03.0 # homeassistant.components.oncue aiooncue==0.3.5 From e95ce2d390ec7bb452e26c78a121c07a7bd31e90 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 8 Mar 2024 20:27:56 -0700 Subject: [PATCH 24/99] Make sure Notion saves new refresh token upon startup (#112676) * Make sure Notion saves new refresh token upon startup * Code review * Typing * Smoother syntax * Fix tests * Fix tests for real --- homeassistant/components/notion/__init__.py | 4 ++++ tests/components/notion/conftest.py | 7 ++++--- tests/components/notion/test_config_flow.py | 16 ++++++++++++---- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 3ed2c7bdb93..33a74990928 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -180,6 +180,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Create a callback to save the refresh token when it changes: entry.async_on_unload(client.add_refresh_token_callback(async_save_refresh_token)) + # Save the client's refresh token if it's different than what we already have: + if (token := client.refresh_token) and token != entry.data[CONF_REFRESH_TOKEN]: + async_save_refresh_token(token) + hass.config_entries.async_update_entry(entry, **entry_updates) async def async_update() -> NotionData: diff --git a/tests/components/notion/conftest.py b/tests/components/notion/conftest.py index 61a54ca58ba..366f78b8c6c 100644 --- a/tests/components/notion/conftest.py +++ b/tests/components/notion/conftest.py @@ -9,8 +9,8 @@ from aionotion.sensor.models import Sensor from aionotion.user.models import UserPreferences import pytest -from homeassistant.components.notion import DOMAIN -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.components.notion import CONF_REFRESH_TOKEN, CONF_USER_UUID, DOMAIN +from homeassistant.const import CONF_USERNAME from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -81,7 +81,8 @@ def config_fixture(): """Define a config entry data fixture.""" return { CONF_USERNAME: TEST_USERNAME, - CONF_PASSWORD: TEST_PASSWORD, + CONF_USER_UUID: TEST_USER_UUID, + CONF_REFRESH_TOKEN: TEST_REFRESH_TOKEN, } diff --git a/tests/components/notion/test_config_flow.py b/tests/components/notion/test_config_flow.py index 646bd7a6e87..72bb3dfee0b 100644 --- a/tests/components/notion/test_config_flow.py +++ b/tests/components/notion/test_config_flow.py @@ -10,7 +10,7 @@ from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from .conftest import TEST_REFRESH_TOKEN, TEST_USER_UUID, TEST_USERNAME +from .conftest import TEST_PASSWORD, TEST_REFRESH_TOKEN, TEST_USER_UUID, TEST_USERNAME pytestmark = pytest.mark.usefixtures("mock_setup_entry") @@ -26,7 +26,6 @@ pytestmark = pytest.mark.usefixtures("mock_setup_entry") async def test_create_entry( hass: HomeAssistant, client, - config, errors, get_client_with_exception, mock_aionotion, @@ -44,13 +43,22 @@ async def test_create_entry( get_client_with_exception, ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=config + DOMAIN, + context={"source": SOURCE_USER}, + data={ + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, ) assert result["type"] == data_entry_flow.FlowResultType.FORM assert result["errors"] == errors result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=config + result["flow_id"], + user_input={ + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + }, ) assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY assert result["title"] == TEST_USERNAME From 9e977f2c70dd72e6a5d0e81807d99afe6c96f431 Mon Sep 17 00:00:00 2001 From: Alin Balutoiu Date: Fri, 8 Mar 2024 21:31:02 +0000 Subject: [PATCH 25/99] Fix tado climate service (#112686) --- homeassistant/components/tado/climate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 5d17655c104..47bd2bc16f3 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -401,9 +401,9 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): def set_timer( self, - temperature: float, - time_period: int, - requested_overlay: str, + temperature: float | None = None, + time_period: int | None = None, + requested_overlay: str | None = None, ): """Set the timer on the entity, and temperature if supported.""" From c2543289b7aae4fe128ccde6aad63165669d7f31 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 8 Mar 2024 14:54:00 +0100 Subject: [PATCH 26/99] Downgrade `pysnmp-lextudio` to version `5.0.34` (#112696) Downgrade pysnmp-lextudio to version 5.0.34 Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/snmp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index dd9a2f5270a..8d046b609b8 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/snmp", "iot_class": "local_polling", "loggers": ["pyasn1", "pysmi", "pysnmp"], - "requirements": ["pysnmp-lextudio==6.0.2"] + "requirements": ["pysnmp-lextudio==5.0.34"] } diff --git a/requirements_all.txt b/requirements_all.txt index a1227ce6eed..99ee92183bb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2155,7 +2155,7 @@ pysmartthings==0.7.8 pysml==0.0.12 # homeassistant.components.snmp -pysnmp-lextudio==6.0.2 +pysnmp-lextudio==5.0.34 # homeassistant.components.snooz pysnooz==0.8.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d89d9675ce4..68bee472ef8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1673,7 +1673,7 @@ pysmartthings==0.7.8 pysml==0.0.12 # homeassistant.components.snmp -pysnmp-lextudio==6.0.2 +pysnmp-lextudio==5.0.34 # homeassistant.components.snooz pysnooz==0.8.6 From f7b64244b8c641d0a3f8bbc7834a42a0ac9698d7 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Fri, 8 Mar 2024 15:10:35 +0100 Subject: [PATCH 27/99] Allow duplicate names in different modbus entities (#112701) Allow duplicate names in different entities. --- homeassistant/components/modbus/validators.py | 6 +- tests/components/modbus/test_init.py | 59 +++++++++++++++++++ 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/modbus/validators.py b/homeassistant/components/modbus/validators.py index 202425bca48..650083dc7e4 100644 --- a/homeassistant/components/modbus/validators.py +++ b/homeassistant/components/modbus/validators.py @@ -301,13 +301,14 @@ def check_config(config: dict) -> dict: def validate_entity( hub_name: str, + component: str, entity: dict, minimum_scan_interval: int, ent_names: set, ent_addr: set, ) -> bool: """Validate entity.""" - name = entity[CONF_NAME] + name = f"{component}.{entity[CONF_NAME]}" addr = f"{hub_name}{entity[CONF_ADDRESS]}" scan_interval = entity.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) if 0 < scan_interval < 5: @@ -370,7 +371,7 @@ def check_config(config: dict) -> dict: continue minimum_scan_interval = 9999 no_entities = True - for _component, conf_key in PLATFORMS: + for component, conf_key in PLATFORMS: if conf_key not in hub: continue no_entities = False @@ -379,6 +380,7 @@ def check_config(config: dict) -> dict: while entity_inx < len(entities): if not validate_entity( hub[CONF_NAME], + component, entities[entity_inx], minimum_scan_interval, ent_names, diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 4de9a439a01..bd590a9e15c 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -858,6 +858,30 @@ async def test_duplicate_fan_mode_validator(do_config) -> None: ], 2, ), + ( + [ + { + CONF_NAME: TEST_MODBUS_NAME, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 117, + CONF_SLAVE: 0, + }, + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 1179, + CONF_SLAVE: 0, + }, + ], + }, + ], + 1, + ), ], ) async def test_duplicate_addresses(do_config, sensor_cnt) -> None: @@ -867,6 +891,41 @@ async def test_duplicate_addresses(do_config, sensor_cnt) -> None: assert len(do_config[use_inx][CONF_SENSORS]) == sensor_cnt +@pytest.mark.parametrize( + "do_config", + [ + [ + { + CONF_NAME: TEST_MODBUS_NAME, + CONF_TYPE: TCP, + CONF_HOST: TEST_MODBUS_HOST, + CONF_PORT: TEST_PORT_TCP, + CONF_TIMEOUT: 3, + CONF_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME, + CONF_ADDRESS: 117, + CONF_SLAVE: 0, + }, + ], + CONF_BINARY_SENSORS: [ + { + CONF_NAME: TEST_ENTITY_NAME + "1", + CONF_ADDRESS: 1179, + CONF_SLAVE: 0, + }, + ], + }, + ], + ], +) +async def test_no_duplicate_names(do_config) -> None: + """Test duplicate entity validator.""" + check_config(do_config) + assert len(do_config[0][CONF_SENSORS]) == 1 + assert len(do_config[0][CONF_BINARY_SENSORS]) == 1 + + @pytest.mark.parametrize( "do_config", [ From 5a125bf379f02a50c258a44cf4d39c019ad934cd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 8 Mar 2024 11:34:07 -1000 Subject: [PATCH 28/99] Guard against db corruption when renaming entities (#112718) --- homeassistant/components/recorder/core.py | 2 +- .../components/recorder/entity_registry.py | 7 +- .../components/recorder/statistics.py | 68 +++-------- .../table_managers/statistics_meta.py | 9 +- homeassistant/components/recorder/util.py | 54 +++++++- .../recorder/test_entity_registry.py | 99 +++++++++++++++ tests/components/recorder/test_init.py | 70 +++++++++++ tests/components/recorder/test_statistics.py | 115 +++++++++++++++++- 8 files changed, 364 insertions(+), 60 deletions(-) diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 8885116dbfd..547c78e2a7a 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -924,7 +924,7 @@ class Recorder(threading.Thread): # that is pending before running the task if TYPE_CHECKING: assert isinstance(task, RecorderTask) - if not task.commit_before: + if task.commit_before: self._commit_event_session_or_retry() return task.run(self) except exc.DatabaseError as err: diff --git a/homeassistant/components/recorder/entity_registry.py b/homeassistant/components/recorder/entity_registry.py index fbf6e691777..24f33cd815e 100644 --- a/homeassistant/components/recorder/entity_registry.py +++ b/homeassistant/components/recorder/entity_registry.py @@ -6,7 +6,7 @@ from homeassistant.helpers import entity_registry as er from homeassistant.helpers.start import async_at_start from .core import Recorder -from .util import get_instance, session_scope +from .util import filter_unique_constraint_integrity_error, get_instance, session_scope _LOGGER = logging.getLogger(__name__) @@ -61,7 +61,10 @@ def update_states_metadata( ) return - with session_scope(session=instance.get_session()) as session: + with session_scope( + session=instance.get_session(), + exception_filter=filter_unique_constraint_integrity_error(instance, "state"), + ) as session: if not states_meta_manager.update_metadata(session, entity_id, new_entity_id): _LOGGER.warning( "Cannot migrate history for entity_id `%s` to `%s` " diff --git a/homeassistant/components/recorder/statistics.py b/homeassistant/components/recorder/statistics.py index 5abe395a8d7..ab4626c192b 100644 --- a/homeassistant/components/recorder/statistics.py +++ b/homeassistant/components/recorder/statistics.py @@ -3,7 +3,6 @@ from __future__ import annotations from collections import defaultdict from collections.abc import Callable, Iterable, Sequence -import contextlib import dataclasses from datetime import datetime, timedelta from functools import lru_cache, partial @@ -15,7 +14,7 @@ from typing import TYPE_CHECKING, Any, Literal, TypedDict, cast from sqlalchemy import Select, and_, bindparam, func, lambda_stmt, select, text from sqlalchemy.engine.row import Row -from sqlalchemy.exc import SQLAlchemyError, StatementError +from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.orm.session import Session from sqlalchemy.sql.lambdas import StatementLambdaElement import voluptuous as vol @@ -72,6 +71,7 @@ from .models import ( from .util import ( execute, execute_stmt_lambda_element, + filter_unique_constraint_integrity_error, get_instance, retryable_database_job, session_scope, @@ -454,7 +454,9 @@ def compile_missing_statistics(instance: Recorder) -> bool: with session_scope( session=instance.get_session(), - exception_filter=_filter_unique_constraint_integrity_error(instance), + exception_filter=filter_unique_constraint_integrity_error( + instance, "statistic" + ), ) as session: # Find the newest statistics run, if any if last_run := session.query(func.max(StatisticsRuns.start)).scalar(): @@ -486,7 +488,9 @@ def compile_statistics(instance: Recorder, start: datetime, fire_events: bool) - # Return if we already have 5-minute statistics for the requested period with session_scope( session=instance.get_session(), - exception_filter=_filter_unique_constraint_integrity_error(instance), + exception_filter=filter_unique_constraint_integrity_error( + instance, "statistic" + ), ) as session: modified_statistic_ids = _compile_statistics( instance, session, start, fire_events @@ -737,7 +741,9 @@ def update_statistics_metadata( if new_statistic_id is not UNDEFINED and new_statistic_id is not None: with session_scope( session=instance.get_session(), - exception_filter=_filter_unique_constraint_integrity_error(instance), + exception_filter=filter_unique_constraint_integrity_error( + instance, "statistic" + ), ) as session: statistics_meta_manager.update_statistic_id( session, DOMAIN, statistic_id, new_statistic_id @@ -2246,54 +2252,6 @@ def async_add_external_statistics( _async_import_statistics(hass, metadata, statistics) -def _filter_unique_constraint_integrity_error( - instance: Recorder, -) -> Callable[[Exception], bool]: - def _filter_unique_constraint_integrity_error(err: Exception) -> bool: - """Handle unique constraint integrity errors.""" - if not isinstance(err, StatementError): - return False - - assert instance.engine is not None - dialect_name = instance.engine.dialect.name - - ignore = False - if ( - dialect_name == SupportedDialect.SQLITE - and "UNIQUE constraint failed" in str(err) - ): - ignore = True - if ( - dialect_name == SupportedDialect.POSTGRESQL - and err.orig - and hasattr(err.orig, "pgcode") - and err.orig.pgcode == "23505" - ): - ignore = True - if ( - dialect_name == SupportedDialect.MYSQL - and err.orig - and hasattr(err.orig, "args") - ): - with contextlib.suppress(TypeError): - if err.orig.args[0] == 1062: - ignore = True - - if ignore: - _LOGGER.warning( - ( - "Blocked attempt to insert duplicated statistic rows, please report" - " at %s" - ), - "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+recorder%22", - exc_info=err, - ) - - return ignore - - return _filter_unique_constraint_integrity_error - - def _import_statistics_with_session( instance: Recorder, session: Session, @@ -2397,7 +2355,9 @@ def import_statistics( with session_scope( session=instance.get_session(), - exception_filter=_filter_unique_constraint_integrity_error(instance), + exception_filter=filter_unique_constraint_integrity_error( + instance, "statistic" + ), ) as session: return _import_statistics_with_session( instance, session, metadata, statistics, table diff --git a/homeassistant/components/recorder/table_managers/statistics_meta.py b/homeassistant/components/recorder/table_managers/statistics_meta.py index 76def3a22fe..d6c69e2682b 100644 --- a/homeassistant/components/recorder/table_managers/statistics_meta.py +++ b/homeassistant/components/recorder/table_managers/statistics_meta.py @@ -307,11 +307,18 @@ class StatisticsMetaManager: recorder thread. """ self._assert_in_recorder_thread() + if self.get(session, new_statistic_id): + _LOGGER.error( + "Cannot rename statistic_id `%s` to `%s` because the new statistic_id is already in use", + old_statistic_id, + new_statistic_id, + ) + return session.query(StatisticsMeta).filter( (StatisticsMeta.statistic_id == old_statistic_id) & (StatisticsMeta.source == source) ).update({StatisticsMeta.statistic_id: new_statistic_id}) - self._clear_cache([old_statistic_id, new_statistic_id]) + self._clear_cache([old_statistic_id]) def delete(self, session: Session, statistic_ids: list[str]) -> None: """Clear statistics for a list of statistic_ids. diff --git a/homeassistant/components/recorder/util.py b/homeassistant/components/recorder/util.py index f684160f86f..8ed5c3e8cdc 100644 --- a/homeassistant/components/recorder/util.py +++ b/homeassistant/components/recorder/util.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Callable, Collection, Generator, Iterable, Sequence +import contextlib from contextlib import contextmanager from datetime import date, datetime, timedelta import functools @@ -21,7 +22,7 @@ import ciso8601 from sqlalchemy import inspect, text from sqlalchemy.engine import Result, Row from sqlalchemy.engine.interfaces import DBAPIConnection -from sqlalchemy.exc import OperationalError, SQLAlchemyError +from sqlalchemy.exc import OperationalError, SQLAlchemyError, StatementError from sqlalchemy.orm.query import Query from sqlalchemy.orm.session import Session from sqlalchemy.sql.lambdas import StatementLambdaElement @@ -906,3 +907,54 @@ def get_index_by_name(session: Session, table_name: str, index_name: str) -> str ), None, ) + + +def filter_unique_constraint_integrity_error( + instance: Recorder, row_type: str +) -> Callable[[Exception], bool]: + """Create a filter for unique constraint integrity errors.""" + + def _filter_unique_constraint_integrity_error(err: Exception) -> bool: + """Handle unique constraint integrity errors.""" + if not isinstance(err, StatementError): + return False + + assert instance.engine is not None + dialect_name = instance.engine.dialect.name + + ignore = False + if ( + dialect_name == SupportedDialect.SQLITE + and "UNIQUE constraint failed" in str(err) + ): + ignore = True + if ( + dialect_name == SupportedDialect.POSTGRESQL + and err.orig + and hasattr(err.orig, "pgcode") + and err.orig.pgcode == "23505" + ): + ignore = True + if ( + dialect_name == SupportedDialect.MYSQL + and err.orig + and hasattr(err.orig, "args") + ): + with contextlib.suppress(TypeError): + if err.orig.args[0] == 1062: + ignore = True + + if ignore: + _LOGGER.warning( + ( + "Blocked attempt to insert duplicated %s rows, please report" + " at %s" + ), + row_type, + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+recorder%22", + exc_info=err, + ) + + return ignore + + return _filter_unique_constraint_integrity_error diff --git a/tests/components/recorder/test_entity_registry.py b/tests/components/recorder/test_entity_registry.py index 0d675574e12..9ec3087d477 100644 --- a/tests/components/recorder/test_entity_registry.py +++ b/tests/components/recorder/test_entity_registry.py @@ -1,10 +1,12 @@ """The tests for sensor recorder platform.""" from collections.abc import Callable +from unittest.mock import patch import pytest from sqlalchemy import select from sqlalchemy.orm import Session +from homeassistant.components import recorder from homeassistant.components.recorder import history from homeassistant.components.recorder.db_schema import StatesMeta from homeassistant.components.recorder.util import session_scope @@ -260,4 +262,101 @@ def test_rename_entity_collision( assert _count_entity_id_in_states_meta(hass, session, "sensor.test99") == 1 assert _count_entity_id_in_states_meta(hass, session, "sensor.test1") == 1 + # We should hit the safeguard in the states_meta_manager assert "the new entity_id is already in use" in caplog.text + + # We should not hit the safeguard in the entity_registry + assert "Blocked attempt to insert duplicated state rows" not in caplog.text + + +def test_rename_entity_collision_without_states_meta_safeguard( + hass_recorder: Callable[..., HomeAssistant], caplog: pytest.LogCaptureFixture +) -> None: + """Test states meta is not migrated when there is a collision. + + This test disables the safeguard in the states_meta_manager + and relies on the filter_unique_constraint_integrity_error safeguard. + """ + hass = hass_recorder() + setup_component(hass, "sensor", {}) + + entity_reg = mock_registry(hass) + + @callback + def add_entry(): + reg_entry = entity_reg.async_get_or_create( + "sensor", + "test", + "unique_0000", + suggested_object_id="test1", + ) + assert reg_entry.entity_id == "sensor.test1" + + hass.add_job(add_entry) + hass.block_till_done() + + zero, four, states = record_states(hass) + hist = history.get_significant_states( + hass, zero, four, list(set(states) | {"sensor.test99", "sensor.test1"}) + ) + assert_dict_of_states_equal_without_context_and_last_changed(states, hist) + assert len(hist["sensor.test1"]) == 3 + + hass.states.set("sensor.test99", "collision") + hass.states.remove("sensor.test99") + + hass.block_till_done() + wait_recording_done(hass) + + # Verify history before collision + hist = history.get_significant_states( + hass, zero, four, list(set(states) | {"sensor.test99", "sensor.test1"}) + ) + assert len(hist["sensor.test1"]) == 3 + assert len(hist["sensor.test99"]) == 2 + + instance = recorder.get_instance(hass) + # Patch out the safeguard in the states meta manager + # so that we hit the filter_unique_constraint_integrity_error safeguard in the entity_registry + with patch.object(instance.states_meta_manager, "get", return_value=None): + # Rename entity sensor.test1 to sensor.test99 + @callback + def rename_entry(): + entity_reg.async_update_entity( + "sensor.test1", new_entity_id="sensor.test99" + ) + + hass.add_job(rename_entry) + wait_recording_done(hass) + + # History is not migrated on collision + hist = history.get_significant_states( + hass, zero, four, list(set(states) | {"sensor.test99", "sensor.test1"}) + ) + assert len(hist["sensor.test1"]) == 3 + assert len(hist["sensor.test99"]) == 2 + + with session_scope(hass=hass) as session: + assert _count_entity_id_in_states_meta(hass, session, "sensor.test99") == 1 + + hass.states.set("sensor.test99", "post_migrate") + wait_recording_done(hass) + + new_hist = history.get_significant_states( + hass, + zero, + dt_util.utcnow(), + list(set(states) | {"sensor.test99", "sensor.test1"}), + ) + assert new_hist["sensor.test99"][-1].state == "post_migrate" + assert len(hist["sensor.test99"]) == 2 + + with session_scope(hass=hass) as session: + assert _count_entity_id_in_states_meta(hass, session, "sensor.test99") == 1 + assert _count_entity_id_in_states_meta(hass, session, "sensor.test1") == 1 + + # We should not hit the safeguard in the states_meta_manager + assert "the new entity_id is already in use" not in caplog.text + + # We should hit the safeguard in the entity_registry + assert "Blocked attempt to insert duplicated state rows" in caplog.text diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 1c8e9da551e..b1380cb300c 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -2485,3 +2485,73 @@ async def test_events_are_recorded_until_final_write( await hass.async_block_till_done() assert not instance.engine + + +async def test_commit_before_commits_pending_writes( + async_setup_recorder_instance: RecorderInstanceGenerator, + hass: HomeAssistant, + recorder_db_url: str, + tmp_path: Path, +) -> None: + """Test commit_before with a non-zero commit interval. + + All of our test run with a commit interval of 0 by + default, so we need to test this with a non-zero commit + """ + config = { + recorder.CONF_DB_URL: recorder_db_url, + recorder.CONF_COMMIT_INTERVAL: 60, + } + + recorder_helper.async_initialize_recorder(hass) + hass.create_task(async_setup_recorder_instance(hass, config)) + await recorder_helper.async_wait_recorder(hass) + instance = get_instance(hass) + assert instance.commit_interval == 60 + verify_states_in_queue_future = hass.loop.create_future() + verify_session_commit_future = hass.loop.create_future() + + class VerifyCommitBeforeTask(recorder.tasks.RecorderTask): + """Task to verify that commit before ran. + + If commit_before is true, we should have no pending writes. + """ + + commit_before = True + + def run(self, instance: Recorder) -> None: + if not instance._event_session_has_pending_writes: + hass.loop.call_soon_threadsafe( + verify_session_commit_future.set_result, None + ) + return + hass.loop.call_soon_threadsafe( + verify_session_commit_future.set_exception, + RuntimeError("Session still has pending write"), + ) + + class VerifyStatesInQueueTask(recorder.tasks.RecorderTask): + """Task to verify that states are in the queue.""" + + commit_before = False + + def run(self, instance: Recorder) -> None: + if instance._event_session_has_pending_writes: + hass.loop.call_soon_threadsafe( + verify_states_in_queue_future.set_result, None + ) + return + hass.loop.call_soon_threadsafe( + verify_states_in_queue_future.set_exception, + RuntimeError("Session has no pending write"), + ) + + # First insert an event + instance.queue_task(Event("fake_event")) + # Next verify that the event session has pending writes + instance.queue_task(VerifyStatesInQueueTask()) + # Finally, verify that the session was committed + instance.queue_task(VerifyCommitBeforeTask()) + + await verify_states_in_queue_future + await verify_session_commit_future diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py index 16033188549..69d42a6e7bd 100644 --- a/tests/components/recorder/test_statistics.py +++ b/tests/components/recorder/test_statistics.py @@ -453,7 +453,11 @@ def test_statistics_during_period_set_back_compat( def test_rename_entity_collision( hass_recorder: Callable[..., HomeAssistant], caplog: pytest.LogCaptureFixture ) -> None: - """Test statistics is migrated when entity_id is changed.""" + """Test statistics is migrated when entity_id is changed. + + This test relies on the the safeguard in the statistics_meta_manager + and should not hit the filter_unique_constraint_integrity_error safeguard. + """ hass = hass_recorder() setup_component(hass, "sensor", {}) @@ -530,8 +534,117 @@ def test_rename_entity_collision( # Statistics failed to migrate due to the collision stats = statistics_during_period(hass, zero, period="5minute") assert stats == {"sensor.test1": expected_stats1, "sensor.test2": expected_stats2} + + # Verify the safeguard in the states meta manager was hit + assert ( + "Cannot rename statistic_id `sensor.test1` to `sensor.test99` " + "because the new statistic_id is already in use" + ) in caplog.text + + # Verify the filter_unique_constraint_integrity_error safeguard was not hit + assert "Blocked attempt to insert duplicated statistic rows" not in caplog.text + + +def test_rename_entity_collision_states_meta_check_disabled( + hass_recorder: Callable[..., HomeAssistant], caplog: pytest.LogCaptureFixture +) -> None: + """Test statistics is migrated when entity_id is changed. + + This test disables the safeguard in the statistics_meta_manager + and relies on the filter_unique_constraint_integrity_error safeguard. + """ + hass = hass_recorder() + setup_component(hass, "sensor", {}) + + entity_reg = mock_registry(hass) + + @callback + def add_entry(): + reg_entry = entity_reg.async_get_or_create( + "sensor", + "test", + "unique_0000", + suggested_object_id="test1", + ) + assert reg_entry.entity_id == "sensor.test1" + + hass.add_job(add_entry) + hass.block_till_done() + + zero, four, states = record_states(hass) + hist = history.get_significant_states(hass, zero, four, list(states)) + assert_dict_of_states_equal_without_context_and_last_changed(states, hist) + + for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}): + stats = statistics_during_period(hass, zero, period="5minute", **kwargs) + assert stats == {} + stats = get_last_short_term_statistics( + hass, + 0, + "sensor.test1", + True, + {"last_reset", "max", "mean", "min", "state", "sum"}, + ) + assert stats == {} + + do_adhoc_statistics(hass, start=zero) + wait_recording_done(hass) + expected_1 = { + "start": process_timestamp(zero).timestamp(), + "end": process_timestamp(zero + timedelta(minutes=5)).timestamp(), + "mean": pytest.approx(14.915254237288135), + "min": pytest.approx(10.0), + "max": pytest.approx(20.0), + "last_reset": None, + "state": None, + "sum": None, + } + expected_stats1 = [expected_1] + expected_stats2 = [expected_1] + + stats = statistics_during_period(hass, zero, period="5minute") + assert stats == {"sensor.test1": expected_stats1, "sensor.test2": expected_stats2} + + # Insert metadata for sensor.test99 + metadata_1 = { + "has_mean": True, + "has_sum": False, + "name": "Total imported energy", + "source": "test", + "statistic_id": "sensor.test99", + "unit_of_measurement": "kWh", + } + + with session_scope(hass=hass) as session: + session.add(recorder.db_schema.StatisticsMeta.from_meta(metadata_1)) + + instance = recorder.get_instance(hass) + # Patch out the safeguard in the states meta manager + # so that we hit the filter_unique_constraint_integrity_error safeguard in the statistics + with patch.object(instance.statistics_meta_manager, "get", return_value=None): + # Rename entity sensor.test1 to sensor.test99 + @callback + def rename_entry(): + entity_reg.async_update_entity( + "sensor.test1", new_entity_id="sensor.test99" + ) + + hass.add_job(rename_entry) + wait_recording_done(hass) + + # Statistics failed to migrate due to the collision + stats = statistics_during_period(hass, zero, period="5minute") + assert stats == {"sensor.test1": expected_stats1, "sensor.test2": expected_stats2} + + # Verify the filter_unique_constraint_integrity_error safeguard was hit assert "Blocked attempt to insert duplicated statistic rows" in caplog.text + # Verify the safeguard in the states meta manager was not hit + assert ( + "Cannot rename statistic_id `sensor.test1` to `sensor.test99` " + "because the new statistic_id is already in use" + ) not in caplog.text + def test_statistics_duplicated( hass_recorder: Callable[..., HomeAssistant], caplog: pytest.LogCaptureFixture From d99b9f7a700fa8298080bc3f7402d7289d7eb99f Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 8 Mar 2024 19:28:04 -0800 Subject: [PATCH 29/99] Fix local calendar handling of empty recurrence ids (#112745) * Fix handling of empty recurrence ids * Revert logging changes --- homeassistant/components/calendar/__init__.py | 13 ++- .../local_calendar/test_calendar.py | 92 +++++++++++++++++++ 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index bef0e2fc09f..670d448a430 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -189,6 +189,11 @@ def _validate_rrule(value: Any) -> str: return str(value) +def _empty_as_none(value: str | None) -> str | None: + """Convert any empty string values to None.""" + return value or None + + CREATE_EVENT_SERVICE = "create_event" CREATE_EVENT_SCHEMA = vol.All( cv.has_at_least_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN), @@ -733,7 +738,9 @@ async def handle_calendar_event_create( vol.Required("type"): "calendar/event/delete", vol.Required("entity_id"): cv.entity_id, vol.Required(EVENT_UID): cv.string, - vol.Optional(EVENT_RECURRENCE_ID): cv.string, + vol.Optional(EVENT_RECURRENCE_ID): vol.Any( + vol.All(cv.string, _empty_as_none), None + ), vol.Optional(EVENT_RECURRENCE_RANGE): cv.string, } ) @@ -777,7 +784,9 @@ async def handle_calendar_event_delete( vol.Required("type"): "calendar/event/update", vol.Required("entity_id"): cv.entity_id, vol.Required(EVENT_UID): cv.string, - vol.Optional(EVENT_RECURRENCE_ID): cv.string, + vol.Optional(EVENT_RECURRENCE_ID): vol.Any( + vol.All(cv.string, _empty_as_none), None + ), vol.Optional(EVENT_RECURRENCE_RANGE): cv.string, vol.Required(CONF_EVENT): WEBSOCKET_EVENT_SCHEMA, } diff --git a/tests/components/local_calendar/test_calendar.py b/tests/components/local_calendar/test_calendar.py index 060ea114973..2fa0063dfd8 100644 --- a/tests/components/local_calendar/test_calendar.py +++ b/tests/components/local_calendar/test_calendar.py @@ -408,6 +408,46 @@ async def test_websocket_delete_recurring( ] +async def test_websocket_delete_empty_recurrence_id( + ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn +) -> None: + """Test websocket delete command with an empty recurrence id no-op.""" + client = await ws_client() + await client.cmd_result( + "create", + { + "entity_id": TEST_ENTITY, + "event": { + "summary": "Bastille Day Party", + "dtstart": "1997-07-14T17:00:00+00:00", + "dtend": "1997-07-15T04:00:00+00:00", + }, + }, + ) + + events = await get_events("1997-07-14T00:00:00", "1997-07-16T00:00:00") + assert list(map(event_fields, events)) == [ + { + "summary": "Bastille Day Party", + "start": {"dateTime": "1997-07-14T11:00:00-06:00"}, + "end": {"dateTime": "1997-07-14T22:00:00-06:00"}, + } + ] + uid = events[0]["uid"] + + # Delete the event with an empty recurrence id + await client.cmd_result( + "delete", + { + "entity_id": TEST_ENTITY, + "uid": uid, + "recurrence_id": "", + }, + ) + events = await get_events("1997-07-14T00:00:00", "1997-07-16T00:00:00") + assert list(map(event_fields, events)) == [] + + async def test_websocket_update( ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn ) -> None: @@ -458,6 +498,58 @@ async def test_websocket_update( ] +async def test_websocket_update_empty_recurrence( + ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn +) -> None: + """Test an edit with an empty recurrence id (no-op).""" + client = await ws_client() + await client.cmd_result( + "create", + { + "entity_id": TEST_ENTITY, + "event": { + "summary": "Bastille Day Party", + "dtstart": "1997-07-14T17:00:00+00:00", + "dtend": "1997-07-15T04:00:00+00:00", + }, + }, + ) + + events = await get_events("1997-07-14T00:00:00", "1997-07-16T00:00:00") + assert list(map(event_fields, events)) == [ + { + "summary": "Bastille Day Party", + "start": {"dateTime": "1997-07-14T11:00:00-06:00"}, + "end": {"dateTime": "1997-07-14T22:00:00-06:00"}, + } + ] + uid = events[0]["uid"] + + # Update the event with an empty string for the recurrence id which should + # have no effect. + await client.cmd_result( + "update", + { + "entity_id": TEST_ENTITY, + "uid": uid, + "recurrence_id": "", + "event": { + "summary": "Bastille Day Party [To be rescheduled]", + "dtstart": "1997-07-15T11:00:00-06:00", + "dtend": "1997-07-15T22:00:00-06:00", + }, + }, + ) + events = await get_events("1997-07-14T00:00:00", "1997-07-16T00:00:00") + assert list(map(event_fields, events)) == [ + { + "summary": "Bastille Day Party [To be rescheduled]", + "start": {"dateTime": "1997-07-15T11:00:00-06:00"}, + "end": {"dateTime": "1997-07-15T22:00:00-06:00"}, + } + ] + + async def test_websocket_update_recurring_this_and_future( ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn ) -> None: From 3f22ad4eac6b8da606951646185bb1bc5f5bf190 Mon Sep 17 00:00:00 2001 From: Arie Catsman <120491684+catsmanac@users.noreply.github.com> Date: Fri, 8 Mar 2024 21:28:03 +0100 Subject: [PATCH 30/99] Bump pyenphase to 1.19.2 (#112747) --- homeassistant/components/enphase_envoy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/enphase_envoy/manifest.json b/homeassistant/components/enphase_envoy/manifest.json index 63e10547ead..9f437ee9945 100644 --- a/homeassistant/components/enphase_envoy/manifest.json +++ b/homeassistant/components/enphase_envoy/manifest.json @@ -6,7 +6,7 @@ "documentation": "https://www.home-assistant.io/integrations/enphase_envoy", "iot_class": "local_polling", "loggers": ["pyenphase"], - "requirements": ["pyenphase==1.19.1"], + "requirements": ["pyenphase==1.19.2"], "zeroconf": [ { "type": "_enphase-envoy._tcp.local." diff --git a/requirements_all.txt b/requirements_all.txt index 99ee92183bb..2ec4da83b00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1794,7 +1794,7 @@ pyedimax==0.2.1 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.19.1 +pyenphase==1.19.2 # homeassistant.components.envisalink pyenvisalink==4.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 68bee472ef8..70edc623d60 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1390,7 +1390,7 @@ pyeconet==0.1.22 pyefergy==22.1.1 # homeassistant.components.enphase_envoy -pyenphase==1.19.1 +pyenphase==1.19.2 # homeassistant.components.everlights pyeverlights==0.1.0 From 70389521bf42419c6a8d56b584394a3995d7ec9f Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 9 Mar 2024 05:28:56 +0200 Subject: [PATCH 31/99] Bump bthome-ble to 3.7.0 (#112783) --- homeassistant/components/bthome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index a3e974bf71e..764dff3770c 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -20,5 +20,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/bthome", "iot_class": "local_push", - "requirements": ["bthome-ble==3.6.0"] + "requirements": ["bthome-ble==3.7.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2ec4da83b00..e3e707fb14f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -621,7 +621,7 @@ brunt==1.2.0 bt-proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==3.6.0 +bthome-ble==3.7.0 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70edc623d60..e6c555c7e69 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -529,7 +529,7 @@ brottsplatskartan==1.0.5 brunt==1.2.0 # homeassistant.components.bthome -bthome-ble==3.6.0 +bthome-ble==3.7.0 # homeassistant.components.buienradar buienradar==1.0.5 From 93a01938a4b13f82da270c1893b1039f54d5ec6b Mon Sep 17 00:00:00 2001 From: Lex Li <425130+lextm@users.noreply.github.com> Date: Sat, 9 Mar 2024 14:51:08 -0500 Subject: [PATCH 32/99] Upgrade `pysnmp-lextudio` to version `6.0.9` (#112795) --- homeassistant/components/snmp/manifest.json | 2 +- homeassistant/components/snmp/switch.py | 2 +- pyproject.toml | 3 --- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index 8d046b609b8..cd6c1dd9152 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/snmp", "iot_class": "local_polling", "loggers": ["pyasn1", "pysmi", "pysnmp"], - "requirements": ["pysnmp-lextudio==5.0.34"] + "requirements": ["pysnmp-lextudio==6.0.9"] } diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index a30cf93bcde..3578f0e0c1d 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -237,7 +237,7 @@ class SnmpSwitch(SwitchEntity): async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" - # If vartype set, use it - http://snmplabs.com/pysnmp/docs/api-reference.html#pysnmp.smi.rfc1902.ObjectType + # If vartype set, use it - https://www.pysnmp.com/pysnmp/docs/api-reference.html#pysnmp.smi.rfc1902.ObjectType await self._execute_command(self._command_payload_on) async def async_turn_off(self, **kwargs: Any) -> None: diff --git a/pyproject.toml b/pyproject.toml index b99dac7d54e..2764c1a3e08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -528,9 +528,6 @@ filterwarnings = [ "ignore:loop argument is deprecated:DeprecationWarning:emulated_roku", # https://github.com/thecynic/pylutron - v0.2.10 "ignore:setDaemon\\(\\) is deprecated, set the daemon attribute instead:DeprecationWarning:pylutron", - # Fixed for Python 3.12 - # https://github.com/lextudio/pysnmp/issues/10 - "ignore:The asyncore module is deprecated and will be removed in Python 3.12:DeprecationWarning:pysnmp.carrier.asyncore.base", # Wrong stacklevel # https://bugs.launchpad.net/beautifulsoup/+bug/2034451 "ignore:It looks like you're parsing an XML document using an HTML parser:UserWarning:bs4.builder", diff --git a/requirements_all.txt b/requirements_all.txt index e3e707fb14f..b54126915ad 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2155,7 +2155,7 @@ pysmartthings==0.7.8 pysml==0.0.12 # homeassistant.components.snmp -pysnmp-lextudio==5.0.34 +pysnmp-lextudio==6.0.9 # homeassistant.components.snooz pysnooz==0.8.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e6c555c7e69..0b14c0b02ec 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1673,7 +1673,7 @@ pysmartthings==0.7.8 pysml==0.0.12 # homeassistant.components.snmp -pysnmp-lextudio==5.0.34 +pysnmp-lextudio==6.0.9 # homeassistant.components.snooz pysnooz==0.8.6 From 8f2f9b8184fa3ead4ab91b5b6482389524fe86e7 Mon Sep 17 00:00:00 2001 From: mrchi Date: Wed, 13 Mar 2024 04:12:24 +0800 Subject: [PATCH 33/99] Bump openwrt-luci-rpc version to 1.1.17 (#112796) --- homeassistant/components/luci/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/luci/manifest.json b/homeassistant/components/luci/manifest.json index 2412aaad0a1..597aad30648 100644 --- a/homeassistant/components/luci/manifest.json +++ b/homeassistant/components/luci/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/luci", "iot_class": "local_polling", "loggers": ["openwrt_luci_rpc"], - "requirements": ["openwrt-luci-rpc==1.1.16"] + "requirements": ["openwrt-luci-rpc==1.1.17"] } diff --git a/requirements_all.txt b/requirements_all.txt index b54126915ad..12af2b0e6cc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1474,7 +1474,7 @@ opensensemap-api==0.2.0 openwebifpy==4.2.4 # homeassistant.components.luci -openwrt-luci-rpc==1.1.16 +openwrt-luci-rpc==1.1.17 # homeassistant.components.ubus openwrt-ubus-rpc==0.0.2 From 2d7de216a7f1606e94c3c6b086857b880b3cf063 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Tue, 12 Mar 2024 09:07:20 +0100 Subject: [PATCH 34/99] Fix google_asssistant sensor state reporting (#112838) * Fix post google_assistant sensor values as float not string * Fix aqi reporting and improve tests * Fix _air_quality_description_for_aqi and test --- .../components/google_assistant/trait.py | 15 ++++--- .../components/google_assistant/test_trait.py | 40 ++++++++++++------- 2 files changed, 35 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 169fa30386d..aba6e461778 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -2706,10 +2706,9 @@ class SensorStateTrait(_Trait): name = TRAIT_SENSOR_STATE commands: list[str] = [] - def _air_quality_description_for_aqi(self, aqi): - if aqi is None or aqi.isnumeric() is False: + def _air_quality_description_for_aqi(self, aqi: float | None) -> str: + if aqi is None or aqi < 0: return "unknown" - aqi = int(aqi) if aqi <= 50: return "healthy" if aqi <= 100: @@ -2764,11 +2763,17 @@ class SensorStateTrait(_Trait): if device_class is None or data is None: return {} - sensor_data = {"name": data[0], "rawValue": self.state.state} + try: + value = float(self.state.state) + except ValueError: + value = None + if self.state.state == STATE_UNKNOWN: + value = None + sensor_data = {"name": data[0], "rawValue": value} if device_class == sensor.SensorDeviceClass.AQI: sensor_data["currentSensorState"] = self._air_quality_description_for_aqi( - self.state.state + value ) return {"currentSensorStateData": [sensor_data]} diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 58cbc5dce0e..c3b60a32850 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1,5 +1,6 @@ """Tests for the Google Assistant traits.""" from datetime import datetime, timedelta +from typing import Any from unittest.mock import ANY, patch from freezegun.api import FrozenDateTimeFactory @@ -3925,16 +3926,15 @@ async def test_air_quality_description_for_aqi(hass: HomeAssistant) -> None: BASIC_CONFIG, ) - assert trt._air_quality_description_for_aqi("0") == "healthy" - assert trt._air_quality_description_for_aqi("75") == "moderate" + assert trt._air_quality_description_for_aqi(0) == "healthy" + assert trt._air_quality_description_for_aqi(75) == "moderate" assert ( - trt._air_quality_description_for_aqi("125") == "unhealthy for sensitive groups" + trt._air_quality_description_for_aqi(125.0) == "unhealthy for sensitive groups" ) - assert trt._air_quality_description_for_aqi("175") == "unhealthy" - assert trt._air_quality_description_for_aqi("250") == "very unhealthy" - assert trt._air_quality_description_for_aqi("350") == "hazardous" - assert trt._air_quality_description_for_aqi("-1") == "unknown" - assert trt._air_quality_description_for_aqi("non-numeric") == "unknown" + assert trt._air_quality_description_for_aqi(175) == "unhealthy" + assert trt._air_quality_description_for_aqi(250) == "very unhealthy" + assert trt._air_quality_description_for_aqi(350) == "hazardous" + assert trt._air_quality_description_for_aqi(-1) == "unknown" async def test_null_device_class(hass: HomeAssistant) -> None: @@ -3955,7 +3955,19 @@ async def test_null_device_class(hass: HomeAssistant) -> None: assert trt.query_attributes() == {} -async def test_sensorstate(hass: HomeAssistant) -> None: +@pytest.mark.parametrize( + ("value", "published", "aqi"), + [ + (100.0, 100.0, "moderate"), + (10.0, 10.0, "healthy"), + (0, 0.0, "healthy"), + ("", None, "unknown"), + ("unknown", None, "unknown"), + ], +) +async def test_sensorstate( + hass: HomeAssistant, value: Any, published: Any, aqi: Any +) -> None: """Test SensorState trait support for sensor domain.""" sensor_types = { sensor.SensorDeviceClass.AQI: ("AirQuality", "AQI"), @@ -3977,7 +3989,7 @@ async def test_sensorstate(hass: HomeAssistant) -> None: hass, State( "sensor.test", - 100.0, + value, { "device_class": sensor_type, }, @@ -4023,16 +4035,14 @@ async def test_sensorstate(hass: HomeAssistant) -> None: "currentSensorStateData": [ { "name": name, - "currentSensorState": trt._air_quality_description_for_aqi( - trt.state.state - ), - "rawValue": trt.state.state, + "currentSensorState": aqi, + "rawValue": published, }, ] } else: assert trt.query_attributes() == { - "currentSensorStateData": [{"name": name, "rawValue": trt.state.state}] + "currentSensorStateData": [{"name": name, "rawValue": published}] } assert helpers.get_google_type(sensor.DOMAIN, None) is not None From fc2ca1646abacf3624a04efb320e8e725e200abb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 9 Mar 2024 18:02:42 -1000 Subject: [PATCH 35/99] Fix MJPEG fallback when still image URL is missing with basic auth (#112861) * Fix MJPEG fallback when still image URL is missing with basic auth I picked up an old DCS-930L (circa 2010) camera to test with to fix #94877 * Fix MJPEG fallback when still image URL is missing with basic auth I picked up an old DCS-930L (circa 2010) camera to test with to fix #94877 * Fix MJPEG fallback when still image URL is missing with basic auth I picked up an old DCS-930L (circa 2010) camera to test with to fix #94877 * Fix MJPEG fallback when still image URL is missing with basic auth I picked up an old DCS-930L (circa 2010) camera to test with to fix #94877 --- homeassistant/components/mjpeg/camera.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index d424df620cf..6a5c4e3203b 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -134,12 +134,11 @@ class MjpegCamera(Camera): self, width: int | None = None, height: int | None = None ) -> bytes | None: """Return a still image response from the camera.""" - # DigestAuth is not supported if ( self._authentication == HTTP_DIGEST_AUTHENTICATION or self._still_image_url is None ): - return await self._async_digest_camera_image() + return await self._async_digest_or_fallback_camera_image() websession = async_get_clientsession(self.hass, verify_ssl=self._verify_ssl) try: @@ -157,15 +156,17 @@ class MjpegCamera(Camera): return None - def _get_digest_auth(self) -> httpx.DigestAuth: - """Return a DigestAuth object.""" + def _get_httpx_auth(self) -> httpx.Auth: + """Return a httpx auth object.""" username = "" if self._username is None else self._username - return httpx.DigestAuth(username, self._password) + digest_auth = self._authentication == HTTP_DIGEST_AUTHENTICATION + cls = httpx.DigestAuth if digest_auth else httpx.BasicAuth + return cls(username, self._password) - async def _async_digest_camera_image(self) -> bytes | None: + async def _async_digest_or_fallback_camera_image(self) -> bytes | None: """Return a still image response from the camera using digest authentication.""" client = get_async_client(self.hass, verify_ssl=self._verify_ssl) - auth = self._get_digest_auth() + auth = self._get_httpx_auth() try: if self._still_image_url: # Fallback to MJPEG stream if still image URL is not available @@ -196,7 +197,7 @@ class MjpegCamera(Camera): ) -> web.StreamResponse | None: """Generate an HTTP MJPEG stream from the camera using digest authentication.""" async with get_async_client(self.hass, verify_ssl=self._verify_ssl).stream( - "get", self._mjpeg_url, auth=self._get_digest_auth(), timeout=TIMEOUT + "get", self._mjpeg_url, auth=self._get_httpx_auth(), timeout=TIMEOUT ) as stream: response = web.StreamResponse(headers=stream.headers) await response.prepare(request) From 0d262ea9d4eb81fe01d995a8ef92818b2d0955aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=98=9F=E9=87=8ESKY?= <87404327+FlyingFeng2021@users.noreply.github.com> Date: Mon, 11 Mar 2024 18:11:32 +0800 Subject: [PATCH 36/99] Bump boschshcpy to 0.2.82 (#112890) --- homeassistant/components/bosch_shc/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bosch_shc/manifest.json b/homeassistant/components/bosch_shc/manifest.json index e29865153b3..efae159adc2 100644 --- a/homeassistant/components/bosch_shc/manifest.json +++ b/homeassistant/components/bosch_shc/manifest.json @@ -7,7 +7,7 @@ "documentation": "https://www.home-assistant.io/integrations/bosch_shc", "iot_class": "local_push", "loggers": ["boschshcpy"], - "requirements": ["boschshcpy==0.2.75"], + "requirements": ["boschshcpy==0.2.82"], "zeroconf": [ { "type": "_http._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 12af2b0e6cc..421535a42f3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -596,7 +596,7 @@ bluetooth-data-tools==1.19.0 bond-async==0.2.1 # homeassistant.components.bosch_shc -boschshcpy==0.2.75 +boschshcpy==0.2.82 # homeassistant.components.amazon_polly # homeassistant.components.route53 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0b14c0b02ec..b2351604ce7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -511,7 +511,7 @@ bluetooth-data-tools==1.19.0 bond-async==0.2.1 # homeassistant.components.bosch_shc -boschshcpy==0.2.75 +boschshcpy==0.2.82 # homeassistant.components.bring bring-api==0.5.6 From def4f3cb098b16071bf6eb488bf829ab9011e667 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Sun, 10 Mar 2024 16:20:46 -0400 Subject: [PATCH 37/99] Add missing translation placeholder in Hydrawise (#113007) Add missing translation placeholder --- homeassistant/components/hydrawise/config_flow.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/hydrawise/config_flow.py b/homeassistant/components/hydrawise/config_flow.py index 72df86606d7..ea3b9e58926 100644 --- a/homeassistant/components/hydrawise/config_flow.py +++ b/homeassistant/components/hydrawise/config_flow.py @@ -52,7 +52,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): is_fixable=False, severity=IssueSeverity.ERROR, translation_key="deprecated_yaml_import_issue", - translation_placeholders={"error_type": error_type}, + translation_placeholders={ + "error_type": error_type, + "url": "/config/integrations/dashboard/add?domain=hydrawise", + }, ) return self.async_abort(reason=error_type) From bbe88c2a5e0b03dc9edd384db7be52dce73a5ff3 Mon Sep 17 00:00:00 2001 From: Ernst Klamer Date: Mon, 11 Mar 2024 04:25:04 +0100 Subject: [PATCH 38/99] Bump bthome-ble to 3.8.0 (#113008) Bump bthome-ble --- homeassistant/components/bthome/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bthome/manifest.json b/homeassistant/components/bthome/manifest.json index 764dff3770c..5fb90bb5998 100644 --- a/homeassistant/components/bthome/manifest.json +++ b/homeassistant/components/bthome/manifest.json @@ -20,5 +20,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/bthome", "iot_class": "local_push", - "requirements": ["bthome-ble==3.7.0"] + "requirements": ["bthome-ble==3.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 421535a42f3..8f6695dc7bc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -621,7 +621,7 @@ brunt==1.2.0 bt-proximity==0.2.1 # homeassistant.components.bthome -bthome-ble==3.7.0 +bthome-ble==3.8.0 # homeassistant.components.bt_home_hub_5 bthomehub5-devicelist==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b2351604ce7..d0a39ebba04 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -529,7 +529,7 @@ brottsplatskartan==1.0.5 brunt==1.2.0 # homeassistant.components.bthome -bthome-ble==3.7.0 +bthome-ble==3.8.0 # homeassistant.components.buienradar buienradar==1.0.5 From 76cf25228f60a581ed142a1d0d97c347d43a2330 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 11 Mar 2024 04:17:12 +0100 Subject: [PATCH 39/99] Bump axis to v53 (#113019) --- homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 9bfe37da9da..65d18347415 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -26,7 +26,7 @@ "iot_class": "local_push", "loggers": ["axis"], "quality_scale": "platinum", - "requirements": ["axis==52"], + "requirements": ["axis==53"], "ssdp": [ { "manufacturer": "AXIS" diff --git a/requirements_all.txt b/requirements_all.txt index 8f6695dc7bc..6e35c7b6dc6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==52 +axis==53 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d0a39ebba04..90c3e7953d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -454,7 +454,7 @@ auroranoaa==0.0.3 aurorapy==0.2.7 # homeassistant.components.axis -axis==52 +axis==53 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 From 7b5f8793057886aeeb1529cbc151e7e96622cb23 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 12 Mar 2024 17:41:16 +0100 Subject: [PATCH 40/99] Fix availability for GIOS index sensors (#113021) * Fix availability for index sensors * Improve test_availability() --------- Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/gios/sensor.py | 6 +-- tests/components/gios/test_sensor.py | 67 +++++++++++++------------ 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/gios/sensor.py b/homeassistant/components/gios/sensor.py index 1b13430128f..0437f8f6172 100644 --- a/homeassistant/components/gios/sensor.py +++ b/homeassistant/components/gios/sensor.py @@ -229,11 +229,11 @@ class GiosSensor(CoordinatorEntity[GiosDataUpdateCoordinator], SensorEntity): @property def available(self) -> bool: """Return if entity is available.""" - available = super().available sensor_data = getattr(self.coordinator.data, self.entity_description.key) + available = super().available and bool(sensor_data) # Sometimes the API returns sensor data without indexes - if self.entity_description.subkey: + if self.entity_description.subkey and available: return available and bool(sensor_data.index) - return available and bool(sensor_data) + return available diff --git a/tests/components/gios/test_sensor.py b/tests/components/gios/test_sensor.py index 7a7a735ff42..6714882ad3f 100644 --- a/tests/components/gios/test_sensor.py +++ b/tests/components/gios/test_sensor.py @@ -1,4 +1,6 @@ """Test sensor of GIOS integration.""" + +from copy import deepcopy from datetime import timedelta import json from unittest.mock import patch @@ -276,22 +278,24 @@ async def test_availability(hass: HomeAssistant) -> None: async_fire_time_changed(hass, future) await hass.async_block_till_done() - state = hass.states.get("sensor.home_pm2_5") - assert state - assert state.state == STATE_UNAVAILABLE + state = hass.states.get("sensor.home_pm2_5") + assert state + assert state.state == STATE_UNAVAILABLE - state = hass.states.get("sensor.home_pm2_5_index") - assert state - assert state.state == STATE_UNAVAILABLE + state = hass.states.get("sensor.home_pm2_5_index") + assert state + assert state.state == STATE_UNAVAILABLE - state = hass.states.get("sensor.home_air_quality_index") - assert state - assert state.state == STATE_UNAVAILABLE + state = hass.states.get("sensor.home_air_quality_index") + assert state + assert state.state == STATE_UNAVAILABLE + incomplete_sensors = deepcopy(sensors) + incomplete_sensors["pm2.5"] = {} future = utcnow() + timedelta(minutes=120) with patch( "homeassistant.components.gios.Gios._get_all_sensors", - return_value=sensors, + return_value=incomplete_sensors, ), patch( "homeassistant.components.gios.Gios._get_indexes", return_value={}, @@ -299,21 +303,22 @@ async def test_availability(hass: HomeAssistant) -> None: async_fire_time_changed(hass, future) await hass.async_block_till_done() - state = hass.states.get("sensor.home_pm2_5") - assert state - assert state.state == "4" + # There is no PM2.5 data so the state should be unavailable + state = hass.states.get("sensor.home_pm2_5") + assert state + assert state.state == STATE_UNAVAILABLE - # Indexes are empty so the state should be unavailable - state = hass.states.get("sensor.home_air_quality_index") - assert state - assert state.state == STATE_UNAVAILABLE + # Indexes are empty so the state should be unavailable + state = hass.states.get("sensor.home_air_quality_index") + assert state + assert state.state == STATE_UNAVAILABLE - # Indexes are empty so the state should be unavailable - state = hass.states.get("sensor.home_pm2_5_index") - assert state - assert state.state == STATE_UNAVAILABLE + # Indexes are empty so the state should be unavailable + state = hass.states.get("sensor.home_pm2_5_index") + assert state + assert state.state == STATE_UNAVAILABLE - future = utcnow() + timedelta(minutes=180) + future = utcnow() + timedelta(minutes=180) with patch( "homeassistant.components.gios.Gios._get_all_sensors", return_value=sensors ), patch( @@ -323,17 +328,17 @@ async def test_availability(hass: HomeAssistant) -> None: async_fire_time_changed(hass, future) await hass.async_block_till_done() - state = hass.states.get("sensor.home_pm2_5") - assert state - assert state.state == "4" + state = hass.states.get("sensor.home_pm2_5") + assert state + assert state.state == "4" - state = hass.states.get("sensor.home_pm2_5_index") - assert state - assert state.state == "good" + state = hass.states.get("sensor.home_pm2_5_index") + assert state + assert state.state == "good" - state = hass.states.get("sensor.home_air_quality_index") - assert state - assert state.state == "good" + state = hass.states.get("sensor.home_air_quality_index") + assert state + assert state.state == "good" async def test_invalid_indexes(hass: HomeAssistant) -> None: From a448c904d36d132599958d4405f6092c7fa84c61 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 10 Mar 2024 20:13:02 -0700 Subject: [PATCH 41/99] Bump ical to 7.0.1 and always use home assistant timezone for local todo dtstart (#113034) --- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- homeassistant/components/local_todo/todo.py | 10 +++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 12 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 01c20595c55..a08daee8961 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==6.0.3", "oauth2client==4.1.3", "ical==7.0.0"] + "requirements": ["gcal-sync==6.0.3", "oauth2client==4.1.3", "ical==7.0.1"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index 53fd61a2924..25ec9f2ccc6 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==7.0.0"] + "requirements": ["ical==7.0.1"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index b45eec12e62..81f0f9dc199 100644 --- a/homeassistant/components/local_todo/manifest.json +++ b/homeassistant/components/local_todo/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_todo", "iot_class": "local_polling", - "requirements": ["ical==7.0.0"] + "requirements": ["ical==7.0.1"] } diff --git a/homeassistant/components/local_todo/todo.py b/homeassistant/components/local_todo/todo.py index 292f8237776..5b25abf8e21 100644 --- a/homeassistant/components/local_todo/todo.py +++ b/homeassistant/components/local_todo/todo.py @@ -18,6 +18,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.util import dt as dt_util from .const import CONF_TODO_LIST_NAME, DOMAIN from .store import LocalTodoListStore @@ -124,6 +125,9 @@ class LocalTodoListEntity(TodoListEntity): self._attr_name = name.capitalize() self._attr_unique_id = unique_id + def _new_todo_store(self) -> TodoStore: + return TodoStore(self._calendar, tzinfo=dt_util.DEFAULT_TIME_ZONE) + async def async_update(self) -> None: """Update entity state based on the local To-do items.""" todo_items = [] @@ -147,20 +151,20 @@ class LocalTodoListEntity(TodoListEntity): async def async_create_todo_item(self, item: TodoItem) -> None: """Add an item to the To-do list.""" todo = _convert_item(item) - TodoStore(self._calendar).add(todo) + self._new_todo_store().add(todo) await self.async_save() await self.async_update_ha_state(force_refresh=True) async def async_update_todo_item(self, item: TodoItem) -> None: """Update an item to the To-do list.""" todo = _convert_item(item) - TodoStore(self._calendar).edit(todo.uid, todo) + self._new_todo_store().edit(todo.uid, todo) await self.async_save() await self.async_update_ha_state(force_refresh=True) async def async_delete_todo_items(self, uids: list[str]) -> None: """Delete an item from the To-do list.""" - store = TodoStore(self._calendar) + store = self._new_todo_store() for uid in uids: store.delete(uid) await self.async_save() diff --git a/requirements_all.txt b/requirements_all.txt index 6e35c7b6dc6..8304b581470 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1115,7 +1115,7 @@ ibmiotf==0.3.4 # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==7.0.0 +ical==7.0.1 # homeassistant.components.ping icmplib==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 90c3e7953d0..139448e228a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -905,7 +905,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==7.0.0 +ical==7.0.1 # homeassistant.components.ping icmplib==3.0 From 2dbc63809d1153891377f9b97410a4961d3c17e3 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Tue, 12 Mar 2024 11:43:25 -0500 Subject: [PATCH 42/99] Fix some handle leaks in rainforest_raven (#113035) There were leaks when * The component was shutdown * There was a timeout during the initial device opening Additionally, the device was not closed/reopened when there was a timeout reading regular data. --- .../rainforest_raven/coordinator.py | 30 ++++++++++++------- .../rainforest_raven/test_coordinator.py | 17 +++++++++++ 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/rainforest_raven/coordinator.py b/homeassistant/components/rainforest_raven/coordinator.py index edae4f11433..dbb7203768b 100644 --- a/homeassistant/components/rainforest_raven/coordinator.py +++ b/homeassistant/components/rainforest_raven/coordinator.py @@ -132,16 +132,27 @@ class RAVEnDataCoordinator(DataUpdateCoordinator): ) return None + async def async_shutdown(self) -> None: + """Shutdown the coordinator.""" + await self._cleanup_device() + await super().async_shutdown() + async def _async_update_data(self) -> dict[str, Any]: try: device = await self._get_device() async with asyncio.timeout(5): return await _get_all_data(device, self.entry.data[CONF_MAC]) except RAVEnConnectionError as err: - if self._raven_device: - await self._raven_device.close() - self._raven_device = None + await self._cleanup_device() raise UpdateFailed(f"RAVEnConnectionError: {err}") from err + except TimeoutError: + await self._cleanup_device() + raise + + async def _cleanup_device(self) -> None: + device, self._raven_device = self._raven_device, None + if device is not None: + await device.close() async def _get_device(self) -> RAVEnSerialDevice: if self._raven_device is not None: @@ -149,15 +160,14 @@ class RAVEnDataCoordinator(DataUpdateCoordinator): device = RAVEnSerialDevice(self.entry.data[CONF_DEVICE]) - async with asyncio.timeout(5): - await device.open() - - try: + try: + async with asyncio.timeout(5): + await device.open() await device.synchronize() self._device_info = await device.get_device_info() - except Exception: - await device.close() - raise + except: + await device.close() + raise self._raven_device = device return device diff --git a/tests/components/rainforest_raven/test_coordinator.py b/tests/components/rainforest_raven/test_coordinator.py index 6b29c944aeb..1a5f4d3d3f7 100644 --- a/tests/components/rainforest_raven/test_coordinator.py +++ b/tests/components/rainforest_raven/test_coordinator.py @@ -1,4 +1,8 @@ """Tests for the Rainforest RAVEn data coordinator.""" + +import asyncio +import functools + from aioraven.device import RAVEnConnectionError import pytest @@ -83,6 +87,19 @@ async def test_coordinator_device_error_update(hass: HomeAssistant, mock_device) assert coordinator.last_update_success is False +async def test_coordinator_device_timeout_update(hass: HomeAssistant, mock_device): + """Test handling of a device timeout during an update.""" + entry = create_mock_entry() + coordinator = RAVEnDataCoordinator(hass, entry) + + await coordinator.async_config_entry_first_refresh() + assert coordinator.last_update_success is True + + mock_device.get_network_info.side_effect = functools.partial(asyncio.sleep, 10) + await coordinator.async_refresh() + assert coordinator.last_update_success is False + + async def test_coordinator_comm_error(hass: HomeAssistant, mock_device): """Test handling of an error parsing or reading raw device data.""" entry = create_mock_entry() From 911b39666d0d3baef48a02895af42deb908f7bf5 Mon Sep 17 00:00:00 2001 From: Folke Lemaitre Date: Mon, 11 Mar 2024 15:05:31 +0100 Subject: [PATCH 43/99] Fix hvac_mode for viessmann devices with heatingCooling mode (#113054) --- homeassistant/components/vicare/climate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 10cc1a15c9e..c419e50e98c 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -52,6 +52,7 @@ SERVICE_SET_VICARE_MODE_ATTR_MODE = "vicare_mode" VICARE_MODE_DHW = "dhw" VICARE_MODE_HEATING = "heating" +VICARE_MODE_HEATINGCOOLING = "heatingCooling" VICARE_MODE_DHWANDHEATING = "dhwAndHeating" VICARE_MODE_DHWANDHEATINGCOOLING = "dhwAndHeatingCooling" VICARE_MODE_FORCEDREDUCED = "forcedReduced" @@ -71,6 +72,7 @@ VICARE_TO_HA_HVAC_HEATING: dict[str, HVACMode] = { VICARE_MODE_DHW: HVACMode.OFF, VICARE_MODE_DHWANDHEATINGCOOLING: HVACMode.AUTO, VICARE_MODE_DHWANDHEATING: HVACMode.AUTO, + VICARE_MODE_HEATINGCOOLING: HVACMode.AUTO, VICARE_MODE_HEATING: HVACMode.AUTO, VICARE_MODE_FORCEDNORMAL: HVACMode.HEAT, } From 8ac5da95f8e89dffdeec84c92e46736ace07fe5e Mon Sep 17 00:00:00 2001 From: Alistair Francis Date: Tue, 12 Mar 2024 03:58:43 +1000 Subject: [PATCH 44/99] components/gardena_bluetooth: Improve avaliability reliability (#113056) * components/gardena_bluetooth: Improve avaliability reliability The available() function incorrectly returns false even though the device is accessible. The last_update_success property should correctly indicate if the device isn't contactable, so we don't need to call async_address_present(). This is confirmed by seeing that no other users are calling async_address_present() in the available() function. This commit removes the call to async_address_present() to help fix the sensor being unavailable when using a ESPHome BLE proxy. Signed-off-by: Alistair Francis --------- Signed-off-by: Alistair Francis Co-authored-by: Joakim Plate --- .../components/gardena_bluetooth/coordinator.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/homeassistant/components/gardena_bluetooth/coordinator.py b/homeassistant/components/gardena_bluetooth/coordinator.py index 73552e25c03..12a212fe44b 100644 --- a/homeassistant/components/gardena_bluetooth/coordinator.py +++ b/homeassistant/components/gardena_bluetooth/coordinator.py @@ -12,7 +12,6 @@ from gardena_bluetooth.exceptions import ( ) from gardena_bluetooth.parse import Characteristic, CharacteristicType -from homeassistant.components import bluetooth from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo @@ -117,13 +116,7 @@ class GardenaBluetoothEntity(CoordinatorEntity[Coordinator]): @property def available(self) -> bool: """Return if entity is available.""" - return ( - self.coordinator.last_update_success - and bluetooth.async_address_present( - self.hass, self.coordinator.address, True - ) - and self._attr_available - ) + return self.coordinator.last_update_success and self._attr_available class GardenaBluetoothDescriptorEntity(GardenaBluetoothEntity): From a2e9ecfcde6916bb8078e23fd0e21fe18eb6b45e Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 11 Mar 2024 14:49:52 +0100 Subject: [PATCH 45/99] Fix for controlling Hue switch entities (#113064) --- homeassistant/components/hue/switch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/hue/switch.py b/homeassistant/components/hue/switch.py index c9da30a779c..c0689a85f21 100644 --- a/homeassistant/components/hue/switch.py +++ b/homeassistant/components/hue/switch.py @@ -57,7 +57,7 @@ async def async_setup_entry( event_type: EventType, resource: BehaviorInstance | LightLevel | Motion ) -> None: """Add entity from Hue resource.""" - async_add_entities([switch_class(bridge, api.sensors.motion, resource)]) + async_add_entities([switch_class(bridge, controller, resource)]) # add all current items in controller for item in controller: From 66cd6c0d2373cd193b131a290f15894df3c9a746 Mon Sep 17 00:00:00 2001 From: Lukas de Boer Date: Mon, 11 Mar 2024 17:30:51 +0100 Subject: [PATCH 46/99] Bump rova to 0.4.1 (#113066) --- homeassistant/components/rova/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rova/manifest.json b/homeassistant/components/rova/manifest.json index 03c6ddbb12c..a87ec224122 100644 --- a/homeassistant/components/rova/manifest.json +++ b/homeassistant/components/rova/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/rova", "iot_class": "cloud_polling", "loggers": ["rova"], - "requirements": ["rova==0.4.0"] + "requirements": ["rova==0.4.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8304b581470..b8cc49008b7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2469,7 +2469,7 @@ roombapy==1.6.13 roonapi==0.1.6 # homeassistant.components.rova -rova==0.4.0 +rova==0.4.1 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 From 0b2322c4668cab3da834a7494f5c0576f36ec08f Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Mon, 11 Mar 2024 19:04:44 +0100 Subject: [PATCH 47/99] Fix colormode attribute on grouped Hue light (#113071) --- homeassistant/components/hue/v2/group.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/homeassistant/components/hue/v2/group.py b/homeassistant/components/hue/v2/group.py index 8ce6d287551..8f9def48a27 100644 --- a/homeassistant/components/hue/v2/group.py +++ b/homeassistant/components/hue/v2/group.py @@ -269,10 +269,7 @@ class GroupedHueLight(HueBaseEntity, LightEntity): self._dynamic_mode_active = lights_in_dynamic_mode > 0 self._attr_supported_color_modes = supported_color_modes # pick a winner for the current colormode - if ( - lights_with_color_temp_support > 0 - and lights_in_colortemp_mode == lights_with_color_temp_support - ): + if lights_with_color_temp_support > 0 and lights_in_colortemp_mode > 0: self._attr_color_mode = ColorMode.COLOR_TEMP elif lights_with_color_support > 0: self._attr_color_mode = ColorMode.XY From cac22154a82aa613fd8793be6a4de533a2e5cbb8 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 12 Mar 2024 00:41:33 +0100 Subject: [PATCH 48/99] Bump axis to v54 (#113091) Co-authored-by: J. Nick Koston --- homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 65d18347415..f56df16918e 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -26,7 +26,7 @@ "iot_class": "local_push", "loggers": ["axis"], "quality_scale": "platinum", - "requirements": ["axis==53"], + "requirements": ["axis==54"], "ssdp": [ { "manufacturer": "AXIS" diff --git a/requirements_all.txt b/requirements_all.txt index b8cc49008b7..4f3555cd21b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==53 +axis==54 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 139448e228a..03f32b64112 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -454,7 +454,7 @@ auroranoaa==0.0.3 aurorapy==0.2.7 # homeassistant.components.axis -axis==53 +axis==54 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 From a63bf748863f83df2bd2444ff5309573936bb005 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Mar 2024 12:01:06 -1000 Subject: [PATCH 49/99] Bump aiodhcpwatcher to 0.8.1 (#113096) --- homeassistant/components/dhcp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index d609e9ec7ae..95c925a8d33 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -15,7 +15,7 @@ ], "quality_scale": "internal", "requirements": [ - "aiodhcpwatcher==0.8.0", + "aiodhcpwatcher==0.8.1", "aiodiscover==1.6.1", "cached_ipaddress==0.3.0" ] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5d9889d2855..0b5c6790777 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ # Automatically generated by gen_requirements_all.py, do not edit -aiodhcpwatcher==0.8.0 +aiodhcpwatcher==0.8.1 aiodiscover==1.6.1 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.3.1 diff --git a/requirements_all.txt b/requirements_all.txt index 4f3555cd21b..3e8995504b2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -221,7 +221,7 @@ aiobotocore==2.9.1 aiocomelit==0.9.0 # homeassistant.components.dhcp -aiodhcpwatcher==0.8.0 +aiodhcpwatcher==0.8.1 # homeassistant.components.dhcp aiodiscover==1.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 03f32b64112..dfa96ed5ffe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -200,7 +200,7 @@ aiobotocore==2.9.1 aiocomelit==0.9.0 # homeassistant.components.dhcp -aiodhcpwatcher==0.8.0 +aiodhcpwatcher==0.8.1 # homeassistant.components.dhcp aiodiscover==1.6.1 From d010df7116b952e950e401116f6d1d66afddf2d3 Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Tue, 12 Mar 2024 08:55:21 +0100 Subject: [PATCH 50/99] bump pytedee_async to 0.2.16 (#113135) --- homeassistant/components/tedee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tedee/manifest.json b/homeassistant/components/tedee/manifest.json index 6291a585009..1f2a2405a44 100644 --- a/homeassistant/components/tedee/manifest.json +++ b/homeassistant/components/tedee/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/tedee", "iot_class": "local_push", "loggers": ["pytedee_async"], - "requirements": ["pytedee-async==0.2.15"] + "requirements": ["pytedee-async==0.2.16"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3e8995504b2..628c41a2709 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2182,7 +2182,7 @@ pyswitchbee==1.8.0 pytautulli==23.1.1 # homeassistant.components.tedee -pytedee-async==0.2.15 +pytedee-async==0.2.16 # homeassistant.components.tfiac pytfiac==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dfa96ed5ffe..81f0470f440 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1697,7 +1697,7 @@ pyswitchbee==1.8.0 pytautulli==23.1.1 # homeassistant.components.tedee -pytedee-async==0.2.15 +pytedee-async==0.2.16 # homeassistant.components.motionmount python-MotionMount==0.3.1 From 095d0d0779a9c3b663858238d3818e9d5e348178 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Wed, 13 Mar 2024 11:29:39 +0100 Subject: [PATCH 51/99] Add message from Bad Request errors to HassioAPIError (#113144) Co-authored-by: Mike Degatano --- homeassistant/components/hassio/const.py | 1 + homeassistant/components/hassio/handler.py | 21 +++++++++---- .../components/hassio/websocket_api.py | 4 --- tests/components/hassio/test_websocket_api.py | 31 +++++++++++++++++++ 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index b495745e87d..b31c0f1cf15 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -19,6 +19,7 @@ ATTR_HOMEASSISTANT = "homeassistant" ATTR_HOMEASSISTANT_EXCLUDE_DATABASE = "homeassistant_exclude_database" ATTR_INPUT = "input" ATTR_ISSUES = "issues" +ATTR_MESSAGE = "message" ATTR_METHOD = "method" ATTR_PANELS = "panels" ATTR_PASSWORD = "password" diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 9b8e6367647..65c0824dbd2 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -21,7 +21,7 @@ from homeassistant.const import SERVER_PORT from homeassistant.core import HomeAssistant from homeassistant.loader import bind_hass -from .const import ATTR_DISCOVERY, DOMAIN, X_HASS_SOURCE +from .const import ATTR_DISCOVERY, ATTR_MESSAGE, ATTR_RESULT, DOMAIN, X_HASS_SOURCE _P = ParamSpec("_P") @@ -576,7 +576,7 @@ class HassIO: raise HassioAPIError() try: - request = await self.websession.request( + response = await self.websession.request( method, joined_url, json=payload, @@ -589,14 +589,23 @@ class HassIO: timeout=aiohttp.ClientTimeout(total=timeout), ) - if request.status != HTTPStatus.OK: - _LOGGER.error("%s return code %d", command, request.status) + if response.status != HTTPStatus.OK: + error = await response.json(encoding="utf-8") + if error.get(ATTR_RESULT) == "error": + raise HassioAPIError(error.get(ATTR_MESSAGE)) + + _LOGGER.error( + "Request to %s method %s returned with code %d", + command, + method, + response.status, + ) raise HassioAPIError() if return_text: - return await request.text(encoding="utf-8") + return await response.text(encoding="utf-8") - return await request.json(encoding="utf-8") + return await response.json(encoding="utf-8") except TimeoutError: _LOGGER.error("Timeout on %s request", command) diff --git a/homeassistant/components/hassio/websocket_api.py b/homeassistant/components/hassio/websocket_api.py index cf59f8de7f7..13c258dd68c 100644 --- a/homeassistant/components/hassio/websocket_api.py +++ b/homeassistant/components/hassio/websocket_api.py @@ -21,7 +21,6 @@ from .const import ( ATTR_DATA, ATTR_ENDPOINT, ATTR_METHOD, - ATTR_RESULT, ATTR_SESSION_DATA_USER_ID, ATTR_TIMEOUT, ATTR_WS_EVENT, @@ -131,9 +130,6 @@ async def websocket_supervisor_api( payload=payload, source="core.websocket_api", ) - - if result.get(ATTR_RESULT) == "error": - raise HassioAPIError(result.get("message")) except HassioAPIError as err: _LOGGER.error("Failed to to call %s - %s", msg[ATTR_ENDPOINT], err) connection.send_error( diff --git a/tests/components/hassio/test_websocket_api.py b/tests/components/hassio/test_websocket_api.py index b2f9e06cb43..ee17ad62e74 100644 --- a/tests/components/hassio/test_websocket_api.py +++ b/tests/components/hassio/test_websocket_api.py @@ -171,6 +171,7 @@ async def test_websocket_supervisor_api_error( aioclient_mock.get( "http://127.0.0.1/ping", json={"result": "error", "message": "example error"}, + status=400, ) await websocket_client.send_json( @@ -183,9 +184,39 @@ async def test_websocket_supervisor_api_error( ) msg = await websocket_client.receive_json() + assert msg["error"]["code"] == "unknown_error" assert msg["error"]["message"] == "example error" +async def test_websocket_supervisor_api_error_without_msg( + hassio_env, + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + aioclient_mock: AiohttpClientMocker, +) -> None: + """Test Supervisor websocket api error.""" + assert await async_setup_component(hass, "hassio", {}) + websocket_client = await hass_ws_client(hass) + aioclient_mock.get( + "http://127.0.0.1/ping", + json={}, + status=400, + ) + + await websocket_client.send_json( + { + WS_ID: 1, + WS_TYPE: WS_TYPE_API, + ATTR_ENDPOINT: "/ping", + ATTR_METHOD: "get", + } + ) + + msg = await websocket_client.receive_json() + assert msg["error"]["code"] == "unknown_error" + assert msg["error"]["message"] == "" + + async def test_websocket_non_admin_user( hassio_env, hass: HomeAssistant, From 962e5ec92ae753e909d012dc2d7c6536785af0eb Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Tue, 12 Mar 2024 11:45:14 -0500 Subject: [PATCH 52/99] Bump intents to 2023.3.12 (#113160) Bump intents --- homeassistant/components/conversation/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/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 4e3339d227b..00f645ea0f3 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -7,5 +7,5 @@ "integration_type": "system", "iot_class": "local_push", "quality_scale": "internal", - "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.3.6"] + "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.3.12"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 0b5c6790777..1b850316b91 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240307.0 -home-assistant-intents==2024.3.6 +home-assistant-intents==2024.3.12 httpx==0.27.0 ifaddr==0.2.0 janus==1.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index 628c41a2709..4a3121d78cf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1077,7 +1077,7 @@ holidays==0.44 home-assistant-frontend==20240307.0 # homeassistant.components.conversation -home-assistant-intents==2024.3.6 +home-assistant-intents==2024.3.12 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 81f0470f440..f2b42cd3a36 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,7 +876,7 @@ holidays==0.44 home-assistant-frontend==20240307.0 # homeassistant.components.conversation -home-assistant-intents==2024.3.6 +home-assistant-intents==2024.3.12 # homeassistant.components.home_connect homeconnect==0.7.2 From 6fdfc554a6c7f85fb59d3a72f3b6cdb6f450c4cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Wed, 13 Mar 2024 02:48:36 +0100 Subject: [PATCH 53/99] Bump airthings_ble to 0.7.1 (#113172) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ståle Storø Hauknes --- homeassistant/components/airthings_ble/__init__.py | 3 +-- homeassistant/components/airthings_ble/manifest.json | 2 +- homeassistant/components/airthings_ble/sensor.py | 6 ++---- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/airthings_ble/__init__.py | 9 ++++++--- tests/components/airthings_ble/test_config_flow.py | 8 +++----- 7 files changed, 15 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/airthings_ble/__init__.py b/homeassistant/components/airthings_ble/__init__.py index 3a97813741b..8258f7baf3d 100644 --- a/homeassistant/components/airthings_ble/__init__.py +++ b/homeassistant/components/airthings_ble/__init__.py @@ -27,7 +27,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data.setdefault(DOMAIN, {}) address = entry.unique_id - elevation = hass.config.elevation is_metric = hass.config.units is METRIC_SYSTEM assert address is not None @@ -40,7 +39,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: f"Could not find Airthings device with address {address}" ) - airthings = AirthingsBluetoothDeviceData(_LOGGER, elevation, is_metric) + airthings = AirthingsBluetoothDeviceData(_LOGGER, is_metric) async def _async_update_method() -> AirthingsDevice: """Get data from Airthings BLE.""" diff --git a/homeassistant/components/airthings_ble/manifest.json b/homeassistant/components/airthings_ble/manifest.json index 97e27793da2..3f7bd02a33e 100644 --- a/homeassistant/components/airthings_ble/manifest.json +++ b/homeassistant/components/airthings_ble/manifest.json @@ -24,5 +24,5 @@ "dependencies": ["bluetooth_adapters"], "documentation": "https://www.home-assistant.io/integrations/airthings_ble", "iot_class": "local_polling", - "requirements": ["airthings-ble==0.6.1"] + "requirements": ["airthings-ble==0.7.1"] } diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py index 39c55e0b465..2bc2d5e726a 100644 --- a/homeassistant/components/airthings_ble/sensor.py +++ b/homeassistant/components/airthings_ble/sensor.py @@ -16,7 +16,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_MILLION, - LIGHT_LUX, PERCENTAGE, EntityCategory, Platform, @@ -106,8 +105,7 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = { ), "illuminance": SensorEntityDescription( key="illuminance", - device_class=SensorDeviceClass.ILLUMINANCE, - native_unit_of_measurement=LIGHT_LUX, + native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), } @@ -222,7 +220,7 @@ class AirthingsSensor( manufacturer=airthings_device.manufacturer, hw_version=airthings_device.hw_version, sw_version=airthings_device.sw_version, - model=airthings_device.model, + model=airthings_device.model.name, ) @property diff --git a/requirements_all.txt b/requirements_all.txt index 4a3121d78cf..629b029cbbf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -419,7 +419,7 @@ aioymaps==1.2.2 airly==1.1.0 # homeassistant.components.airthings_ble -airthings-ble==0.6.1 +airthings-ble==0.7.1 # homeassistant.components.airthings airthings-cloud==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f2b42cd3a36..e9d63c8612e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -392,7 +392,7 @@ aioymaps==1.2.2 airly==1.1.0 # homeassistant.components.airthings_ble -airthings-ble==0.6.1 +airthings-ble==0.7.1 # homeassistant.components.airthings airthings-cloud==0.2.0 diff --git a/tests/components/airthings_ble/__init__.py b/tests/components/airthings_ble/__init__.py index 231ec12cb5f..50e3e0069bb 100644 --- a/tests/components/airthings_ble/__init__.py +++ b/tests/components/airthings_ble/__init__.py @@ -3,7 +3,11 @@ from __future__ import annotations from unittest.mock import patch -from airthings_ble import AirthingsBluetoothDeviceData, AirthingsDevice +from airthings_ble import ( + AirthingsBluetoothDeviceData, + AirthingsDevice, + AirthingsDeviceType, +) from homeassistant.components.airthings_ble.const import DOMAIN from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak @@ -161,8 +165,7 @@ WAVE_DEVICE_INFO = AirthingsDevice( manufacturer="Airthings AS", hw_version="REV A", sw_version="G-BLE-1.5.3-master+0", - model="Wave Plus", - model_raw="2930", + model=AirthingsDeviceType.WAVE_PLUS, name="Airthings Wave+", identifier="123456", sensors={ diff --git a/tests/components/airthings_ble/test_config_flow.py b/tests/components/airthings_ble/test_config_flow.py index 65ec91e69c2..2f20d889a85 100644 --- a/tests/components/airthings_ble/test_config_flow.py +++ b/tests/components/airthings_ble/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Airthings BLE config flow.""" from unittest.mock import patch -from airthings_ble import AirthingsDevice +from airthings_ble import AirthingsDevice, AirthingsDeviceType from bleak import BleakError from homeassistant.components.airthings_ble.const import DOMAIN @@ -28,8 +28,7 @@ async def test_bluetooth_discovery(hass: HomeAssistant) -> None: with patch_async_ble_device_from_address(WAVE_SERVICE_INFO), patch_airthings_ble( AirthingsDevice( manufacturer="Airthings AS", - model="Wave Plus", - model_raw="2930", + model=AirthingsDeviceType.WAVE_PLUS, name="Airthings Wave Plus", identifier="123456", ) @@ -111,8 +110,7 @@ async def test_user_setup(hass: HomeAssistant) -> None: ), patch_async_ble_device_from_address(WAVE_SERVICE_INFO), patch_airthings_ble( AirthingsDevice( manufacturer="Airthings AS", - model="Wave Plus", - model_raw="2930", + model=AirthingsDeviceType.WAVE_PLUS, name="Airthings Wave Plus", identifier="123456", ) From f7da6b5e816e17166b6c92e3151f089953a4e34d Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 13 Mar 2024 04:40:39 -0500 Subject: [PATCH 54/99] Bump rokuecp to 0.19.2 (#113198) --- homeassistant/components/roku/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 50a4b57fcdd..ce4513fb316 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -11,7 +11,7 @@ "iot_class": "local_polling", "loggers": ["rokuecp"], "quality_scale": "silver", - "requirements": ["rokuecp==0.19.1"], + "requirements": ["rokuecp==0.19.2"], "ssdp": [ { "st": "roku:ecp", diff --git a/requirements_all.txt b/requirements_all.txt index 629b029cbbf..60143e7d795 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2457,7 +2457,7 @@ rjpl==0.3.6 rocketchat-API==0.6.1 # homeassistant.components.roku -rokuecp==0.19.1 +rokuecp==0.19.2 # homeassistant.components.romy romy==0.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e9d63c8612e..3723bfa484c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1885,7 +1885,7 @@ rflink==0.0.66 ring-doorbell[listen]==0.8.7 # homeassistant.components.roku -rokuecp==0.19.1 +rokuecp==0.19.2 # homeassistant.components.romy romy==0.0.7 From 5769ba023cb04e92d990bec3a8573571e4dc177f Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 13 Mar 2024 17:50:29 +0100 Subject: [PATCH 55/99] Bump `brother` library to version `4.0.2` (#113235) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/brother/__init__.py | 2 +- homeassistant/components/brother/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index 32fee44de99..56d16ba7731 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -33,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: brother = await Brother.create( host, printer_type=printer_type, snmp_engine=snmp_engine ) - except (ConnectionError, SnmpError) as error: + except (ConnectionError, SnmpError, TimeoutError) as error: raise ConfigEntryNotReady from error coordinator = BrotherDataUpdateCoordinator(hass, brother) diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 26317b39ab5..9ca18a95a1e 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "loggers": ["brother", "pyasn1", "pysmi", "pysnmp"], "quality_scale": "platinum", - "requirements": ["brother==4.0.0"], + "requirements": ["brother==4.0.2"], "zeroconf": [ { "type": "_printer._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 60143e7d795..20e485a3781 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -609,7 +609,7 @@ bring-api==0.5.6 broadlink==0.18.3 # homeassistant.components.brother -brother==4.0.0 +brother==4.0.2 # homeassistant.components.brottsplatskartan brottsplatskartan==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3723bfa484c..24d019f51f6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -520,7 +520,7 @@ bring-api==0.5.6 broadlink==0.18.3 # homeassistant.components.brother -brother==4.0.0 +brother==4.0.2 # homeassistant.components.brottsplatskartan brottsplatskartan==1.0.5 From 525b20ca8e420d91070d3c654b30cfd354630b37 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 13 Mar 2024 19:15:13 +0100 Subject: [PATCH 56/99] Bump version to 2024.3.1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 78085695b0e..847387e76ae 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 3 -PATCH_VERSION: Final = "0" +PATCH_VERSION: Final = "1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 2764c1a3e08..d8a1545fbb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.3.0" +version = "2024.3.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From b88cdd78bce701bc38ffcd099f1ac37e7d3b2085 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 13 Mar 2024 19:35:43 +0100 Subject: [PATCH 57/99] Hotfix import error in ZHA for 2024.3.1 patch release (#113250) --- homeassistant/components/zha/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index e544b2a415a..3e6dd05a8b5 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -500,7 +500,7 @@ class ZhaConfigFlowHandler(BaseZhaFlow, config_entries.ConfigFlow, domain=DOMAIN if not current_entry: return - if current_entry.source != SOURCE_IGNORE: + if current_entry.source != config_entries.SOURCE_IGNORE: self._abort_if_unique_id_configured() else: # Only update the current entry if it is an ignored discovery From eb04365590dc063a27b8136155a244ee6fdd4610 Mon Sep 17 00:00:00 2001 From: Jonny Bergdahl <128166901+jonnybergdahl@users.noreply.github.com> Date: Wed, 13 Mar 2024 20:16:00 +0100 Subject: [PATCH 58/99] Fix Twitch auth token refresh (#112833) * Fix for expired token * Add auth token refresh. * Eliminate extra auth call * Fixed mock client --------- Co-authored-by: Jonny Bergdahl --- homeassistant/components/twitch/__init__.py | 7 +++-- homeassistant/components/twitch/const.py | 2 ++ homeassistant/components/twitch/sensor.py | 29 ++++++++++++++++----- tests/components/twitch/__init__.py | 1 + 4 files changed, 30 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/twitch/__init__.py b/homeassistant/components/twitch/__init__.py index a26b7e94035..fdc1a74d2d2 100644 --- a/homeassistant/components/twitch/__init__.py +++ b/homeassistant/components/twitch/__init__.py @@ -16,7 +16,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( async_get_config_entry_implementation, ) -from .const import DOMAIN, OAUTH_SCOPES, PLATFORMS +from .const import CLIENT, DOMAIN, OAUTH_SCOPES, PLATFORMS, SESSION async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -45,7 +45,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client.auto_refresh_auth = False await client.set_user_authentication(access_token, scope=OAUTH_SCOPES) - hass.data.setdefault(DOMAIN, {})[entry.entry_id] = client + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + CLIENT: client, + SESSION: session, + } await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/twitch/const.py b/homeassistant/components/twitch/const.py index 22286437eab..e99e76a94e9 100644 --- a/homeassistant/components/twitch/const.py +++ b/homeassistant/components/twitch/const.py @@ -16,5 +16,7 @@ CONF_REFRESH_TOKEN = "refresh_token" DOMAIN = "twitch" CONF_CHANNELS = "channels" +CLIENT = "client" +SESSION = "session" OAUTH_SCOPES = [AuthScope.USER_READ_SUBSCRIPTIONS, AuthScope.USER_READ_FOLLOWS] diff --git a/homeassistant/components/twitch/sensor.py b/homeassistant/components/twitch/sensor.py index 05fd3fa3e71..2d2a79a6244 100644 --- a/homeassistant/components/twitch/sensor.py +++ b/homeassistant/components/twitch/sensor.py @@ -19,12 +19,13 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_TOKEN from homeassistant.core import HomeAssistant +from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from .const import CONF_CHANNELS, DOMAIN, LOGGER, OAUTH_SCOPES +from .const import CLIENT, CONF_CHANNELS, DOMAIN, LOGGER, OAUTH_SCOPES, SESSION PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -51,6 +52,8 @@ ICON = "mdi:twitch" STATE_OFFLINE = "offline" STATE_STREAMING = "streaming" +PARALLEL_UPDATES = 1 + def chunk_list(lst: list, chunk_size: int) -> list[list]: """Split a list into chunks of chunk_size.""" @@ -97,7 +100,8 @@ async def async_setup_entry( async_add_entities: AddEntitiesCallback, ) -> None: """Initialize entries.""" - client = hass.data[DOMAIN][entry.entry_id] + client = hass.data[DOMAIN][entry.entry_id][CLIENT] + session = hass.data[DOMAIN][entry.entry_id][SESSION] channels = entry.options[CONF_CHANNELS] @@ -107,7 +111,7 @@ async def async_setup_entry( for chunk in chunk_list(channels, 100): entities.extend( [ - TwitchSensor(channel, client) + TwitchSensor(channel, session, client) async for channel in client.get_users(logins=chunk) ] ) @@ -120,8 +124,11 @@ class TwitchSensor(SensorEntity): _attr_icon = ICON - def __init__(self, channel: TwitchUser, client: Twitch) -> None: + def __init__( + self, channel: TwitchUser, session: OAuth2Session, client: Twitch + ) -> None: """Initialize the sensor.""" + self._session = session self._client = client self._channel = channel self._enable_user_auth = client.has_required_auth(AuthType.USER, OAUTH_SCOPES) @@ -130,9 +137,17 @@ class TwitchSensor(SensorEntity): async def async_update(self) -> None: """Update device state.""" - followers = (await self._client.get_channel_followers(self._channel.id)).total + await self._session.async_ensure_token_valid() + await self._client.set_user_authentication( + self._session.token["access_token"], + OAUTH_SCOPES, + self._session.token["refresh_token"], + False, + ) + followers = await self._client.get_channel_followers(self._channel.id) + self._attr_extra_state_attributes = { - ATTR_FOLLOWING: followers, + ATTR_FOLLOWING: followers.total, ATTR_VIEWS: self._channel.view_count, } if self._enable_user_auth: @@ -166,7 +181,7 @@ class TwitchSensor(SensorEntity): self._attr_extra_state_attributes[ATTR_SUBSCRIPTION] = True self._attr_extra_state_attributes[ATTR_SUBSCRIPTION_GIFTED] = sub.is_gift except TwitchResourceNotFound: - LOGGER.debug("User is not subscribed") + LOGGER.debug("User is not subscribed to %s", self._channel.display_name) except TwitchAPIException as exc: LOGGER.error("Error response on check_user_subscription: %s", exc) diff --git a/tests/components/twitch/__init__.py b/tests/components/twitch/__init__.py index 26746c7abb4..268f18d87c1 100644 --- a/tests/components/twitch/__init__.py +++ b/tests/components/twitch/__init__.py @@ -152,6 +152,7 @@ class TwitchMock: self, token: str, scope: list[AuthScope], + refresh_token: str | None = None, validate: bool = True, ) -> None: """Set user authentication.""" From cda9bf705166d0560bacc61a0ba9cdb63075b72b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 11 Mar 2024 11:05:49 -1000 Subject: [PATCH 59/99] Fix failing google diagnostics test (#113095) --- tests/components/google/test_diagnostics.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/components/google/test_diagnostics.py b/tests/components/google/test_diagnostics.py index 5ebc683485b..32c61d0f945 100644 --- a/tests/components/google/test_diagnostics.py +++ b/tests/components/google/test_diagnostics.py @@ -1,5 +1,6 @@ """Tests for diagnostics platform of google calendar.""" from collections.abc import Callable +import time from typing import Any from aiohttp.test_utils import TestClient @@ -15,6 +16,7 @@ from .conftest import TEST_EVENT, ComponentSetup from tests.common import CLIENT_ID, MockConfigEntry, MockUser from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.test_util.aiohttp import AiohttpClientMocker from tests.typing import ClientSessionGenerator @@ -69,8 +71,21 @@ async def test_diagnostics( aiohttp_client: ClientSessionGenerator, socket_enabled: None, snapshot: SnapshotAssertion, + aioclient_mock: AiohttpClientMocker, ) -> None: """Test diagnostics for the calendar.""" + + expires_in = 86400 + expires_at = time.time() + expires_in + aioclient_mock.post( + "https://oauth2.googleapis.com/token", + json={ + "refresh_token": "some-refresh-token", + "access_token": "some-updated-token", + "expires_at": expires_at, + "expires_in": expires_in, + }, + ) mock_events_list_items( [ { From 297c7c11fc1e61aff1b2f2104bd7a27c713ee1a2 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 14 Mar 2024 00:44:07 -0500 Subject: [PATCH 60/99] Add diagnostics for IPP (#113205) --- homeassistant/components/ipp/diagnostics.py | 28 +++++ .../ipp/snapshots/test_diagnostics.ambr | 100 ++++++++++++++++++ tests/components/ipp/test_diagnostics.py | 22 ++++ 3 files changed, 150 insertions(+) create mode 100644 homeassistant/components/ipp/diagnostics.py create mode 100644 tests/components/ipp/snapshots/test_diagnostics.ambr create mode 100644 tests/components/ipp/test_diagnostics.py diff --git a/homeassistant/components/ipp/diagnostics.py b/homeassistant/components/ipp/diagnostics.py new file mode 100644 index 00000000000..67b84183977 --- /dev/null +++ b/homeassistant/components/ipp/diagnostics.py @@ -0,0 +1,28 @@ +"""Diagnostics support for Internet Printing Protocol (IPP).""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN +from .coordinator import IPPDataUpdateCoordinator + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + coordinator: IPPDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id] + + return { + "entry": { + "data": { + **config_entry.data, + }, + "unique_id": config_entry.unique_id, + }, + "data": coordinator.data.as_dict(), + } diff --git a/tests/components/ipp/snapshots/test_diagnostics.ambr b/tests/components/ipp/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..98d0055c982 --- /dev/null +++ b/tests/components/ipp/snapshots/test_diagnostics.ambr @@ -0,0 +1,100 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'data': dict({ + 'info': dict({ + 'command_set': 'ESCPL2,BDC,D4,D4PX,ESCPR7,END4,GENEP,URF', + 'location': None, + 'manufacturer': 'TEST', + 'model': 'HA-1000 Series', + 'more_info': 'http://192.168.1.31:80/PRESENTATION/BONJOUR', + 'name': 'Test HA-1000 Series', + 'printer_info': 'Test HA-1000 Series', + 'printer_name': 'Test Printer', + 'printer_uri_supported': list([ + 'ipps://192.168.1.31:631/ipp/print', + 'ipp://192.168.1.31:631/ipp/print', + ]), + 'serial': '555534593035345555', + 'uptime': 30, + 'uuid': 'cfe92100-67c4-11d4-a45f-f8d027761251', + 'version': '20.23.06HA', + }), + 'markers': list([ + dict({ + 'color': '#000000', + 'high_level': 100, + 'level': 58, + 'low_level': 10, + 'marker_id': 0, + 'marker_type': 'ink-cartridge', + 'name': 'Black ink', + }), + dict({ + 'color': '#00FFFF', + 'high_level': 100, + 'level': 91, + 'low_level': 10, + 'marker_id': 2, + 'marker_type': 'ink-cartridge', + 'name': 'Cyan ink', + }), + dict({ + 'color': '#FF00FF', + 'high_level': 100, + 'level': 73, + 'low_level': 10, + 'marker_id': 4, + 'marker_type': 'ink-cartridge', + 'name': 'Magenta ink', + }), + dict({ + 'color': '#000000', + 'high_level': 100, + 'level': 98, + 'low_level': 10, + 'marker_id': 1, + 'marker_type': 'ink-cartridge', + 'name': 'Photo black ink', + }), + dict({ + 'color': '#FFFF00', + 'high_level': 100, + 'level': 95, + 'low_level': 10, + 'marker_id': 3, + 'marker_type': 'ink-cartridge', + 'name': 'Yellow ink', + }), + ]), + 'state': dict({ + 'message': None, + 'printer_state': 'idle', + 'reasons': None, + }), + 'uris': list([ + dict({ + 'authentication': None, + 'security': 'tls', + 'uri': 'ipps://192.168.1.31:631/ipp/print', + }), + dict({ + 'authentication': None, + 'security': None, + 'uri': 'ipp://192.168.1.31:631/ipp/print', + }), + ]), + }), + 'entry': dict({ + 'data': dict({ + 'base_path': '/ipp/print', + 'host': '192.168.1.31', + 'port': 631, + 'ssl': False, + 'uuid': 'cfe92100-67c4-11d4-a45f-f8d027761251', + 'verify_ssl': True, + }), + 'unique_id': 'cfe92100-67c4-11d4-a45f-f8d027761251', + }), + }) +# --- diff --git a/tests/components/ipp/test_diagnostics.py b/tests/components/ipp/test_diagnostics.py new file mode 100644 index 00000000000..08446601e69 --- /dev/null +++ b/tests/components/ipp/test_diagnostics.py @@ -0,0 +1,22 @@ +"""Tests for the diagnostics data provided by the Internet Printing Protocol (IPP) integration.""" + +from syrupy import SnapshotAssertion + +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +async def test_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + init_integration: MockConfigEntry, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics for config entry.""" + assert ( + await get_diagnostics_for_config_entry(hass, hass_client, init_integration) + == snapshot + ) From 45ef5a3edf2411b73a5ea5164cae1fc577999b2a Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Thu, 14 Mar 2024 05:53:55 -0400 Subject: [PATCH 61/99] Apply suggestion failures fail supervisor repair (#113372) --- homeassistant/components/hassio/handler.py | 5 +- homeassistant/components/hassio/repairs.py | 11 ++-- tests/components/hassio/test_issues.py | 3 +- tests/components/hassio/test_repairs.py | 72 ++++++++++++++++++++++ 4 files changed, 79 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index 65c0824dbd2..82a2db3c234 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -262,10 +262,7 @@ async def async_update_core( @bind_hass @_api_bool async def async_apply_suggestion(hass: HomeAssistant, suggestion_uuid: str) -> dict: - """Apply a suggestion from supervisor's resolution center. - - The caller of the function should handle HassioAPIError. - """ + """Apply a suggestion from supervisor's resolution center.""" hassio: HassIO = hass.data[DOMAIN] command = f"/resolution/suggestion/{suggestion_uuid}" return await hassio.send_command(command, timeout=None) diff --git a/homeassistant/components/hassio/repairs.py b/homeassistant/components/hassio/repairs.py index fcfe23dda6e..4538d9e1b33 100644 --- a/homeassistant/components/hassio/repairs.py +++ b/homeassistant/components/hassio/repairs.py @@ -18,7 +18,7 @@ from .const import ( PLACEHOLDER_KEY_REFERENCE, SupervisorIssueContext, ) -from .handler import HassioAPIError, async_apply_suggestion +from .handler import async_apply_suggestion from .issues import Issue, Suggestion SUGGESTION_CONFIRMATION_REQUIRED = {"system_execute_reboot"} @@ -109,12 +109,9 @@ class SupervisorIssueRepairFlow(RepairsFlow): if not confirmed and suggestion.key in SUGGESTION_CONFIRMATION_REQUIRED: return self._async_form_for_suggestion(suggestion) - try: - await async_apply_suggestion(self.hass, suggestion.uuid) - except HassioAPIError: - return self.async_abort(reason="apply_suggestion_fail") - - return self.async_create_entry(data={}) + if await async_apply_suggestion(self.hass, suggestion.uuid): + return self.async_create_entry(data={}) + return self.async_abort(reason="apply_suggestion_fail") @staticmethod def _async_step( diff --git a/tests/components/hassio/test_issues.py b/tests/components/hassio/test_issues.py index 21cd249bd53..83cb65b8cca 100644 --- a/tests/components/hassio/test_issues.py +++ b/tests/components/hassio/test_issues.py @@ -40,6 +40,7 @@ def mock_resolution_info( unsupported: list[str] | None = None, unhealthy: list[str] | None = None, issues: list[dict[str, str]] | None = None, + suggestion_result: str = "ok", ): """Mock resolution/info endpoint with unsupported/unhealthy reasons and/or issues.""" aioclient_mock.get( @@ -76,7 +77,7 @@ def mock_resolution_info( for suggestion in suggestions: aioclient_mock.post( f"http://127.0.0.1/resolution/suggestion/{suggestion['uuid']}", - json={"result": "ok"}, + json={"result": suggestion_result}, ) diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py index 5dd73a21615..97a13fe1e5d 100644 --- a/tests/components/hassio/test_repairs.py +++ b/tests/components/hassio/test_repairs.py @@ -404,6 +404,78 @@ async def test_supervisor_issue_repair_flow_skip_confirmation( ) +async def test_mount_failed_repair_flow_error( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_client: ClientSessionGenerator, + issue_registry: ir.IssueRegistry, + all_setup_requests, +) -> None: + """Test repair flow fails when repair fails to apply.""" + mock_resolution_info( + aioclient_mock, + issues=[ + { + "uuid": "1234", + "type": "mount_failed", + "context": "mount", + "reference": "backup_share", + "suggestions": [ + { + "uuid": "1235", + "type": "execute_reload", + "context": "mount", + "reference": "backup_share", + }, + { + "uuid": "1236", + "type": "execute_remove", + "context": "mount", + "reference": "backup_share", + }, + ], + }, + ], + suggestion_result=False, + ) + + assert await async_setup_component(hass, "hassio", {}) + + repair_issue = issue_registry.async_get_issue(domain="hassio", issue_id="1234") + assert repair_issue + + client = await hass_client() + + resp = await client.post( + "/api/repairs/issues/fix", + json={"handler": "hassio", "issue_id": repair_issue.issue_id}, + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + flow_id = data["flow_id"] + + resp = await client.post( + f"/api/repairs/issues/fix/{flow_id}", + json={"next_step_id": "mount_execute_reload"}, + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "type": "abort", + "flow_id": flow_id, + "handler": "hassio", + "reason": "apply_suggestion_fail", + "result": None, + "description_placeholders": None, + } + + assert issue_registry.async_get_issue(domain="hassio", issue_id="1234") + + async def test_mount_failed_repair_flow( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, From 6ca837b4e1346a6c49971179a1fb1f1654c5e971 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Thu, 14 Mar 2024 05:55:04 -0400 Subject: [PATCH 62/99] Supervisor issues update retries on failure (#113373) --- homeassistant/components/hassio/issues.py | 12 +++- tests/components/hassio/test_issues.py | 78 ++++++++++++++++++++++- 2 files changed, 87 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hassio/issues.py b/homeassistant/components/hassio/issues.py index 8bd47faef08..925c2d70afb 100644 --- a/homeassistant/components/hassio/issues.py +++ b/homeassistant/components/hassio/issues.py @@ -3,11 +3,13 @@ from __future__ import annotations import asyncio from dataclasses import dataclass, field +from datetime import datetime import logging from typing import Any, NotRequired, TypedDict -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HassJob, HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.event import async_call_later from homeassistant.helpers.issue_registry import ( IssueSeverity, async_create_issue, @@ -35,6 +37,7 @@ from .const import ( EVENT_SUPPORTED_CHANGED, ISSUE_KEY_SYSTEM_DOCKER_CONFIG, PLACEHOLDER_KEY_REFERENCE, + REQUEST_REFRESH_DELAY, UPDATE_KEY_SUPERVISOR, SupervisorIssueContext, ) @@ -302,12 +305,17 @@ class SupervisorIssues: self._hass, EVENT_SUPERVISOR_EVENT, self._supervisor_events_to_issues ) - async def update(self) -> None: + async def update(self, _: datetime | None = None) -> None: """Update issues from Supervisor resolution center.""" try: data = await self._client.get_resolution_info() except HassioAPIError as err: _LOGGER.error("Failed to update supervisor issues: %r", err) + async_call_later( + self._hass, + REQUEST_REFRESH_DELAY, + HassJob(self.update, cancel_on_shutdown=True), + ) return self.unhealthy_reasons = set(data[ATTR_UNHEALTHY]) self.unsupported_reasons = set(data[ATTR_UNSUPPORTED]) diff --git a/tests/components/hassio/test_issues.py b/tests/components/hassio/test_issues.py index 83cb65b8cca..b5a852710fe 100644 --- a/tests/components/hassio/test_issues.py +++ b/tests/components/hassio/test_issues.py @@ -1,6 +1,8 @@ """Test issues from supervisor issues.""" from __future__ import annotations +import asyncio +from http import HTTPStatus import os from typing import Any from unittest.mock import ANY, patch @@ -13,7 +15,7 @@ from homeassistant.setup import async_setup_component from .test_init import MOCK_ENVIRON -from tests.test_util.aiohttp import AiohttpClientMocker +from tests.test_util.aiohttp import AiohttpClientMocker, AiohttpClientMockResponse from tests.typing import WebSocketGenerator @@ -529,6 +531,80 @@ async def test_supervisor_issues( ) +async def test_supervisor_issues_initial_failure( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test issues manager retries after initial update failure.""" + responses = [ + AiohttpClientMockResponse( + method="get", + url="http://127.0.0.1/resolution/info", + status=HTTPStatus.BAD_REQUEST, + json={ + "result": "error", + "message": "System is not ready with state: setup", + }, + ), + AiohttpClientMockResponse( + method="get", + url="http://127.0.0.1/resolution/info", + status=HTTPStatus.OK, + json={ + "result": "ok", + "data": { + "unsupported": [], + "unhealthy": [], + "suggestions": [], + "issues": [ + { + "uuid": "1234", + "type": "reboot_required", + "context": "system", + "reference": None, + }, + ], + "checks": [ + {"enabled": True, "slug": "supervisor_trust"}, + {"enabled": True, "slug": "free_space"}, + ], + }, + }, + ), + ] + + async def mock_responses(*args): + nonlocal responses + return responses.pop(0) + + aioclient_mock.get( + "http://127.0.0.1/resolution/info", + side_effect=mock_responses, + ) + aioclient_mock.get( + "http://127.0.0.1/resolution/issue/1234/suggestions", + json={"result": "ok", "data": {"suggestions": []}}, + ) + + with patch("homeassistant.components.hassio.issues.REQUEST_REFRESH_DELAY", new=0.1): + result = await async_setup_component(hass, "hassio", {}) + assert result + + client = await hass_ws_client(hass) + + await client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 0 + + await asyncio.sleep(0.1) + await client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["issues"]) == 1 + + async def test_supervisor_issues_add_remove( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, From 63e3da1aca8ead23fa6c20e57cb724abbe5b4eef Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Thu, 14 Mar 2024 09:29:11 +0100 Subject: [PATCH 63/99] Add loggers to Husqvarna Automower (#113381) --- homeassistant/components/husqvarna_automower/manifest.json | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index 525f057c1ff..f00baad661d 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -6,5 +6,6 @@ "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", "iot_class": "cloud_push", + "loggers": ["aioautomower"], "requirements": ["aioautomower==2024.3.0"] } From 05c04166446d94dff37b6712611e59bcafd41588 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Wed, 13 Mar 2024 10:22:07 -0500 Subject: [PATCH 64/99] Bump pyipp to 0.15.0 (#113204) update pyipp to 0.15.0 --- homeassistant/components/ipp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index 3625a2d867e..5168c5de1fa 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -8,6 +8,6 @@ "iot_class": "local_polling", "loggers": ["deepmerge", "pyipp"], "quality_scale": "platinum", - "requirements": ["pyipp==0.14.5"], + "requirements": ["pyipp==0.15.0"], "zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 20e485a3781..add077d5b82 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1878,7 +1878,7 @@ pyintesishome==1.8.0 pyipma==3.0.7 # homeassistant.components.ipp -pyipp==0.14.5 +pyipp==0.15.0 # homeassistant.components.iqvia pyiqvia==2022.04.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 24d019f51f6..a50fefa2290 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1453,7 +1453,7 @@ pyinsteon==1.5.3 pyipma==3.0.7 # homeassistant.components.ipp -pyipp==0.14.5 +pyipp==0.15.0 # homeassistant.components.iqvia pyiqvia==2022.04.0 From 10fc40e415466967014a1e0b5c7d8200a512ae5a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 10 Mar 2024 11:04:17 -0600 Subject: [PATCH 65/99] Streamline Notion config entry updates (refresh token and user ID) (#112832) --- homeassistant/components/notion/__init__.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 33a74990928..6d2b6c48e1b 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -165,9 +165,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except NotionError as err: raise ConfigEntryNotReady("Config entry failed to load") from err - # Always update the config entry with the latest refresh token and user UUID: - entry_updates["data"][CONF_REFRESH_TOKEN] = client.refresh_token - entry_updates["data"][CONF_USER_UUID] = client.user_uuid + # Update the Notion user UUID and refresh token if they've changed: + for key, value in ( + (CONF_REFRESH_TOKEN, client.refresh_token), + (CONF_USER_UUID, client.user_uuid), + ): + if entry.data[key] == value: + continue + entry_updates["data"][key] = value + + hass.config_entries.async_update_entry(entry, **entry_updates) @callback def async_save_refresh_token(refresh_token: str) -> None: @@ -180,12 +187,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Create a callback to save the refresh token when it changes: entry.async_on_unload(client.add_refresh_token_callback(async_save_refresh_token)) - # Save the client's refresh token if it's different than what we already have: - if (token := client.refresh_token) and token != entry.data[CONF_REFRESH_TOKEN]: - async_save_refresh_token(token) - - hass.config_entries.async_update_entry(entry, **entry_updates) - async def async_update() -> NotionData: """Get the latest data from the Notion API.""" data = NotionData(hass=hass, entry=entry) From 8b00229868fa4e67e0ea59ed63fb7f84b89b84ac Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Tue, 12 Mar 2024 18:50:57 +0100 Subject: [PATCH 66/99] Bump aioautomower to 2024.3.2 (#113162) --- homeassistant/components/husqvarna_automower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index f00baad661d..49a554f2e0a 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", "iot_class": "cloud_push", "loggers": ["aioautomower"], - "requirements": ["aioautomower==2024.3.0"] + "requirements": ["aioautomower==2024.3.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index add077d5b82..b0ea4f3e246 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -206,7 +206,7 @@ aioaseko==0.0.2 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.3.0 +aioautomower==2024.3.2 # homeassistant.components.azure_devops aioazuredevops==1.3.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a50fefa2290..c81a8fbb2dc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -185,7 +185,7 @@ aioaseko==0.0.2 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.3.0 +aioautomower==2024.3.2 # homeassistant.components.azure_devops aioazuredevops==1.3.5 From a167b0acaf691b8411e2fff4e53b888186bf3aac Mon Sep 17 00:00:00 2001 From: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Date: Thu, 14 Mar 2024 17:51:24 +0100 Subject: [PATCH 67/99] Bump aioautomower to 2024.3.3 (#113430) --- .../husqvarna_automower/manifest.json | 2 +- .../components/husqvarna_automower/sensor.py | 7 +++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../husqvarna_automower/test_sensor.py | 30 +++++++++++++++---- 5 files changed, 35 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index 49a554f2e0a..ed013f2e0c2 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", "iot_class": "cloud_push", "loggers": ["aioautomower"], - "requirements": ["aioautomower==2024.3.2"] + "requirements": ["aioautomower==2024.3.3"] } diff --git a/homeassistant/components/husqvarna_automower/sensor.py b/homeassistant/components/husqvarna_automower/sensor.py index 970c444737c..31eebde9c81 100644 --- a/homeassistant/components/husqvarna_automower/sensor.py +++ b/homeassistant/components/husqvarna_automower/sensor.py @@ -69,6 +69,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.SECONDS, suggested_unit_of_measurement=UnitOfTime.HOURS, + exists_fn=lambda data: data.statistics.total_charging_time is not None, value_fn=lambda data: data.statistics.total_charging_time, ), AutomowerSensorEntityDescription( @@ -79,6 +80,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.SECONDS, suggested_unit_of_measurement=UnitOfTime.HOURS, + exists_fn=lambda data: data.statistics.total_cutting_time is not None, value_fn=lambda data: data.statistics.total_cutting_time, ), AutomowerSensorEntityDescription( @@ -89,6 +91,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.SECONDS, suggested_unit_of_measurement=UnitOfTime.HOURS, + exists_fn=lambda data: data.statistics.total_running_time is not None, value_fn=lambda data: data.statistics.total_running_time, ), AutomowerSensorEntityDescription( @@ -99,6 +102,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.SECONDS, suggested_unit_of_measurement=UnitOfTime.HOURS, + exists_fn=lambda data: data.statistics.total_searching_time is not None, value_fn=lambda data: data.statistics.total_searching_time, ), AutomowerSensorEntityDescription( @@ -107,6 +111,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( icon="mdi:battery-sync-outline", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL, + exists_fn=lambda data: data.statistics.number_of_charging_cycles is not None, value_fn=lambda data: data.statistics.number_of_charging_cycles, ), AutomowerSensorEntityDescription( @@ -115,6 +120,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( icon="mdi:counter", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL, + exists_fn=lambda data: data.statistics.number_of_collisions is not None, value_fn=lambda data: data.statistics.number_of_collisions, ), AutomowerSensorEntityDescription( @@ -125,6 +131,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.DISTANCE, native_unit_of_measurement=UnitOfLength.METERS, suggested_unit_of_measurement=UnitOfLength.KILOMETERS, + exists_fn=lambda data: data.statistics.total_drive_distance is not None, value_fn=lambda data: data.statistics.total_drive_distance, ), AutomowerSensorEntityDescription( diff --git a/requirements_all.txt b/requirements_all.txt index b0ea4f3e246..fb1f88b2975 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -206,7 +206,7 @@ aioaseko==0.0.2 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.3.2 +aioautomower==2024.3.3 # homeassistant.components.azure_devops aioazuredevops==1.3.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c81a8fbb2dc..b8f639afa1a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -185,7 +185,7 @@ aioaseko==0.0.2 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.3.2 +aioautomower==2024.3.3 # homeassistant.components.azure_devops aioazuredevops==1.3.5 diff --git a/tests/components/husqvarna_automower/test_sensor.py b/tests/components/husqvarna_automower/test_sensor.py index 1775caac7f8..feae870478e 100644 --- a/tests/components/husqvarna_automower/test_sensor.py +++ b/tests/components/husqvarna_automower/test_sensor.py @@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, patch from aioautomower.model import MowerModes from aioautomower.utils import mower_list_to_dictionary_dataclass from freezegun.api import FrozenDateTimeFactory +import pytest from syrupy import SnapshotAssertion from homeassistant.components.husqvarna_automower.const import DOMAIN @@ -59,17 +60,36 @@ async def test_cutting_blade_usage_time_sensor( assert state is not None assert state.state == "0.034" - entry = hass.config_entries.async_entries(DOMAIN)[0] - await hass.config_entries.async_remove(entry.entry_id) - await hass.async_block_till_done() + +@pytest.mark.parametrize( + ("sensor_to_test"), + [ + ("cutting_blade_usage_time"), + ("number_of_charging_cycles"), + ("number_of_collisions"), + ("total_charging_time"), + ("total_cutting_time"), + ("total_running_time"), + ("total_searching_time"), + ("total_drive_distance"), + ], +) +async def test_statistics_not_available( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + sensor_to_test: str, +) -> None: + """Test if this sensor is only added, if data is available.""" + values = mower_list_to_dictionary_dataclass( load_json_value_fixture("mower.json", DOMAIN) ) - delattr(values[TEST_MOWER_ID].statistics, "cutting_blade_usage_time") + delattr(values[TEST_MOWER_ID].statistics, sensor_to_test) mock_automower_client.get_status.return_value = values await setup_integration(hass, mock_config_entry) - state = hass.states.get("sensor.test_mower_1_cutting_blade_usage_time") + state = hass.states.get(f"sensor.test_mower_1_{sensor_to_test}") assert state is None From 273d01c0f40746d1e10ecc476095abb0ec76c87f Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 14 Mar 2024 15:07:54 -0400 Subject: [PATCH 68/99] Check for EA release channel for UniFi Protect (#113432) Co-authored-by: J. Nick Koston --- homeassistant/components/unifiprotect/__init__.py | 13 +++++++------ homeassistant/components/unifiprotect/const.py | 2 +- homeassistant/components/unifiprotect/repairs.py | 5 +++-- .../components/unifiprotect/strings.json | 12 ++++++------ tests/components/unifiprotect/test_config_flow.py | 2 +- tests/components/unifiprotect/test_repairs.py | 15 ++++++++++----- 6 files changed, 28 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index c4a6bc88068..076095a16b3 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -6,6 +6,7 @@ import logging from aiohttp.client_exceptions import ServerDisconnectedError from pyunifiprotect.data import Bootstrap +from pyunifiprotect.data.types import FirmwareReleaseChannel from pyunifiprotect.exceptions import ClientError, NotAuthorized # Import the test_util.anonymize module from the pyunifiprotect package @@ -111,19 +112,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, data_service.async_stop) ) - if ( - not entry.options.get(CONF_ALLOW_EA, False) - and await nvr_info.get_is_prerelease() + if not entry.options.get(CONF_ALLOW_EA, False) and ( + await nvr_info.get_is_prerelease() + or nvr_info.release_channel != FirmwareReleaseChannel.RELEASE ): ir.async_create_issue( hass, DOMAIN, - "ea_warning", + "ea_channel_warning", is_fixable=True, is_persistent=True, learn_more_url="https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access", severity=IssueSeverity.WARNING, - translation_key="ea_warning", + translation_key="ea_channel_warning", translation_placeholders={"version": str(nvr_info.version)}, data={"entry_id": entry.entry_id}, ) @@ -149,7 +150,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "version": str(nvr_info.version), }, ) - ir.async_delete_issue(hass, DOMAIN, "ea_warning") + ir.async_delete_issue(hass, DOMAIN, "ea_channel_warning") _LOGGER.exception("Error setting up UniFi Protect integration: %s", err) raise diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 2982ca29c4a..39be5f0e7cb 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -24,7 +24,7 @@ CONF_DISABLE_RTSP = "disable_rtsp" CONF_ALL_UPDATES = "all_updates" CONF_OVERRIDE_CHOST = "override_connection_host" CONF_MAX_MEDIA = "max_media" -CONF_ALLOW_EA = "allow_ea" +CONF_ALLOW_EA = "allow_ea_channel" CONFIG_OPTIONS = [ CONF_ALL_UPDATES, diff --git a/homeassistant/components/unifiprotect/repairs.py b/homeassistant/components/unifiprotect/repairs.py index ddc0a257c14..254984da515 100644 --- a/homeassistant/components/unifiprotect/repairs.py +++ b/homeassistant/components/unifiprotect/repairs.py @@ -6,6 +6,7 @@ import logging from typing import cast from pyunifiprotect import ProtectApiClient +from pyunifiprotect.data.types import FirmwareReleaseChannel import voluptuous as vol from homeassistant import data_entry_flow @@ -68,7 +69,7 @@ class EAConfirm(ProtectRepair): ) nvr = await self._api.get_nvr() - if await nvr.get_is_prerelease(): + if nvr.release_channel != FirmwareReleaseChannel.RELEASE: return await self.async_step_confirm() await self.hass.config_entries.async_reload(self._entry.entry_id) return self.async_create_entry(data={}) @@ -124,7 +125,7 @@ async def async_create_fix_flow( data: dict[str, str | int | float | None] | None, ) -> RepairsFlow: """Create flow.""" - if data is not None and issue_id == "ea_warning": + if data is not None and issue_id == "ea_channel_warning": entry_id = cast(str, data["entry_id"]) if (entry := hass.config_entries.async_get_entry(entry_id)) is not None: api = async_create_api_client(hass, entry) diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index eccf5829332..bdc46217ab5 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -61,16 +61,16 @@ } }, "issues": { - "ea_warning": { - "title": "UniFi Protect v{version} is an Early Access version", + "ea_channel_warning": { + "title": "UniFi Protect Early Access enabled", "fix_flow": { "step": { "start": { - "title": "v{version} is an Early Access version", - "description": "You are using v{version} of UniFi Protect which is an Early Access version. [Early Access versions are not supported by Home Assistant](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) and it is recommended to go back to a stable release as soon as possible.\n\nBy submitting this form you have either [downgraded UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) or you agree to run an unsupported version of UniFi Protect." + "title": "UniFi Protect Early Access enabled", + "description": "You are either running an Early Access version of UniFi Protect (v{version}) or opt-ed into a release channel that is not the Official Release Channel. [Home Assistant does not support Early Access versions](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access), so you should immediately switch to the Official Release Channel. Accidentally upgrading to an Early Access version can break your UniFi Protect integration.\n\nBy submitting this form, you have switched back to the Official Release Channel or agree to run an unsupported version of UniFi Protect, which may break your Home Assistant integration at any time." }, "confirm": { - "title": "[%key:component::unifiprotect::issues::ea_warning::fix_flow::step::start::title%]", + "title": "[%key:component::unifiprotect::issues::ea_channel_warning::fix_flow::step::start::title%]", "description": "Are you sure you want to run unsupported versions of UniFi Protect? This may cause your Home Assistant integration to break." } } @@ -78,7 +78,7 @@ }, "ea_setup_failed": { "title": "Setup error using Early Access version", - "description": "You are using v{version} of UniFi Protect which is an Early Access version. An unrecoverable error occurred while trying to load the integration. Please [downgrade to a stable version](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) of UniFi Protect to continue using the integration.\n\nError: {error}" + "description": "You are using v{version} of UniFi Protect which is an Early Access version. An unrecoverable error occurred while trying to load the integration. Please restore a backup of a stable release of UniFi Protect to continue using the integration.\n\nError: {error}" }, "cloud_user": { "title": "Ubiquiti Cloud Users are not Supported", diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index a9ff98fc681..04eee1b8319 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -318,7 +318,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - "disable_rtsp": True, "override_connection_host": True, "max_media": 1000, - "allow_ea": False, + "allow_ea_channel": False, } await hass.config_entries.async_unload(mock_config.entry_id) diff --git a/tests/components/unifiprotect/test_repairs.py b/tests/components/unifiprotect/test_repairs.py index 12701604306..0c939a9791d 100644 --- a/tests/components/unifiprotect/test_repairs.py +++ b/tests/components/unifiprotect/test_repairs.py @@ -45,12 +45,14 @@ async def test_ea_warning_ignore( assert len(msg["result"]["issues"]) > 0 issue = None for i in msg["result"]["issues"]: - if i["issue_id"] == "ea_warning": + if i["issue_id"] == "ea_channel_warning": issue = i assert issue is not None url = RepairsFlowIndexView.url - resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "ea_warning"}) + resp = await client.post( + url, json={"handler": DOMAIN, "issue_id": "ea_channel_warning"} + ) assert resp.status == HTTPStatus.OK data = await resp.json() @@ -103,12 +105,14 @@ async def test_ea_warning_fix( assert len(msg["result"]["issues"]) > 0 issue = None for i in msg["result"]["issues"]: - if i["issue_id"] == "ea_warning": + if i["issue_id"] == "ea_channel_warning": issue = i assert issue is not None url = RepairsFlowIndexView.url - resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "ea_warning"}) + resp = await client.post( + url, json={"handler": DOMAIN, "issue_id": "ea_channel_warning"} + ) assert resp.status == HTTPStatus.OK data = await resp.json() @@ -121,8 +125,9 @@ async def test_ea_warning_fix( new_nvr = copy(ufp.api.bootstrap.nvr) new_nvr.version = Version("2.2.6") + new_nvr.release_channel = "release" mock_msg = Mock() - mock_msg.changed_data = {"version": "2.2.6"} + mock_msg.changed_data = {"version": "2.2.6", "releaseChannel": "release"} mock_msg.new_obj = new_nvr ufp.api.bootstrap.nvr = new_nvr From 26b26a3b1f8897941e92132fc0dc2cbcbd56eb6e Mon Sep 17 00:00:00 2001 From: Lex Li <425130+lextm@users.noreply.github.com> Date: Thu, 14 Mar 2024 22:19:18 -0400 Subject: [PATCH 69/99] Bump `pysnmp-lextudio` to version `6.0.11` (#113463) --- homeassistant/components/snmp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index cd6c1dd9152..c4aa82f2a74 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/snmp", "iot_class": "local_polling", "loggers": ["pyasn1", "pysmi", "pysnmp"], - "requirements": ["pysnmp-lextudio==6.0.9"] + "requirements": ["pysnmp-lextudio==6.0.11"] } diff --git a/requirements_all.txt b/requirements_all.txt index fb1f88b2975..b31beea3226 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2155,7 +2155,7 @@ pysmartthings==0.7.8 pysml==0.0.12 # homeassistant.components.snmp -pysnmp-lextudio==6.0.9 +pysnmp-lextudio==6.0.11 # homeassistant.components.snooz pysnooz==0.8.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b8f639afa1a..128db237cb8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1673,7 +1673,7 @@ pysmartthings==0.7.8 pysml==0.0.12 # homeassistant.components.snmp -pysnmp-lextudio==6.0.9 +pysnmp-lextudio==6.0.11 # homeassistant.components.snooz pysnooz==0.8.6 From a5994d1d5fff0fe2c2d06a473ebdb890a9aeafef Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Wed, 20 Mar 2024 11:29:15 +0100 Subject: [PATCH 70/99] Tado fix water heater (#113464) Co-authored-by: Joostlek --- homeassistant/components/tado/water_heater.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index cdbc041f535..d3ec29ae356 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -2,7 +2,6 @@ import logging from typing import Any -import PyTado import voluptuous as vol from homeassistant.components.water_heater import ( @@ -29,8 +28,6 @@ from .const import ( DATA, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED, - TADO_DEFAULT_MAX_TEMP, - TADO_DEFAULT_MIN_TEMP, TYPE_HOT_WATER, ) from .entity import TadoZoneEntity @@ -133,8 +130,8 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): zone_name: str, zone_id: int, supports_temperature_control: bool, - min_temp: float | None = None, - max_temp: float | None = None, + min_temp, + max_temp, ) -> None: """Initialize of Tado water heater entity.""" self._tado = tado @@ -146,8 +143,8 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): self._device_is_active = False self._supports_temperature_control = supports_temperature_control - self._min_temperature = min_temp or TADO_DEFAULT_MIN_TEMP - self._max_temperature = max_temp or TADO_DEFAULT_MAX_TEMP + self._min_temperature = min_temp + self._max_temperature = max_temp self._target_temp: float | None = None @@ -157,7 +154,7 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): self._current_tado_hvac_mode = CONST_MODE_SMART_SCHEDULE self._overlay_mode = CONST_MODE_SMART_SCHEDULE - self._tado_zone_data: PyTado.TadoZone = {} + self._tado_zone_data: Any = None async def async_added_to_hass(self) -> None: """Register for sensor updates.""" From 099c22816986cc941ee6f24435f9a37afd6071c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 14 Mar 2024 13:09:58 -1000 Subject: [PATCH 71/99] Bump aiodhcpwatcher to 0.8.2 (#113466) --- homeassistant/components/dhcp/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 95c925a8d33..673a80b8d9f 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -15,7 +15,7 @@ ], "quality_scale": "internal", "requirements": [ - "aiodhcpwatcher==0.8.1", + "aiodhcpwatcher==0.8.2", "aiodiscover==1.6.1", "cached_ipaddress==0.3.0" ] diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1b850316b91..c92dad2ae35 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ # Automatically generated by gen_requirements_all.py, do not edit -aiodhcpwatcher==0.8.1 +aiodhcpwatcher==0.8.2 aiodiscover==1.6.1 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.3.1 diff --git a/requirements_all.txt b/requirements_all.txt index b31beea3226..0b2240ff0b1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -221,7 +221,7 @@ aiobotocore==2.9.1 aiocomelit==0.9.0 # homeassistant.components.dhcp -aiodhcpwatcher==0.8.1 +aiodhcpwatcher==0.8.2 # homeassistant.components.dhcp aiodiscover==1.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 128db237cb8..1a52448e86a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -200,7 +200,7 @@ aiobotocore==2.9.1 aiocomelit==0.9.0 # homeassistant.components.dhcp -aiodhcpwatcher==0.8.1 +aiodhcpwatcher==0.8.2 # homeassistant.components.dhcp aiodiscover==1.6.1 From de966b0eb141d7f169f61316fca9aa3a9b9d0c0e Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 15 Mar 2024 09:38:47 +0100 Subject: [PATCH 72/99] Bump axis to v55 (#113479) --- homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index f56df16918e..c1471d370a5 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -26,7 +26,7 @@ "iot_class": "local_push", "loggers": ["axis"], "quality_scale": "platinum", - "requirements": ["axis==54"], + "requirements": ["axis==55"], "ssdp": [ { "manufacturer": "AXIS" diff --git a/requirements_all.txt b/requirements_all.txt index 0b2240ff0b1..f9f61f9525e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==54 +axis==55 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1a52448e86a..9ddb3e2fc06 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -454,7 +454,7 @@ auroranoaa==0.0.3 aurorapy==0.2.7 # homeassistant.components.axis -axis==54 +axis==55 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 From 8bae8fdd75ea6ee54d2a9aad3ccfdbf6a26a242c Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Fri, 15 Mar 2024 13:00:08 +0200 Subject: [PATCH 73/99] Bump croniter to 2.0.2 (#113494) --- homeassistant/components/utility_meter/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/utility_meter/manifest.json b/homeassistant/components/utility_meter/manifest.json index 11aaf5307c8..25e803e6a2d 100644 --- a/homeassistant/components/utility_meter/manifest.json +++ b/homeassistant/components/utility_meter/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["croniter"], "quality_scale": "internal", - "requirements": ["croniter==1.0.6"] + "requirements": ["croniter==2.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index f9f61f9525e..dc6b3111fb9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -669,7 +669,7 @@ connect-box==0.2.8 construct==2.10.68 # homeassistant.components.utility_meter -croniter==1.0.6 +croniter==2.0.2 # homeassistant.components.crownstone crownstone-cloud==1.4.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9ddb3e2fc06..985fbdb7320 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -553,7 +553,7 @@ colorthief==0.2.1 construct==2.10.68 # homeassistant.components.utility_meter -croniter==1.0.6 +croniter==2.0.2 # homeassistant.components.crownstone crownstone-cloud==1.4.9 From 5163b5f888aeb5449ee954efe26e22e3e5870c75 Mon Sep 17 00:00:00 2001 From: On Freund Date: Fri, 15 Mar 2024 12:42:53 +0200 Subject: [PATCH 74/99] Revert setting communication delay in Risco init (#113497) --- homeassistant/components/risco/__init__.py | 34 +++++----------------- tests/components/risco/conftest.py | 10 ------- tests/components/risco/test_init.py | 21 ------------- 3 files changed, 8 insertions(+), 57 deletions(-) delete mode 100644 tests/components/risco/test_init.py diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index d1e1c4f430c..efc17c48a06 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -37,12 +37,10 @@ from homeassistant.helpers.storage import Store from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( - CONF_COMMUNICATION_DELAY, DATA_COORDINATOR, DEFAULT_SCAN_INTERVAL, DOMAIN, EVENTS_COORDINATOR, - MAX_COMMUNICATION_DELAY, TYPE_LOCAL, ) @@ -85,31 +83,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_setup_local_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data = entry.data - comm_delay = initial_delay = data.get(CONF_COMMUNICATION_DELAY, 0) + risco = RiscoLocal(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN]) - while True: - risco = RiscoLocal( - data[CONF_HOST], - data[CONF_PORT], - data[CONF_PIN], - communication_delay=comm_delay, - ) - try: - await risco.connect() - except CannotConnectError as error: - if comm_delay >= MAX_COMMUNICATION_DELAY: - raise ConfigEntryNotReady() from error - comm_delay += 1 - except UnauthorizedError: - _LOGGER.exception("Failed to login to Risco cloud") - return False - else: - break - - if comm_delay > initial_delay: - new_data = data.copy() - new_data[CONF_COMMUNICATION_DELAY] = comm_delay - hass.config_entries.async_update_entry(entry, data=new_data) + try: + await risco.connect() + except CannotConnectError as error: + raise ConfigEntryNotReady() from error + except UnauthorizedError: + _LOGGER.exception("Failed to login to Risco cloud") + return False async def _error(error: Exception) -> None: _LOGGER.error("Error in Risco library: %s", error) diff --git a/tests/components/risco/conftest.py b/tests/components/risco/conftest.py index a8a764cd502..e08e6b29852 100644 --- a/tests/components/risco/conftest.py +++ b/tests/components/risco/conftest.py @@ -171,16 +171,6 @@ def connect_with_error(exception): yield -@pytest.fixture -def connect_with_single_error(exception): - """Fixture to simulate error on connect.""" - with patch( - "homeassistant.components.risco.RiscoLocal.connect", - side_effect=[exception, None], - ): - yield - - @pytest.fixture async def setup_risco_local(hass, local_config_entry): """Set up a local Risco integration for testing.""" diff --git a/tests/components/risco/test_init.py b/tests/components/risco/test_init.py deleted file mode 100644 index a1a9e3bd6a7..00000000000 --- a/tests/components/risco/test_init.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Tests for the Risco initialization.""" -import pytest - -from homeassistant.components.risco import CannotConnectError -from homeassistant.components.risco.const import CONF_COMMUNICATION_DELAY -from homeassistant.core import HomeAssistant - - -@pytest.mark.parametrize("exception", [CannotConnectError]) -async def test_single_error_on_connect( - hass: HomeAssistant, connect_with_single_error, local_config_entry -) -> None: - """Test single error on connect to validate communication delay update from 0 (default) to 1.""" - expected_data = { - **local_config_entry.data, - **{"type": "local", CONF_COMMUNICATION_DELAY: 1}, - } - - await hass.config_entries.async_setup(local_config_entry.entry_id) - await hass.async_block_till_done() - assert local_config_entry.data == expected_data From 05b900321b9cf06b5410d9bb93dbad6085197928 Mon Sep 17 00:00:00 2001 From: On Freund Date: Fri, 15 Mar 2024 14:38:39 +0200 Subject: [PATCH 75/99] Bump pyrisco to 0.5.10 (#113505) --- 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 ca28af3d8e5..b5d8c4442fd 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.5.8"] + "requirements": ["pyrisco==0.5.10"] } diff --git a/requirements_all.txt b/requirements_all.txt index dc6b3111fb9..b939d524ad9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2090,7 +2090,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.5.8 +pyrisco==0.5.10 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 985fbdb7320..a5b6dcd6077 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1617,7 +1617,7 @@ pyqwikswitch==0.93 pyrainbird==4.0.2 # homeassistant.components.risco -pyrisco==0.5.8 +pyrisco==0.5.10 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 From d5864a40a87005955bb4dddf4a584d93623a28a1 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sat, 16 Mar 2024 23:37:24 +0100 Subject: [PATCH 76/99] Fix missing context when running script from template entity (#113523) Co-authored-by: J. Nick Koston --- homeassistant/components/template/coordinator.py | 7 +++++-- tests/components/template/test_sensor.py | 9 +++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/template/coordinator.py b/homeassistant/components/template/coordinator.py index 047d58d9208..a5a38bf7b1d 100644 --- a/homeassistant/components/template/coordinator.py +++ b/homeassistant/components/template/coordinator.py @@ -3,7 +3,7 @@ from collections.abc import Callable import logging from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.core import CoreState, callback +from homeassistant.core import Context, CoreState, callback from homeassistant.helpers import discovery, trigger as trigger_helper from homeassistant.helpers.script import Script from homeassistant.helpers.typing import ConfigType @@ -90,7 +90,10 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator): ) async def _handle_triggered_with_script(self, run_variables, context=None): - if script_result := await self._script.async_run(run_variables, context): + # Create a context referring to the trigger context. + trigger_context_id = None if context is None else context.id + script_context = Context(parent_id=trigger_context_id) + if script_result := await self._script.async_run(run_variables, script_context): run_variables = script_result.variables self._handle_triggered(run_variables, context) diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 314218fc849..8026618e7cd 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -29,6 +29,7 @@ import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, assert_setup_component, + async_capture_events, async_fire_time_changed, mock_restore_cache_with_extra_data, ) @@ -1848,6 +1849,7 @@ async def test_trigger_entity_restore_state( "my_variable": "{{ trigger.event.data.beer + 1 }}" }, }, + {"event": "test_event2", "event_data": {"hello": "world"}}, ], "sensor": [ { @@ -1864,6 +1866,10 @@ async def test_trigger_action( hass: HomeAssistant, start_ha, entity_registry: er.EntityRegistry ) -> None: """Test trigger entity with an action works.""" + event = "test_event2" + context = Context() + events = async_capture_events(hass, event) + state = hass.states.get("sensor.hello_name") assert state is not None assert state.state == STATE_UNKNOWN @@ -1875,3 +1881,6 @@ async def test_trigger_action( state = hass.states.get("sensor.hello_name") assert state.state == "3" assert state.context is context + + assert len(events) == 1 + assert events[0].context.parent_id == context.id From 4a620e015f095fa1817675734e7dc0182f1ee0ae Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sat, 16 Mar 2024 15:03:26 -0700 Subject: [PATCH 77/99] Bump ical to 7.0.3 to fix local-todo persisted with invalid DTSTART values (#113526) --- homeassistant/components/google/manifest.json | 2 +- .../components/local_calendar/manifest.json | 2 +- .../components/local_todo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../local_todo/snapshots/test_todo.ambr | 10 ++++++++ tests/components/local_todo/test_todo.py | 23 +++++++++++++++++++ 7 files changed, 38 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index a08daee8961..ec9fb7018d6 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==6.0.3", "oauth2client==4.1.3", "ical==7.0.1"] + "requirements": ["gcal-sync==6.0.3", "oauth2client==4.1.3", "ical==7.0.3"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index 25ec9f2ccc6..1c13970503d 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==7.0.1"] + "requirements": ["ical==7.0.3"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index 81f0f9dc199..3bcb8af9f43 100644 --- a/homeassistant/components/local_todo/manifest.json +++ b/homeassistant/components/local_todo/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_todo", "iot_class": "local_polling", - "requirements": ["ical==7.0.1"] + "requirements": ["ical==7.0.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index b939d524ad9..19a8c7794d9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1115,7 +1115,7 @@ ibmiotf==0.3.4 # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==7.0.1 +ical==7.0.3 # homeassistant.components.ping icmplib==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a5b6dcd6077..79d9c357454 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -905,7 +905,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==7.0.1 +ical==7.0.3 # homeassistant.components.ping icmplib==3.0 diff --git a/tests/components/local_todo/snapshots/test_todo.ambr b/tests/components/local_todo/snapshots/test_todo.ambr index db4403f301c..15a44ff8c27 100644 --- a/tests/components/local_todo/snapshots/test_todo.ambr +++ b/tests/components/local_todo/snapshots/test_todo.ambr @@ -22,6 +22,16 @@ list([ ]) # --- +# name: test_parse_existing_ics[invalid_dtstart_tzname] + list([ + dict({ + 'due': '2023-10-24T11:30:00', + 'status': 'needs_action', + 'summary': 'Task', + 'uid': '077cb7f2-6c89-11ee-b2a9-0242ac110002', + }), + ]) +# --- # name: test_parse_existing_ics[migrate_legacy_due] list([ dict({ diff --git a/tests/components/local_todo/test_todo.py b/tests/components/local_todo/test_todo.py index 231f56b0afb..760b0260dbb 100644 --- a/tests/components/local_todo/test_todo.py +++ b/tests/components/local_todo/test_todo.py @@ -671,6 +671,28 @@ async def test_move_item_previous_unknown( ), "1", ), + ( + textwrap.dedent( + """\ + BEGIN:VCALENDAR + PRODID:-//homeassistant.io//local_todo 2.0//EN + VERSION:2.0 + BEGIN:VTODO + DTSTAMP:20231024T014011 + UID:077cb7f2-6c89-11ee-b2a9-0242ac110002 + CREATED:20231017T010348 + LAST-MODIFIED:20231024T014011 + SEQUENCE:1 + STATUS:NEEDS-ACTION + SUMMARY:Task + DUE:20231024T113000 + DTSTART;TZID=CST:20231024T113000 + END:VTODO + END:VCALENDAR + """ + ), + "1", + ), ], ids=( "empty", @@ -679,6 +701,7 @@ async def test_move_item_previous_unknown( "needs_action", "migrate_legacy_due", "due", + "invalid_dtstart_tzname", ), ) async def test_parse_existing_ics( From a7908d82507e528050323f30cd0b9ebb098d5450 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 15 Mar 2024 23:48:47 +0100 Subject: [PATCH 78/99] Fix Airthings BLE illuminance sensor name (#113560) --- homeassistant/components/airthings_ble/sensor.py | 1 + homeassistant/components/airthings_ble/strings.json | 3 +++ 2 files changed, 4 insertions(+) diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py index 2bc2d5e726a..6abe3e5d174 100644 --- a/homeassistant/components/airthings_ble/sensor.py +++ b/homeassistant/components/airthings_ble/sensor.py @@ -105,6 +105,7 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = { ), "illuminance": SensorEntityDescription( key="illuminance", + translation_key="illuminance", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), diff --git a/homeassistant/components/airthings_ble/strings.json b/homeassistant/components/airthings_ble/strings.json index b7343377a2b..6f17b9a317e 100644 --- a/homeassistant/components/airthings_ble/strings.json +++ b/homeassistant/components/airthings_ble/strings.json @@ -33,6 +33,9 @@ }, "radon_longterm_level": { "name": "Radon longterm level" + }, + "illuminance": { + "name": "[%key:component::sensor::entity_component::illuminance::name%]" } } } From 0a64ae2f7a149003ea40656a5137014ae487da06 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 16 Mar 2024 16:18:41 +0200 Subject: [PATCH 79/99] Ignore Shelly block update with cfgChanged None (#113587) --- homeassistant/components/shelly/coordinator.py | 2 +- tests/components/shelly/test_coordinator.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 4afe66199f0..d41282b1f0b 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -216,7 +216,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): # Check for input events and config change cfg_changed = 0 for block in self.device.blocks: - if block.type == "device": + if block.type == "device" and block.cfgChanged is not None: cfg_changed = block.cfgChanged # Shelly TRV sends information about changing the configuration for no diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 67df09a5adb..940ab2123f0 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -67,6 +67,18 @@ async def test_block_reload_on_cfg_change( mock_block_device.mock_update() await hass.async_block_till_done() + # Make sure cfgChanged with None is ignored + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "cfgChanged", None) + mock_block_device.mock_update() + await hass.async_block_till_done() + + # Wait for debouncer + freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("switch.test_name_channel_1") is not None + # Generate config change from switch to light monkeypatch.setitem( mock_block_device.settings["relays"][RELAY_BLOCK_ID], "appliance_type", "light" From 2e2d303291486ed4e98d5cdc6e9139785397641a Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 16 Mar 2024 16:01:48 +0100 Subject: [PATCH 80/99] Catch `TimeoutError` in `Brother` config flow (#113593) * Catch TimeoutError in Brother config flow * Update tests * Remove unnecessary parentheses --------- Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/brother/config_flow.py | 4 ++-- tests/components/brother/test_config_flow.py | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index 55d47bb0c2c..3c60ccba5f0 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -58,7 +58,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=title, data=user_input) except InvalidHost: errors[CONF_HOST] = "wrong_host" - except ConnectionError: + except (ConnectionError, TimeoutError): errors["base"] = "cannot_connect" except SnmpError: errors["base"] = "snmp_error" @@ -88,7 +88,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.brother.async_update() except UnsupportedModelError: return self.async_abort(reason="unsupported_model") - except (ConnectionError, SnmpError): + except (ConnectionError, SnmpError, TimeoutError): return self.async_abort(reason="cannot_connect") # Check if already configured diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index f83f882b8a0..3d83ecfcb7c 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -93,10 +93,11 @@ async def test_invalid_hostname(hass: HomeAssistant) -> None: assert result["errors"] == {CONF_HOST: "wrong_host"} -async def test_connection_error(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("exc", [ConnectionError, TimeoutError]) +async def test_connection_error(hass: HomeAssistant, exc: Exception) -> None: """Test connection to host error.""" with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data", side_effect=ConnectionError() + "brother.Brother._get_data", side_effect=exc ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG @@ -147,10 +148,11 @@ async def test_device_exists_abort(hass: HomeAssistant) -> None: assert result["reason"] == "already_configured" -async def test_zeroconf_snmp_error(hass: HomeAssistant) -> None: - """Test we abort zeroconf flow on SNMP error.""" +@pytest.mark.parametrize("exc", [ConnectionError, TimeoutError, SnmpError("error")]) +async def test_zeroconf_exception(hass: HomeAssistant, exc: Exception) -> None: + """Test we abort zeroconf flow on exception.""" with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data", side_effect=SnmpError("error") + "brother.Brother._get_data", side_effect=exc ): result = await hass.config_entries.flow.async_init( DOMAIN, From fa9f5bd6477b85de44aa2484409ce8fe1920a77f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 16 Mar 2024 23:02:52 +0100 Subject: [PATCH 81/99] Bump axis to v56 (#113608) --- homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index c1471d370a5..346afc4b4fe 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -26,7 +26,7 @@ "iot_class": "local_push", "loggers": ["axis"], "quality_scale": "platinum", - "requirements": ["axis==55"], + "requirements": ["axis==56"], "ssdp": [ { "manufacturer": "AXIS" diff --git a/requirements_all.txt b/requirements_all.txt index 19a8c7794d9..7ac465663c5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==55 +axis==56 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 79d9c357454..9febcf2defd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -454,7 +454,7 @@ auroranoaa==0.0.3 aurorapy==0.2.7 # homeassistant.components.axis -axis==55 +axis==56 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 From 686487e59c588397a1f39cd8cfb7a2db23c219f0 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sat, 16 Mar 2024 18:26:56 -0400 Subject: [PATCH 82/99] Bump pyunifiprotect to 5.0.1 (#113630) --- homeassistant/components/unifiprotect/manifest.json | 2 +- homeassistant/components/unifiprotect/select.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index eba2b934e05..1654539d91d 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -42,7 +42,7 @@ "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==4.23.3", "unifi-discovery==1.1.8"], + "requirements": ["pyunifiprotect==5.0.1", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index 5611ba79eca..e07a174659c 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -45,6 +45,7 @@ INFRARED_MODES = [ {"id": IRLEDMode.AUTO.value, "name": "Auto"}, {"id": IRLEDMode.ON.value, "name": "Always Enable"}, {"id": IRLEDMode.AUTO_NO_LED.value, "name": "Auto (Filter Only, no LED's)"}, + {"id": IRLEDMode.CUSTOM.value, "name": "Auto (Custom Lux)"}, {"id": IRLEDMode.OFF.value, "name": "Always Disable"}, ] diff --git a/requirements_all.txt b/requirements_all.txt index 7ac465663c5..46ccc244b8e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2340,7 +2340,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.23.3 +pyunifiprotect==5.0.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9febcf2defd..1d5c3bad1a8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1801,7 +1801,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.23.3 +pyunifiprotect==5.0.1 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From eb8a8424a59257595dd067590cfff5ecc0faaf77 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Sat, 16 Mar 2024 22:15:18 -0400 Subject: [PATCH 83/99] Bump pyunifiprotect to 5.0.2 (#113651) --- homeassistant/components/unifiprotect/data.py | 27 ++----------------- .../components/unifiprotect/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 2825c2a4f3c..b82e9ff37f1 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -19,6 +19,7 @@ from pyunifiprotect.data import ( WSSubscriptionMessage, ) from pyunifiprotect.exceptions import ClientError, NotAuthorized +from pyunifiprotect.utils import log_event from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback @@ -41,11 +42,6 @@ from .utils import async_dispatch_id as _ufpd, async_get_devices_by_type _LOGGER = logging.getLogger(__name__) ProtectDeviceType = ProtectAdoptableDeviceModel | NVR -SMART_EVENTS = { - EventType.SMART_DETECT, - EventType.SMART_AUDIO_DETECT, - EventType.SMART_DETECT_LINE, -} @callback @@ -230,26 +226,7 @@ class ProtectData: # trigger updates for camera that the event references elif isinstance(obj, Event): # type: ignore[unreachable] if _LOGGER.isEnabledFor(logging.DEBUG): - _LOGGER.debug("event WS msg: %s", obj.dict()) - if obj.type in SMART_EVENTS: - if obj.camera is not None: - if obj.end is None: - _LOGGER.debug( - "%s (%s): New smart detection started for %s (%s)", - obj.camera.name, - obj.camera.mac, - obj.smart_detect_types, - obj.id, - ) - else: - _LOGGER.debug( - "%s (%s): Smart detection ended for %s (%s)", - obj.camera.name, - obj.camera.mac, - obj.smart_detect_types, - obj.id, - ) - + log_event(obj) if obj.type is EventType.DEVICE_ADOPTED: if obj.metadata is not None and obj.metadata.device_id is not None: device = self.api.bootstrap.get_device_from_id( diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 1654539d91d..1eb37befca0 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -42,7 +42,7 @@ "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==5.0.1", "unifi-discovery==1.1.8"], + "requirements": ["pyunifiprotect==5.0.2", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 46ccc244b8e..50e99d674e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2340,7 +2340,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==5.0.1 +pyunifiprotect==5.0.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1d5c3bad1a8..865fec1c2e1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1801,7 +1801,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==5.0.1 +pyunifiprotect==5.0.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 368586c9d17727d63ab0edec297e8bd85ec68285 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 17 Mar 2024 21:21:42 +0100 Subject: [PATCH 84/99] Add removal condition to Shelly battery sensor (#113703) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/shelly/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index b88b6886b84..82fc4fe6d78 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -941,6 +941,7 @@ RPC_SENSORS: Final = { device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, + removal_condition=lambda _config, status, key: (status[key]["battery"] is None), ), "voltmeter": RpcSensorDescription( key="voltmeter", From 6859bae0b13375c94483e6fde8102f3d28b4a253 Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Wed, 20 Mar 2024 04:57:37 -0500 Subject: [PATCH 85/99] Bump aioraven to 0.5.2 (#113714) --- homeassistant/components/rainforest_raven/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/rainforest_raven/manifest.json b/homeassistant/components/rainforest_raven/manifest.json index 3e463af9ba4..ad161d32201 100644 --- a/homeassistant/components/rainforest_raven/manifest.json +++ b/homeassistant/components/rainforest_raven/manifest.json @@ -6,7 +6,7 @@ "dependencies": ["usb"], "documentation": "https://www.home-assistant.io/integrations/rainforest_raven", "iot_class": "local_polling", - "requirements": ["aioraven==0.5.1"], + "requirements": ["aioraven==0.5.2"], "usb": [ { "vid": "0403", diff --git a/requirements_all.txt b/requirements_all.txt index 50e99d674e9..f11d17fc8cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -350,7 +350,7 @@ aiopyarr==23.4.0 aioqsw==0.3.5 # homeassistant.components.rainforest_raven -aioraven==0.5.1 +aioraven==0.5.2 # homeassistant.components.recollect_waste aiorecollect==2023.09.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 865fec1c2e1..d11bba443cc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -323,7 +323,7 @@ aiopyarr==23.4.0 aioqsw==0.3.5 # homeassistant.components.rainforest_raven -aioraven==0.5.1 +aioraven==0.5.2 # homeassistant.components.recollect_waste aiorecollect==2023.09.0 From 33678ff5a47b6e7b91f1b959070ccb27d846504d Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Mon, 18 Mar 2024 10:12:46 +0100 Subject: [PATCH 86/99] Fix unknown values in onewire (#113731) * Fix unknown values in onewire * Update tests --- homeassistant/components/onewire/binary_sensor.py | 4 +++- homeassistant/components/onewire/switch.py | 6 ++++-- tests/components/onewire/const.py | 8 ++++++-- .../components/onewire/snapshots/test_binary_sensor.ambr | 4 ++-- tests/components/onewire/snapshots/test_switch.ambr | 4 ++-- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index e7e30588f8a..5cd3fa65a60 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -143,6 +143,8 @@ class OneWireBinarySensor(OneWireEntity, BinarySensorEntity): entity_description: OneWireBinarySensorEntityDescription @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if sensor is on.""" + if self._state is None: + return None return bool(self._state) diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index 00a3f8f65f4..c63198ccf05 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -204,8 +204,10 @@ class OneWireSwitch(OneWireEntity, SwitchEntity): entity_description: OneWireSwitchEntityDescription @property - def is_on(self) -> bool: - """Return true if sensor is on.""" + def is_on(self) -> bool | None: + """Return true if switch is on.""" + if self._state is None: + return None return bool(self._state) def turn_on(self, **kwargs: Any) -> None: diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 3da37a72459..cb9fca54f3d 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -155,7 +155,9 @@ MOCK_OWPROXY_DEVICES = { {ATTR_INJECT_READS: b" 1"}, {ATTR_INJECT_READS: b" 0"}, {ATTR_INJECT_READS: b" 0"}, - {ATTR_INJECT_READS: b" 0"}, + { + ATTR_INJECT_READS: ProtocolError, + }, {ATTR_INJECT_READS: b" 0"}, {ATTR_INJECT_READS: b" 0"}, {ATTR_INJECT_READS: b" 0"}, @@ -165,7 +167,9 @@ MOCK_OWPROXY_DEVICES = { {ATTR_INJECT_READS: b" 1"}, {ATTR_INJECT_READS: b" 0"}, {ATTR_INJECT_READS: b" 1"}, - {ATTR_INJECT_READS: b" 0"}, + { + ATTR_INJECT_READS: ProtocolError, + }, {ATTR_INJECT_READS: b" 1"}, {ATTR_INJECT_READS: b" 0"}, {ATTR_INJECT_READS: b" 1"}, diff --git a/tests/components/onewire/snapshots/test_binary_sensor.ambr b/tests/components/onewire/snapshots/test_binary_sensor.ambr index 2aa415f0345..0523c969ade 100644 --- a/tests/components/onewire/snapshots/test_binary_sensor.ambr +++ b/tests/components/onewire/snapshots/test_binary_sensor.ambr @@ -851,13 +851,13 @@ 'attributes': ReadOnlyDict({ 'device_file': '/29.111111111111/sensed.3', 'friendly_name': '29.111111111111 Sensed 3', - 'raw_value': 0.0, + 'raw_value': None, }), 'context': , 'entity_id': 'binary_sensor.29_111111111111_sensed_3', 'last_changed': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', }), StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/onewire/snapshots/test_switch.ambr b/tests/components/onewire/snapshots/test_switch.ambr index 8fbb977948b..4f6498419a9 100644 --- a/tests/components/onewire/snapshots/test_switch.ambr +++ b/tests/components/onewire/snapshots/test_switch.ambr @@ -1271,13 +1271,13 @@ 'attributes': ReadOnlyDict({ 'device_file': '/29.111111111111/PIO.3', 'friendly_name': '29.111111111111 Programmed input-output 3', - 'raw_value': 0.0, + 'raw_value': None, }), 'context': , 'entity_id': 'switch.29_111111111111_programmed_input_output_3', 'last_changed': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', }), StateSnapshot({ 'attributes': ReadOnlyDict({ From d67cd2af0c8dc05e33d0fb8b195b5646d8904433 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 19 Mar 2024 09:37:36 +0100 Subject: [PATCH 87/99] Bump pymodbus v3.6.6 (#113796) --- homeassistant/components/modbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 6b072457144..14faad789fe 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_polling", "loggers": ["pymodbus"], "quality_scale": "gold", - "requirements": ["pymodbus==3.6.5"] + "requirements": ["pymodbus==3.6.6"] } diff --git a/requirements_all.txt b/requirements_all.txt index f11d17fc8cb..c147e265ec2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1971,7 +1971,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.6.5 +pymodbus==3.6.6 # homeassistant.components.monoprice pymonoprice==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d11bba443cc..7636d8c6b18 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1525,7 +1525,7 @@ pymeteoclimatic==0.1.0 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.6.5 +pymodbus==3.6.6 # homeassistant.components.monoprice pymonoprice==0.4 From 4132a3d2ea7abc91b3b00d65ee3d7a4f9a2e67a4 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 20 Mar 2024 13:10:35 +0100 Subject: [PATCH 88/99] Catch API errors in cast media_player service handlers (#113839) * Catch API errors in cast media_player service handlers * Remove left over debug code * Fix wrapping of coroutine function with api_error --- homeassistant/components/cast/media_player.py | 68 +++++++++++++++++-- 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index b2893a54310..5e907b0a659 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -4,9 +4,10 @@ from __future__ import annotations from collections.abc import Callable from contextlib import suppress from datetime import datetime +from functools import wraps import json import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar import pychromecast from pychromecast.controllers.homeassistant import HomeAssistantController @@ -18,6 +19,7 @@ from pychromecast.controllers.media import ( ) from pychromecast.controllers.multizone import MultizoneManager from pychromecast.controllers.receiver import VOLUME_CONTROL_TYPE_FIXED +from pychromecast.error import PyChromecastError from pychromecast.quick_play import quick_play from pychromecast.socket_client import ( CONNECTION_STATUS_CONNECTED, @@ -83,6 +85,34 @@ APP_IDS_UNRELIABLE_MEDIA_INFO = ("Netflix",) CAST_SPLASH = "https://www.home-assistant.io/images/cast/splash.png" +_CastDeviceT = TypeVar("_CastDeviceT", bound="CastDevice") +_R = TypeVar("_R") +_P = ParamSpec("_P") + +_FuncType = Callable[Concatenate[_CastDeviceT, _P], _R] +_ReturnFuncType = Callable[Concatenate[_CastDeviceT, _P], _R] + + +def api_error( + func: _FuncType[_CastDeviceT, _P, _R], +) -> _ReturnFuncType[_CastDeviceT, _P, _R]: + """Handle PyChromecastError and reraise a HomeAssistantError.""" + + @wraps(func) + def wrapper(self: _CastDeviceT, *args: _P.args, **kwargs: _P.kwargs) -> _R: + """Wrap a CastDevice method.""" + try: + return_value = func(self, *args, **kwargs) + except PyChromecastError as err: + raise HomeAssistantError( + f"{self.__class__.__name__}.{func.__name__} Failed: {err}" + ) from err + + return return_value + + return wrapper + + @callback def _async_create_cast_device(hass: HomeAssistant, info: ChromecastInfo): """Create a CastDevice entity or dynamic group from the chromecast object. @@ -476,6 +506,21 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): return media_controller + @api_error + def _quick_play(self, app_name: str, data: dict[str, Any]) -> None: + """Launch the app `app_name` and start playing media defined by `data`.""" + quick_play(self._get_chromecast(), app_name, data) + + @api_error + def _quit_app(self) -> None: + """Quit the currently running app.""" + self._get_chromecast().quit_app() + + @api_error + def _start_app(self, app_id: str) -> None: + """Start an app.""" + self._get_chromecast().start_app(app_id) + def turn_on(self) -> None: """Turn on the cast device.""" @@ -486,52 +531,61 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): if chromecast.app_id is not None: # Quit the previous app before starting splash screen or media player - chromecast.quit_app() + self._quit_app() # The only way we can turn the Chromecast is on is by launching an app if chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST: app_data = {"media_id": CAST_SPLASH, "media_type": "image/png"} - quick_play(chromecast, "default_media_receiver", app_data) + self._quick_play("default_media_receiver", app_data) else: - chromecast.start_app(pychromecast.config.APP_MEDIA_RECEIVER) + self._start_app(pychromecast.config.APP_MEDIA_RECEIVER) + @api_error def turn_off(self) -> None: """Turn off the cast device.""" self._get_chromecast().quit_app() + @api_error def mute_volume(self, mute: bool) -> None: """Mute the volume.""" self._get_chromecast().set_volume_muted(mute) + @api_error def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self._get_chromecast().set_volume(volume) + @api_error def media_play(self) -> None: """Send play command.""" media_controller = self._media_controller() media_controller.play() + @api_error def media_pause(self) -> None: """Send pause command.""" media_controller = self._media_controller() media_controller.pause() + @api_error def media_stop(self) -> None: """Send stop command.""" media_controller = self._media_controller() media_controller.stop() + @api_error def media_previous_track(self) -> None: """Send previous track command.""" media_controller = self._media_controller() media_controller.queue_prev() + @api_error def media_next_track(self) -> None: """Send next track command.""" media_controller = self._media_controller() media_controller.queue_next() + @api_error def media_seek(self, position: float) -> None: """Seek the media to a specific location.""" media_controller = self._media_controller() @@ -644,7 +698,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): if "app_id" in app_data: app_id = app_data.pop("app_id") _LOGGER.info("Starting Cast app by ID %s", app_id) - await self.hass.async_add_executor_job(chromecast.start_app, app_id) + await self.hass.async_add_executor_job(self._start_app, app_id) if app_data: _LOGGER.warning( "Extra keys %s were ignored. Please use app_name to cast media", @@ -655,7 +709,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): app_name = app_data.pop("app_name") try: await self.hass.async_add_executor_job( - quick_play, chromecast, app_name, app_data + self._quick_play, app_name, app_data ) except NotImplementedError: _LOGGER.error("App %s not supported", app_name) @@ -729,7 +783,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): app_data, ) await self.hass.async_add_executor_job( - quick_play, chromecast, "default_media_receiver", app_data + self._quick_play, "default_media_receiver", app_data ) def _media_status(self): From 14c4cdc08953e02539c3a066c6873e2e70d268bc Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 19 Mar 2024 22:41:10 +0100 Subject: [PATCH 89/99] Bump pychromecast to 14.0.1 (#113841) --- homeassistant/components/cast/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index d02bcd3558a..1d06ae23ca2 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/cast", "iot_class": "local_polling", "loggers": ["casttube", "pychromecast"], - "requirements": ["PyChromecast==14.0.0"], + "requirements": ["PyChromecast==14.0.1"], "zeroconf": ["_googlecast._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index c147e265ec2..b7e161445a0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -54,7 +54,7 @@ ProgettiHWSW==0.1.3 # PyBluez==0.22 # homeassistant.components.cast -PyChromecast==14.0.0 +PyChromecast==14.0.1 # homeassistant.components.flick_electric PyFlick==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7636d8c6b18..60439019cc3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -45,7 +45,7 @@ PlexAPI==4.15.10 ProgettiHWSW==0.1.3 # homeassistant.components.cast -PyChromecast==14.0.0 +PyChromecast==14.0.1 # homeassistant.components.flick_electric PyFlick==0.0.2 From 8056886c66a7f88e9d5027c9ecfcd13cb2902766 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 19 Mar 2024 22:37:29 +0100 Subject: [PATCH 90/99] Fix startup race in cast (#113843) --- homeassistant/components/cast/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 8c574e0792b..9aed870d9b4 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -24,9 +24,9 @@ PLATFORMS = [Platform.MEDIA_PLAYER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Cast from a config entry.""" + hass.data[DOMAIN] = {"cast_platform": {}, "unknown_models": {}} await home_assistant_cast.async_setup_ha_cast(hass, entry) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - hass.data[DOMAIN] = {"cast_platform": {}, "unknown_models": {}} await async_process_integration_platforms(hass, DOMAIN, _register_cast_platform) return True From 1e57f52ba27a48814cf84c920c2547d113e401ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Wed, 20 Mar 2024 08:23:16 +0100 Subject: [PATCH 91/99] Redact the area of traccar server geofences (#113861) --- homeassistant/components/traccar_server/diagnostics.py | 7 ++++++- .../traccar_server/snapshots/test_diagnostics.ambr | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/traccar_server/diagnostics.py b/homeassistant/components/traccar_server/diagnostics.py index 15b94a2b880..f4b1cc799cb 100644 --- a/homeassistant/components/traccar_server/diagnostics.py +++ b/homeassistant/components/traccar_server/diagnostics.py @@ -12,7 +12,12 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from .const import DOMAIN from .coordinator import TraccarServerCoordinator -TO_REDACT = {CONF_ADDRESS, CONF_LATITUDE, CONF_LONGITUDE} +TO_REDACT = { + CONF_ADDRESS, + CONF_LATITUDE, + CONF_LONGITUDE, + "area", # This is the polygon area of a geofence +} async def async_get_config_entry_diagnostics( diff --git a/tests/components/traccar_server/snapshots/test_diagnostics.ambr b/tests/components/traccar_server/snapshots/test_diagnostics.ambr index 1726f1c3d45..20d01e427ea 100644 --- a/tests/components/traccar_server/snapshots/test_diagnostics.ambr +++ b/tests/components/traccar_server/snapshots/test_diagnostics.ambr @@ -34,7 +34,7 @@ 'uniqueId': 'abc123', }), 'geofence': dict({ - 'area': 'string', + 'area': '**REDACTED**', 'attributes': dict({ }), 'calendarId': 0, @@ -134,7 +134,7 @@ 'uniqueId': 'abc123', }), 'geofence': dict({ - 'area': 'string', + 'area': '**REDACTED**', 'attributes': dict({ }), 'calendarId': 0, From 6c274abc5010ca5476b187cda24b1993086e836e Mon Sep 17 00:00:00 2001 From: Josef Zweck <24647999+zweckj@users.noreply.github.com> Date: Thu, 21 Mar 2024 15:27:51 +0100 Subject: [PATCH 92/99] Bump pytedee_async to 0.2.17 (#113933) --- homeassistant/components/tedee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tedee/manifest.json b/homeassistant/components/tedee/manifest.json index 1f2a2405a44..db3a88f3113 100644 --- a/homeassistant/components/tedee/manifest.json +++ b/homeassistant/components/tedee/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/tedee", "iot_class": "local_push", "loggers": ["pytedee_async"], - "requirements": ["pytedee-async==0.2.16"] + "requirements": ["pytedee-async==0.2.17"] } diff --git a/requirements_all.txt b/requirements_all.txt index b7e161445a0..f5a4c1da417 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2182,7 +2182,7 @@ pyswitchbee==1.8.0 pytautulli==23.1.1 # homeassistant.components.tedee -pytedee-async==0.2.16 +pytedee-async==0.2.17 # homeassistant.components.tfiac pytfiac==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 60439019cc3..c0f77531e1b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1697,7 +1697,7 @@ pyswitchbee==1.8.0 pytautulli==23.1.1 # homeassistant.components.tedee -pytedee-async==0.2.16 +pytedee-async==0.2.17 # homeassistant.components.motionmount python-MotionMount==0.3.1 From 19ef92735c67e0abe1684456959355ddef2bec8f Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 21 Mar 2024 21:42:42 +0100 Subject: [PATCH 93/99] Bump axis to v57 (#113952) --- homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 346afc4b4fe..44d615bf534 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -26,7 +26,7 @@ "iot_class": "local_push", "loggers": ["axis"], "quality_scale": "platinum", - "requirements": ["axis==56"], + "requirements": ["axis==57"], "ssdp": [ { "manufacturer": "AXIS" diff --git a/requirements_all.txt b/requirements_all.txt index f5a4c1da417..fe927e87424 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==56 +axis==57 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c0f77531e1b..6307d864186 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -454,7 +454,7 @@ auroranoaa==0.0.3 aurorapy==0.2.7 # homeassistant.components.axis -axis==56 +axis==57 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 From 91bb321d8f4d3cebc1852816e2c0671a2c0525fb Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Mar 2024 20:25:19 -0400 Subject: [PATCH 94/99] Bump version to 2024.3.2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 847387e76ae..f5efc37f352 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 3 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index d8a1545fbb3..496a29eb31c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.3.1" +version = "2024.3.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From f10d924e8b4e7f136466334c5d88fde046d1c35f Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 21 Mar 2024 21:57:36 -0400 Subject: [PATCH 95/99] 2024.3.2 (#113973) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Streamline Notion config entry updates (refresh token and user ID) (#112832) * Bump aioautomower to 2024.3.2 (#113162) * Bump aioautomower to 2024.3.3 (#113430) * Check for EA release channel for UniFi Protect (#113432) Co-authored-by: J. Nick Koston * Bump `pysnmp-lextudio` to version `6.0.11` (#113463) * Tado fix water heater (#113464) Co-authored-by: Joostlek * Bump aiodhcpwatcher to 0.8.2 (#113466) * Bump axis to v55 (#113479) * Bump croniter to 2.0.2 (#113494) * Revert setting communication delay in Risco init (#113497) * Bump pyrisco to 0.5.10 (#113505) * Fix missing context when running script from template entity (#113523) Co-authored-by: J. Nick Koston * Bump ical to 7.0.3 to fix local-todo persisted with invalid DTSTART values (#113526) * Fix Airthings BLE illuminance sensor name (#113560) * Ignore Shelly block update with cfgChanged None (#113587) * Catch `TimeoutError` in `Brother` config flow (#113593) * Catch TimeoutError in Brother config flow * Update tests * Remove unnecessary parentheses --------- Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> * Bump axis to v56 (#113608) * Bump pyunifiprotect to 5.0.1 (#113630) * Bump pyunifiprotect to 5.0.2 (#113651) * Add removal condition to Shelly battery sensor (#113703) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> * Bump aioraven to 0.5.2 (#113714) * Fix unknown values in onewire (#113731) * Fix unknown values in onewire * Update tests * Bump pymodbus v3.6.6 (#113796) * Catch API errors in cast media_player service handlers (#113839) * Catch API errors in cast media_player service handlers * Remove left over debug code * Fix wrapping of coroutine function with api_error * Bump pychromecast to 14.0.1 (#113841) * Fix startup race in cast (#113843) * Redact the area of traccar server geofences (#113861) * Bump pytedee_async to 0.2.17 (#113933) * Bump axis to v57 (#113952) * Bump version to 2024.3.2 --------- Co-authored-by: Aaron Bach Co-authored-by: Thomas55555 <59625598+Thomas55555@users.noreply.github.com> Co-authored-by: Christopher Bailey Co-authored-by: J. Nick Koston Co-authored-by: Lex Li <425130+lextm@users.noreply.github.com> Co-authored-by: Erwin Douna Co-authored-by: Joostlek Co-authored-by: Robert Svensson Co-authored-by: Diogo Gomes Co-authored-by: On Freund Co-authored-by: Erik Montnemery Co-authored-by: Allen Porter Co-authored-by: Shay Levy Co-authored-by: Maciej Bieniek Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> Co-authored-by: Scott K Logan Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> Co-authored-by: jan iversen Co-authored-by: Joakim Sørensen Co-authored-by: Josef Zweck <24647999+zweckj@users.noreply.github.com> --- .../components/airthings_ble/sensor.py | 1 + .../components/airthings_ble/strings.json | 3 + homeassistant/components/axis/manifest.json | 2 +- .../components/brother/config_flow.py | 4 +- homeassistant/components/cast/__init__.py | 2 +- homeassistant/components/cast/manifest.json | 2 +- homeassistant/components/cast/media_player.py | 68 +++++++++++++++++-- homeassistant/components/dhcp/manifest.json | 2 +- homeassistant/components/google/manifest.json | 2 +- .../husqvarna_automower/manifest.json | 2 +- .../components/husqvarna_automower/sensor.py | 7 ++ .../components/local_calendar/manifest.json | 2 +- .../components/local_todo/manifest.json | 2 +- homeassistant/components/modbus/manifest.json | 2 +- homeassistant/components/notion/__init__.py | 19 +++--- .../components/onewire/binary_sensor.py | 4 +- homeassistant/components/onewire/switch.py | 6 +- .../components/rainforest_raven/manifest.json | 2 +- homeassistant/components/risco/__init__.py | 34 +++------- homeassistant/components/risco/manifest.json | 2 +- .../components/shelly/coordinator.py | 2 +- homeassistant/components/shelly/sensor.py | 1 + homeassistant/components/snmp/manifest.json | 2 +- homeassistant/components/tado/water_heater.py | 13 ++-- homeassistant/components/tedee/manifest.json | 2 +- .../components/template/coordinator.py | 7 +- .../components/traccar_server/diagnostics.py | 7 +- .../components/unifiprotect/__init__.py | 13 ++-- .../components/unifiprotect/const.py | 2 +- homeassistant/components/unifiprotect/data.py | 27 +------- .../components/unifiprotect/manifest.json | 2 +- .../components/unifiprotect/repairs.py | 5 +- .../components/unifiprotect/select.py | 1 + .../components/unifiprotect/strings.json | 12 ++-- .../components/utility_meter/manifest.json | 2 +- homeassistant/const.py | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements_all.txt | 24 +++---- requirements_test_all.txt | 24 +++---- tests/components/brother/test_config_flow.py | 12 ++-- .../husqvarna_automower/test_sensor.py | 30 ++++++-- .../local_todo/snapshots/test_todo.ambr | 10 +++ tests/components/local_todo/test_todo.py | 23 +++++++ tests/components/onewire/const.py | 8 ++- .../onewire/snapshots/test_binary_sensor.ambr | 4 +- .../onewire/snapshots/test_switch.ambr | 4 +- tests/components/risco/conftest.py | 10 --- tests/components/risco/test_init.py | 21 ------ tests/components/shelly/test_coordinator.py | 12 ++++ tests/components/template/test_sensor.py | 9 +++ .../snapshots/test_diagnostics.ambr | 4 +- .../unifiprotect/test_config_flow.py | 2 +- tests/components/unifiprotect/test_repairs.py | 15 ++-- 54 files changed, 288 insertions(+), 196 deletions(-) delete mode 100644 tests/components/risco/test_init.py diff --git a/homeassistant/components/airthings_ble/sensor.py b/homeassistant/components/airthings_ble/sensor.py index 2bc2d5e726a..6abe3e5d174 100644 --- a/homeassistant/components/airthings_ble/sensor.py +++ b/homeassistant/components/airthings_ble/sensor.py @@ -105,6 +105,7 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = { ), "illuminance": SensorEntityDescription( key="illuminance", + translation_key="illuminance", native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), diff --git a/homeassistant/components/airthings_ble/strings.json b/homeassistant/components/airthings_ble/strings.json index b7343377a2b..6f17b9a317e 100644 --- a/homeassistant/components/airthings_ble/strings.json +++ b/homeassistant/components/airthings_ble/strings.json @@ -33,6 +33,9 @@ }, "radon_longterm_level": { "name": "Radon longterm level" + }, + "illuminance": { + "name": "[%key:component::sensor::entity_component::illuminance::name%]" } } } diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index f56df16918e..44d615bf534 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -26,7 +26,7 @@ "iot_class": "local_push", "loggers": ["axis"], "quality_scale": "platinum", - "requirements": ["axis==54"], + "requirements": ["axis==57"], "ssdp": [ { "manufacturer": "AXIS" diff --git a/homeassistant/components/brother/config_flow.py b/homeassistant/components/brother/config_flow.py index 55d47bb0c2c..3c60ccba5f0 100644 --- a/homeassistant/components/brother/config_flow.py +++ b/homeassistant/components/brother/config_flow.py @@ -58,7 +58,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=title, data=user_input) except InvalidHost: errors[CONF_HOST] = "wrong_host" - except ConnectionError: + except (ConnectionError, TimeoutError): errors["base"] = "cannot_connect" except SnmpError: errors["base"] = "snmp_error" @@ -88,7 +88,7 @@ class BrotherConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.brother.async_update() except UnsupportedModelError: return self.async_abort(reason="unsupported_model") - except (ConnectionError, SnmpError): + except (ConnectionError, SnmpError, TimeoutError): return self.async_abort(reason="cannot_connect") # Check if already configured diff --git a/homeassistant/components/cast/__init__.py b/homeassistant/components/cast/__init__.py index 8c574e0792b..9aed870d9b4 100644 --- a/homeassistant/components/cast/__init__.py +++ b/homeassistant/components/cast/__init__.py @@ -24,9 +24,9 @@ PLATFORMS = [Platform.MEDIA_PLAYER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Cast from a config entry.""" + hass.data[DOMAIN] = {"cast_platform": {}, "unknown_models": {}} await home_assistant_cast.async_setup_ha_cast(hass, entry) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - hass.data[DOMAIN] = {"cast_platform": {}, "unknown_models": {}} await async_process_integration_platforms(hass, DOMAIN, _register_cast_platform) return True diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index d02bcd3558a..1d06ae23ca2 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -14,6 +14,6 @@ "documentation": "https://www.home-assistant.io/integrations/cast", "iot_class": "local_polling", "loggers": ["casttube", "pychromecast"], - "requirements": ["PyChromecast==14.0.0"], + "requirements": ["PyChromecast==14.0.1"], "zeroconf": ["_googlecast._tcp.local."] } diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index b2893a54310..5e907b0a659 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -4,9 +4,10 @@ from __future__ import annotations from collections.abc import Callable from contextlib import suppress from datetime import datetime +from functools import wraps import json import logging -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar import pychromecast from pychromecast.controllers.homeassistant import HomeAssistantController @@ -18,6 +19,7 @@ from pychromecast.controllers.media import ( ) from pychromecast.controllers.multizone import MultizoneManager from pychromecast.controllers.receiver import VOLUME_CONTROL_TYPE_FIXED +from pychromecast.error import PyChromecastError from pychromecast.quick_play import quick_play from pychromecast.socket_client import ( CONNECTION_STATUS_CONNECTED, @@ -83,6 +85,34 @@ APP_IDS_UNRELIABLE_MEDIA_INFO = ("Netflix",) CAST_SPLASH = "https://www.home-assistant.io/images/cast/splash.png" +_CastDeviceT = TypeVar("_CastDeviceT", bound="CastDevice") +_R = TypeVar("_R") +_P = ParamSpec("_P") + +_FuncType = Callable[Concatenate[_CastDeviceT, _P], _R] +_ReturnFuncType = Callable[Concatenate[_CastDeviceT, _P], _R] + + +def api_error( + func: _FuncType[_CastDeviceT, _P, _R], +) -> _ReturnFuncType[_CastDeviceT, _P, _R]: + """Handle PyChromecastError and reraise a HomeAssistantError.""" + + @wraps(func) + def wrapper(self: _CastDeviceT, *args: _P.args, **kwargs: _P.kwargs) -> _R: + """Wrap a CastDevice method.""" + try: + return_value = func(self, *args, **kwargs) + except PyChromecastError as err: + raise HomeAssistantError( + f"{self.__class__.__name__}.{func.__name__} Failed: {err}" + ) from err + + return return_value + + return wrapper + + @callback def _async_create_cast_device(hass: HomeAssistant, info: ChromecastInfo): """Create a CastDevice entity or dynamic group from the chromecast object. @@ -476,6 +506,21 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): return media_controller + @api_error + def _quick_play(self, app_name: str, data: dict[str, Any]) -> None: + """Launch the app `app_name` and start playing media defined by `data`.""" + quick_play(self._get_chromecast(), app_name, data) + + @api_error + def _quit_app(self) -> None: + """Quit the currently running app.""" + self._get_chromecast().quit_app() + + @api_error + def _start_app(self, app_id: str) -> None: + """Start an app.""" + self._get_chromecast().start_app(app_id) + def turn_on(self) -> None: """Turn on the cast device.""" @@ -486,52 +531,61 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): if chromecast.app_id is not None: # Quit the previous app before starting splash screen or media player - chromecast.quit_app() + self._quit_app() # The only way we can turn the Chromecast is on is by launching an app if chromecast.cast_type == pychromecast.const.CAST_TYPE_CHROMECAST: app_data = {"media_id": CAST_SPLASH, "media_type": "image/png"} - quick_play(chromecast, "default_media_receiver", app_data) + self._quick_play("default_media_receiver", app_data) else: - chromecast.start_app(pychromecast.config.APP_MEDIA_RECEIVER) + self._start_app(pychromecast.config.APP_MEDIA_RECEIVER) + @api_error def turn_off(self) -> None: """Turn off the cast device.""" self._get_chromecast().quit_app() + @api_error def mute_volume(self, mute: bool) -> None: """Mute the volume.""" self._get_chromecast().set_volume_muted(mute) + @api_error def set_volume_level(self, volume: float) -> None: """Set volume level, range 0..1.""" self._get_chromecast().set_volume(volume) + @api_error def media_play(self) -> None: """Send play command.""" media_controller = self._media_controller() media_controller.play() + @api_error def media_pause(self) -> None: """Send pause command.""" media_controller = self._media_controller() media_controller.pause() + @api_error def media_stop(self) -> None: """Send stop command.""" media_controller = self._media_controller() media_controller.stop() + @api_error def media_previous_track(self) -> None: """Send previous track command.""" media_controller = self._media_controller() media_controller.queue_prev() + @api_error def media_next_track(self) -> None: """Send next track command.""" media_controller = self._media_controller() media_controller.queue_next() + @api_error def media_seek(self, position: float) -> None: """Seek the media to a specific location.""" media_controller = self._media_controller() @@ -644,7 +698,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): if "app_id" in app_data: app_id = app_data.pop("app_id") _LOGGER.info("Starting Cast app by ID %s", app_id) - await self.hass.async_add_executor_job(chromecast.start_app, app_id) + await self.hass.async_add_executor_job(self._start_app, app_id) if app_data: _LOGGER.warning( "Extra keys %s were ignored. Please use app_name to cast media", @@ -655,7 +709,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): app_name = app_data.pop("app_name") try: await self.hass.async_add_executor_job( - quick_play, chromecast, app_name, app_data + self._quick_play, app_name, app_data ) except NotImplementedError: _LOGGER.error("App %s not supported", app_name) @@ -729,7 +783,7 @@ class CastMediaPlayerEntity(CastDevice, MediaPlayerEntity): app_data, ) await self.hass.async_add_executor_job( - quick_play, chromecast, "default_media_receiver", app_data + self._quick_play, "default_media_receiver", app_data ) def _media_status(self): diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json index 95c925a8d33..673a80b8d9f 100644 --- a/homeassistant/components/dhcp/manifest.json +++ b/homeassistant/components/dhcp/manifest.json @@ -15,7 +15,7 @@ ], "quality_scale": "internal", "requirements": [ - "aiodhcpwatcher==0.8.1", + "aiodhcpwatcher==0.8.2", "aiodiscover==1.6.1", "cached_ipaddress==0.3.0" ] diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index a08daee8961..ec9fb7018d6 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/calendar.google", "iot_class": "cloud_polling", "loggers": ["googleapiclient"], - "requirements": ["gcal-sync==6.0.3", "oauth2client==4.1.3", "ical==7.0.1"] + "requirements": ["gcal-sync==6.0.3", "oauth2client==4.1.3", "ical==7.0.3"] } diff --git a/homeassistant/components/husqvarna_automower/manifest.json b/homeassistant/components/husqvarna_automower/manifest.json index f00baad661d..ed013f2e0c2 100644 --- a/homeassistant/components/husqvarna_automower/manifest.json +++ b/homeassistant/components/husqvarna_automower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/husqvarna_automower", "iot_class": "cloud_push", "loggers": ["aioautomower"], - "requirements": ["aioautomower==2024.3.0"] + "requirements": ["aioautomower==2024.3.3"] } diff --git a/homeassistant/components/husqvarna_automower/sensor.py b/homeassistant/components/husqvarna_automower/sensor.py index 970c444737c..31eebde9c81 100644 --- a/homeassistant/components/husqvarna_automower/sensor.py +++ b/homeassistant/components/husqvarna_automower/sensor.py @@ -69,6 +69,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.SECONDS, suggested_unit_of_measurement=UnitOfTime.HOURS, + exists_fn=lambda data: data.statistics.total_charging_time is not None, value_fn=lambda data: data.statistics.total_charging_time, ), AutomowerSensorEntityDescription( @@ -79,6 +80,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.SECONDS, suggested_unit_of_measurement=UnitOfTime.HOURS, + exists_fn=lambda data: data.statistics.total_cutting_time is not None, value_fn=lambda data: data.statistics.total_cutting_time, ), AutomowerSensorEntityDescription( @@ -89,6 +91,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.SECONDS, suggested_unit_of_measurement=UnitOfTime.HOURS, + exists_fn=lambda data: data.statistics.total_running_time is not None, value_fn=lambda data: data.statistics.total_running_time, ), AutomowerSensorEntityDescription( @@ -99,6 +102,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.SECONDS, suggested_unit_of_measurement=UnitOfTime.HOURS, + exists_fn=lambda data: data.statistics.total_searching_time is not None, value_fn=lambda data: data.statistics.total_searching_time, ), AutomowerSensorEntityDescription( @@ -107,6 +111,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( icon="mdi:battery-sync-outline", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL, + exists_fn=lambda data: data.statistics.number_of_charging_cycles is not None, value_fn=lambda data: data.statistics.number_of_charging_cycles, ), AutomowerSensorEntityDescription( @@ -115,6 +120,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( icon="mdi:counter", entity_category=EntityCategory.DIAGNOSTIC, state_class=SensorStateClass.TOTAL, + exists_fn=lambda data: data.statistics.number_of_collisions is not None, value_fn=lambda data: data.statistics.number_of_collisions, ), AutomowerSensorEntityDescription( @@ -125,6 +131,7 @@ SENSOR_TYPES: tuple[AutomowerSensorEntityDescription, ...] = ( device_class=SensorDeviceClass.DISTANCE, native_unit_of_measurement=UnitOfLength.METERS, suggested_unit_of_measurement=UnitOfLength.KILOMETERS, + exists_fn=lambda data: data.statistics.total_drive_distance is not None, value_fn=lambda data: data.statistics.total_drive_distance, ), AutomowerSensorEntityDescription( diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index 25ec9f2ccc6..1c13970503d 100644 --- a/homeassistant/components/local_calendar/manifest.json +++ b/homeassistant/components/local_calendar/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/local_calendar", "iot_class": "local_polling", "loggers": ["ical"], - "requirements": ["ical==7.0.1"] + "requirements": ["ical==7.0.3"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index 81f0f9dc199..3bcb8af9f43 100644 --- a/homeassistant/components/local_todo/manifest.json +++ b/homeassistant/components/local_todo/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/local_todo", "iot_class": "local_polling", - "requirements": ["ical==7.0.1"] + "requirements": ["ical==7.0.3"] } diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 6b072457144..14faad789fe 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_polling", "loggers": ["pymodbus"], "quality_scale": "gold", - "requirements": ["pymodbus==3.6.5"] + "requirements": ["pymodbus==3.6.6"] } diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 33a74990928..6d2b6c48e1b 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -165,9 +165,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: except NotionError as err: raise ConfigEntryNotReady("Config entry failed to load") from err - # Always update the config entry with the latest refresh token and user UUID: - entry_updates["data"][CONF_REFRESH_TOKEN] = client.refresh_token - entry_updates["data"][CONF_USER_UUID] = client.user_uuid + # Update the Notion user UUID and refresh token if they've changed: + for key, value in ( + (CONF_REFRESH_TOKEN, client.refresh_token), + (CONF_USER_UUID, client.user_uuid), + ): + if entry.data[key] == value: + continue + entry_updates["data"][key] = value + + hass.config_entries.async_update_entry(entry, **entry_updates) @callback def async_save_refresh_token(refresh_token: str) -> None: @@ -180,12 +187,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: # Create a callback to save the refresh token when it changes: entry.async_on_unload(client.add_refresh_token_callback(async_save_refresh_token)) - # Save the client's refresh token if it's different than what we already have: - if (token := client.refresh_token) and token != entry.data[CONF_REFRESH_TOKEN]: - async_save_refresh_token(token) - - hass.config_entries.async_update_entry(entry, **entry_updates) - async def async_update() -> NotionData: """Get the latest data from the Notion API.""" data = NotionData(hass=hass, entry=entry) diff --git a/homeassistant/components/onewire/binary_sensor.py b/homeassistant/components/onewire/binary_sensor.py index e7e30588f8a..5cd3fa65a60 100644 --- a/homeassistant/components/onewire/binary_sensor.py +++ b/homeassistant/components/onewire/binary_sensor.py @@ -143,6 +143,8 @@ class OneWireBinarySensor(OneWireEntity, BinarySensorEntity): entity_description: OneWireBinarySensorEntityDescription @property - def is_on(self) -> bool: + def is_on(self) -> bool | None: """Return true if sensor is on.""" + if self._state is None: + return None return bool(self._state) diff --git a/homeassistant/components/onewire/switch.py b/homeassistant/components/onewire/switch.py index 00a3f8f65f4..c63198ccf05 100644 --- a/homeassistant/components/onewire/switch.py +++ b/homeassistant/components/onewire/switch.py @@ -204,8 +204,10 @@ class OneWireSwitch(OneWireEntity, SwitchEntity): entity_description: OneWireSwitchEntityDescription @property - def is_on(self) -> bool: - """Return true if sensor is on.""" + def is_on(self) -> bool | None: + """Return true if switch is on.""" + if self._state is None: + return None return bool(self._state) def turn_on(self, **kwargs: Any) -> None: diff --git a/homeassistant/components/rainforest_raven/manifest.json b/homeassistant/components/rainforest_raven/manifest.json index 3e463af9ba4..ad161d32201 100644 --- a/homeassistant/components/rainforest_raven/manifest.json +++ b/homeassistant/components/rainforest_raven/manifest.json @@ -6,7 +6,7 @@ "dependencies": ["usb"], "documentation": "https://www.home-assistant.io/integrations/rainforest_raven", "iot_class": "local_polling", - "requirements": ["aioraven==0.5.1"], + "requirements": ["aioraven==0.5.2"], "usb": [ { "vid": "0403", diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index d1e1c4f430c..efc17c48a06 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -37,12 +37,10 @@ from homeassistant.helpers.storage import Store from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( - CONF_COMMUNICATION_DELAY, DATA_COORDINATOR, DEFAULT_SCAN_INTERVAL, DOMAIN, EVENTS_COORDINATOR, - MAX_COMMUNICATION_DELAY, TYPE_LOCAL, ) @@ -85,31 +83,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_setup_local_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data = entry.data - comm_delay = initial_delay = data.get(CONF_COMMUNICATION_DELAY, 0) + risco = RiscoLocal(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN]) - while True: - risco = RiscoLocal( - data[CONF_HOST], - data[CONF_PORT], - data[CONF_PIN], - communication_delay=comm_delay, - ) - try: - await risco.connect() - except CannotConnectError as error: - if comm_delay >= MAX_COMMUNICATION_DELAY: - raise ConfigEntryNotReady() from error - comm_delay += 1 - except UnauthorizedError: - _LOGGER.exception("Failed to login to Risco cloud") - return False - else: - break - - if comm_delay > initial_delay: - new_data = data.copy() - new_data[CONF_COMMUNICATION_DELAY] = comm_delay - hass.config_entries.async_update_entry(entry, data=new_data) + try: + await risco.connect() + except CannotConnectError as error: + raise ConfigEntryNotReady() from error + except UnauthorizedError: + _LOGGER.exception("Failed to login to Risco cloud") + return False async def _error(error: Exception) -> None: _LOGGER.error("Error in Risco library: %s", error) diff --git a/homeassistant/components/risco/manifest.json b/homeassistant/components/risco/manifest.json index ca28af3d8e5..b5d8c4442fd 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.5.8"] + "requirements": ["pyrisco==0.5.10"] } diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index 4afe66199f0..d41282b1f0b 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -216,7 +216,7 @@ class ShellyBlockCoordinator(ShellyCoordinatorBase[BlockDevice]): # Check for input events and config change cfg_changed = 0 for block in self.device.blocks: - if block.type == "device": + if block.type == "device" and block.cfgChanged is not None: cfg_changed = block.cfgChanged # Shelly TRV sends information about changing the configuration for no diff --git a/homeassistant/components/shelly/sensor.py b/homeassistant/components/shelly/sensor.py index b88b6886b84..82fc4fe6d78 100644 --- a/homeassistant/components/shelly/sensor.py +++ b/homeassistant/components/shelly/sensor.py @@ -941,6 +941,7 @@ RPC_SENSORS: Final = { device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, entity_category=EntityCategory.DIAGNOSTIC, + removal_condition=lambda _config, status, key: (status[key]["battery"] is None), ), "voltmeter": RpcSensorDescription( key="voltmeter", diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index cd6c1dd9152..c4aa82f2a74 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/snmp", "iot_class": "local_polling", "loggers": ["pyasn1", "pysmi", "pysnmp"], - "requirements": ["pysnmp-lextudio==6.0.9"] + "requirements": ["pysnmp-lextudio==6.0.11"] } diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index cdbc041f535..d3ec29ae356 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -2,7 +2,6 @@ import logging from typing import Any -import PyTado import voluptuous as vol from homeassistant.components.water_heater import ( @@ -29,8 +28,6 @@ from .const import ( DATA, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED, - TADO_DEFAULT_MAX_TEMP, - TADO_DEFAULT_MIN_TEMP, TYPE_HOT_WATER, ) from .entity import TadoZoneEntity @@ -133,8 +130,8 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): zone_name: str, zone_id: int, supports_temperature_control: bool, - min_temp: float | None = None, - max_temp: float | None = None, + min_temp, + max_temp, ) -> None: """Initialize of Tado water heater entity.""" self._tado = tado @@ -146,8 +143,8 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): self._device_is_active = False self._supports_temperature_control = supports_temperature_control - self._min_temperature = min_temp or TADO_DEFAULT_MIN_TEMP - self._max_temperature = max_temp or TADO_DEFAULT_MAX_TEMP + self._min_temperature = min_temp + self._max_temperature = max_temp self._target_temp: float | None = None @@ -157,7 +154,7 @@ class TadoWaterHeater(TadoZoneEntity, WaterHeaterEntity): self._current_tado_hvac_mode = CONST_MODE_SMART_SCHEDULE self._overlay_mode = CONST_MODE_SMART_SCHEDULE - self._tado_zone_data: PyTado.TadoZone = {} + self._tado_zone_data: Any = None async def async_added_to_hass(self) -> None: """Register for sensor updates.""" diff --git a/homeassistant/components/tedee/manifest.json b/homeassistant/components/tedee/manifest.json index 1f2a2405a44..db3a88f3113 100644 --- a/homeassistant/components/tedee/manifest.json +++ b/homeassistant/components/tedee/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/tedee", "iot_class": "local_push", "loggers": ["pytedee_async"], - "requirements": ["pytedee-async==0.2.16"] + "requirements": ["pytedee-async==0.2.17"] } diff --git a/homeassistant/components/template/coordinator.py b/homeassistant/components/template/coordinator.py index 047d58d9208..a5a38bf7b1d 100644 --- a/homeassistant/components/template/coordinator.py +++ b/homeassistant/components/template/coordinator.py @@ -3,7 +3,7 @@ from collections.abc import Callable import logging from homeassistant.const import EVENT_HOMEASSISTANT_START -from homeassistant.core import CoreState, callback +from homeassistant.core import Context, CoreState, callback from homeassistant.helpers import discovery, trigger as trigger_helper from homeassistant.helpers.script import Script from homeassistant.helpers.typing import ConfigType @@ -90,7 +90,10 @@ class TriggerUpdateCoordinator(DataUpdateCoordinator): ) async def _handle_triggered_with_script(self, run_variables, context=None): - if script_result := await self._script.async_run(run_variables, context): + # Create a context referring to the trigger context. + trigger_context_id = None if context is None else context.id + script_context = Context(parent_id=trigger_context_id) + if script_result := await self._script.async_run(run_variables, script_context): run_variables = script_result.variables self._handle_triggered(run_variables, context) diff --git a/homeassistant/components/traccar_server/diagnostics.py b/homeassistant/components/traccar_server/diagnostics.py index 15b94a2b880..f4b1cc799cb 100644 --- a/homeassistant/components/traccar_server/diagnostics.py +++ b/homeassistant/components/traccar_server/diagnostics.py @@ -12,7 +12,12 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from .const import DOMAIN from .coordinator import TraccarServerCoordinator -TO_REDACT = {CONF_ADDRESS, CONF_LATITUDE, CONF_LONGITUDE} +TO_REDACT = { + CONF_ADDRESS, + CONF_LATITUDE, + CONF_LONGITUDE, + "area", # This is the polygon area of a geofence +} async def async_get_config_entry_diagnostics( diff --git a/homeassistant/components/unifiprotect/__init__.py b/homeassistant/components/unifiprotect/__init__.py index c4a6bc88068..076095a16b3 100644 --- a/homeassistant/components/unifiprotect/__init__.py +++ b/homeassistant/components/unifiprotect/__init__.py @@ -6,6 +6,7 @@ import logging from aiohttp.client_exceptions import ServerDisconnectedError from pyunifiprotect.data import Bootstrap +from pyunifiprotect.data.types import FirmwareReleaseChannel from pyunifiprotect.exceptions import ClientError, NotAuthorized # Import the test_util.anonymize module from the pyunifiprotect package @@ -111,19 +112,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, data_service.async_stop) ) - if ( - not entry.options.get(CONF_ALLOW_EA, False) - and await nvr_info.get_is_prerelease() + if not entry.options.get(CONF_ALLOW_EA, False) and ( + await nvr_info.get_is_prerelease() + or nvr_info.release_channel != FirmwareReleaseChannel.RELEASE ): ir.async_create_issue( hass, DOMAIN, - "ea_warning", + "ea_channel_warning", is_fixable=True, is_persistent=True, learn_more_url="https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access", severity=IssueSeverity.WARNING, - translation_key="ea_warning", + translation_key="ea_channel_warning", translation_placeholders={"version": str(nvr_info.version)}, data={"entry_id": entry.entry_id}, ) @@ -149,7 +150,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: "version": str(nvr_info.version), }, ) - ir.async_delete_issue(hass, DOMAIN, "ea_warning") + ir.async_delete_issue(hass, DOMAIN, "ea_channel_warning") _LOGGER.exception("Error setting up UniFi Protect integration: %s", err) raise diff --git a/homeassistant/components/unifiprotect/const.py b/homeassistant/components/unifiprotect/const.py index 2982ca29c4a..39be5f0e7cb 100644 --- a/homeassistant/components/unifiprotect/const.py +++ b/homeassistant/components/unifiprotect/const.py @@ -24,7 +24,7 @@ CONF_DISABLE_RTSP = "disable_rtsp" CONF_ALL_UPDATES = "all_updates" CONF_OVERRIDE_CHOST = "override_connection_host" CONF_MAX_MEDIA = "max_media" -CONF_ALLOW_EA = "allow_ea" +CONF_ALLOW_EA = "allow_ea_channel" CONFIG_OPTIONS = [ CONF_ALL_UPDATES, diff --git a/homeassistant/components/unifiprotect/data.py b/homeassistant/components/unifiprotect/data.py index 2825c2a4f3c..b82e9ff37f1 100644 --- a/homeassistant/components/unifiprotect/data.py +++ b/homeassistant/components/unifiprotect/data.py @@ -19,6 +19,7 @@ from pyunifiprotect.data import ( WSSubscriptionMessage, ) from pyunifiprotect.exceptions import ClientError, NotAuthorized +from pyunifiprotect.utils import log_event from homeassistant.config_entries import ConfigEntry from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback @@ -41,11 +42,6 @@ from .utils import async_dispatch_id as _ufpd, async_get_devices_by_type _LOGGER = logging.getLogger(__name__) ProtectDeviceType = ProtectAdoptableDeviceModel | NVR -SMART_EVENTS = { - EventType.SMART_DETECT, - EventType.SMART_AUDIO_DETECT, - EventType.SMART_DETECT_LINE, -} @callback @@ -230,26 +226,7 @@ class ProtectData: # trigger updates for camera that the event references elif isinstance(obj, Event): # type: ignore[unreachable] if _LOGGER.isEnabledFor(logging.DEBUG): - _LOGGER.debug("event WS msg: %s", obj.dict()) - if obj.type in SMART_EVENTS: - if obj.camera is not None: - if obj.end is None: - _LOGGER.debug( - "%s (%s): New smart detection started for %s (%s)", - obj.camera.name, - obj.camera.mac, - obj.smart_detect_types, - obj.id, - ) - else: - _LOGGER.debug( - "%s (%s): Smart detection ended for %s (%s)", - obj.camera.name, - obj.camera.mac, - obj.smart_detect_types, - obj.id, - ) - + log_event(obj) if obj.type is EventType.DEVICE_ADOPTED: if obj.metadata is not None and obj.metadata.device_id is not None: device = self.api.bootstrap.get_device_from_id( diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index eba2b934e05..1eb37befca0 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -42,7 +42,7 @@ "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==4.23.3", "unifi-discovery==1.1.8"], + "requirements": ["pyunifiprotect==5.0.2", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifiprotect/repairs.py b/homeassistant/components/unifiprotect/repairs.py index ddc0a257c14..254984da515 100644 --- a/homeassistant/components/unifiprotect/repairs.py +++ b/homeassistant/components/unifiprotect/repairs.py @@ -6,6 +6,7 @@ import logging from typing import cast from pyunifiprotect import ProtectApiClient +from pyunifiprotect.data.types import FirmwareReleaseChannel import voluptuous as vol from homeassistant import data_entry_flow @@ -68,7 +69,7 @@ class EAConfirm(ProtectRepair): ) nvr = await self._api.get_nvr() - if await nvr.get_is_prerelease(): + if nvr.release_channel != FirmwareReleaseChannel.RELEASE: return await self.async_step_confirm() await self.hass.config_entries.async_reload(self._entry.entry_id) return self.async_create_entry(data={}) @@ -124,7 +125,7 @@ async def async_create_fix_flow( data: dict[str, str | int | float | None] | None, ) -> RepairsFlow: """Create flow.""" - if data is not None and issue_id == "ea_warning": + if data is not None and issue_id == "ea_channel_warning": entry_id = cast(str, data["entry_id"]) if (entry := hass.config_entries.async_get_entry(entry_id)) is not None: api = async_create_api_client(hass, entry) diff --git a/homeassistant/components/unifiprotect/select.py b/homeassistant/components/unifiprotect/select.py index 5611ba79eca..e07a174659c 100644 --- a/homeassistant/components/unifiprotect/select.py +++ b/homeassistant/components/unifiprotect/select.py @@ -45,6 +45,7 @@ INFRARED_MODES = [ {"id": IRLEDMode.AUTO.value, "name": "Auto"}, {"id": IRLEDMode.ON.value, "name": "Always Enable"}, {"id": IRLEDMode.AUTO_NO_LED.value, "name": "Auto (Filter Only, no LED's)"}, + {"id": IRLEDMode.CUSTOM.value, "name": "Auto (Custom Lux)"}, {"id": IRLEDMode.OFF.value, "name": "Always Disable"}, ] diff --git a/homeassistant/components/unifiprotect/strings.json b/homeassistant/components/unifiprotect/strings.json index eccf5829332..bdc46217ab5 100644 --- a/homeassistant/components/unifiprotect/strings.json +++ b/homeassistant/components/unifiprotect/strings.json @@ -61,16 +61,16 @@ } }, "issues": { - "ea_warning": { - "title": "UniFi Protect v{version} is an Early Access version", + "ea_channel_warning": { + "title": "UniFi Protect Early Access enabled", "fix_flow": { "step": { "start": { - "title": "v{version} is an Early Access version", - "description": "You are using v{version} of UniFi Protect which is an Early Access version. [Early Access versions are not supported by Home Assistant](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access) and it is recommended to go back to a stable release as soon as possible.\n\nBy submitting this form you have either [downgraded UniFi Protect](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) or you agree to run an unsupported version of UniFi Protect." + "title": "UniFi Protect Early Access enabled", + "description": "You are either running an Early Access version of UniFi Protect (v{version}) or opt-ed into a release channel that is not the Official Release Channel. [Home Assistant does not support Early Access versions](https://www.home-assistant.io/integrations/unifiprotect#about-unifi-early-access), so you should immediately switch to the Official Release Channel. Accidentally upgrading to an Early Access version can break your UniFi Protect integration.\n\nBy submitting this form, you have switched back to the Official Release Channel or agree to run an unsupported version of UniFi Protect, which may break your Home Assistant integration at any time." }, "confirm": { - "title": "[%key:component::unifiprotect::issues::ea_warning::fix_flow::step::start::title%]", + "title": "[%key:component::unifiprotect::issues::ea_channel_warning::fix_flow::step::start::title%]", "description": "Are you sure you want to run unsupported versions of UniFi Protect? This may cause your Home Assistant integration to break." } } @@ -78,7 +78,7 @@ }, "ea_setup_failed": { "title": "Setup error using Early Access version", - "description": "You are using v{version} of UniFi Protect which is an Early Access version. An unrecoverable error occurred while trying to load the integration. Please [downgrade to a stable version](https://www.home-assistant.io/integrations/unifiprotect#downgrading-unifi-protect) of UniFi Protect to continue using the integration.\n\nError: {error}" + "description": "You are using v{version} of UniFi Protect which is an Early Access version. An unrecoverable error occurred while trying to load the integration. Please restore a backup of a stable release of UniFi Protect to continue using the integration.\n\nError: {error}" }, "cloud_user": { "title": "Ubiquiti Cloud Users are not Supported", diff --git a/homeassistant/components/utility_meter/manifest.json b/homeassistant/components/utility_meter/manifest.json index 11aaf5307c8..25e803e6a2d 100644 --- a/homeassistant/components/utility_meter/manifest.json +++ b/homeassistant/components/utility_meter/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["croniter"], "quality_scale": "internal", - "requirements": ["croniter==1.0.6"] + "requirements": ["croniter==2.0.2"] } diff --git a/homeassistant/const.py b/homeassistant/const.py index 847387e76ae..f5efc37f352 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 3 -PATCH_VERSION: Final = "1" +PATCH_VERSION: Final = "2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1b850316b91..c92dad2ae35 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,6 +1,6 @@ # Automatically generated by gen_requirements_all.py, do not edit -aiodhcpwatcher==0.8.1 +aiodhcpwatcher==0.8.2 aiodiscover==1.6.1 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.3.1 diff --git a/pyproject.toml b/pyproject.toml index d8a1545fbb3..496a29eb31c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.3.1" +version = "2024.3.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" diff --git a/requirements_all.txt b/requirements_all.txt index add077d5b82..fe927e87424 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -54,7 +54,7 @@ ProgettiHWSW==0.1.3 # PyBluez==0.22 # homeassistant.components.cast -PyChromecast==14.0.0 +PyChromecast==14.0.1 # homeassistant.components.flick_electric PyFlick==0.0.2 @@ -206,7 +206,7 @@ aioaseko==0.0.2 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.3.0 +aioautomower==2024.3.3 # homeassistant.components.azure_devops aioazuredevops==1.3.5 @@ -221,7 +221,7 @@ aiobotocore==2.9.1 aiocomelit==0.9.0 # homeassistant.components.dhcp -aiodhcpwatcher==0.8.1 +aiodhcpwatcher==0.8.2 # homeassistant.components.dhcp aiodiscover==1.6.1 @@ -350,7 +350,7 @@ aiopyarr==23.4.0 aioqsw==0.3.5 # homeassistant.components.rainforest_raven -aioraven==0.5.1 +aioraven==0.5.2 # homeassistant.components.recollect_waste aiorecollect==2023.09.0 @@ -514,7 +514,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==54 +axis==57 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 @@ -669,7 +669,7 @@ connect-box==0.2.8 construct==2.10.68 # homeassistant.components.utility_meter -croniter==1.0.6 +croniter==2.0.2 # homeassistant.components.crownstone crownstone-cloud==1.4.9 @@ -1115,7 +1115,7 @@ ibmiotf==0.3.4 # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==7.0.1 +ical==7.0.3 # homeassistant.components.ping icmplib==3.0 @@ -1971,7 +1971,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.6.5 +pymodbus==3.6.6 # homeassistant.components.monoprice pymonoprice==0.4 @@ -2090,7 +2090,7 @@ pyrecswitch==1.0.2 pyrepetierng==0.1.0 # homeassistant.components.risco -pyrisco==0.5.8 +pyrisco==0.5.10 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 @@ -2155,7 +2155,7 @@ pysmartthings==0.7.8 pysml==0.0.12 # homeassistant.components.snmp -pysnmp-lextudio==6.0.9 +pysnmp-lextudio==6.0.11 # homeassistant.components.snooz pysnooz==0.8.6 @@ -2182,7 +2182,7 @@ pyswitchbee==1.8.0 pytautulli==23.1.1 # homeassistant.components.tedee -pytedee-async==0.2.16 +pytedee-async==0.2.17 # homeassistant.components.tfiac pytfiac==0.4 @@ -2340,7 +2340,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.23.3 +pyunifiprotect==5.0.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a50fefa2290..6307d864186 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -45,7 +45,7 @@ PlexAPI==4.15.10 ProgettiHWSW==0.1.3 # homeassistant.components.cast -PyChromecast==14.0.0 +PyChromecast==14.0.1 # homeassistant.components.flick_electric PyFlick==0.0.2 @@ -185,7 +185,7 @@ aioaseko==0.0.2 aioasuswrt==1.4.0 # homeassistant.components.husqvarna_automower -aioautomower==2024.3.0 +aioautomower==2024.3.3 # homeassistant.components.azure_devops aioazuredevops==1.3.5 @@ -200,7 +200,7 @@ aiobotocore==2.9.1 aiocomelit==0.9.0 # homeassistant.components.dhcp -aiodhcpwatcher==0.8.1 +aiodhcpwatcher==0.8.2 # homeassistant.components.dhcp aiodiscover==1.6.1 @@ -323,7 +323,7 @@ aiopyarr==23.4.0 aioqsw==0.3.5 # homeassistant.components.rainforest_raven -aioraven==0.5.1 +aioraven==0.5.2 # homeassistant.components.recollect_waste aiorecollect==2023.09.0 @@ -454,7 +454,7 @@ auroranoaa==0.0.3 aurorapy==0.2.7 # homeassistant.components.axis -axis==54 +axis==57 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 @@ -553,7 +553,7 @@ colorthief==0.2.1 construct==2.10.68 # homeassistant.components.utility_meter -croniter==1.0.6 +croniter==2.0.2 # homeassistant.components.crownstone crownstone-cloud==1.4.9 @@ -905,7 +905,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==7.0.1 +ical==7.0.3 # homeassistant.components.ping icmplib==3.0 @@ -1525,7 +1525,7 @@ pymeteoclimatic==0.1.0 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.6.5 +pymodbus==3.6.6 # homeassistant.components.monoprice pymonoprice==0.4 @@ -1617,7 +1617,7 @@ pyqwikswitch==0.93 pyrainbird==4.0.2 # homeassistant.components.risco -pyrisco==0.5.8 +pyrisco==0.5.10 # homeassistant.components.rituals_perfume_genie pyrituals==0.0.6 @@ -1673,7 +1673,7 @@ pysmartthings==0.7.8 pysml==0.0.12 # homeassistant.components.snmp -pysnmp-lextudio==6.0.9 +pysnmp-lextudio==6.0.11 # homeassistant.components.snooz pysnooz==0.8.6 @@ -1697,7 +1697,7 @@ pyswitchbee==1.8.0 pytautulli==23.1.1 # homeassistant.components.tedee -pytedee-async==0.2.16 +pytedee-async==0.2.17 # homeassistant.components.motionmount python-MotionMount==0.3.1 @@ -1801,7 +1801,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==4.23.3 +pyunifiprotect==5.0.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index f83f882b8a0..3d83ecfcb7c 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -93,10 +93,11 @@ async def test_invalid_hostname(hass: HomeAssistant) -> None: assert result["errors"] == {CONF_HOST: "wrong_host"} -async def test_connection_error(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("exc", [ConnectionError, TimeoutError]) +async def test_connection_error(hass: HomeAssistant, exc: Exception) -> None: """Test connection to host error.""" with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data", side_effect=ConnectionError() + "brother.Brother._get_data", side_effect=exc ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG @@ -147,10 +148,11 @@ async def test_device_exists_abort(hass: HomeAssistant) -> None: assert result["reason"] == "already_configured" -async def test_zeroconf_snmp_error(hass: HomeAssistant) -> None: - """Test we abort zeroconf flow on SNMP error.""" +@pytest.mark.parametrize("exc", [ConnectionError, TimeoutError, SnmpError("error")]) +async def test_zeroconf_exception(hass: HomeAssistant, exc: Exception) -> None: + """Test we abort zeroconf flow on exception.""" with patch("brother.Brother.initialize"), patch( - "brother.Brother._get_data", side_effect=SnmpError("error") + "brother.Brother._get_data", side_effect=exc ): result = await hass.config_entries.flow.async_init( DOMAIN, diff --git a/tests/components/husqvarna_automower/test_sensor.py b/tests/components/husqvarna_automower/test_sensor.py index 1775caac7f8..feae870478e 100644 --- a/tests/components/husqvarna_automower/test_sensor.py +++ b/tests/components/husqvarna_automower/test_sensor.py @@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, patch from aioautomower.model import MowerModes from aioautomower.utils import mower_list_to_dictionary_dataclass from freezegun.api import FrozenDateTimeFactory +import pytest from syrupy import SnapshotAssertion from homeassistant.components.husqvarna_automower.const import DOMAIN @@ -59,17 +60,36 @@ async def test_cutting_blade_usage_time_sensor( assert state is not None assert state.state == "0.034" - entry = hass.config_entries.async_entries(DOMAIN)[0] - await hass.config_entries.async_remove(entry.entry_id) - await hass.async_block_till_done() + +@pytest.mark.parametrize( + ("sensor_to_test"), + [ + ("cutting_blade_usage_time"), + ("number_of_charging_cycles"), + ("number_of_collisions"), + ("total_charging_time"), + ("total_cutting_time"), + ("total_running_time"), + ("total_searching_time"), + ("total_drive_distance"), + ], +) +async def test_statistics_not_available( + hass: HomeAssistant, + mock_automower_client: AsyncMock, + mock_config_entry: MockConfigEntry, + sensor_to_test: str, +) -> None: + """Test if this sensor is only added, if data is available.""" + values = mower_list_to_dictionary_dataclass( load_json_value_fixture("mower.json", DOMAIN) ) - delattr(values[TEST_MOWER_ID].statistics, "cutting_blade_usage_time") + delattr(values[TEST_MOWER_ID].statistics, sensor_to_test) mock_automower_client.get_status.return_value = values await setup_integration(hass, mock_config_entry) - state = hass.states.get("sensor.test_mower_1_cutting_blade_usage_time") + state = hass.states.get(f"sensor.test_mower_1_{sensor_to_test}") assert state is None diff --git a/tests/components/local_todo/snapshots/test_todo.ambr b/tests/components/local_todo/snapshots/test_todo.ambr index db4403f301c..15a44ff8c27 100644 --- a/tests/components/local_todo/snapshots/test_todo.ambr +++ b/tests/components/local_todo/snapshots/test_todo.ambr @@ -22,6 +22,16 @@ list([ ]) # --- +# name: test_parse_existing_ics[invalid_dtstart_tzname] + list([ + dict({ + 'due': '2023-10-24T11:30:00', + 'status': 'needs_action', + 'summary': 'Task', + 'uid': '077cb7f2-6c89-11ee-b2a9-0242ac110002', + }), + ]) +# --- # name: test_parse_existing_ics[migrate_legacy_due] list([ dict({ diff --git a/tests/components/local_todo/test_todo.py b/tests/components/local_todo/test_todo.py index 231f56b0afb..760b0260dbb 100644 --- a/tests/components/local_todo/test_todo.py +++ b/tests/components/local_todo/test_todo.py @@ -671,6 +671,28 @@ async def test_move_item_previous_unknown( ), "1", ), + ( + textwrap.dedent( + """\ + BEGIN:VCALENDAR + PRODID:-//homeassistant.io//local_todo 2.0//EN + VERSION:2.0 + BEGIN:VTODO + DTSTAMP:20231024T014011 + UID:077cb7f2-6c89-11ee-b2a9-0242ac110002 + CREATED:20231017T010348 + LAST-MODIFIED:20231024T014011 + SEQUENCE:1 + STATUS:NEEDS-ACTION + SUMMARY:Task + DUE:20231024T113000 + DTSTART;TZID=CST:20231024T113000 + END:VTODO + END:VCALENDAR + """ + ), + "1", + ), ], ids=( "empty", @@ -679,6 +701,7 @@ async def test_move_item_previous_unknown( "needs_action", "migrate_legacy_due", "due", + "invalid_dtstart_tzname", ), ) async def test_parse_existing_ics( diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index 3da37a72459..cb9fca54f3d 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -155,7 +155,9 @@ MOCK_OWPROXY_DEVICES = { {ATTR_INJECT_READS: b" 1"}, {ATTR_INJECT_READS: b" 0"}, {ATTR_INJECT_READS: b" 0"}, - {ATTR_INJECT_READS: b" 0"}, + { + ATTR_INJECT_READS: ProtocolError, + }, {ATTR_INJECT_READS: b" 0"}, {ATTR_INJECT_READS: b" 0"}, {ATTR_INJECT_READS: b" 0"}, @@ -165,7 +167,9 @@ MOCK_OWPROXY_DEVICES = { {ATTR_INJECT_READS: b" 1"}, {ATTR_INJECT_READS: b" 0"}, {ATTR_INJECT_READS: b" 1"}, - {ATTR_INJECT_READS: b" 0"}, + { + ATTR_INJECT_READS: ProtocolError, + }, {ATTR_INJECT_READS: b" 1"}, {ATTR_INJECT_READS: b" 0"}, {ATTR_INJECT_READS: b" 1"}, diff --git a/tests/components/onewire/snapshots/test_binary_sensor.ambr b/tests/components/onewire/snapshots/test_binary_sensor.ambr index 2aa415f0345..0523c969ade 100644 --- a/tests/components/onewire/snapshots/test_binary_sensor.ambr +++ b/tests/components/onewire/snapshots/test_binary_sensor.ambr @@ -851,13 +851,13 @@ 'attributes': ReadOnlyDict({ 'device_file': '/29.111111111111/sensed.3', 'friendly_name': '29.111111111111 Sensed 3', - 'raw_value': 0.0, + 'raw_value': None, }), 'context': , 'entity_id': 'binary_sensor.29_111111111111_sensed_3', 'last_changed': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', }), StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/onewire/snapshots/test_switch.ambr b/tests/components/onewire/snapshots/test_switch.ambr index 8fbb977948b..4f6498419a9 100644 --- a/tests/components/onewire/snapshots/test_switch.ambr +++ b/tests/components/onewire/snapshots/test_switch.ambr @@ -1271,13 +1271,13 @@ 'attributes': ReadOnlyDict({ 'device_file': '/29.111111111111/PIO.3', 'friendly_name': '29.111111111111 Programmed input-output 3', - 'raw_value': 0.0, + 'raw_value': None, }), 'context': , 'entity_id': 'switch.29_111111111111_programmed_input_output_3', 'last_changed': , 'last_updated': , - 'state': 'off', + 'state': 'unknown', }), StateSnapshot({ 'attributes': ReadOnlyDict({ diff --git a/tests/components/risco/conftest.py b/tests/components/risco/conftest.py index a8a764cd502..e08e6b29852 100644 --- a/tests/components/risco/conftest.py +++ b/tests/components/risco/conftest.py @@ -171,16 +171,6 @@ def connect_with_error(exception): yield -@pytest.fixture -def connect_with_single_error(exception): - """Fixture to simulate error on connect.""" - with patch( - "homeassistant.components.risco.RiscoLocal.connect", - side_effect=[exception, None], - ): - yield - - @pytest.fixture async def setup_risco_local(hass, local_config_entry): """Set up a local Risco integration for testing.""" diff --git a/tests/components/risco/test_init.py b/tests/components/risco/test_init.py deleted file mode 100644 index a1a9e3bd6a7..00000000000 --- a/tests/components/risco/test_init.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Tests for the Risco initialization.""" -import pytest - -from homeassistant.components.risco import CannotConnectError -from homeassistant.components.risco.const import CONF_COMMUNICATION_DELAY -from homeassistant.core import HomeAssistant - - -@pytest.mark.parametrize("exception", [CannotConnectError]) -async def test_single_error_on_connect( - hass: HomeAssistant, connect_with_single_error, local_config_entry -) -> None: - """Test single error on connect to validate communication delay update from 0 (default) to 1.""" - expected_data = { - **local_config_entry.data, - **{"type": "local", CONF_COMMUNICATION_DELAY: 1}, - } - - await hass.config_entries.async_setup(local_config_entry.entry_id) - await hass.async_block_till_done() - assert local_config_entry.data == expected_data diff --git a/tests/components/shelly/test_coordinator.py b/tests/components/shelly/test_coordinator.py index 67df09a5adb..940ab2123f0 100644 --- a/tests/components/shelly/test_coordinator.py +++ b/tests/components/shelly/test_coordinator.py @@ -67,6 +67,18 @@ async def test_block_reload_on_cfg_change( mock_block_device.mock_update() await hass.async_block_till_done() + # Make sure cfgChanged with None is ignored + monkeypatch.setattr(mock_block_device.blocks[DEVICE_BLOCK_ID], "cfgChanged", None) + mock_block_device.mock_update() + await hass.async_block_till_done() + + # Wait for debouncer + freezer.tick(timedelta(seconds=ENTRY_RELOAD_COOLDOWN)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert hass.states.get("switch.test_name_channel_1") is not None + # Generate config change from switch to light monkeypatch.setitem( mock_block_device.settings["relays"][RELAY_BLOCK_ID], "appliance_type", "light" diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index 314218fc849..8026618e7cd 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -29,6 +29,7 @@ import homeassistant.util.dt as dt_util from tests.common import ( MockConfigEntry, assert_setup_component, + async_capture_events, async_fire_time_changed, mock_restore_cache_with_extra_data, ) @@ -1848,6 +1849,7 @@ async def test_trigger_entity_restore_state( "my_variable": "{{ trigger.event.data.beer + 1 }}" }, }, + {"event": "test_event2", "event_data": {"hello": "world"}}, ], "sensor": [ { @@ -1864,6 +1866,10 @@ async def test_trigger_action( hass: HomeAssistant, start_ha, entity_registry: er.EntityRegistry ) -> None: """Test trigger entity with an action works.""" + event = "test_event2" + context = Context() + events = async_capture_events(hass, event) + state = hass.states.get("sensor.hello_name") assert state is not None assert state.state == STATE_UNKNOWN @@ -1875,3 +1881,6 @@ async def test_trigger_action( state = hass.states.get("sensor.hello_name") assert state.state == "3" assert state.context is context + + assert len(events) == 1 + assert events[0].context.parent_id == context.id diff --git a/tests/components/traccar_server/snapshots/test_diagnostics.ambr b/tests/components/traccar_server/snapshots/test_diagnostics.ambr index 1726f1c3d45..20d01e427ea 100644 --- a/tests/components/traccar_server/snapshots/test_diagnostics.ambr +++ b/tests/components/traccar_server/snapshots/test_diagnostics.ambr @@ -34,7 +34,7 @@ 'uniqueId': 'abc123', }), 'geofence': dict({ - 'area': 'string', + 'area': '**REDACTED**', 'attributes': dict({ }), 'calendarId': 0, @@ -134,7 +134,7 @@ 'uniqueId': 'abc123', }), 'geofence': dict({ - 'area': 'string', + 'area': '**REDACTED**', 'attributes': dict({ }), 'calendarId': 0, diff --git a/tests/components/unifiprotect/test_config_flow.py b/tests/components/unifiprotect/test_config_flow.py index a9ff98fc681..04eee1b8319 100644 --- a/tests/components/unifiprotect/test_config_flow.py +++ b/tests/components/unifiprotect/test_config_flow.py @@ -318,7 +318,7 @@ async def test_form_options(hass: HomeAssistant, ufp_client: ProtectApiClient) - "disable_rtsp": True, "override_connection_host": True, "max_media": 1000, - "allow_ea": False, + "allow_ea_channel": False, } await hass.config_entries.async_unload(mock_config.entry_id) diff --git a/tests/components/unifiprotect/test_repairs.py b/tests/components/unifiprotect/test_repairs.py index 12701604306..0c939a9791d 100644 --- a/tests/components/unifiprotect/test_repairs.py +++ b/tests/components/unifiprotect/test_repairs.py @@ -45,12 +45,14 @@ async def test_ea_warning_ignore( assert len(msg["result"]["issues"]) > 0 issue = None for i in msg["result"]["issues"]: - if i["issue_id"] == "ea_warning": + if i["issue_id"] == "ea_channel_warning": issue = i assert issue is not None url = RepairsFlowIndexView.url - resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "ea_warning"}) + resp = await client.post( + url, json={"handler": DOMAIN, "issue_id": "ea_channel_warning"} + ) assert resp.status == HTTPStatus.OK data = await resp.json() @@ -103,12 +105,14 @@ async def test_ea_warning_fix( assert len(msg["result"]["issues"]) > 0 issue = None for i in msg["result"]["issues"]: - if i["issue_id"] == "ea_warning": + if i["issue_id"] == "ea_channel_warning": issue = i assert issue is not None url = RepairsFlowIndexView.url - resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "ea_warning"}) + resp = await client.post( + url, json={"handler": DOMAIN, "issue_id": "ea_channel_warning"} + ) assert resp.status == HTTPStatus.OK data = await resp.json() @@ -121,8 +125,9 @@ async def test_ea_warning_fix( new_nvr = copy(ufp.api.bootstrap.nvr) new_nvr.version = Version("2.2.6") + new_nvr.release_channel = "release" mock_msg = Mock() - mock_msg.changed_data = {"version": "2.2.6"} + mock_msg.changed_data = {"version": "2.2.6", "releaseChannel": "release"} mock_msg.new_obj = new_nvr ufp.api.bootstrap.nvr = new_nvr From 93289c9f09d55195f12805c31e8b6d49b38db3f5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Mar 2024 20:13:44 +0100 Subject: [PATCH 96/99] Bump home-assistant/builder from 2024.01.0 to 2024.03.5 (#113887) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/builder.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 333c31ce841..6446d41fb71 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -207,7 +207,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2024.01.0 + uses: home-assistant/builder@2024.03.5 with: args: | $BUILD_ARGS \ @@ -284,7 +284,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build base image - uses: home-assistant/builder@2024.01.0 + uses: home-assistant/builder@2024.03.5 with: args: | $BUILD_ARGS \ From d1644f371391e286f0f8ddfc097cfa99085d15b9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 22 Mar 2024 13:27:06 +0100 Subject: [PATCH 97/99] Update cosign to 2.2.3 (#113996) --- .github/workflows/builder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 6446d41fb71..2d6c2521171 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -343,7 +343,7 @@ jobs: - name: Install Cosign uses: sigstore/cosign-installer@v3.4.0 with: - cosign-release: "v2.0.2" + cosign-release: "v2.2.3" - name: Login to DockerHub uses: docker/login-action@v3.0.0 From 32b4814f2a66a745f8d06bc84e1267d8caa5464f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 22 Mar 2024 16:35:21 +0100 Subject: [PATCH 98/99] Bump version to 2024.3.3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f5efc37f352..fc39bccf854 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -16,7 +16,7 @@ from .helpers.deprecation import ( APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 3 -PATCH_VERSION: Final = "2" +PATCH_VERSION: Final = "3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 11, 0) diff --git a/pyproject.toml b/pyproject.toml index 496a29eb31c..fbab6c50d43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.3.2" +version = "2024.3.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2d2249386e1c8266f8f5c3790015e9968a19670c Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 22 Mar 2024 17:52:45 +0100 Subject: [PATCH 99/99] Bump axis to v58 (#114008) --- homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 44d615bf534..f2a2dd40740 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -26,7 +26,7 @@ "iot_class": "local_push", "loggers": ["axis"], "quality_scale": "platinum", - "requirements": ["axis==57"], + "requirements": ["axis==58"], "ssdp": [ { "manufacturer": "AXIS" diff --git a/requirements_all.txt b/requirements_all.txt index fe927e87424..d65690c3a42 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==57 +axis==58 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6307d864186..2698706aac8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -454,7 +454,7 @@ auroranoaa==0.0.3 aurorapy==0.2.7 # homeassistant.components.axis -axis==57 +axis==58 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1