From b2e7808114eecb40c72556ddea222216741d9143 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 27 Mar 2024 18:27:38 +0100 Subject: [PATCH 001/198] Bump version to 2024.4.0b0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ee15cfd72c3..d458a66b865 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -18,7 +18,7 @@ from .util.signal_type import SignalType APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 4 -PATCH_VERSION: Final = "0.dev0" +PATCH_VERSION: Final = "0b0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 965827f41ea..c84405c2764 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.4.0.dev0" +version = "2024.4.0b0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 9319528e0e31ebf6cde2b4db8f73048e2a7955e0 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 28 Mar 2024 15:44:50 +0100 Subject: [PATCH 002/198] Use fallback voice for selected language in cloud (#114246) Co-authored-by: Erik Montnemery --- homeassistant/components/cloud/tts.py | 24 +++++- tests/components/cloud/test_tts.py | 109 ++++++++++++++++++++++---- 2 files changed, 115 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py index 7922fc80201..42e4b94a189 100644 --- a/homeassistant/components/cloud/tts.py +++ b/homeassistant/components/cloud/tts.py @@ -140,7 +140,6 @@ class CloudTTSEntity(TextToSpeechEntity): """Return a dict include default options.""" return { ATTR_AUDIO_OUTPUT: AudioOutput.MP3, - ATTR_VOICE: self._voice, } @property @@ -178,7 +177,18 @@ class CloudTTSEntity(TextToSpeechEntity): gender: Gender | str | None = options.get(ATTR_GENDER) gender = handle_deprecated_gender(self.hass, gender) original_voice: str | None = options.get(ATTR_VOICE) + if original_voice is None and language == self._language: + original_voice = self._voice voice = handle_deprecated_voice(self.hass, original_voice) + if voice not in TTS_VOICES[language]: + default_voice = TTS_VOICES[language][0] + _LOGGER.debug( + "Unsupported voice %s detected, falling back to default %s for %s", + voice, + default_voice, + language, + ) + voice = default_voice # Process TTS try: data = await self.cloud.voice.process_tts( @@ -237,7 +247,6 @@ class CloudProvider(Provider): """Return a dict include default options.""" return { ATTR_AUDIO_OUTPUT: AudioOutput.MP3, - ATTR_VOICE: self._voice, } async def async_get_tts_audio( @@ -248,7 +257,18 @@ class CloudProvider(Provider): gender: Gender | str | None = options.get(ATTR_GENDER) gender = handle_deprecated_gender(self.hass, gender) original_voice: str | None = options.get(ATTR_VOICE) + if original_voice is None and language == self._language: + original_voice = self._voice voice = handle_deprecated_voice(self.hass, original_voice) + if voice not in TTS_VOICES[language]: + default_voice = TTS_VOICES[language][0] + _LOGGER.debug( + "Unsupported voice %s detected, falling back to default %s for %s", + voice, + default_voice, + language, + ) + voice = default_voice # Process TTS try: data = await self.cloud.voice.process_tts( diff --git a/tests/components/cloud/test_tts.py b/tests/components/cloud/test_tts.py index 3fd9ec5e4a4..06dbcf174a7 100644 --- a/tests/components/cloud/test_tts.py +++ b/tests/components/cloud/test_tts.py @@ -12,10 +12,20 @@ import voluptuous as vol from homeassistant.components.assist_pipeline.pipeline import STORAGE_KEY from homeassistant.components.cloud import DOMAIN, const, tts -from homeassistant.components.tts import DOMAIN as TTS_DOMAIN +from homeassistant.components.media_player import ( + ATTR_MEDIA_CONTENT_ID, + DOMAIN as DOMAIN_MP, + SERVICE_PLAY_MEDIA, +) +from homeassistant.components.tts import ( + ATTR_LANGUAGE, + ATTR_MEDIA_PLAYER_ENTITY_ID, + ATTR_MESSAGE, + DOMAIN as TTS_DOMAIN, +) from homeassistant.components.tts.helper import get_engine_instance from homeassistant.config import async_process_ha_core_config -from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.const import ATTR_ENTITY_ID, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import EntityRegistry from homeassistant.helpers.issue_registry import IssueRegistry, IssueSeverity @@ -23,6 +33,8 @@ from homeassistant.setup import async_setup_component from . import PIPELINE_DATA +from tests.common import async_mock_service +from tests.components.tts.common import get_media_source_url from tests.typing import ClientSessionGenerator @@ -120,13 +132,13 @@ async def test_prefs_default_voice( assert engine is not None # The platform config provider will be overridden by the discovery info provider. assert engine.default_language == "en-US" - assert engine.default_options == {"audio_output": "mp3", "voice": "JennyNeural"} + assert engine.default_options == {"audio_output": "mp3"} await set_cloud_prefs({"tts_default_voice": ("nl-NL", "MaartenNeural")}) await hass.async_block_till_done() assert engine.default_language == "nl-NL" - assert engine.default_options == {"audio_output": "mp3", "voice": "MaartenNeural"} + assert engine.default_options == {"audio_output": "mp3"} async def test_deprecated_platform_config( @@ -228,11 +240,11 @@ async def test_get_tts_audio( "url": ( "http://example.local:8123/api/tts_proxy/" "42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_5c97d21c48_{expected_url_suffix}.mp3" + f"_en-us_6e8b81ac47_{expected_url_suffix}.mp3" ), "path": ( "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_5c97d21c48_{expected_url_suffix}.mp3" + f"_en-us_6e8b81ac47_{expected_url_suffix}.mp3" ), } await hass.async_block_till_done() @@ -242,6 +254,7 @@ async def test_get_tts_audio( assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." assert mock_process_tts.call_args.kwargs["language"] == "en-US" assert mock_process_tts.call_args.kwargs["gender"] is None + assert mock_process_tts.call_args.kwargs["voice"] == "JennyNeural" assert mock_process_tts.call_args.kwargs["output"] == "mp3" @@ -280,11 +293,11 @@ async def test_get_tts_audio_logged_out( "url": ( "http://example.local:8123/api/tts_proxy/" "42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_5c97d21c48_{expected_url_suffix}.mp3" + f"_en-us_6e8b81ac47_{expected_url_suffix}.mp3" ), "path": ( "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_5c97d21c48_{expected_url_suffix}.mp3" + f"_en-us_6e8b81ac47_{expected_url_suffix}.mp3" ), } await hass.async_block_till_done() @@ -294,6 +307,7 @@ async def test_get_tts_audio_logged_out( assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." assert mock_process_tts.call_args.kwargs["language"] == "en-US" assert mock_process_tts.call_args.kwargs["gender"] is None + assert mock_process_tts.call_args.kwargs["voice"] == "JennyNeural" assert mock_process_tts.call_args.kwargs["output"] == "mp3" @@ -344,11 +358,11 @@ async def test_tts_entity( "url": ( "http://example.local:8123/api/tts_proxy/" "42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_5c97d21c48_{entity_id}.mp3" + f"_en-us_6e8b81ac47_{entity_id}.mp3" ), "path": ( "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_5c97d21c48_{entity_id}.mp3" + f"_en-us_6e8b81ac47_{entity_id}.mp3" ), } await hass.async_block_till_done() @@ -358,6 +372,7 @@ async def test_tts_entity( assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." assert mock_process_tts.call_args.kwargs["language"] == "en-US" assert mock_process_tts.call_args.kwargs["gender"] is None + assert mock_process_tts.call_args.kwargs["voice"] == "JennyNeural" assert mock_process_tts.call_args.kwargs["output"] == "mp3" state = hass.states.get(entity_id) @@ -632,11 +647,11 @@ async def test_deprecated_gender( "url": ( "http://example.local:8123/api/tts_proxy/" "42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_{language.lower()}_5c97d21c48_{expected_url_suffix}.mp3" + f"_{language.lower()}_6e8b81ac47_{expected_url_suffix}.mp3" ), "path": ( "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_{language.lower()}_5c97d21c48_{expected_url_suffix}.mp3" + f"_{language.lower()}_6e8b81ac47_{expected_url_suffix}.mp3" ), } await hass.async_block_till_done() @@ -645,7 +660,7 @@ async def test_deprecated_gender( assert mock_process_tts.call_args is not None assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." assert mock_process_tts.call_args.kwargs["language"] == language - assert mock_process_tts.call_args.kwargs["voice"] == "JennyNeural" + assert mock_process_tts.call_args.kwargs["voice"] == "XiaoxiaoNeural" assert mock_process_tts.call_args.kwargs["output"] == "mp3" issue = issue_registry.async_get_issue("cloud", "deprecated_gender") assert issue is None @@ -662,11 +677,11 @@ async def test_deprecated_gender( "url": ( "http://example.local:8123/api/tts_proxy/" "42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_{language.lower()}_5dded72256_{expected_url_suffix}.mp3" + f"_{language.lower()}_dd0e95eb04_{expected_url_suffix}.mp3" ), "path": ( "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_{language.lower()}_5dded72256_{expected_url_suffix}.mp3" + f"_{language.lower()}_dd0e95eb04_{expected_url_suffix}.mp3" ), } await hass.async_block_till_done() @@ -678,7 +693,7 @@ async def test_deprecated_gender( assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." assert mock_process_tts.call_args.kwargs["language"] == language assert mock_process_tts.call_args.kwargs["gender"] == gender_option - assert mock_process_tts.call_args.kwargs["voice"] == "JennyNeural" + assert mock_process_tts.call_args.kwargs["voice"] == "XiaoxiaoNeural" assert mock_process_tts.call_args.kwargs["output"] == "mp3" issue = issue_registry.async_get_issue("cloud", issue_id) assert issue is not None @@ -733,3 +748,65 @@ async def test_deprecated_gender( } assert not issue_registry.async_get_issue(DOMAIN, issue_id) + + +@pytest.mark.parametrize( + ("service", "service_data"), + [ + ( + "speak", + { + ATTR_ENTITY_ID: "tts.home_assistant_cloud", + ATTR_LANGUAGE: "id-ID", + ATTR_MEDIA_PLAYER_ENTITY_ID: "media_player.something", + ATTR_MESSAGE: "There is someone at the door.", + }, + ), + ( + "cloud_say", + { + ATTR_ENTITY_ID: "media_player.something", + ATTR_LANGUAGE: "id-ID", + ATTR_MESSAGE: "There is someone at the door.", + }, + ), + ], +) +async def test_tts_services( + hass: HomeAssistant, + cloud: MagicMock, + hass_client: ClientSessionGenerator, + service: str, + service_data: dict[str, Any], +) -> None: + """Test tts services.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + mock_process_tts = AsyncMock(return_value=b"") + cloud.voice.process_tts = mock_process_tts + + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + await cloud.login("test-user", "test-pass") + client = await hass_client() + + await hass.services.async_call( + domain=TTS_DOMAIN, + service=service, + service_data=service_data, + blocking=True, + ) + + assert len(calls) == 1 + + url = await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) + await hass.async_block_till_done() + response = await client.get(url) + assert response.status == HTTPStatus.OK + await hass.async_block_till_done() + + assert mock_process_tts.call_count == 1 + assert mock_process_tts.call_args is not None + assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." + assert mock_process_tts.call_args.kwargs["language"] == service_data[ATTR_LANGUAGE] + assert mock_process_tts.call_args.kwargs["voice"] == "GadisNeural" + assert mock_process_tts.call_args.kwargs["output"] == "mp3" From e2710184cb11cf58a461120039550ada1b41ea43 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Wed, 27 Mar 2024 18:53:18 +0100 Subject: [PATCH 003/198] Bump zha-quirks to 0.0.113 (#114311) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e85966e870f..e9d75584064 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -24,7 +24,7 @@ "bellows==0.38.1", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.112", + "zha-quirks==0.0.113", "zigpy-deconz==0.23.1", "zigpy==0.63.5", "zigpy-xbee==0.20.1", diff --git a/requirements_all.txt b/requirements_all.txt index 1aedcb73671..42e92c3de6f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2931,7 +2931,7 @@ zeroconf==0.131.0 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.112 +zha-quirks==0.0.113 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 9795ef99481..3548eb7fadc 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2266,7 +2266,7 @@ zeroconf==0.131.0 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.112 +zha-quirks==0.0.113 # homeassistant.components.zha zigpy-deconz==0.23.1 From a18184a4c08621e111ac154d6f622d4215a2d546 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Thu, 28 Mar 2024 00:43:34 +0100 Subject: [PATCH 004/198] Bump pyduotecno to 2024.3.2 (#114320) --- homeassistant/components/duotecno/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/duotecno/manifest.json b/homeassistant/components/duotecno/manifest.json index 7b33784a612..0c8eab8f0a0 100644 --- a/homeassistant/components/duotecno/manifest.json +++ b/homeassistant/components/duotecno/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_push", "loggers": ["pyduotecno", "pyduotecno-node", "pyduotecno-unit"], "quality_scale": "silver", - "requirements": ["pyDuotecno==2024.1.2"] + "requirements": ["pyDuotecno==2024.3.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 42e92c3de6f..ace129c69f2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1648,7 +1648,7 @@ pyCEC==0.5.2 pyControl4==1.1.0 # homeassistant.components.duotecno -pyDuotecno==2024.1.2 +pyDuotecno==2024.3.2 # homeassistant.components.electrasmart pyElectra==1.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3548eb7fadc..b01a7ca8ba4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1298,7 +1298,7 @@ pyCEC==0.5.2 pyControl4==1.1.0 # homeassistant.components.duotecno -pyDuotecno==2024.1.2 +pyDuotecno==2024.3.2 # homeassistant.components.electrasmart pyElectra==1.2.0 From 00993a6be3dd454828c944f5e9db8e03df5209da Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Wed, 27 Mar 2024 22:35:08 +0100 Subject: [PATCH 005/198] Fix Matter airconditioner discovery of climate platform (#114326) * Discover Thermostat platform for Room Airconditioner device * add test * Adjust docstring Co-authored-by: TheJulianJES --------- Co-authored-by: Martin Hjelmare Co-authored-by: TheJulianJES --- homeassistant/components/matter/climate.py | 2 +- homeassistant/components/matter/switch.py | 1 + .../fixtures/nodes/room-airconditioner.json | 256 ++++++++++++++++++ tests/components/matter/test_climate.py | 25 ++ 4 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 tests/components/matter/fixtures/nodes/room-airconditioner.json diff --git a/homeassistant/components/matter/climate.py b/homeassistant/components/matter/climate.py index 5ae1f7ca486..1b949d3ebfb 100644 --- a/homeassistant/components/matter/climate.py +++ b/homeassistant/components/matter/climate.py @@ -313,6 +313,6 @@ DISCOVERY_SCHEMAS = [ clusters.Thermostat.Attributes.UnoccupiedCoolingSetpoint, clusters.Thermostat.Attributes.UnoccupiedHeatingSetpoint, ), - device_type=(device_types.Thermostat,), + device_type=(device_types.Thermostat, device_types.RoomAirConditioner), ), ] diff --git a/homeassistant/components/matter/switch.py b/homeassistant/components/matter/switch.py index 91a28bdab8c..9bc858d40c0 100644 --- a/homeassistant/components/matter/switch.py +++ b/homeassistant/components/matter/switch.py @@ -86,6 +86,7 @@ DISCOVERY_SCHEMAS = [ device_types.ColorDimmerSwitch, device_types.DimmerSwitch, device_types.Thermostat, + device_types.RoomAirConditioner, ), ), ] diff --git a/tests/components/matter/fixtures/nodes/room-airconditioner.json b/tests/components/matter/fixtures/nodes/room-airconditioner.json new file mode 100644 index 00000000000..11c29b0d8f4 --- /dev/null +++ b/tests/components/matter/fixtures/nodes/room-airconditioner.json @@ -0,0 +1,256 @@ +{ + "node_id": 36, + "date_commissioned": "2024-03-27T17:31:23.745932", + "last_interview": "2024-03-27T17:31:23.745939", + "interview_version": 6, + "available": true, + "is_bridge": false, + "attributes": { + "0/29/0": [ + { + "0": 22, + "1": 1 + } + ], + "0/29/1": [29, 31, 40, 48, 49, 51, 60, 62, 63], + "0/29/2": [], + "0/29/3": [1, 2], + "0/29/65532": 0, + "0/29/65533": 2, + "0/29/65528": [], + "0/29/65529": [], + "0/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "0/31/0": [ + { + "254": 5 + }, + { + "1": 5, + "2": 2, + "3": [112233], + "4": null, + "254": 6 + } + ], + "0/31/1": [], + "0/31/2": 4, + "0/31/3": 3, + "0/31/4": 4, + "0/31/65532": 0, + "0/31/65533": 1, + "0/31/65528": [], + "0/31/65529": [], + "0/31/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/40/0": 17, + "0/40/1": "TEST_VENDOR", + "0/40/2": 65521, + "0/40/3": "Room AirConditioner", + "0/40/4": 32774, + "0/40/5": "", + "0/40/6": "**REDACTED**", + "0/40/7": 0, + "0/40/8": "TEST_VERSION", + "0/40/9": 1, + "0/40/10": "1.0", + "0/40/11": "20200101", + "0/40/12": "", + "0/40/13": "", + "0/40/14": "", + "0/40/15": "TEST_SN", + "0/40/16": false, + "0/40/18": "E47F334E22A56610", + "0/40/19": { + "0": 3, + "1": 3 + }, + "0/40/65532": 0, + "0/40/65533": 1, + "0/40/65528": [], + "0/40/65529": [], + "0/40/65531": [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 19, 65528, + 65529, 65531, 65532, 65533 + ], + "0/48/0": 0, + "0/48/1": { + "0": 60, + "1": 900 + }, + "0/48/2": 0, + "0/48/3": 0, + "0/48/4": true, + "0/48/65532": 0, + "0/48/65533": 1, + "0/48/65528": [1, 3, 5], + "0/48/65529": [0, 2, 4], + "0/48/65531": [0, 1, 2, 3, 4, 65528, 65529, 65531, 65532, 65533], + "0/49/0": 0, + "0/49/1": null, + "0/49/2": 0, + "0/49/3": 0, + "0/49/4": false, + "0/49/5": 0, + "0/49/6": "", + "0/49/7": 0, + "0/49/65532": 2, + "0/49/65533": 1, + "0/49/65528": [1, 5, 7], + "0/49/65529": [0, 2, 3, 4, 6, 8], + "0/49/65531": [0, 1, 2, 3, 4, 5, 6, 7, 65528, 65529, 65531, 65532, 65533], + "0/51/0": [], + "0/51/1": 0, + "0/51/8": false, + "0/51/65532": 0, + "0/51/65533": 1, + "0/51/65528": [], + "0/51/65529": [], + "0/51/65531": [0, 1, 8, 65528, 65529, 65531, 65532, 65533], + "0/60/0": 0, + "0/60/1": null, + "0/60/2": null, + "0/60/65532": 0, + "0/60/65533": 1, + "0/60/65528": [], + "0/60/65529": [0, 1, 2], + "0/60/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533], + "0/62/0": [ + { + "254": 5 + }, + { + "1": "FTABAQEkAgE3AyQTAhgmBIAigScmBYAlTTo3BiQVAiQRJBgkBwEkCAEwCUEE7pKHHHlljFuw2MAQJFOAzVR5tPPIXOjxHrLr7el8KqThQ6CuCFwdmNztUaIQgBcPZm6QRoEn6OGoFoAG8vB0KTcKNQEoARgkAgE2AwQCBAEYMAQUEvPPXEC80Bhik9ZDF3HK0Jo0RG0wBRQ2kjqIaJL5W4CHyhTHPUFcjBrNmxgwC0BJN+cSZw9fkFlIZGzsfS4WYFxzouEZ6LXLjqJXqwhi6uoQqoEhHPITp6sQ8u1ZF7OuQ35q0tZBwt84ZvAo+i59GA==", + "2": "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQTAhgkBwEkCAEwCUEEB0u1A8srBwhdMy9S5+W8C38qv6l9JxhOaVO1E8f3FHDpv6eTSEDWXvUKEOxZcce5cGUF/9tdW2z5M+pwjt2B9jcKNQEpARgkAmAwBBQ2kjqIaJL5W4CHyhTHPUFcjBrNmzAFFJOvH2V2J30vUkl3ZbhqhwBP2wVXGDALQJHZ9heIDcBg2DGc2b18rirq/5aZ2rsyP9BAE1zeTqSYj/pqKyeMS+hCx69jOqh/eAeDpeAzvL7JmKVLB0JLV1sY", + "254": 6 + } + ], + "0/62/1": [ + { + "1": "BER19ZLOakFRLvKKC9VsWzN+xv5V5yHHBFdX7ip/cNhnzVfnaNLLHKGU/DtcNZtU/YH+8kUcWKYvknk1TCcrG4k=", + "2": 24582, + "3": 9865010379846957597, + "4": 3118002441518404838, + "5": "", + "254": 5 + }, + { + "1": "BJUrvCXfXiwdfapIXt1qCtJoem+s2gZJ2KBDQZcPVP1cAYECu6Fjjz2MhMy6OW8ASGmWuke+YavIzIZWYEd6BJU=", + "2": 4939, + "3": 2, + "4": 36, + "5": "", + "254": 6 + } + ], + "0/62/2": 5, + "0/62/3": 2, + "0/62/4": [ + "FTABAQEkAgE3AycU3rGzlMtTrxYYJgQAus0sJgUAwGVSNwYnFN6xs5TLU68WGCQHASQIATAJQQREdfWSzmpBUS7yigvVbFszfsb+VechxwRXV+4qf3DYZ81X52jSyxyhlPw7XDWbVP2B/vJFHFimL5J5NUwnKxuJNwo1ASkBGCQCYDAEFMurIH6818tAIcTnwEZO5c+1WAH8MAUUy6sgfrzXy0AhxOfARk7lz7VYAfwYMAtAM2db17wMsM+JMtR4c2Iaz8nHLI4mVbsPGILOBujrzguB2C7p8Q9x8Cw0NgJP7hDV52F9j7IfHjO37aXZA4LqqBg=", + "FTABAQEkAgE3AyQUARgmBIAigScmBYAlTTo3BiQUARgkBwEkCAEwCUEElSu8Jd9eLB19qkhe3WoK0mh6b6zaBknYoENBlw9U/VwBgQK7oWOPPYyEzLo5bwBIaZa6R75hq8jMhlZgR3oElTcKNQEpARgkAmAwBBSTrx9ldid9L1JJd2W4aocAT9sFVzAFFJOvH2V2J30vUkl3ZbhqhwBP2wVXGDALQPMYkhQcsrqT5v1vgN1LXJr9skDJ6nnuG0QWfs8SVODLGjU73iO1aQVq+Ir5et9RTD/4VrfnI63DW9RA0N+qgCkY" + ], + "0/62/5": 6, + "0/62/65532": 0, + "0/62/65533": 1, + "0/62/65528": [1, 3, 5, 8], + "0/62/65529": [0, 2, 4, 6, 7, 9, 10, 11], + "0/62/65531": [0, 1, 2, 3, 4, 5, 65528, 65529, 65531, 65532, 65533], + "0/63/0": [], + "0/63/1": [], + "0/63/2": 4, + "0/63/3": 3, + "0/63/65532": 0, + "0/63/65533": 2, + "0/63/65528": [2, 5], + "0/63/65529": [0, 1, 3, 4], + "0/63/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/3/0": 0, + "1/3/1": 0, + "1/3/65532": 0, + "1/3/65533": 4, + "1/3/65528": [], + "1/3/65529": [0, 64], + "1/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "1/6/0": false, + "1/6/65532": 0, + "1/6/65533": 5, + "1/6/65528": [], + "1/6/65529": [0, 1, 2], + "1/6/65531": [0, 65528, 65529, 65531, 65532, 65533], + "1/29/0": [ + { + "0": 114, + "1": 1 + } + ], + "1/29/1": [3, 6, 29, 513, 514], + "1/29/2": [], + "1/29/3": [2], + "1/29/65532": 0, + "1/29/65533": 2, + "1/29/65528": [], + "1/29/65529": [], + "1/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "1/513/0": 2000, + "1/513/3": 1600, + "1/513/4": 3200, + "1/513/5": 1600, + "1/513/6": 3200, + "1/513/17": 2600, + "1/513/18": 2000, + "1/513/25": 0, + "1/513/27": 4, + "1/513/28": 1, + "1/513/65532": 35, + "1/513/65533": 6, + "1/513/65528": [], + "1/513/65529": [0], + "1/513/65531": [ + 0, 3, 4, 5, 6, 17, 18, 25, 27, 28, 65528, 65529, 65531, 65532, 65533 + ], + "1/514/0": 0, + "1/514/1": 2, + "1/514/2": 0, + "1/514/3": 0, + "1/514/4": 3, + "1/514/5": 0, + "1/514/6": 0, + "1/514/9": 1, + "1/514/10": 0, + "1/514/65532": 11, + "1/514/65533": 4, + "1/514/65528": [], + "1/514/65529": [], + "1/514/65531": [ + 0, 1, 2, 3, 4, 5, 6, 9, 10, 65528, 65529, 65531, 65532, 65533 + ], + "2/3/0": 0, + "2/3/1": 0, + "2/3/65532": 0, + "2/3/65533": 4, + "2/3/65528": [], + "2/3/65529": [0, 64], + "2/3/65531": [0, 1, 65528, 65529, 65531, 65532, 65533], + "2/29/0": [ + { + "0": 770, + "1": 1 + } + ], + "2/29/1": [3, 29, 1026], + "2/29/2": [], + "2/29/3": [], + "2/29/65532": 0, + "2/29/65533": 2, + "2/29/65528": [], + "2/29/65529": [], + "2/29/65531": [0, 1, 2, 3, 65528, 65529, 65531, 65532, 65533], + "2/1026/0": 0, + "2/1026/1": -500, + "2/1026/2": 6000, + "2/1026/65532": 0, + "2/1026/65533": 1, + "2/1026/65528": [], + "2/1026/65529": [], + "2/1026/65531": [0, 1, 2, 65528, 65529, 65531, 65532, 65533] + }, + "attribute_subscriptions": [] +} diff --git a/tests/components/matter/test_climate.py b/tests/components/matter/test_climate.py index 80e2d1b72da..de4626ef3d1 100644 --- a/tests/components/matter/test_climate.py +++ b/tests/components/matter/test_climate.py @@ -25,6 +25,16 @@ async def thermostat_fixture( return await setup_integration_with_node_fixture(hass, "thermostat", matter_client) +@pytest.fixture(name="room_airconditioner") +async def room_airconditioner( + hass: HomeAssistant, matter_client: MagicMock +) -> MatterNode: + """Fixture for a room air conditioner node.""" + return await setup_integration_with_node_fixture( + hass, "room-airconditioner", matter_client + ) + + # This tests needs to be adjusted to remove lingering tasks @pytest.mark.parametrize("expected_lingering_tasks", [True]) async def test_thermostat( @@ -387,3 +397,18 @@ async def test_thermostat( clusters.Thermostat.Enums.SetpointAdjustMode.kCool, -40 ), ) + + +# This tests needs to be adjusted to remove lingering tasks +@pytest.mark.parametrize("expected_lingering_tasks", [True]) +async def test_room_airconditioner( + hass: HomeAssistant, + matter_client: MagicMock, + room_airconditioner: MatterNode, +) -> None: + """Test if a climate entity is created for a Room Airconditioner device.""" + state = hass.states.get("climate.room_airconditioner") + assert state + assert state.attributes["current_temperature"] == 20 + assert state.attributes["min_temp"] == 16 + assert state.attributes["max_temp"] == 32 From f8edab0c12628b2724fd6d540e67efc0cfbc06a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Matheson=20Wergeland?= Date: Thu, 28 Mar 2024 13:31:55 +0100 Subject: [PATCH 006/198] =?UTF-8?q?Avoid=20changing=20local=20time=20on=20?= =?UTF-8?q?Nob=C3=B8=20Ecohub=20(#114332)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nobo_hub: Pass timezone to avoid changing local time on Nobø Ecohub in handshake --- homeassistant/components/nobo_hub/__init__.py | 9 ++++++++- homeassistant/components/nobo_hub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nobo_hub/__init__.py b/homeassistant/components/nobo_hub/__init__.py index 15a4b48c315..f9d2ce2e3da 100644 --- a/homeassistant/components/nobo_hub/__init__.py +++ b/homeassistant/components/nobo_hub/__init__.py @@ -7,6 +7,7 @@ from pynobo import nobo from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import HomeAssistant +import homeassistant.util.dt as dt_util from .const import CONF_AUTO_DISCOVERED, CONF_SERIAL, DOMAIN @@ -19,7 +20,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: serial = entry.data[CONF_SERIAL] discover = entry.data[CONF_AUTO_DISCOVERED] ip_address = None if discover else entry.data[CONF_IP_ADDRESS] - hub = nobo(serial=serial, ip=ip_address, discover=discover, synchronous=False) + hub = nobo( + serial=serial, + ip=ip_address, + discover=discover, + synchronous=False, + timezone=dt_util.DEFAULT_TIME_ZONE, + ) await hub.connect() hass.data.setdefault(DOMAIN, {}) diff --git a/homeassistant/components/nobo_hub/manifest.json b/homeassistant/components/nobo_hub/manifest.json index 9ddbed7dadc..4741eb39e29 100644 --- a/homeassistant/components/nobo_hub/manifest.json +++ b/homeassistant/components/nobo_hub/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/nobo_hub", "integration_type": "hub", "iot_class": "local_push", - "requirements": ["pynobo==1.6.0"] + "requirements": ["pynobo==1.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index ace129c69f2..a93913d7272 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1991,7 +1991,7 @@ pynetgear==0.10.10 pynetio==0.1.9.1 # homeassistant.components.nobo_hub -pynobo==1.6.0 +pynobo==1.8.0 # homeassistant.components.nuki pynuki==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b01a7ca8ba4..106b8debcdf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1545,7 +1545,7 @@ pymysensors==0.24.0 pynetgear==0.10.10 # homeassistant.components.nobo_hub -pynobo==1.6.0 +pynobo==1.8.0 # homeassistant.components.nuki pynuki==1.6.3 From a91c03b16472db1d716b922c326fc194310aaa1d Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Mar 2024 00:44:01 +0100 Subject: [PATCH 007/198] Don't access States.last_reported_ts before it's added (#114333) --- homeassistant/components/recorder/const.py | 1 + homeassistant/components/recorder/core.py | 3 +- .../components/recorder/history/modern.py | 27 +- tests/components/recorder/db_schema_42.py | 838 +++++++++++ .../recorder/test_history_db_schema_42.py | 1278 +++++++++++++++++ 5 files changed, 2138 insertions(+), 9 deletions(-) create mode 100644 tests/components/recorder/db_schema_42.py create mode 100644 tests/components/recorder/test_history_db_schema_42.py diff --git a/homeassistant/components/recorder/const.py b/homeassistant/components/recorder/const.py index 66d46c0c20e..1869bb32239 100644 --- a/homeassistant/components/recorder/const.py +++ b/homeassistant/components/recorder/const.py @@ -53,6 +53,7 @@ STATISTICS_ROWS_SCHEMA_VERSION = 23 CONTEXT_ID_AS_BINARY_SCHEMA_VERSION = 36 EVENT_TYPE_IDS_SCHEMA_VERSION = 37 STATES_META_SCHEMA_VERSION = 38 +LAST_REPORTED_SCHEMA_VERSION = 43 LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION = 28 diff --git a/homeassistant/components/recorder/core.py b/homeassistant/components/recorder/core.py index 7de9cf46311..0e404ce4da0 100644 --- a/homeassistant/components/recorder/core.py +++ b/homeassistant/components/recorder/core.py @@ -47,6 +47,7 @@ from .const import ( DOMAIN, ESTIMATED_QUEUE_ITEM_SIZE, KEEPALIVE_TIME, + LAST_REPORTED_SCHEMA_VERSION, LEGACY_STATES_EVENT_ID_INDEX_SCHEMA_VERSION, MARIADB_PYMYSQL_URL_PREFIX, MARIADB_URL_PREFIX, @@ -1203,7 +1204,7 @@ class Recorder(threading.Thread): if ( pending_last_reported := self.states_manager.get_pending_last_reported_timestamp() - ): + ) and self.schema_version >= LAST_REPORTED_SCHEMA_VERSION: with session.no_autoflush: session.execute( update(States), diff --git a/homeassistant/components/recorder/history/modern.py b/homeassistant/components/recorder/history/modern.py index a909f799ea9..5fd4f415e02 100644 --- a/homeassistant/components/recorder/history/modern.py +++ b/homeassistant/components/recorder/history/modern.py @@ -27,6 +27,7 @@ from homeassistant.core import HomeAssistant, State, split_entity_id import homeassistant.util.dt as dt_util from ... import recorder +from ..const import LAST_REPORTED_SCHEMA_VERSION from ..db_schema import SHARED_ATTR_OR_LEGACY_ATTRIBUTES, StateAttributes, States from ..filters import Filters from ..models import ( @@ -327,9 +328,10 @@ def _state_changed_during_period_stmt( limit: int | None, include_start_time_state: bool, run_start_ts: float | None, + include_last_reported: bool, ) -> Select | CompoundSelect: stmt = ( - _stmt_and_join_attributes(no_attributes, False, True) + _stmt_and_join_attributes(no_attributes, False, include_last_reported) .filter( ( (States.last_changed_ts == States.last_updated_ts) @@ -361,22 +363,22 @@ def _state_changed_during_period_stmt( single_metadata_id, no_attributes, False, - True, + include_last_reported, ).subquery(), no_attributes, False, - True, + include_last_reported, ), _select_from_subquery( stmt.subquery(), no_attributes, False, - True, + include_last_reported, ), ).subquery(), no_attributes, False, - True, + include_last_reported, ) @@ -391,6 +393,9 @@ def state_changes_during_period( include_start_time_state: bool = True, ) -> MutableMapping[str, list[State]]: """Return states changes during UTC period start_time - end_time.""" + has_last_reported = ( + recorder.get_instance(hass).schema_version >= LAST_REPORTED_SCHEMA_VERSION + ) if not entity_id: raise ValueError("entity_id must be provided") entity_ids = [entity_id.lower()] @@ -423,12 +428,14 @@ def state_changes_during_period( limit, include_start_time_state, run_start_ts, + has_last_reported, ), track_on=[ bool(end_time_ts), no_attributes, bool(limit), include_start_time_state, + has_last_reported, ], ) return cast( @@ -475,10 +482,10 @@ def _get_last_state_changes_single_stmt(metadata_id: int) -> Select: def _get_last_state_changes_multiple_stmt( - number_of_states: int, metadata_id: int + number_of_states: int, metadata_id: int, include_last_reported: bool ) -> Select: return ( - _stmt_and_join_attributes(False, False, True) + _stmt_and_join_attributes(False, False, include_last_reported) .where( States.state_id == ( @@ -500,6 +507,9 @@ def get_last_state_changes( hass: HomeAssistant, number_of_states: int, entity_id: str ) -> MutableMapping[str, list[State]]: """Return the last number_of_states.""" + has_last_reported = ( + recorder.get_instance(hass).schema_version >= LAST_REPORTED_SCHEMA_VERSION + ) entity_id_lower = entity_id.lower() entity_ids = [entity_id_lower] @@ -524,8 +534,9 @@ def get_last_state_changes( else: stmt = lambda_stmt( lambda: _get_last_state_changes_multiple_stmt( - number_of_states, metadata_id + number_of_states, metadata_id, has_last_reported ), + track_on=[has_last_reported], ) states = list(execute_stmt_lambda_element(session, stmt, orm_rows=False)) return cast( diff --git a/tests/components/recorder/db_schema_42.py b/tests/components/recorder/db_schema_42.py new file mode 100644 index 00000000000..b8e49aef592 --- /dev/null +++ b/tests/components/recorder/db_schema_42.py @@ -0,0 +1,838 @@ +"""Models for SQLAlchemy. + +This file contains the model definitions for schema version 42. +It is used to test the schema migration logic. +""" + +from __future__ import annotations + +from collections.abc import Callable +from datetime import datetime, timedelta +import logging +import time +from typing import Any, Self, cast + +import ciso8601 +from fnv_hash_fast import fnv1a_32 +from sqlalchemy import ( + CHAR, + JSON, + BigInteger, + Boolean, + ColumnElement, + DateTime, + Float, + ForeignKey, + Identity, + Index, + Integer, + LargeBinary, + SmallInteger, + String, + Text, + case, + type_coerce, +) +from sqlalchemy.dialects import mysql, oracle, postgresql, sqlite +from sqlalchemy.engine.interfaces import Dialect +from sqlalchemy.ext.compiler import compiles +from sqlalchemy.orm import DeclarativeBase, Mapped, aliased, mapped_column, relationship +from sqlalchemy.types import TypeDecorator + +from homeassistant.components.recorder.const import ( + ALL_DOMAIN_EXCLUDE_ATTRS, + SupportedDialect, +) +from homeassistant.components.recorder.models import ( + StatisticData, + StatisticDataTimestamp, + StatisticMetaData, + bytes_to_ulid_or_none, + bytes_to_uuid_hex_or_none, + datetime_to_timestamp_or_none, + process_timestamp, + ulid_to_bytes_or_none, + uuid_hex_to_bytes_or_none, +) +from homeassistant.const import ( + MAX_LENGTH_EVENT_EVENT_TYPE, + MAX_LENGTH_STATE_ENTITY_ID, + MAX_LENGTH_STATE_STATE, +) +from homeassistant.core import Context, Event, EventOrigin, State +from homeassistant.helpers.json import JSON_DUMP, json_bytes, json_bytes_strip_null +import homeassistant.util.dt as dt_util +from homeassistant.util.json import ( + JSON_DECODE_EXCEPTIONS, + json_loads, + json_loads_object, +) + + +# SQLAlchemy Schema +class Base(DeclarativeBase): + """Base class for tables.""" + + +SCHEMA_VERSION = 42 + +_LOGGER = logging.getLogger(__name__) + +TABLE_EVENTS = "events" +TABLE_EVENT_DATA = "event_data" +TABLE_EVENT_TYPES = "event_types" +TABLE_STATES = "states" +TABLE_STATE_ATTRIBUTES = "state_attributes" +TABLE_STATES_META = "states_meta" +TABLE_RECORDER_RUNS = "recorder_runs" +TABLE_SCHEMA_CHANGES = "schema_changes" +TABLE_STATISTICS = "statistics" +TABLE_STATISTICS_META = "statistics_meta" +TABLE_STATISTICS_RUNS = "statistics_runs" +TABLE_STATISTICS_SHORT_TERM = "statistics_short_term" + +STATISTICS_TABLES = ("statistics", "statistics_short_term") + +MAX_STATE_ATTRS_BYTES = 16384 +MAX_EVENT_DATA_BYTES = 32768 + +PSQL_DIALECT = SupportedDialect.POSTGRESQL + +ALL_TABLES = [ + TABLE_STATES, + TABLE_STATE_ATTRIBUTES, + TABLE_EVENTS, + TABLE_EVENT_DATA, + TABLE_EVENT_TYPES, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, + TABLE_STATES_META, + TABLE_STATISTICS, + TABLE_STATISTICS_META, + TABLE_STATISTICS_RUNS, + TABLE_STATISTICS_SHORT_TERM, +] + +TABLES_TO_CHECK = [ + TABLE_STATES, + TABLE_EVENTS, + TABLE_RECORDER_RUNS, + TABLE_SCHEMA_CHANGES, +] + +LAST_UPDATED_INDEX_TS = "ix_states_last_updated_ts" +METADATA_ID_LAST_UPDATED_INDEX_TS = "ix_states_metadata_id_last_updated_ts" +EVENTS_CONTEXT_ID_BIN_INDEX = "ix_events_context_id_bin" +STATES_CONTEXT_ID_BIN_INDEX = "ix_states_context_id_bin" +LEGACY_STATES_EVENT_ID_INDEX = "ix_states_event_id" +LEGACY_STATES_ENTITY_ID_LAST_UPDATED_INDEX = "ix_states_entity_id_last_updated_ts" +CONTEXT_ID_BIN_MAX_LENGTH = 16 + +MYSQL_COLLATE = "utf8mb4_unicode_ci" +MYSQL_DEFAULT_CHARSET = "utf8mb4" +MYSQL_ENGINE = "InnoDB" + +_DEFAULT_TABLE_ARGS = { + "mysql_default_charset": MYSQL_DEFAULT_CHARSET, + "mysql_collate": MYSQL_COLLATE, + "mysql_engine": MYSQL_ENGINE, + "mariadb_default_charset": MYSQL_DEFAULT_CHARSET, + "mariadb_collate": MYSQL_COLLATE, + "mariadb_engine": MYSQL_ENGINE, +} + + +class UnusedDateTime(DateTime): + """An unused column type that behaves like a datetime.""" + + +class Unused(CHAR): + """An unused column type that behaves like a string.""" + + +@compiles(UnusedDateTime, "mysql", "mariadb", "sqlite") # type: ignore[misc,no-untyped-call] +@compiles(Unused, "mysql", "mariadb", "sqlite") # type: ignore[misc,no-untyped-call] +def compile_char_zero(type_: TypeDecorator, compiler: Any, **kw: Any) -> str: + """Compile UnusedDateTime and Unused as CHAR(0) on mysql, mariadb, and sqlite.""" + return "CHAR(0)" # Uses 1 byte on MySQL (no change on sqlite) + + +@compiles(Unused, "postgresql") # type: ignore[misc,no-untyped-call] +def compile_char_one(type_: TypeDecorator, compiler: Any, **kw: Any) -> str: + """Compile Unused as CHAR(1) on postgresql.""" + return "CHAR(1)" # Uses 1 byte + + +class FAST_PYSQLITE_DATETIME(sqlite.DATETIME): + """Use ciso8601 to parse datetimes instead of sqlalchemy built-in regex.""" + + def result_processor(self, dialect, coltype): # type: ignore[no-untyped-def] + """Offload the datetime parsing to ciso8601.""" + return lambda value: None if value is None else ciso8601.parse_datetime(value) + + +class NativeLargeBinary(LargeBinary): + """A faster version of LargeBinary for engines that support python bytes natively.""" + + def result_processor(self, dialect, coltype): # type: ignore[no-untyped-def] + """No conversion needed for engines that support native bytes.""" + return None + + +# For MariaDB and MySQL we can use an unsigned integer type since it will fit 2**32 +# for sqlite and postgresql we use a bigint +UINT_32_TYPE = BigInteger().with_variant( + mysql.INTEGER(unsigned=True), # type: ignore[no-untyped-call] + "mysql", + "mariadb", +) +JSON_VARIANT_CAST = Text().with_variant( + postgresql.JSON(none_as_null=True), # type: ignore[no-untyped-call] + "postgresql", +) +JSONB_VARIANT_CAST = Text().with_variant( + postgresql.JSONB(none_as_null=True), # type: ignore[no-untyped-call] + "postgresql", +) +DATETIME_TYPE = ( + DateTime(timezone=True) + .with_variant(mysql.DATETIME(timezone=True, fsp=6), "mysql", "mariadb") # type: ignore[no-untyped-call] + .with_variant(FAST_PYSQLITE_DATETIME(), "sqlite") # type: ignore[no-untyped-call] +) +DOUBLE_TYPE = ( + Float() + .with_variant(mysql.DOUBLE(asdecimal=False), "mysql", "mariadb") # type: ignore[no-untyped-call] + .with_variant(oracle.DOUBLE_PRECISION(), "oracle") + .with_variant(postgresql.DOUBLE_PRECISION(), "postgresql") +) +UNUSED_LEGACY_COLUMN = Unused(0) +UNUSED_LEGACY_DATETIME_COLUMN = UnusedDateTime(timezone=True) +UNUSED_LEGACY_INTEGER_COLUMN = SmallInteger() +DOUBLE_PRECISION_TYPE_SQL = "DOUBLE PRECISION" +CONTEXT_BINARY_TYPE = LargeBinary(CONTEXT_ID_BIN_MAX_LENGTH).with_variant( + NativeLargeBinary(CONTEXT_ID_BIN_MAX_LENGTH), "mysql", "mariadb", "sqlite" +) + +TIMESTAMP_TYPE = DOUBLE_TYPE + + +class JSONLiteral(JSON): + """Teach SA how to literalize json.""" + + def literal_processor(self, dialect: Dialect) -> Callable[[Any], str]: + """Processor to convert a value to JSON.""" + + def process(value: Any) -> str: + """Dump json.""" + return JSON_DUMP(value) + + return process + + +EVENT_ORIGIN_ORDER = [EventOrigin.local, EventOrigin.remote] +EVENT_ORIGIN_TO_IDX = {origin: idx for idx, origin in enumerate(EVENT_ORIGIN_ORDER)} + + +class Events(Base): + """Event history data.""" + + __table_args__ = ( + # Used for fetching events at a specific time + # see logbook + Index( + "ix_events_event_type_id_time_fired_ts", "event_type_id", "time_fired_ts" + ), + Index( + EVENTS_CONTEXT_ID_BIN_INDEX, + "context_id_bin", + mysql_length=CONTEXT_ID_BIN_MAX_LENGTH, + mariadb_length=CONTEXT_ID_BIN_MAX_LENGTH, + ), + _DEFAULT_TABLE_ARGS, + ) + __tablename__ = TABLE_EVENTS + event_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + event_type: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + event_data: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + origin: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + origin_idx: Mapped[int | None] = mapped_column(SmallInteger) + time_fired: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + time_fired_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE, index=True) + context_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + context_user_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + context_parent_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + data_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("event_data.data_id"), index=True + ) + context_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + context_user_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + context_parent_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + event_type_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("event_types.event_type_id") + ) + event_data_rel: Mapped[EventData | None] = relationship("EventData") + event_type_rel: Mapped[EventTypes | None] = relationship("EventTypes") + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + @property + def _time_fired_isotime(self) -> str | None: + """Return time_fired as an isotime string.""" + date_time: datetime | None + if self.time_fired_ts is not None: + date_time = dt_util.utc_from_timestamp(self.time_fired_ts) + else: + date_time = process_timestamp(self.time_fired) + if date_time is None: + return None + return date_time.isoformat(sep=" ", timespec="seconds") + + @staticmethod + def from_event(event: Event) -> Events: + """Create an event database object from a native event.""" + return Events( + event_type=None, + event_data=None, + origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), + time_fired=None, + time_fired_ts=event.time_fired_timestamp, + context_id=None, + context_id_bin=ulid_to_bytes_or_none(event.context.id), + context_user_id=None, + context_user_id_bin=uuid_hex_to_bytes_or_none(event.context.user_id), + context_parent_id=None, + context_parent_id_bin=ulid_to_bytes_or_none(event.context.parent_id), + ) + + def to_native(self, validate_entity_id: bool = True) -> Event | None: + """Convert to a native HA Event.""" + context = Context( + id=bytes_to_ulid_or_none(self.context_id_bin), + user_id=bytes_to_uuid_hex_or_none(self.context_user_id_bin), + parent_id=bytes_to_ulid_or_none(self.context_parent_id_bin), + ) + try: + return Event( + self.event_type or "", + json_loads_object(self.event_data) if self.event_data else {}, + EventOrigin(self.origin) + if self.origin + else EVENT_ORIGIN_ORDER[self.origin_idx or 0], + dt_util.utc_from_timestamp(self.time_fired_ts or 0), + context=context, + ) + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails + _LOGGER.exception("Error converting to event: %s", self) + return None + + +class EventData(Base): + """Event data history.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_EVENT_DATA + data_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + hash: Mapped[int | None] = mapped_column(UINT_32_TYPE, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_data: Mapped[str | None] = mapped_column( + Text().with_variant(mysql.LONGTEXT, "mysql", "mariadb") + ) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + @staticmethod + def shared_data_bytes_from_event( + event: Event, dialect: SupportedDialect | None + ) -> bytes: + """Create shared_data from an event.""" + if dialect == SupportedDialect.POSTGRESQL: + bytes_result = json_bytes_strip_null(event.data) + bytes_result = json_bytes(event.data) + if len(bytes_result) > MAX_EVENT_DATA_BYTES: + _LOGGER.warning( + "Event data for %s exceed maximum size of %s bytes. " + "This can cause database performance issues; Event data " + "will not be stored", + event.event_type, + MAX_EVENT_DATA_BYTES, + ) + return b"{}" + return bytes_result + + @staticmethod + def hash_shared_data_bytes(shared_data_bytes: bytes) -> int: + """Return the hash of json encoded shared data.""" + return fnv1a_32(shared_data_bytes) + + def to_native(self) -> dict[str, Any]: + """Convert to an event data dictionary.""" + shared_data = self.shared_data + if shared_data is None: + return {} + try: + return cast(dict[str, Any], json_loads(shared_data)) + except JSON_DECODE_EXCEPTIONS: + _LOGGER.exception("Error converting row to event data: %s", self) + return {} + + +class EventTypes(Base): + """Event type history.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_EVENT_TYPES + event_type_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + event_type: Mapped[str | None] = mapped_column( + String(MAX_LENGTH_EVENT_EVENT_TYPE), index=True, unique=True + ) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + +class States(Base): + """State change history.""" + + __table_args__ = ( + # Used for fetching the state of entities at a specific time + # (get_states in history.py) + Index(METADATA_ID_LAST_UPDATED_INDEX_TS, "metadata_id", "last_updated_ts"), + Index( + STATES_CONTEXT_ID_BIN_INDEX, + "context_id_bin", + mysql_length=CONTEXT_ID_BIN_MAX_LENGTH, + mariadb_length=CONTEXT_ID_BIN_MAX_LENGTH, + ), + _DEFAULT_TABLE_ARGS, + ) + __tablename__ = TABLE_STATES + state_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + entity_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + state: Mapped[str | None] = mapped_column(String(MAX_LENGTH_STATE_STATE)) + attributes: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + event_id: Mapped[int | None] = mapped_column(UNUSED_LEGACY_INTEGER_COLUMN) + last_changed: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + last_changed_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE) + last_updated: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + last_updated_ts: Mapped[float | None] = mapped_column( + TIMESTAMP_TYPE, default=time.time, index=True + ) + old_state_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("states.state_id"), index=True + ) + attributes_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("state_attributes.attributes_id"), index=True + ) + context_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + context_user_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + context_parent_id: Mapped[str | None] = mapped_column(UNUSED_LEGACY_COLUMN) + origin_idx: Mapped[int | None] = mapped_column( + SmallInteger + ) # 0 is local, 1 is remote + old_state: Mapped[States | None] = relationship("States", remote_side=[state_id]) + state_attributes: Mapped[StateAttributes | None] = relationship("StateAttributes") + context_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + context_user_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + context_parent_id_bin: Mapped[bytes | None] = mapped_column(CONTEXT_BINARY_TYPE) + metadata_id: Mapped[int | None] = mapped_column( + Integer, ForeignKey("states_meta.metadata_id") + ) + states_meta_rel: Mapped[StatesMeta | None] = relationship("StatesMeta") + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @property + def _last_updated_isotime(self) -> str | None: + """Return last_updated as an isotime string.""" + date_time: datetime | None + if self.last_updated_ts is not None: + date_time = dt_util.utc_from_timestamp(self.last_updated_ts) + else: + date_time = process_timestamp(self.last_updated) + if date_time is None: + return None + return date_time.isoformat(sep=" ", timespec="seconds") + + @staticmethod + def from_event(event: Event) -> States: + """Create object from a state_changed event.""" + entity_id = event.data["entity_id"] + state: State | None = event.data.get("new_state") + dbstate = States( + entity_id=entity_id, + attributes=None, + context_id=None, + context_id_bin=ulid_to_bytes_or_none(event.context.id), + context_user_id=None, + context_user_id_bin=uuid_hex_to_bytes_or_none(event.context.user_id), + context_parent_id=None, + context_parent_id_bin=ulid_to_bytes_or_none(event.context.parent_id), + origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin), + last_updated=None, + last_changed=None, + ) + # None state means the state was removed from the state machine + if state is None: + dbstate.state = "" + dbstate.last_updated_ts = event.time_fired_timestamp + dbstate.last_changed_ts = None + return dbstate + + dbstate.state = state.state + dbstate.last_updated_ts = state.last_updated_timestamp + if state.last_updated == state.last_changed: + dbstate.last_changed_ts = None + else: + dbstate.last_changed_ts = state.last_changed_timestamp + + return dbstate + + def to_native(self, validate_entity_id: bool = True) -> State | None: + """Convert to an HA state object.""" + context = Context( + id=bytes_to_ulid_or_none(self.context_id_bin), + user_id=bytes_to_uuid_hex_or_none(self.context_user_id_bin), + parent_id=bytes_to_ulid_or_none(self.context_parent_id_bin), + ) + try: + attrs = json_loads_object(self.attributes) if self.attributes else {} + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails + _LOGGER.exception("Error converting row to state: %s", self) + return None + if self.last_changed_ts is None or self.last_changed_ts == self.last_updated_ts: + last_changed = last_updated = dt_util.utc_from_timestamp( + self.last_updated_ts or 0 + ) + else: + last_updated = dt_util.utc_from_timestamp(self.last_updated_ts or 0) + last_changed = dt_util.utc_from_timestamp(self.last_changed_ts or 0) + return State( + self.entity_id or "", + self.state, # type: ignore[arg-type] + # Join the state_attributes table on attributes_id to get the attributes + # for newer states + attrs, + last_changed, + last_updated, + context=context, + validate_entity_id=validate_entity_id, + ) + + +class StateAttributes(Base): + """State attribute change history.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_STATE_ATTRIBUTES + attributes_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + hash: Mapped[int | None] = mapped_column(UINT_32_TYPE, index=True) + # Note that this is not named attributes to avoid confusion with the states table + shared_attrs: Mapped[str | None] = mapped_column( + Text().with_variant(mysql.LONGTEXT, "mysql", "mariadb") + ) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + @staticmethod + def shared_attrs_bytes_from_event( + event: Event, + dialect: SupportedDialect | None, + ) -> bytes: + """Create shared_attrs from a state_changed event.""" + state: State | None = event.data.get("new_state") + # None state means the state was removed from the state machine + if state is None: + return b"{}" + if state_info := state.state_info: + exclude_attrs = { + *ALL_DOMAIN_EXCLUDE_ATTRS, + *state_info["unrecorded_attributes"], + } + else: + exclude_attrs = ALL_DOMAIN_EXCLUDE_ATTRS + encoder = json_bytes_strip_null if dialect == PSQL_DIALECT else json_bytes + bytes_result = encoder( + {k: v for k, v in state.attributes.items() if k not in exclude_attrs} + ) + if len(bytes_result) > MAX_STATE_ATTRS_BYTES: + _LOGGER.warning( + "State attributes for %s exceed maximum size of %s bytes. " + "This can cause database performance issues; Attributes " + "will not be stored", + state.entity_id, + MAX_STATE_ATTRS_BYTES, + ) + return b"{}" + return bytes_result + + @staticmethod + def hash_shared_attrs_bytes(shared_attrs_bytes: bytes) -> int: + """Return the hash of json encoded shared attributes.""" + return fnv1a_32(shared_attrs_bytes) + + def to_native(self) -> dict[str, Any]: + """Convert to a state attributes dictionary.""" + shared_attrs = self.shared_attrs + if shared_attrs is None: + return {} + try: + return cast(dict[str, Any], json_loads(shared_attrs)) + except JSON_DECODE_EXCEPTIONS: + # When json_loads fails + _LOGGER.exception("Error converting row to state attributes: %s", self) + return {} + + +class StatesMeta(Base): + """Metadata for states.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_STATES_META + metadata_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + entity_id: Mapped[str | None] = mapped_column( + String(MAX_LENGTH_STATE_ENTITY_ID), index=True, unique=True + ) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + +class StatisticsBase: + """Statistics base class.""" + + id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + created: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + created_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE, default=time.time) + metadata_id: Mapped[int | None] = mapped_column( + Integer, + ForeignKey(f"{TABLE_STATISTICS_META}.id", ondelete="CASCADE"), + ) + start: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + start_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE, index=True) + mean: Mapped[float | None] = mapped_column(DOUBLE_TYPE) + min: Mapped[float | None] = mapped_column(DOUBLE_TYPE) + max: Mapped[float | None] = mapped_column(DOUBLE_TYPE) + last_reset: Mapped[datetime | None] = mapped_column(UNUSED_LEGACY_DATETIME_COLUMN) + last_reset_ts: Mapped[float | None] = mapped_column(TIMESTAMP_TYPE) + state: Mapped[float | None] = mapped_column(DOUBLE_TYPE) + sum: Mapped[float | None] = mapped_column(DOUBLE_TYPE) + + duration: timedelta + + @classmethod + def from_stats(cls, metadata_id: int, stats: StatisticData) -> Self: + """Create object from a statistics with datatime objects.""" + return cls( # type: ignore[call-arg] + metadata_id=metadata_id, + created=None, + created_ts=time.time(), + start=None, + start_ts=dt_util.utc_to_timestamp(stats["start"]), + mean=stats.get("mean"), + min=stats.get("min"), + max=stats.get("max"), + last_reset=None, + last_reset_ts=datetime_to_timestamp_or_none(stats.get("last_reset")), + state=stats.get("state"), + sum=stats.get("sum"), + ) + + @classmethod + def from_stats_ts(cls, metadata_id: int, stats: StatisticDataTimestamp) -> Self: + """Create object from a statistics with timestamps.""" + return cls( # type: ignore[call-arg] + metadata_id=metadata_id, + created=None, + created_ts=time.time(), + start=None, + start_ts=stats["start_ts"], + mean=stats.get("mean"), + min=stats.get("min"), + max=stats.get("max"), + last_reset=None, + last_reset_ts=stats.get("last_reset_ts"), + state=stats.get("state"), + sum=stats.get("sum"), + ) + + +class Statistics(Base, StatisticsBase): + """Long term statistics.""" + + duration = timedelta(hours=1) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index( + "ix_statistics_statistic_id_start_ts", + "metadata_id", + "start_ts", + unique=True, + ), + ) + __tablename__ = TABLE_STATISTICS + + +class StatisticsShortTerm(Base, StatisticsBase): + """Short term statistics.""" + + duration = timedelta(minutes=5) + + __table_args__ = ( + # Used for fetching statistics for a certain entity at a specific time + Index( + "ix_statistics_short_term_statistic_id_start_ts", + "metadata_id", + "start_ts", + unique=True, + ), + ) + __tablename__ = TABLE_STATISTICS_SHORT_TERM + + +class StatisticsMeta(Base): + """Statistics meta data.""" + + __table_args__ = (_DEFAULT_TABLE_ARGS,) + __tablename__ = TABLE_STATISTICS_META + id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + statistic_id: Mapped[str | None] = mapped_column( + String(255), index=True, unique=True + ) + source: Mapped[str | None] = mapped_column(String(32)) + unit_of_measurement: Mapped[str | None] = mapped_column(String(255)) + has_mean: Mapped[bool | None] = mapped_column(Boolean) + has_sum: Mapped[bool | None] = mapped_column(Boolean) + name: Mapped[str | None] = mapped_column(String(255)) + + @staticmethod + def from_meta(meta: StatisticMetaData) -> StatisticsMeta: + """Create object from meta data.""" + return StatisticsMeta(**meta) + + +class RecorderRuns(Base): + """Representation of recorder run.""" + + __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) + __tablename__ = TABLE_RECORDER_RUNS + run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + start: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow) + end: Mapped[datetime | None] = mapped_column(DATETIME_TYPE) + closed_incorrect: Mapped[bool] = mapped_column(Boolean, default=False) + created: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + end = ( + f"'{self.end.isoformat(sep=' ', timespec='seconds')}'" if self.end else None + ) + return ( + f"" + ) + + def to_native(self, validate_entity_id: bool = True) -> Self: + """Return self, native format is this model.""" + return self + + +class SchemaChanges(Base): + """Representation of schema version changes.""" + + __tablename__ = TABLE_SCHEMA_CHANGES + change_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + schema_version: Mapped[int | None] = mapped_column(Integer) + changed: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + "" + ) + + +class StatisticsRuns(Base): + """Representation of statistics run.""" + + __tablename__ = TABLE_STATISTICS_RUNS + run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) + start: Mapped[datetime] = mapped_column(DATETIME_TYPE, index=True) + + def __repr__(self) -> str: + """Return string representation of instance for debugging.""" + return ( + f"" + ) + + +EVENT_DATA_JSON = type_coerce( + EventData.shared_data.cast(JSONB_VARIANT_CAST), JSONLiteral(none_as_null=True) +) +OLD_FORMAT_EVENT_DATA_JSON = type_coerce( + Events.event_data.cast(JSONB_VARIANT_CAST), JSONLiteral(none_as_null=True) +) + +SHARED_ATTRS_JSON = type_coerce( + StateAttributes.shared_attrs.cast(JSON_VARIANT_CAST), JSON(none_as_null=True) +) +OLD_FORMAT_ATTRS_JSON = type_coerce( + States.attributes.cast(JSON_VARIANT_CAST), JSON(none_as_null=True) +) + +ENTITY_ID_IN_EVENT: ColumnElement = EVENT_DATA_JSON["entity_id"] +OLD_ENTITY_ID_IN_EVENT: ColumnElement = OLD_FORMAT_EVENT_DATA_JSON["entity_id"] +DEVICE_ID_IN_EVENT: ColumnElement = EVENT_DATA_JSON["device_id"] +OLD_STATE = aliased(States, name="old_state") + +SHARED_ATTR_OR_LEGACY_ATTRIBUTES = case( + (StateAttributes.shared_attrs.is_(None), States.attributes), + else_=StateAttributes.shared_attrs, +).label("attributes") +SHARED_DATA_OR_LEGACY_EVENT_DATA = case( + (EventData.shared_data.is_(None), Events.event_data), else_=EventData.shared_data +).label("event_data") diff --git a/tests/components/recorder/test_history_db_schema_42.py b/tests/components/recorder/test_history_db_schema_42.py new file mode 100644 index 00000000000..98ed6089de6 --- /dev/null +++ b/tests/components/recorder/test_history_db_schema_42.py @@ -0,0 +1,1278 @@ +"""The tests the History component.""" + +from __future__ import annotations + +from collections.abc import Callable +from copy import copy +from datetime import datetime, timedelta +import json +from unittest.mock import patch, sentinel + +from freezegun import freeze_time +import pytest +from sqlalchemy import text + +from homeassistant.components import recorder +from homeassistant.components.recorder import Recorder, get_instance, history +from homeassistant.components.recorder.filters import Filters +from homeassistant.components.recorder.history import legacy +from homeassistant.components.recorder.models import process_timestamp +from homeassistant.components.recorder.models.legacy import ( + LegacyLazyState, + LegacyLazyStatePreSchema31, +) +from homeassistant.components.recorder.util import session_scope +import homeassistant.core as ha +from homeassistant.core import HomeAssistant, State +from homeassistant.helpers.json import JSONEncoder +import homeassistant.util.dt as dt_util + +from .common import ( + assert_dict_of_states_equal_without_context_and_last_changed, + assert_multiple_states_equal_without_context, + assert_multiple_states_equal_without_context_and_last_changed, + assert_states_equal_without_context, + async_recorder_block_till_done, + async_wait_recording_done, + old_db_schema, + wait_recording_done, +) +from .db_schema_42 import Events, RecorderRuns, StateAttributes, States, StatesMeta + +from tests.typing import RecorderInstanceGenerator + + +@pytest.fixture(autouse=True) +def db_schema_42(): + """Fixture to initialize the db with the old schema 42.""" + with old_db_schema("42"): + yield + + +async def _async_get_states( + hass: HomeAssistant, + utc_point_in_time: datetime, + entity_ids: list[str] | None = None, + run: RecorderRuns | None = None, + no_attributes: bool = False, +): + """Get states from the database.""" + + def _get_states_with_session(): + with session_scope(hass=hass, read_only=True) as session: + attr_cache = {} + pre_31_schema = get_instance(hass).schema_version < 31 + return [ + LegacyLazyStatePreSchema31(row, attr_cache, None) + if pre_31_schema + else LegacyLazyState( + row, + attr_cache, + None, + row.entity_id, + ) + for row in legacy._get_rows_with_session( + hass, + session, + utc_point_in_time, + entity_ids, + run, + no_attributes, + ) + ] + + return await recorder.get_instance(hass).async_add_executor_job( + _get_states_with_session + ) + + +def _add_db_entries( + hass: ha.HomeAssistant, point: datetime, entity_ids: list[str] +) -> None: + with session_scope(hass=hass) as session: + for idx, entity_id in enumerate(entity_ids): + session.add( + Events( + event_id=1001 + idx, + event_type="state_changed", + event_data="{}", + origin="LOCAL", + time_fired=point, + ) + ) + session.add( + States( + entity_id=entity_id, + state="on", + attributes='{"name":"the light"}', + last_changed=None, + last_updated=point, + event_id=1001 + idx, + attributes_id=1002 + idx, + ) + ) + session.add( + StateAttributes( + shared_attrs='{"name":"the shared light"}', + hash=1234 + idx, + attributes_id=1002 + idx, + ) + ) + + +def test_get_full_significant_states_with_session_entity_no_matches( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test getting states at a specific point in time for entities that never have been recorded.""" + hass = hass_recorder() + now = dt_util.utcnow() + time_before_recorder_ran = now - timedelta(days=1000) + with session_scope(hass=hass, read_only=True) as session: + assert ( + history.get_full_significant_states_with_session( + hass, session, time_before_recorder_ran, now, entity_ids=["demo.id"] + ) + == {} + ) + assert ( + history.get_full_significant_states_with_session( + hass, + session, + time_before_recorder_ran, + now, + entity_ids=["demo.id", "demo.id2"], + ) + == {} + ) + + +def test_significant_states_with_session_entity_minimal_response_no_matches( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test getting states at a specific point in time for entities that never have been recorded.""" + hass = hass_recorder() + now = dt_util.utcnow() + time_before_recorder_ran = now - timedelta(days=1000) + with session_scope(hass=hass, read_only=True) as session: + assert ( + history.get_significant_states_with_session( + hass, + session, + time_before_recorder_ran, + now, + entity_ids=["demo.id"], + minimal_response=True, + ) + == {} + ) + assert ( + history.get_significant_states_with_session( + hass, + session, + time_before_recorder_ran, + now, + entity_ids=["demo.id", "demo.id2"], + minimal_response=True, + ) + == {} + ) + + +def test_significant_states_with_session_single_entity( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test get_significant_states_with_session with a single entity.""" + hass = hass_recorder() + hass.states.set("demo.id", "any", {"attr": True}) + hass.states.set("demo.id", "any2", {"attr": True}) + wait_recording_done(hass) + now = dt_util.utcnow() + with session_scope(hass=hass, read_only=True) as session: + states = history.get_significant_states_with_session( + hass, + session, + now - timedelta(days=1), + now, + entity_ids=["demo.id"], + minimal_response=False, + ) + assert len(states["demo.id"]) == 2 + + +@pytest.mark.parametrize( + ("attributes", "no_attributes", "limit"), + [ + ({"attr": True}, False, 5000), + ({}, True, 5000), + ({"attr": True}, False, 3), + ({}, True, 3), + ], +) +def test_state_changes_during_period( + hass_recorder: Callable[..., HomeAssistant], attributes, no_attributes, limit +) -> None: + """Test state change during period.""" + hass = hass_recorder() + entity_id = "media_player.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state, attributes) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + end = point + timedelta(seconds=1) + + with freeze_time(start) as freezer: + set_state("idle") + set_state("YouTube") + + freezer.move_to(point) + states = [ + set_state("idle"), + set_state("Netflix"), + set_state("Plex"), + set_state("YouTube"), + ] + + freezer.move_to(end) + set_state("Netflix") + set_state("Plex") + + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes, limit=limit + ) + + assert_multiple_states_equal_without_context(states[:limit], hist[entity_id]) + + +def test_state_changes_during_period_last_reported( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test state change during period.""" + hass = hass_recorder() + entity_id = "media_player.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return ha.State.from_dict(hass.states.get(entity_id).as_dict()) + + start = dt_util.utcnow() + point1 = start + timedelta(seconds=1) + point2 = point1 + timedelta(seconds=1) + end = point2 + timedelta(seconds=1) + + with freeze_time(start) as freezer: + set_state("idle") + + freezer.move_to(point1) + states = [set_state("YouTube")] + + freezer.move_to(point2) + set_state("YouTube") + + freezer.move_to(end) + set_state("Netflix") + + hist = history.state_changes_during_period(hass, start, end, entity_id) + + assert_multiple_states_equal_without_context(states, hist[entity_id]) + + +def test_state_changes_during_period_descending( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test state change during period descending.""" + hass = hass_recorder() + entity_id = "media_player.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state, {"any": 1}) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow().replace(microsecond=0) + point = start + timedelta(seconds=1) + point2 = start + timedelta(seconds=1, microseconds=100) + point3 = start + timedelta(seconds=1, microseconds=200) + point4 = start + timedelta(seconds=1, microseconds=300) + end = point + timedelta(seconds=1, microseconds=400) + + with freeze_time(start) as freezer: + set_state("idle") + set_state("YouTube") + + freezer.move_to(point) + states = [set_state("idle")] + + freezer.move_to(point2) + states.append(set_state("Netflix")) + + freezer.move_to(point3) + states.append(set_state("Plex")) + + freezer.move_to(point4) + states.append(set_state("YouTube")) + + freezer.move_to(end) + set_state("Netflix") + set_state("Plex") + + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes=False, descending=False + ) + + assert_multiple_states_equal_without_context(states, hist[entity_id]) + + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes=False, descending=True + ) + assert_multiple_states_equal_without_context( + states, list(reversed(list(hist[entity_id]))) + ) + + start_time = point2 + timedelta(microseconds=10) + hist = history.state_changes_during_period( + hass, + start_time, # Pick a point where we will generate a start time state + end, + entity_id, + no_attributes=False, + descending=True, + include_start_time_state=True, + ) + hist_states = list(hist[entity_id]) + assert hist_states[-1].last_updated == start_time + assert hist_states[-1].last_changed == start_time + assert len(hist_states) == 3 + # Make sure they are in descending order + assert ( + hist_states[0].last_updated + > hist_states[1].last_updated + > hist_states[2].last_updated + ) + assert ( + hist_states[0].last_changed + > hist_states[1].last_changed + > hist_states[2].last_changed + ) + hist = history.state_changes_during_period( + hass, + start_time, # Pick a point where we will generate a start time state + end, + entity_id, + no_attributes=False, + descending=False, + include_start_time_state=True, + ) + hist_states = list(hist[entity_id]) + assert hist_states[0].last_updated == start_time + assert hist_states[0].last_changed == start_time + assert len(hist_states) == 3 + # Make sure they are in ascending order + assert ( + hist_states[0].last_updated + < hist_states[1].last_updated + < hist_states[2].last_updated + ) + assert ( + hist_states[0].last_changed + < hist_states[1].last_changed + < hist_states[2].last_changed + ) + + +def test_get_last_state_changes(hass_recorder: Callable[..., HomeAssistant]) -> None: + """Test number of state changes.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + point2 = point + timedelta(minutes=1, seconds=1) + states = [] + + with freeze_time(start) as freezer: + set_state("1") + + freezer.move_to(point) + states.append(set_state("2")) + + freezer.move_to(point2) + states.append(set_state("3")) + + hist = history.get_last_state_changes(hass, 2, entity_id) + + assert_multiple_states_equal_without_context(states, hist[entity_id]) + + +def test_get_last_state_changes_last_reported( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test number of state changes.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return ha.State.from_dict(hass.states.get(entity_id).as_dict()) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + point2 = point + timedelta(minutes=1, seconds=1) + states = [] + + with freeze_time(start) as freezer: + states.append(set_state("1")) + + freezer.move_to(point) + set_state("1") + + freezer.move_to(point2) + states.append(set_state("2")) + + hist = history.get_last_state_changes(hass, 2, entity_id) + + assert_multiple_states_equal_without_context(states, hist[entity_id]) + + +def test_get_last_state_change(hass_recorder: Callable[..., HomeAssistant]) -> None: + """Test getting the last state change for an entity.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + point2 = point + timedelta(minutes=1, seconds=1) + states = [] + + with freeze_time(start) as freezer: + set_state("1") + + freezer.move_to(point) + set_state("2") + + freezer.move_to(point2) + states.append(set_state("3")) + + hist = history.get_last_state_changes(hass, 1, entity_id) + + assert_multiple_states_equal_without_context(states, hist[entity_id]) + + +def test_ensure_state_can_be_copied( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Ensure a state can pass though copy(). + + The filter integration uses copy() on states + from history. + """ + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + + with freeze_time(start) as freezer: + set_state("1") + + freezer.move_to(point) + set_state("2") + + hist = history.get_last_state_changes(hass, 2, entity_id) + + assert_states_equal_without_context(copy(hist[entity_id][0]), hist[entity_id][0]) + assert_states_equal_without_context(copy(hist[entity_id][1]), hist[entity_id][1]) + + +def test_get_significant_states(hass_recorder: Callable[..., HomeAssistant]) -> None: + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + hist = history.get_significant_states(hass, zero, four, entity_ids=list(states)) + assert_dict_of_states_equal_without_context_and_last_changed(states, hist) + + +def test_get_significant_states_minimal_response( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test that only significant states are returned. + + When minimal responses is set only the first and + last states return a complete state. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + hist = history.get_significant_states( + hass, zero, four, minimal_response=True, entity_ids=list(states) + ) + entites_with_reducable_states = [ + "media_player.test", + "media_player.test3", + ] + + # All states for media_player.test state are reduced + # down to last_changed and state when minimal_response + # is set except for the first state. + # is set. We use JSONEncoder to make sure that are + # pre-encoded last_changed is always the same as what + # will happen with encoding a native state + for entity_id in entites_with_reducable_states: + entity_states = states[entity_id] + for state_idx in range(1, len(entity_states)): + input_state = entity_states[state_idx] + orig_last_changed = orig_last_changed = json.dumps( + process_timestamp(input_state.last_changed), + cls=JSONEncoder, + ).replace('"', "") + orig_state = input_state.state + entity_states[state_idx] = { + "last_changed": orig_last_changed, + "state": orig_state, + } + + assert len(hist) == len(states) + assert_states_equal_without_context( + states["media_player.test"][0], hist["media_player.test"][0] + ) + assert states["media_player.test"][1] == hist["media_player.test"][1] + assert states["media_player.test"][2] == hist["media_player.test"][2] + + assert_multiple_states_equal_without_context( + states["media_player.test2"], hist["media_player.test2"] + ) + assert_states_equal_without_context( + states["media_player.test3"][0], hist["media_player.test3"][0] + ) + assert states["media_player.test3"][1] == hist["media_player.test3"][1] + + assert_multiple_states_equal_without_context( + states["script.can_cancel_this_one"], hist["script.can_cancel_this_one"] + ) + assert_multiple_states_equal_without_context_and_last_changed( + states["thermostat.test"], hist["thermostat.test"] + ) + assert_multiple_states_equal_without_context_and_last_changed( + states["thermostat.test2"], hist["thermostat.test2"] + ) + + +@pytest.mark.parametrize("time_zone", ["Europe/Berlin", "US/Hawaii", "UTC"]) +def test_get_significant_states_with_initial( + time_zone, hass_recorder: Callable[..., HomeAssistant] +) -> None: + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + hass.config.set_time_zone(time_zone) + zero, four, states = record_states(hass) + one_and_half = zero + timedelta(seconds=1.5) + for entity_id in states: + if entity_id == "media_player.test": + states[entity_id] = states[entity_id][1:] + for state in states[entity_id]: + # If the state is recorded before the start time + # start it will have its last_updated and last_changed + # set to the start time. + if state.last_updated < one_and_half: + state.last_updated = one_and_half + state.last_changed = one_and_half + + hist = history.get_significant_states( + hass, one_and_half, four, include_start_time_state=True, entity_ids=list(states) + ) + assert_dict_of_states_equal_without_context_and_last_changed(states, hist) + + +def test_get_significant_states_without_initial( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + one = zero + timedelta(seconds=1) + one_with_microsecond = zero + timedelta(seconds=1, microseconds=1) + one_and_half = zero + timedelta(seconds=1.5) + for entity_id in states: + states[entity_id] = list( + filter( + lambda s: s.last_changed != one + and s.last_changed != one_with_microsecond, + states[entity_id], + ) + ) + del states["media_player.test2"] + del states["thermostat.test3"] + + hist = history.get_significant_states( + hass, + one_and_half, + four, + include_start_time_state=False, + entity_ids=list(states), + ) + assert_dict_of_states_equal_without_context_and_last_changed(states, hist) + + +def test_get_significant_states_entity_id( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test that only significant states are returned for one entity.""" + hass = hass_recorder() + zero, four, states = record_states(hass) + del states["media_player.test2"] + del states["media_player.test3"] + del states["thermostat.test"] + del states["thermostat.test2"] + del states["thermostat.test3"] + del states["script.can_cancel_this_one"] + + hist = history.get_significant_states(hass, zero, four, ["media_player.test"]) + assert_dict_of_states_equal_without_context_and_last_changed(states, hist) + + +def test_get_significant_states_multiple_entity_ids( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test that only significant states are returned for one entity.""" + hass = hass_recorder() + zero, four, states = record_states(hass) + + hist = history.get_significant_states( + hass, + zero, + four, + ["media_player.test", "thermostat.test"], + ) + + assert_multiple_states_equal_without_context_and_last_changed( + states["media_player.test"], hist["media_player.test"] + ) + assert_multiple_states_equal_without_context_and_last_changed( + states["thermostat.test"], hist["thermostat.test"] + ) + + +def test_get_significant_states_are_ordered( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test order of results from get_significant_states. + + When entity ids are given, the results should be returned with the data + in the same order. + """ + hass = hass_recorder() + zero, four, _states = record_states(hass) + entity_ids = ["media_player.test", "media_player.test2"] + hist = history.get_significant_states(hass, zero, four, entity_ids) + assert list(hist.keys()) == entity_ids + entity_ids = ["media_player.test2", "media_player.test"] + hist = history.get_significant_states(hass, zero, four, entity_ids) + assert list(hist.keys()) == entity_ids + + +def test_get_significant_states_only( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test significant states when significant_states_only is set.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=4) + points = [start + timedelta(minutes=i) for i in range(1, 4)] + + states = [] + with freeze_time(start) as freezer: + set_state("123", attributes={"attribute": 10.64}) + + freezer.move_to(points[0]) + # Attributes are different, state not + states.append(set_state("123", attributes={"attribute": 21.42})) + + freezer.move_to(points[1]) + # state is different, attributes not + states.append(set_state("32", attributes={"attribute": 21.42})) + + freezer.move_to(points[2]) + # everything is different + states.append(set_state("412", attributes={"attribute": 54.23})) + + hist = history.get_significant_states( + hass, + start, + significant_changes_only=True, + entity_ids=list({state.entity_id for state in states}), + ) + + assert len(hist[entity_id]) == 2 + assert not any( + state.last_updated == states[0].last_updated for state in hist[entity_id] + ) + assert any( + state.last_updated == states[1].last_updated for state in hist[entity_id] + ) + assert any( + state.last_updated == states[2].last_updated for state in hist[entity_id] + ) + + hist = history.get_significant_states( + hass, + start, + significant_changes_only=False, + entity_ids=list({state.entity_id for state in states}), + ) + + assert len(hist[entity_id]) == 3 + assert_multiple_states_equal_without_context_and_last_changed( + states, hist[entity_id] + ) + + +async def test_get_significant_states_only_minimal_response( + recorder_mock: Recorder, hass: HomeAssistant +) -> None: + """Test significant states when significant_states_only is True.""" + now = dt_util.utcnow() + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "attr"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "changed"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "off", attributes={"any": "again"}) + await async_recorder_block_till_done(hass) + hass.states.async_set("sensor.test", "on", attributes={"any": "attr"}) + await async_wait_recording_done(hass) + + hist = history.get_significant_states( + hass, + now, + minimal_response=True, + significant_changes_only=False, + entity_ids=["sensor.test"], + ) + assert len(hist["sensor.test"]) == 3 + + +def record_states(hass) -> tuple[datetime, datetime, dict[str, list[State]]]: + """Record some test states. + + We inject a bunch of state updates from media player, zone and + thermostat. + """ + mp = "media_player.test" + mp2 = "media_player.test2" + mp3 = "media_player.test3" + therm = "thermostat.test" + therm2 = "thermostat.test2" + therm3 = "thermostat.test3" + zone = "zone.home" + script_c = "script.can_cancel_this_one" + + def set_state(entity_id, state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + zero = dt_util.utcnow() + one = zero + timedelta(seconds=1) + two = one + timedelta(seconds=1) + three = two + timedelta(seconds=1) + four = three + timedelta(seconds=1) + + states = {therm: [], therm2: [], therm3: [], mp: [], mp2: [], mp3: [], script_c: []} + with freeze_time(one) as freezer: + states[mp].append( + set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[mp2].append( + set_state(mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[mp3].append( + set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[therm].append( + set_state(therm, 20, attributes={"current_temperature": 19.5}) + ) + # This state will be updated + set_state(therm3, 20, attributes={"current_temperature": 19.5}) + + freezer.move_to(one + timedelta(microseconds=1)) + states[mp].append( + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + + freezer.move_to(two) + # This state will be skipped only different in time + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)}) + # This state will be skipped because domain is excluded + set_state(zone, "zoning") + states[script_c].append( + set_state(script_c, "off", attributes={"can_cancel": True}) + ) + states[therm].append( + set_state(therm, 21, attributes={"current_temperature": 19.8}) + ) + states[therm2].append( + set_state(therm2, 20, attributes={"current_temperature": 19}) + ) + # This state will be updated + set_state(therm3, 20, attributes={"current_temperature": 19.5}) + + freezer.move_to(three) + states[mp].append( + set_state(mp, "Netflix", attributes={"media_title": str(sentinel.mt4)}) + ) + states[mp3].append( + set_state(mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)}) + ) + # Attributes changed even though state is the same + states[therm].append( + set_state(therm, 21, attributes={"current_temperature": 20}) + ) + states[therm3].append( + set_state(therm3, 20, attributes={"current_temperature": 19.5}) + ) + + return zero, four, states + + +async def test_state_changes_during_period_query_during_migration_to_schema_25( + async_setup_recorder_instance: RecorderInstanceGenerator, + hass: HomeAssistant, + recorder_db_url: str, +) -> None: + """Test we can query data prior to schema 25 and during migration to schema 25.""" + if recorder_db_url.startswith(("mysql://", "postgresql://")): + # This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop table state_attributes + return + + instance = await async_setup_recorder_instance(hass, {}) + + with patch.object(instance.states_meta_manager, "active", False): + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + end = point + timedelta(seconds=1) + entity_id = "light.test" + await recorder.get_instance(hass).async_add_executor_job( + _add_db_entries, hass, point, [entity_id] + ) + + no_attributes = True + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes, include_start_time_state=False + ) + state = hist[entity_id][0] + assert state.attributes == {} + + no_attributes = False + hist = history.state_changes_during_period( + hass, start, end, entity_id, no_attributes, include_start_time_state=False + ) + state = hist[entity_id][0] + assert state.attributes == {"name": "the shared light"} + + with instance.engine.connect() as conn: + conn.execute(text("update states set attributes_id=NULL;")) + conn.execute(text("drop table state_attributes;")) + conn.commit() + + with patch.object(instance, "schema_version", 24): + instance.states_meta_manager.active = False + no_attributes = True + hist = history.state_changes_during_period( + hass, + start, + end, + entity_id, + no_attributes, + include_start_time_state=False, + ) + state = hist[entity_id][0] + assert state.attributes == {} + + no_attributes = False + hist = history.state_changes_during_period( + hass, + start, + end, + entity_id, + no_attributes, + include_start_time_state=False, + ) + state = hist[entity_id][0] + assert state.attributes == {"name": "the light"} + + +async def test_get_states_query_during_migration_to_schema_25( + async_setup_recorder_instance: RecorderInstanceGenerator, + hass: HomeAssistant, + recorder_db_url: str, +) -> None: + """Test we can query data prior to schema 25 and during migration to schema 25.""" + if recorder_db_url.startswith(("mysql://", "postgresql://")): + # This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop table state_attributes + return + + instance = await async_setup_recorder_instance(hass, {}) + + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + end = point + timedelta(seconds=1) + entity_id = "light.test" + await instance.async_add_executor_job(_add_db_entries, hass, point, [entity_id]) + assert instance.states_meta_manager.active + + no_attributes = True + hist = await _async_get_states(hass, end, [entity_id], no_attributes=no_attributes) + state = hist[0] + assert state.attributes == {} + + no_attributes = False + hist = await _async_get_states(hass, end, [entity_id], no_attributes=no_attributes) + state = hist[0] + assert state.attributes == {"name": "the shared light"} + + with instance.engine.connect() as conn: + conn.execute(text("update states set attributes_id=NULL;")) + conn.execute(text("drop table state_attributes;")) + conn.commit() + + with patch.object(instance, "schema_version", 24): + instance.states_meta_manager.active = False + no_attributes = True + hist = await _async_get_states( + hass, end, [entity_id], no_attributes=no_attributes + ) + state = hist[0] + assert state.attributes == {} + + no_attributes = False + hist = await _async_get_states( + hass, end, [entity_id], no_attributes=no_attributes + ) + state = hist[0] + assert state.attributes == {"name": "the light"} + + +async def test_get_states_query_during_migration_to_schema_25_multiple_entities( + async_setup_recorder_instance: RecorderInstanceGenerator, + hass: HomeAssistant, + recorder_db_url: str, +) -> None: + """Test we can query data prior to schema 25 and during migration to schema 25.""" + if recorder_db_url.startswith(("mysql://", "postgresql://")): + # This test doesn't run on MySQL / MariaDB / Postgresql; we can't drop table state_attributes + return + + instance = await async_setup_recorder_instance(hass, {}) + + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + end = point + timedelta(seconds=1) + entity_id_1 = "light.test" + entity_id_2 = "switch.test" + entity_ids = [entity_id_1, entity_id_2] + + await instance.async_add_executor_job(_add_db_entries, hass, point, entity_ids) + assert instance.states_meta_manager.active + + no_attributes = True + hist = await _async_get_states(hass, end, entity_ids, no_attributes=no_attributes) + assert hist[0].attributes == {} + assert hist[1].attributes == {} + + no_attributes = False + hist = await _async_get_states(hass, end, entity_ids, no_attributes=no_attributes) + assert hist[0].attributes == {"name": "the shared light"} + assert hist[1].attributes == {"name": "the shared light"} + + with instance.engine.connect() as conn: + conn.execute(text("update states set attributes_id=NULL;")) + conn.execute(text("drop table state_attributes;")) + conn.commit() + + with patch.object(instance, "schema_version", 24): + instance.states_meta_manager.active = False + no_attributes = True + hist = await _async_get_states( + hass, end, entity_ids, no_attributes=no_attributes + ) + assert hist[0].attributes == {} + assert hist[1].attributes == {} + + no_attributes = False + hist = await _async_get_states( + hass, end, entity_ids, no_attributes=no_attributes + ) + assert hist[0].attributes == {"name": "the light"} + assert hist[1].attributes == {"name": "the light"} + + +async def test_get_full_significant_states_handles_empty_last_changed( + async_setup_recorder_instance: RecorderInstanceGenerator, + hass: HomeAssistant, +) -> None: + """Test getting states when last_changed is null.""" + await async_setup_recorder_instance(hass, {}) + + now = dt_util.utcnow() + hass.states.async_set("sensor.one", "on", {"attr": "original"}) + state0 = hass.states.get("sensor.one") + await hass.async_block_till_done() + hass.states.async_set("sensor.one", "on", {"attr": "new"}) + state1 = hass.states.get("sensor.one") + + assert state0.last_changed == state1.last_changed + assert state0.last_updated != state1.last_updated + await async_wait_recording_done(hass) + + def _get_entries(): + with session_scope(hass=hass, read_only=True) as session: + return history.get_full_significant_states_with_session( + hass, + session, + now, + dt_util.utcnow(), + entity_ids=["sensor.one"], + significant_changes_only=False, + ) + + states = await recorder.get_instance(hass).async_add_executor_job(_get_entries) + sensor_one_states: list[State] = states["sensor.one"] + assert_states_equal_without_context(sensor_one_states[0], state0) + assert_states_equal_without_context(sensor_one_states[1], state1) + assert sensor_one_states[0].last_changed == sensor_one_states[1].last_changed + assert sensor_one_states[0].last_updated != sensor_one_states[1].last_updated + + def _fetch_native_states() -> list[State]: + with session_scope(hass=hass, read_only=True) as session: + native_states = [] + db_state_attributes = { + state_attributes.attributes_id: state_attributes + for state_attributes in session.query(StateAttributes) + } + metadata_id_to_entity_id = { + states_meta.metadata_id: states_meta + for states_meta in session.query(StatesMeta) + } + for db_state in session.query(States): + db_state.entity_id = metadata_id_to_entity_id[ + db_state.metadata_id + ].entity_id + state = db_state.to_native() + state.attributes = db_state_attributes[ + db_state.attributes_id + ].to_native() + native_states.append(state) + return native_states + + native_sensor_one_states = await recorder.get_instance(hass).async_add_executor_job( + _fetch_native_states + ) + assert_states_equal_without_context(native_sensor_one_states[0], state0) + assert_states_equal_without_context(native_sensor_one_states[1], state1) + assert ( + native_sensor_one_states[0].last_changed + == native_sensor_one_states[1].last_changed + ) + assert ( + native_sensor_one_states[0].last_updated + != native_sensor_one_states[1].last_updated + ) + + def _fetch_db_states() -> list[States]: + with session_scope(hass=hass, read_only=True) as session: + states = list(session.query(States)) + session.expunge_all() + return states + + db_sensor_one_states = await recorder.get_instance(hass).async_add_executor_job( + _fetch_db_states + ) + assert db_sensor_one_states[0].last_changed is None + assert db_sensor_one_states[0].last_changed_ts is None + + assert ( + process_timestamp( + dt_util.utc_from_timestamp(db_sensor_one_states[1].last_changed_ts) + ) + == state0.last_changed + ) + assert db_sensor_one_states[0].last_updated_ts is not None + assert db_sensor_one_states[1].last_updated_ts is not None + assert ( + db_sensor_one_states[0].last_updated_ts + != db_sensor_one_states[1].last_updated_ts + ) + + +def test_state_changes_during_period_multiple_entities_single_test( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test state change during period with multiple entities in the same test. + + This test ensures the sqlalchemy query cache does not + generate incorrect results. + """ + hass = hass_recorder() + start = dt_util.utcnow() + test_entites = {f"sensor.{i}": str(i) for i in range(30)} + for entity_id, value in test_entites.items(): + hass.states.set(entity_id, value) + + wait_recording_done(hass) + end = dt_util.utcnow() + + for entity_id, value in test_entites.items(): + hist = history.state_changes_during_period(hass, start, end, entity_id) + assert len(hist) == 1 + assert hist[entity_id][0].state == value + + +@pytest.mark.freeze_time("2039-01-19 03:14:07.555555-00:00") +async def test_get_full_significant_states_past_year_2038( + async_setup_recorder_instance: RecorderInstanceGenerator, + hass: HomeAssistant, +) -> None: + """Test we can store times past year 2038.""" + await async_setup_recorder_instance(hass, {}) + past_2038_time = dt_util.parse_datetime("2039-01-19 03:14:07.555555-00:00") + hass.states.async_set("sensor.one", "on", {"attr": "original"}) + state0 = hass.states.get("sensor.one") + await hass.async_block_till_done() + + hass.states.async_set("sensor.one", "on", {"attr": "new"}) + state1 = hass.states.get("sensor.one") + + await async_wait_recording_done(hass) + + def _get_entries(): + with session_scope(hass=hass, read_only=True) as session: + return history.get_full_significant_states_with_session( + hass, + session, + past_2038_time - timedelta(days=365), + past_2038_time + timedelta(days=365), + entity_ids=["sensor.one"], + significant_changes_only=False, + ) + + states = await recorder.get_instance(hass).async_add_executor_job(_get_entries) + sensor_one_states: list[State] = states["sensor.one"] + assert_states_equal_without_context(sensor_one_states[0], state0) + assert_states_equal_without_context(sensor_one_states[1], state1) + assert sensor_one_states[0].last_changed == past_2038_time + assert sensor_one_states[0].last_updated == past_2038_time + + +def test_get_significant_states_without_entity_ids_raises( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test at least one entity id is required for get_significant_states.""" + hass = hass_recorder() + now = dt_util.utcnow() + with pytest.raises(ValueError, match="entity_ids must be provided"): + history.get_significant_states(hass, now, None) + + +def test_state_changes_during_period_without_entity_ids_raises( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test at least one entity id is required for state_changes_during_period.""" + hass = hass_recorder() + now = dt_util.utcnow() + with pytest.raises(ValueError, match="entity_id must be provided"): + history.state_changes_during_period(hass, now, None) + + +def test_get_significant_states_with_filters_raises( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test passing filters is no longer supported.""" + hass = hass_recorder() + now = dt_util.utcnow() + with pytest.raises(NotImplementedError, match="Filters are no longer supported"): + history.get_significant_states( + hass, now, None, ["media_player.test"], Filters() + ) + + +def test_get_significant_states_with_non_existent_entity_ids_returns_empty( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test get_significant_states returns an empty dict when entities not in the db.""" + hass = hass_recorder() + now = dt_util.utcnow() + assert history.get_significant_states(hass, now, None, ["nonexistent.entity"]) == {} + + +def test_state_changes_during_period_with_non_existent_entity_ids_returns_empty( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test state_changes_during_period returns an empty dict when entities not in the db.""" + hass = hass_recorder() + now = dt_util.utcnow() + assert ( + history.state_changes_during_period(hass, now, None, "nonexistent.entity") == {} + ) + + +def test_get_last_state_changes_with_non_existent_entity_ids_returns_empty( + hass_recorder: Callable[..., HomeAssistant], +) -> None: + """Test get_last_state_changes returns an empty dict when entities not in the db.""" + hass = hass_recorder() + assert history.get_last_state_changes(hass, 1, "nonexistent.entity") == {} From 53cc4b8c37feae46f89cf1c0c09ea9e4e5eb8f30 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 27 Mar 2024 22:52:02 +0100 Subject: [PATCH 008/198] Download translations only once in the build pipeline (#114335) --- .github/workflows/builder.yml | 63 ++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 1dc6f7a3938..5dc01eee21e 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -51,6 +51,32 @@ jobs: with: ignore-dev: true + - name: Fail if translations files are checked in + run: | + files=$(find homeassistant/components/*/translations -type f) + + if [ -n "$files" ]; then + echo "Translations files are checked in, please remove the following files:" + echo "$files" + exit 1 + fi + + - name: Download Translations + run: python3 -m script.translations download + env: + LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} + + - name: Archive translations + shell: bash + run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - + + - name: Upload translations + uses: actions/upload-artifact@v4.3.1 + with: + name: translations + path: translations.tar.gz + if-no-files-found: error + build_base: name: Build ${{ matrix.arch }} base core image if: github.repository_owner == 'home-assistant' @@ -159,10 +185,15 @@ jobs: # are not available. sed -i "s|aiohttp-zlib-ng|aiohttp-zlib-ng\[isal\]|g" requirements_all.txt - - name: Download Translations - run: python3 -m script.translations download - env: - LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} + - name: Download translations + uses: actions/download-artifact@v4.1.4 + with: + name: translations + + - name: Extract translations + run: | + tar xvf translations.tar.gz + rm translations.tar.gz - name: Write meta info file shell: bash @@ -186,17 +217,6 @@ jobs: --target /data \ --generic ${{ needs.init.outputs.version }} - - name: Archive translations - shell: bash - run: find ./homeassistant/components/*/translations -name "*.json" | tar zcvf translations.tar.gz -T - - - - name: Upload translations - uses: actions/upload-artifact@v3 - with: - name: translations - path: translations.tar.gz - if-no-files-found: error - build_machine: name: Build ${{ matrix.machine }} machine core image if: github.repository_owner == 'home-assistant' @@ -448,10 +468,15 @@ jobs: with: python-version: ${{ env.DEFAULT_PYTHON }} - - name: Download Translations - run: python3 -m script.translations download - env: - LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} + - name: Download translations + uses: actions/download-artifact@v4.1.4 + with: + name: translations + + - name: Extract translations + run: | + tar xvf translations.tar.gz + rm translations.tar.gz - name: Build package shell: bash From 824d6afa249fcffaa75c3053f401a7305c300d12 Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 27 Mar 2024 22:33:06 +0100 Subject: [PATCH 009/198] Remove checked in translations (#114336) --- .../components/devialet/translations/en.json | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 homeassistant/components/devialet/translations/en.json diff --git a/homeassistant/components/devialet/translations/en.json b/homeassistant/components/devialet/translations/en.json deleted file mode 100644 index af0cfc4c122..00000000000 --- a/homeassistant/components/devialet/translations/en.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "config": { - "abort": { - "already_configured": "Service is already configured" - }, - "error": { - "cannot_connect": "Failed to connect" - }, - "flow_title": "{title}", - "step": { - "confirm": { - "description": "Do you want to set up Devialet device {device}?" - }, - "user": { - "data": { - "host": "Host" - }, - "description": "Please enter the host name or IP address of the Devialet device." - } - } - } -} \ No newline at end of file From 541a6c5f64dad743dec43f07cd0f745462f840db Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Mar 2024 20:58:07 -1000 Subject: [PATCH 010/198] Revert velocity change in powerview (#114337) --- .../components/hunterdouglas_powerview/number.py | 2 +- .../hunterdouglas_powerview/shade_data.py | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/number.py b/homeassistant/components/hunterdouglas_powerview/number.py index 8551a11337e..b37331c08df 100644 --- a/homeassistant/components/hunterdouglas_powerview/number.py +++ b/homeassistant/components/hunterdouglas_powerview/number.py @@ -41,7 +41,7 @@ def store_velocity( value: float | None, ) -> None: """Store the desired shade velocity in the coordinator.""" - coordinator.data.update_shade_position(shade_id, ShadePosition(velocity=value)) + coordinator.data.update_shade_velocity(shade_id, ShadePosition(velocity=value)) NUMBERS: Final = ( diff --git a/homeassistant/components/hunterdouglas_powerview/shade_data.py b/homeassistant/components/hunterdouglas_powerview/shade_data.py index e6b20312f27..fd2f0466467 100644 --- a/homeassistant/components/hunterdouglas_powerview/shade_data.py +++ b/homeassistant/components/hunterdouglas_powerview/shade_data.py @@ -13,14 +13,11 @@ from .util import async_map_data_by_id _LOGGER = logging.getLogger(__name__) -POSITION_FIELDS = fields(ShadePosition) +POSITION_FIELDS = [field for field in fields(ShadePosition) if field.name != "velocity"] def copy_position_data(source: ShadePosition, target: ShadePosition) -> ShadePosition: """Copy position data from source to target for None values only.""" - # the hub will always return a velocity of 0 on initial connect, - # separate definition to store consistent value in HA - # this value is purely driven from HA for field in POSITION_FIELDS: if (value := getattr(source, field.name)) is not None: setattr(target, field.name, value) @@ -76,3 +73,11 @@ class PowerviewShadeData: def update_shade_position(self, shade_id: int, new_position: ShadePosition) -> None: """Update a single shades position.""" copy_position_data(new_position, self.get_shade_position(shade_id)) + + def update_shade_velocity(self, shade_id: int, shade_data: ShadePosition) -> None: + """Update a single shades velocity.""" + # the hub will always return a velocity of 0 on initial connect, + # separate definition to store consistent value in HA + # this value is purely driven from HA + if shade_data.velocity is not None: + self.get_shade_position(shade_id).velocity = shade_data.velocity From 04bfb1de3cb3e826a7c534327cb661e9e5a929db Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 27 Mar 2024 17:19:34 -0500 Subject: [PATCH 011/198] Add more Ollama models (#114339) Add more models --- homeassistant/components/ollama/const.py | 41 ++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/homeassistant/components/ollama/const.py b/homeassistant/components/ollama/const.py index 59f1888cfc7..853370066dc 100644 --- a/homeassistant/components/ollama/const.py +++ b/homeassistant/components/ollama/const.py @@ -110,5 +110,46 @@ MODEL_NAMES = [ # https://ollama.com/library "starcoder", "phind-codellama", "starcoder2", + "yi", + "orca2", + "falcon", + "wizard-math", + "dolphin-phi", + "starling-lm", + "nous-hermes", + "stable-code", + "medllama2", + "bakllava", + "codeup", + "wizardlm-uncensored", + "solar", + "everythinglm", + "sqlcoder", + "dolphincoder", + "nous-hermes2-mixtral", + "stable-beluga", + "yarn-mistral", + "stablelm2", + "samantha-mistral", + "meditron", + "stablelm-zephyr", + "magicoder", + "yarn-llama2", + "llama-pro", + "deepseek-llm", + "wizard-vicuna", + "codebooga", + "mistrallite", + "all-minilm", + "nexusraven", + "open-orca-platypus2", + "goliath", + "notux", + "megadolphin", + "alfred", + "xwinlm", + "wizardlm", + "duckdb-nsql", + "notus", ] DEFAULT_MODEL = "llama2:latest" From f141be73c77619e39a9dd576f34fd5be4f16e4cc Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Thu, 28 Mar 2024 06:57:02 +0100 Subject: [PATCH 012/198] Bump fjaraskupan to 2.3.0 (#114344) Update fjarakupen to 2.3.0 - Support delayed disconnection - Speed up on/off transitions --- homeassistant/components/fjaraskupan/light.py | 5 +++-- homeassistant/components/fjaraskupan/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fjaraskupan/light.py b/homeassistant/components/fjaraskupan/light.py index 7f33d7806ee..b33904c805d 100644 --- a/homeassistant/components/fjaraskupan/light.py +++ b/homeassistant/components/fjaraskupan/light.py @@ -54,13 +54,14 @@ class Light(CoordinatorEntity[FjaraskupanCoordinator], LightEntity): async with self.coordinator.async_connect_and_update() as device: if ATTR_BRIGHTNESS in kwargs: await device.send_dim(int(kwargs[ATTR_BRIGHTNESS] * (100.0 / 255.0))) - elif not self.is_on: - await device.send_command(COMMAND_LIGHT_ON_OFF) + else: + await device.send_dim(100) async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" if self.is_on: async with self.coordinator.async_connect_and_update() as device: + await device.send_dim(0) await device.send_command(COMMAND_LIGHT_ON_OFF) @property diff --git a/homeassistant/components/fjaraskupan/manifest.json b/homeassistant/components/fjaraskupan/manifest.json index f7ad701a756..91c74b68e01 100644 --- a/homeassistant/components/fjaraskupan/manifest.json +++ b/homeassistant/components/fjaraskupan/manifest.json @@ -14,5 +14,5 @@ "documentation": "https://www.home-assistant.io/integrations/fjaraskupan", "iot_class": "local_polling", "loggers": ["bleak", "fjaraskupan"], - "requirements": ["fjaraskupan==2.2.0"] + "requirements": ["fjaraskupan==2.3.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index a93913d7272..810a73b7fc7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -864,7 +864,7 @@ fivem-api==0.1.2 fixerio==1.0.0a0 # homeassistant.components.fjaraskupan -fjaraskupan==2.2.0 +fjaraskupan==2.3.0 # homeassistant.components.flexit_bacnet flexit_bacnet==2.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 106b8debcdf..6f67e8c8b12 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -702,7 +702,7 @@ fitbit==0.3.1 fivem-api==0.1.2 # homeassistant.components.fjaraskupan -fjaraskupan==2.2.0 +fjaraskupan==2.3.0 # homeassistant.components.flexit_bacnet flexit_bacnet==2.1.0 From f204faf20203b333789ba84b2535191200521dc8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 27 Mar 2024 18:29:43 -1000 Subject: [PATCH 013/198] Fix empty delays in script helper (#114346) fixes ``` Logger: homeassistant.components.automation.kamermaster_knop_4_acties_licht Bron: components/automation/__init__.py:726 integratie: Automatisering (documentatie, problemen) Eerst voorgekomen: 22:17:29 (5 gebeurtenissen) Laatst gelogd: 22:59:24 While executing automation automation.kamermaster_knop_4_acties_licht Traceback (most recent call last): File "/usr/src/homeassistant/homeassistant/components/automation/__init__.py", line 726, in async_trigger return await self.action_script.async_run( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 1645, in async_run return await asyncio.shield(create_eager_task(run.async_run())) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 454, in async_run await self._async_step(log_exceptions=False) File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 506, in _async_step self._handle_exception( File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 536, in _handle_exception raise exception File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 504, in _async_step await getattr(self, handler)() File "/usr/src/homeassistant/homeassistant/helpers/script.py", line 626, in _async_delay_step if timeout_future.done(): ^^^^^^^^^^^^^^^^^^^ AttributeError: 'NoneType' object has no attribute 'done' ``` --- homeassistant/helpers/script.py | 5 +++++ tests/helpers/test_script.py | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 560f3227c4f..a86df259f11 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -615,6 +615,11 @@ class _ScriptRun: delay = delay_delta.total_seconds() self._changed() + if not delay: + # Handle an empty delay + trace_set_result(delay=delay, done=True) + return + trace_set_result(delay=delay, done=False) futures, timeout_handle, timeout_future = self._async_futures_with_timeout( delay diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index c1462ccfc2f..86fb84eb582 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -672,6 +672,31 @@ async def test_delay_basic(hass: HomeAssistant) -> None: ) +async def test_empty_delay(hass: HomeAssistant) -> None: + """Test an empty delay.""" + delay_alias = "delay step" + sequence = cv.SCRIPT_SCHEMA({"delay": {"seconds": 0}, "alias": delay_alias}) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + delay_started_flag = async_watch_for_action(script_obj, delay_alias) + + try: + await script_obj.async_run(context=Context()) + await asyncio.wait_for(delay_started_flag.wait(), 1) + except (AssertionError, TimeoutError): + await script_obj.async_stop() + raise + else: + await hass.async_block_till_done() + assert not script_obj.is_running + assert script_obj.last_action is None + + assert_action_trace( + { + "0": [{"result": {"delay": 0.0, "done": True}}], + } + ) + + async def test_multiple_runs_delay(hass: HomeAssistant) -> None: """Test multiple runs with delay in script.""" event = "test_event" From 737e5e70ec2df9576e807f309711d28f49f7ac21 Mon Sep 17 00:00:00 2001 From: Christopher Bailey Date: Thu, 28 Mar 2024 13:48:51 -0400 Subject: [PATCH 014/198] Bump pyunifiprotect to 5.1.2 (#114348) --- homeassistant/components/unifiprotect/config_flow.py | 3 ++- homeassistant/components/unifiprotect/manifest.json | 2 +- homeassistant/components/unifiprotect/utils.py | 3 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/unifiprotect/config_flow.py b/homeassistant/components/unifiprotect/config_flow.py index 555ddcb8d5e..19561a6003d 100644 --- a/homeassistant/components/unifiprotect/config_flow.py +++ b/homeassistant/components/unifiprotect/config_flow.py @@ -261,7 +261,8 @@ class ProtectFlowHandler(ConfigFlow, domain=DOMAIN): username=user_input[CONF_USERNAME], password=user_input[CONF_PASSWORD], verify_ssl=verify_ssl, - cache_dir=Path(self.hass.config.path(STORAGE_DIR, "unifiprotect_cache")), + cache_dir=Path(self.hass.config.path(STORAGE_DIR, "unifiprotect")), + config_dir=Path(self.hass.config.path(STORAGE_DIR, "unifiprotect")), ) errors = {} diff --git a/homeassistant/components/unifiprotect/manifest.json b/homeassistant/components/unifiprotect/manifest.json index 7cfb0ddcc9e..a26fab2e80b 100644 --- a/homeassistant/components/unifiprotect/manifest.json +++ b/homeassistant/components/unifiprotect/manifest.json @@ -41,7 +41,7 @@ "iot_class": "local_push", "loggers": ["pyunifiprotect", "unifi_discovery"], "quality_scale": "platinum", - "requirements": ["pyunifiprotect==5.0.2", "unifi-discovery==1.1.8"], + "requirements": ["pyunifiprotect==5.1.2", "unifi-discovery==1.1.8"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/homeassistant/components/unifiprotect/utils.py b/homeassistant/components/unifiprotect/utils.py index 58474e6a531..8199d729943 100644 --- a/homeassistant/components/unifiprotect/utils.py +++ b/homeassistant/components/unifiprotect/utils.py @@ -145,7 +145,8 @@ def async_create_api_client( override_connection_host=entry.options.get(CONF_OVERRIDE_CHOST, False), ignore_stats=not entry.options.get(CONF_ALL_UPDATES, False), ignore_unadopted=False, - cache_dir=Path(hass.config.path(STORAGE_DIR, "unifiprotect_cache")), + cache_dir=Path(hass.config.path(STORAGE_DIR, "unifiprotect")), + config_dir=Path(hass.config.path(STORAGE_DIR, "unifiprotect")), ) diff --git a/requirements_all.txt b/requirements_all.txt index 810a73b7fc7..ffa57398564 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2339,7 +2339,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==5.0.2 +pyunifiprotect==5.1.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f67e8c8b12..b35ef776b47 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1806,7 +1806,7 @@ pytrydan==0.4.0 pyudev==0.23.2 # homeassistant.components.unifiprotect -pyunifiprotect==5.0.2 +pyunifiprotect==5.1.2 # homeassistant.components.uptimerobot pyuptimerobot==22.2.0 From 21bff95bd7ad7160802979791666f0643361173a Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Thu, 28 Mar 2024 09:11:02 +0100 Subject: [PATCH 015/198] Fix script for checking on existing translations (#114354) --- .github/workflows/builder.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index 5dc01eee21e..217093793d1 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -53,11 +53,9 @@ jobs: - name: Fail if translations files are checked in run: | - files=$(find homeassistant/components/*/translations -type f) - - if [ -n "$files" ]; then + if [ -n "$(find homeassistant/components/*/translations -type f)" ]; then echo "Translations files are checked in, please remove the following files:" - echo "$files" + find homeassistant/components/*/translations -type f exit 1 fi From 42580a1113271911da4b953d9522efc116abeb74 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Mar 2024 10:42:52 +0100 Subject: [PATCH 016/198] Improve utility meter restore state tests (#114356) --- tests/components/utility_meter/test_sensor.py | 91 +++++++++++-------- 1 file changed, 53 insertions(+), 38 deletions(-) diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index c250a66b87a..13b367b1fb7 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -610,7 +610,7 @@ async def test_device_class( "utility_meter": { "energy_bill": { "source": "sensor.energy", - "tariffs": ["onpeak", "midpeak", "offpeak", "superpeak"], + "tariffs": ["tariff1", "tariff2", "tariff3", "tariff4"], } } }, @@ -626,7 +626,7 @@ async def test_device_class( "offset": 0, "periodically_resetting": True, "source": "sensor.energy", - "tariffs": ["onpeak", "midpeak", "offpeak", "superpeak"], + "tariffs": ["tariff1", "tariff2", "tariff3", "tariff4"], }, ), ], @@ -638,82 +638,89 @@ async def test_restore_state( # Home assistant is not runnit yet hass.set_state(CoreState.not_running) - last_reset = "2020-12-21T00:00:00.013073+00:00" + last_reset_1 = "2020-12-21T00:00:00.013073+00:00" + last_reset_2 = "2020-12-22T00:00:00.013073+00:00" mock_restore_cache_with_extra_data( hass, [ + # sensor.energy_bill_tariff1 is restored as expected ( State( - "sensor.energy_bill_onpeak", - "3", + "sensor.energy_bill_tariff1", + "1.1", attributes={ ATTR_STATUS: PAUSED, - ATTR_LAST_RESET: last_reset, - ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + ATTR_LAST_RESET: last_reset_1, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.MEGA_WATT_HOUR, }, ), { "native_value": { "__type": "", - "decimal_str": "3", + "decimal_str": "1.2", }, "native_unit_of_measurement": "kWh", - "last_reset": last_reset, - "last_period": "7", - "last_valid_state": "None", + "last_reset": last_reset_2, + "last_period": "1.3", + "last_valid_state": None, "status": "paused", }, ), + # sensor.energy_bill_tariff2 has missing keys and falls back to + # saved state ( State( - "sensor.energy_bill_midpeak", - "5", + "sensor.energy_bill_tariff2", + "2.1", attributes={ ATTR_STATUS: PAUSED, - ATTR_LAST_RESET: last_reset, + ATTR_LAST_RESET: last_reset_1, ATTR_LAST_VALID_STATE: None, - ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.MEGA_WATT_HOUR, }, ), { "native_value": { "__type": "", - "decimal_str": "3", + "decimal_str": "2.2", }, "native_unit_of_measurement": "kWh", "last_valid_state": "None", }, ), + # sensor.energy_bill_tariff3 has invalid data and falls back to + # saved state ( State( - "sensor.energy_bill_offpeak", - "6", + "sensor.energy_bill_tariff3", + "3.1", attributes={ ATTR_STATUS: COLLECTING, - ATTR_LAST_RESET: last_reset, + ATTR_LAST_RESET: last_reset_1, ATTR_LAST_VALID_STATE: None, - ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.MEGA_WATT_HOUR, }, ), { "native_value": { "__type": "", - "decimal_str": "3f", + "decimal_str": "3f", # Invalid }, "native_unit_of_measurement": "kWh", "last_valid_state": "None", }, ), + # No extra saved data, fall back to saved state ( State( - "sensor.energy_bill_superpeak", + "sensor.energy_bill_tariff4", "error", attributes={ ATTR_STATUS: COLLECTING, - ATTR_LAST_RESET: last_reset, + ATTR_LAST_RESET: last_reset_1, ATTR_LAST_VALID_STATE: None, - ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.MEGA_WATT_HOUR, }, ), {}, @@ -736,25 +743,28 @@ async def test_restore_state( await hass.async_block_till_done() # restore from cache - state = hass.states.get("sensor.energy_bill_onpeak") - assert state.state == "3" + state = hass.states.get("sensor.energy_bill_tariff1") + assert state.state == "1.2" assert state.attributes.get("status") == PAUSED - assert state.attributes.get("last_reset") == last_reset + assert state.attributes.get("last_reset") == last_reset_2 assert state.attributes.get("last_valid_state") == "None" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR - state = hass.states.get("sensor.energy_bill_midpeak") - assert state.state == "5" + state = hass.states.get("sensor.energy_bill_tariff2") + assert state.state == "2.1" + assert state.attributes.get("status") == PAUSED + assert state.attributes.get("last_reset") == last_reset_1 assert state.attributes.get("last_valid_state") == "None" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.MEGA_WATT_HOUR - state = hass.states.get("sensor.energy_bill_offpeak") - assert state.state == "6" + state = hass.states.get("sensor.energy_bill_tariff3") + assert state.state == "3.1" assert state.attributes.get("status") == COLLECTING - assert state.attributes.get("last_reset") == last_reset + assert state.attributes.get("last_reset") == last_reset_1 assert state.attributes.get("last_valid_state") == "None" - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.MEGA_WATT_HOUR - state = hass.states.get("sensor.energy_bill_superpeak") + state = hass.states.get("sensor.energy_bill_tariff4") assert state.state == STATE_UNKNOWN # utility_meter is loaded, now set sensors according to utility_meter: @@ -764,13 +774,18 @@ async def test_restore_state( await hass.async_block_till_done() state = hass.states.get("select.energy_bill") - assert state.state == "onpeak" + assert state.state == "tariff1" - state = hass.states.get("sensor.energy_bill_onpeak") + state = hass.states.get("sensor.energy_bill_tariff1") assert state.attributes.get("status") == COLLECTING - state = hass.states.get("sensor.energy_bill_offpeak") - assert state.attributes.get("status") == PAUSED + for entity_id in ( + "sensor.energy_bill_tariff2", + "sensor.energy_bill_tariff3", + "sensor.energy_bill_tariff4", + ): + state = hass.states.get(entity_id) + assert state.attributes.get("status") == PAUSED @pytest.mark.parametrize( From b143390d8802b0ab69136785ebd3b5d44db78211 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 28 Mar 2024 13:24:44 +0100 Subject: [PATCH 017/198] Improve device class of utility meter (#114368) --- .../components/utility_meter/sensor.py | 37 +++-- tests/components/utility_meter/test_sensor.py | 138 +++++++++++++++--- 2 files changed, 146 insertions(+), 29 deletions(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 4e9be403cf7..26582df1b44 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Mapping from dataclasses import dataclass from datetime import datetime, timedelta from decimal import Decimal, DecimalException, InvalidOperation @@ -13,6 +14,7 @@ import voluptuous as vol from homeassistant.components.sensor import ( ATTR_LAST_RESET, + DEVICE_CLASS_UNITS, RestoreSensor, SensorDeviceClass, SensorExtraStoredData, @@ -21,12 +23,12 @@ from homeassistant.components.sensor import ( from homeassistant.components.sensor.recorder import _suggest_report_issue from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, CONF_UNIQUE_ID, STATE_UNAVAILABLE, STATE_UNKNOWN, - UnitOfEnergy, ) from homeassistant.core import Event, HomeAssistant, State, callback from homeassistant.helpers import ( @@ -47,6 +49,7 @@ from homeassistant.helpers.template import is_number from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util import slugify import homeassistant.util.dt as dt_util +from homeassistant.util.enum import try_parse_enum from .const import ( ATTR_CRON_PATTERN, @@ -97,12 +100,6 @@ ATTR_LAST_PERIOD = "last_period" ATTR_LAST_VALID_STATE = "last_valid_state" ATTR_TARIFF = "tariff" -DEVICE_CLASS_MAP = { - UnitOfEnergy.WATT_HOUR: SensorDeviceClass.ENERGY, - UnitOfEnergy.KILO_WATT_HOUR: SensorDeviceClass.ENERGY, -} - - PRECISION = 3 PAUSED = "paused" COLLECTING = "collecting" @@ -313,6 +310,7 @@ class UtilitySensorExtraStoredData(SensorExtraStoredData): last_reset: datetime | None last_valid_state: Decimal | None status: str + input_device_class: SensorDeviceClass | None def as_dict(self) -> dict[str, Any]: """Return a dict representation of the utility sensor data.""" @@ -324,6 +322,7 @@ class UtilitySensorExtraStoredData(SensorExtraStoredData): str(self.last_valid_state) if self.last_valid_state else None ) data["status"] = self.status + data["input_device_class"] = str(self.input_device_class) return data @@ -343,6 +342,9 @@ class UtilitySensorExtraStoredData(SensorExtraStoredData): else None ) status: str = restored["status"] + input_device_class = try_parse_enum( + SensorDeviceClass, restored.get("input_device_class") + ) except KeyError: # restored is a dict, but does not have all values return None @@ -357,6 +359,7 @@ class UtilitySensorExtraStoredData(SensorExtraStoredData): last_reset, last_valid_state, status, + input_device_class, ) @@ -397,6 +400,7 @@ class UtilityMeterSensor(RestoreSensor): self._last_valid_state = None self._collecting = None self._name = name + self._input_device_class = None self._unit_of_measurement = None self._period = meter_type if meter_type is not None: @@ -416,9 +420,10 @@ class UtilityMeterSensor(RestoreSensor): self._tariff = tariff self._tariff_entity = tariff_entity - def start(self, unit): + def start(self, attributes: Mapping[str, Any]) -> None: """Initialize unit and state upon source initial update.""" - self._unit_of_measurement = unit + self._input_device_class = attributes.get(ATTR_DEVICE_CLASS) + self._unit_of_measurement = attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._state = 0 self.async_write_ha_state() @@ -482,6 +487,7 @@ class UtilityMeterSensor(RestoreSensor): new_state = event.data["new_state"] if new_state is None: return + new_state_attributes: Mapping[str, Any] = new_state.attributes or {} # First check if the new_state is valid (see discussion in PR #88446) if (new_state_val := self._validate_state(new_state)) is None: @@ -498,7 +504,7 @@ class UtilityMeterSensor(RestoreSensor): for sensor in self.hass.data[DATA_UTILITY][self._parent_meter][ DATA_TARIFF_SENSORS ]: - sensor.start(new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)) + sensor.start(new_state_attributes) if self._unit_of_measurement is None: _LOGGER.warning( "Source sensor %s has no unit of measurement. Please %s", @@ -512,7 +518,8 @@ class UtilityMeterSensor(RestoreSensor): # If net_consumption is off, the adjustment must be non-negative self._state += adjustment # type: ignore[operator] # self._state will be set to by the start function if it is None, therefore it always has a valid Decimal value at this line - self._unit_of_measurement = new_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + self._input_device_class = new_state_attributes.get(ATTR_DEVICE_CLASS) + self._unit_of_measurement = new_state_attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._last_valid_state = new_state_val self.async_write_ha_state() @@ -600,6 +607,7 @@ class UtilityMeterSensor(RestoreSensor): if (last_sensor_data := await self.async_get_last_sensor_data()) is not None: # new introduced in 2022.04 self._state = last_sensor_data.native_value + self._input_device_class = last_sensor_data.input_device_class self._unit_of_measurement = last_sensor_data.native_unit_of_measurement self._last_period = last_sensor_data.last_period self._last_reset = last_sensor_data.last_reset @@ -693,7 +701,11 @@ class UtilityMeterSensor(RestoreSensor): @property def device_class(self): """Return the device class of the sensor.""" - return DEVICE_CLASS_MAP.get(self._unit_of_measurement) + if self._input_device_class is not None: + return self._input_device_class + if self._unit_of_measurement in DEVICE_CLASS_UNITS[SensorDeviceClass.ENERGY]: + return SensorDeviceClass.ENERGY + return None @property def state_class(self): @@ -744,6 +756,7 @@ class UtilityMeterSensor(RestoreSensor): self._last_reset, self._last_valid_state, PAUSED if self._collecting is None else COLLECTING, + self._input_device_class, ) async def async_get_last_sensor_data(self) -> UtilitySensorExtraStoredData | None: diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 13b367b1fb7..99a63809329 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -40,6 +40,7 @@ from homeassistant.const import ( STATE_UNAVAILABLE, STATE_UNKNOWN, UnitOfEnergy, + UnitOfVolume, ) from homeassistant.core import CoreState, HomeAssistant, State from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -553,8 +554,66 @@ async def test_entity_name(hass: HomeAssistant, yaml_config, entity_id, name) -> ), ], ) +@pytest.mark.parametrize( + ( + "energy_sensor_attributes", + "gas_sensor_attributes", + "energy_meter_attributes", + "gas_meter_attributes", + ), + [ + ( + {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR}, + {ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit"}, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.ENERGY, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + }, + { + ATTR_DEVICE_CLASS: None, + ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit", + }, + ), + ( + {}, + {}, + { + ATTR_DEVICE_CLASS: None, + ATTR_UNIT_OF_MEASUREMENT: None, + }, + { + ATTR_DEVICE_CLASS: None, + ATTR_UNIT_OF_MEASUREMENT: None, + }, + ), + ( + { + ATTR_DEVICE_CLASS: SensorDeviceClass.GAS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + }, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.WATER, + ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit", + }, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.GAS, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + }, + { + ATTR_DEVICE_CLASS: SensorDeviceClass.WATER, + ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit", + }, + ), + ], +) async def test_device_class( - hass: HomeAssistant, yaml_config, config_entry_configs + hass: HomeAssistant, + yaml_config, + config_entry_configs, + energy_sensor_attributes, + gas_sensor_attributes, + energy_meter_attributes, + gas_meter_attributes, ) -> None: """Test utility device_class.""" if yaml_config: @@ -579,27 +638,23 @@ async def test_device_class( await hass.async_block_till_done() - hass.states.async_set( - entity_id_energy, 2, {ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR} - ) - hass.states.async_set( - entity_id_gas, 2, {ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit"} - ) + hass.states.async_set(entity_id_energy, 2, energy_sensor_attributes) + hass.states.async_set(entity_id_gas, 2, gas_sensor_attributes) await hass.async_block_till_done() state = hass.states.get("sensor.energy_meter") assert state is not None assert state.state == "0" - assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR + for attr, value in energy_meter_attributes.items(): + assert state.attributes.get(attr) == value state = hass.states.get("sensor.gas_meter") assert state is not None assert state.state == "0" - assert state.attributes.get(ATTR_DEVICE_CLASS) is None assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL_INCREASING - assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "some_archaic_unit" + for attr, value in gas_meter_attributes.items(): + assert state.attributes.get(attr) == value @pytest.mark.parametrize( @@ -610,7 +665,13 @@ async def test_device_class( "utility_meter": { "energy_bill": { "source": "sensor.energy", - "tariffs": ["tariff1", "tariff2", "tariff3", "tariff4"], + "tariffs": [ + "tariff0", + "tariff1", + "tariff2", + "tariff3", + "tariff4", + ], } } }, @@ -626,7 +687,13 @@ async def test_device_class( "offset": 0, "periodically_resetting": True, "source": "sensor.energy", - "tariffs": ["tariff1", "tariff2", "tariff3", "tariff4"], + "tariffs": [ + "tariff0", + "tariff1", + "tariff2", + "tariff3", + "tariff4", + ], }, ), ], @@ -644,7 +711,33 @@ async def test_restore_state( mock_restore_cache_with_extra_data( hass, [ - # sensor.energy_bill_tariff1 is restored as expected + # sensor.energy_bill_tariff0 is restored as expected, including device + # class + ( + State( + "sensor.energy_bill_tariff0", + "0.1", + attributes={ + ATTR_STATUS: PAUSED, + ATTR_LAST_RESET: last_reset_1, + ATTR_UNIT_OF_MEASUREMENT: UnitOfVolume.CUBIC_METERS, + }, + ), + { + "native_value": { + "__type": "", + "decimal_str": "0.2", + }, + "native_unit_of_measurement": "gal", + "last_reset": last_reset_2, + "last_period": "1.3", + "last_valid_state": None, + "status": "collecting", + "input_device_class": "water", + }, + ), + # sensor.energy_bill_tariff1 is restored as expected, except device + # class ( State( "sensor.energy_bill_tariff1", @@ -743,12 +836,21 @@ async def test_restore_state( await hass.async_block_till_done() # restore from cache + state = hass.states.get("sensor.energy_bill_tariff0") + assert state.state == "0.2" + assert state.attributes.get("status") == COLLECTING + assert state.attributes.get("last_reset") == last_reset_2 + assert state.attributes.get("last_valid_state") == "None" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfVolume.GALLONS + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.WATER + state = hass.states.get("sensor.energy_bill_tariff1") assert state.state == "1.2" assert state.attributes.get("status") == PAUSED assert state.attributes.get("last_reset") == last_reset_2 assert state.attributes.get("last_valid_state") == "None" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.KILO_WATT_HOUR + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY state = hass.states.get("sensor.energy_bill_tariff2") assert state.state == "2.1" @@ -756,6 +858,7 @@ async def test_restore_state( assert state.attributes.get("last_reset") == last_reset_1 assert state.attributes.get("last_valid_state") == "None" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.MEGA_WATT_HOUR + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY state = hass.states.get("sensor.energy_bill_tariff3") assert state.state == "3.1" @@ -763,6 +866,7 @@ async def test_restore_state( assert state.attributes.get("last_reset") == last_reset_1 assert state.attributes.get("last_valid_state") == "None" assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UnitOfEnergy.MEGA_WATT_HOUR + assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.ENERGY state = hass.states.get("sensor.energy_bill_tariff4") assert state.state == STATE_UNKNOWN @@ -770,16 +874,16 @@ async def test_restore_state( # utility_meter is loaded, now set sensors according to utility_meter: hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() state = hass.states.get("select.energy_bill") - assert state.state == "tariff1" + assert state.state == "tariff0" - state = hass.states.get("sensor.energy_bill_tariff1") + state = hass.states.get("sensor.energy_bill_tariff0") assert state.attributes.get("status") == COLLECTING for entity_id in ( + "sensor.energy_bill_tariff1", "sensor.energy_bill_tariff2", "sensor.energy_bill_tariff3", "sensor.energy_bill_tariff4", From 1c6689be41f0342b2b0918307242483c11128163 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 28 Mar 2024 11:37:57 +0100 Subject: [PATCH 018/198] Update pytile to 2023.12.0 (#114370) --- homeassistant/components/tile/manifest.json | 2 +- pyproject.toml | 2 -- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 6f311fc5593..8dceddcb77f 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_polling", "loggers": ["pytile"], - "requirements": ["pytile==2023.04.0"] + "requirements": ["pytile==2023.12.0"] } diff --git a/pyproject.toml b/pyproject.toml index c84405c2764..40b1f36a58b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -504,8 +504,6 @@ filterwarnings = [ # https://github.com/eclipse/paho.mqtt.python/issues/653 - >=2.0.0 # https://github.com/eclipse/paho.mqtt.python/pull/665 "ignore:ssl.PROTOCOL_TLS is deprecated:DeprecationWarning:paho.mqtt.client", - # https://github.com/bachya/pytile/pull/280 - >=2023.10.0 - "ignore:datetime.*utcfromtimestamp\\(\\) is deprecated and scheduled for removal:DeprecationWarning:pytile.tile", # https://github.com/rytilahti/python-miio/pull/1809 - >=0.6.0.dev0 "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:miio.protocol", "ignore:datetime.*utcnow\\(\\) is deprecated and scheduled for removal:DeprecationWarning:miio.miioprotocol", diff --git a/requirements_all.txt b/requirements_all.txt index ffa57398564..37fcf49a5e5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2311,7 +2311,7 @@ python-vlc==3.0.18122 pythonegardia==1.0.52 # homeassistant.components.tile -pytile==2023.04.0 +pytile==2023.12.0 # homeassistant.components.tomorrowio pytomorrowio==0.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b35ef776b47..60ac93d37c5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1781,7 +1781,7 @@ python-technove==1.2.2 python-telegram-bot[socks]==21.0.1 # homeassistant.components.tile -pytile==2023.04.0 +pytile==2023.12.0 # homeassistant.components.tomorrowio pytomorrowio==0.3.6 From 5e0a0718e32b70c6f3d9c029f0a8a4e0c22d449e Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 28 Mar 2024 11:09:59 +0100 Subject: [PATCH 019/198] Fix streamlabswater feedback (#114371) --- homeassistant/components/streamlabswater/__init__.py | 4 ++-- homeassistant/components/streamlabswater/strings.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/streamlabswater/__init__.py b/homeassistant/components/streamlabswater/__init__.py index c3bbe5a96d4..46acc443d2e 100644 --- a/homeassistant/components/streamlabswater/__init__.py +++ b/homeassistant/components/streamlabswater/__init__.py @@ -81,12 +81,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async_create_issue( hass, DOMAIN, - f"deprecated_yaml_import_issue_${result['reason']}", + f"deprecated_yaml_import_issue_{result['reason']}", breaks_in_ha_version="2024.7.0", is_fixable=False, issue_domain=DOMAIN, severity=IssueSeverity.WARNING, - translation_key=f"deprecated_yaml_import_issue_${result['reason']}", + translation_key=f"deprecated_yaml_import_issue_{result['reason']}", translation_placeholders=ISSUE_PLACEHOLDER, ) return True diff --git a/homeassistant/components/streamlabswater/strings.json b/homeassistant/components/streamlabswater/strings.json index 204f7e831ef..872a0d1f6ac 100644 --- a/homeassistant/components/streamlabswater/strings.json +++ b/homeassistant/components/streamlabswater/strings.json @@ -52,7 +52,7 @@ "issues": { "deprecated_yaml_import_issue_cannot_connect": { "title": "The Streamlabs water YAML configuration import failed", - "description": "Configuring Streamlabs water using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to Streamlabs water works and restart Home Assistant to try again or remove the Streamlabs water YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + "description": "Configuring Streamlabs water using YAML is being removed but there was a connection error importing your YAML configuration.\n\nEnsure connection to Streamlabs water works and restart Home Assistant to try again or remove the Streamlabs water YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." }, "deprecated_yaml_import_issue_unknown": { "title": "The Streamlabs water YAML configuration import failed", From ba12652cbc663c2d6c398e299fdc5591cfde923b Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 28 Mar 2024 11:10:46 +0100 Subject: [PATCH 020/198] Fix Suez water feedback (#114372) --- homeassistant/components/suez_water/sensor.py | 4 ++-- homeassistant/components/suez_water/strings.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/suez_water/sensor.py b/homeassistant/components/suez_water/sensor.py index 7060339250c..f48e78bb153 100644 --- a/homeassistant/components/suez_water/sensor.py +++ b/homeassistant/components/suez_water/sensor.py @@ -74,12 +74,12 @@ async def async_setup_platform( async_create_issue( hass, DOMAIN, - f"deprecated_yaml_import_issue_${result['reason']}", + f"deprecated_yaml_import_issue_{result['reason']}", breaks_in_ha_version="2024.7.0", is_fixable=False, issue_domain=DOMAIN, severity=IssueSeverity.WARNING, - translation_key=f"deprecated_yaml_import_issue_${result['reason']}", + translation_key=f"deprecated_yaml_import_issue_{result['reason']}", translation_placeholders=ISSUE_PLACEHOLDER, ) diff --git a/homeassistant/components/suez_water/strings.json b/homeassistant/components/suez_water/strings.json index b4b81a788b5..fd85565d297 100644 --- a/homeassistant/components/suez_water/strings.json +++ b/homeassistant/components/suez_water/strings.json @@ -32,7 +32,7 @@ }, "deprecated_yaml_import_issue_cannot_connect": { "title": "The Suez water YAML configuration import failed", - "description": "Configuring Suez water using YAML is being removed but there was an connection error importing your YAML configuration.\n\nEnsure connection to Suez water works and restart Home Assistant to try again or remove the Suez water YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." + "description": "Configuring Suez water using YAML is being removed but there was a connection error importing your YAML configuration.\n\nEnsure connection to Suez water works and restart Home Assistant to try again or remove the Suez water YAML configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." }, "deprecated_yaml_import_issue_unknown": { "title": "The Suez water YAML configuration import failed", From 99282d27c6c031be948e6c420f53a4f205c74be5 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 28 Mar 2024 11:11:28 +0100 Subject: [PATCH 021/198] Fix Swiss public transport feedback (#114373) --- homeassistant/components/swiss_public_transport/sensor.py | 4 ++-- homeassistant/components/swiss_public_transport/strings.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/swiss_public_transport/sensor.py b/homeassistant/components/swiss_public_transport/sensor.py index 7c712c8c189..a4a9605a603 100644 --- a/homeassistant/components/swiss_public_transport/sensor.py +++ b/homeassistant/components/swiss_public_transport/sensor.py @@ -131,12 +131,12 @@ async def async_setup_platform( async_create_issue( hass, DOMAIN, - f"deprecated_yaml_import_issue_${result['reason']}", + f"deprecated_yaml_import_issue_{result['reason']}", breaks_in_ha_version="2024.7.0", is_fixable=False, issue_domain=DOMAIN, severity=IssueSeverity.WARNING, - translation_key=f"deprecated_yaml_import_issue_${result['reason']}", + translation_key=f"deprecated_yaml_import_issue_{result['reason']}", translation_placeholders=PLACEHOLDERS, ) diff --git a/homeassistant/components/swiss_public_transport/strings.json b/homeassistant/components/swiss_public_transport/strings.json index c0e88f08b8d..c080e785f2c 100644 --- a/homeassistant/components/swiss_public_transport/strings.json +++ b/homeassistant/components/swiss_public_transport/strings.json @@ -38,7 +38,7 @@ "issues": { "deprecated_yaml_import_issue_cannot_connect": { "title": "The swiss public transport YAML configuration import cannot connect to server", - "description": "Configuring swiss public transport using YAML is being removed but there was an connection error importing your YAML configuration.\n\nMake sure your home assistant can reach the [opendata server]({opendata_url}). In case the server is down, try again later." + "description": "Configuring swiss public transport using YAML is being removed but there was a connection error importing your YAML configuration.\n\nMake sure your home assistant can reach the [opendata server]({opendata_url}). In case the server is down, try again later." }, "deprecated_yaml_import_issue_bad_config": { "title": "The swiss public transport YAML configuration import request failed due to bad config", From 80273b4873a6ca9d02f64cb93154556eaac2445d Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 28 Mar 2024 11:12:02 +0100 Subject: [PATCH 022/198] Fix Lupusec feedback (#114374) --- homeassistant/components/lupusec/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lupusec/__init__.py b/homeassistant/components/lupusec/__init__.py index c471902813a..51bba44aef0 100644 --- a/homeassistant/components/lupusec/__init__.py +++ b/homeassistant/components/lupusec/__init__.py @@ -79,12 +79,12 @@ async def handle_async_init_result(hass: HomeAssistant, domain: str, conf: dict) async_create_issue( hass, DOMAIN, - f"deprecated_yaml_import_issue_${result['reason']}", + f"deprecated_yaml_import_issue_{result['reason']}", breaks_in_ha_version="2024.8.0", is_fixable=False, issue_domain=DOMAIN, severity=IssueSeverity.WARNING, - translation_key=f"deprecated_yaml_import_issue_${result['reason']}", + translation_key=f"deprecated_yaml_import_issue_{result['reason']}", translation_placeholders=ISSUE_PLACEHOLDER, ) From aa301942493c8f5c81ea7c6b0b1711a6b9bdf15a Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 28 Mar 2024 13:25:01 +0100 Subject: [PATCH 023/198] Adapt Tractive integration the latest API changes (#114380) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/tractive/__init__.py | 15 ++------------- homeassistant/components/tractive/const.py | 1 - homeassistant/components/tractive/sensor.py | 5 ++--- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/tractive/__init__.py b/homeassistant/components/tractive/__init__.py index 41e691f783e..136e8b3632a 100644 --- a/homeassistant/components/tractive/__init__.py +++ b/homeassistant/components/tractive/__init__.py @@ -40,7 +40,6 @@ from .const import ( SERVER_UNAVAILABLE, SWITCH_KEY_MAP, TRACKABLES, - TRACKER_ACTIVITY_STATUS_UPDATED, TRACKER_HARDWARE_STATUS_UPDATED, TRACKER_POSITION_UPDATED, TRACKER_SWITCH_STATUS_UPDATED, @@ -220,9 +219,6 @@ class TractiveClient: if server_was_unavailable: _LOGGER.debug("Tractive is back online") server_was_unavailable = False - if event["message"] == "activity_update": - self._send_activity_update(event) - continue if event["message"] == "wellness_overview": self._send_wellness_update(event) continue @@ -291,15 +287,6 @@ class TractiveClient: TRACKER_SWITCH_STATUS_UPDATED, event["tracker_id"], payload ) - def _send_activity_update(self, event: dict[str, Any]) -> None: - payload = { - ATTR_MINUTES_ACTIVE: event["progress"]["achieved_minutes"], - ATTR_DAILY_GOAL: event["progress"]["goal_minutes"], - } - self._dispatch_tracker_event( - TRACKER_ACTIVITY_STATUS_UPDATED, event["pet_id"], payload - ) - def _send_wellness_update(self, event: dict[str, Any]) -> None: sleep_day = None sleep_night = None @@ -309,6 +296,8 @@ class TractiveClient: payload = { ATTR_ACTIVITY_LABEL: event["wellness"].get("activity_label"), ATTR_CALORIES: event["activity"]["calories"], + ATTR_DAILY_GOAL: event["activity"]["minutes_goal"], + ATTR_MINUTES_ACTIVE: event["activity"]["minutes_active"], ATTR_MINUTES_DAY_SLEEP: sleep_day, ATTR_MINUTES_NIGHT_SLEEP: sleep_night, ATTR_MINUTES_REST: event["activity"]["minutes_rest"], diff --git a/homeassistant/components/tractive/const.py b/homeassistant/components/tractive/const.py index acb4f6f7487..f26c0ee2345 100644 --- a/homeassistant/components/tractive/const.py +++ b/homeassistant/components/tractive/const.py @@ -26,7 +26,6 @@ CLIENT_ID = "625e5349c3c3b41c28a669f1" CLIENT = "client" TRACKABLES = "trackables" -TRACKER_ACTIVITY_STATUS_UPDATED = f"{DOMAIN}_tracker_activity_updated" TRACKER_HARDWARE_STATUS_UPDATED = f"{DOMAIN}_tracker_hardware_status_updated" TRACKER_POSITION_UPDATED = f"{DOMAIN}_tracker_position_updated" TRACKER_SWITCH_STATUS_UPDATED = f"{DOMAIN}_tracker_switch_updated" diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index b73b5faba05..5e2f3288f57 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -37,7 +37,6 @@ from .const import ( CLIENT, DOMAIN, TRACKABLES, - TRACKER_ACTIVITY_STATUS_UPDATED, TRACKER_HARDWARE_STATUS_UPDATED, TRACKER_WELLNESS_STATUS_UPDATED, ) @@ -118,7 +117,7 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( key=ATTR_MINUTES_ACTIVE, translation_key="activity_time", native_unit_of_measurement=UnitOfTime.MINUTES, - signal_prefix=TRACKER_ACTIVITY_STATUS_UPDATED, + signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, state_class=SensorStateClass.TOTAL, ), TractiveSensorEntityDescription( @@ -139,7 +138,7 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( key=ATTR_DAILY_GOAL, translation_key="daily_goal", native_unit_of_measurement=UnitOfTime.MINUTES, - signal_prefix=TRACKER_ACTIVITY_STATUS_UPDATED, + signal_prefix=TRACKER_WELLNESS_STATUS_UPDATED, ), TractiveSensorEntityDescription( key=ATTR_MINUTES_DAY_SLEEP, From 8cd871885519c50ac89b8da86faaa8a0276b3203 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Mar 2024 16:20:20 +0100 Subject: [PATCH 024/198] Fix hassfest service icons check for custom integrations (#114389) --- script/hassfest/services.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/script/hassfest/services.py b/script/hassfest/services.py index 34f9b906fb5..c962d84e6e1 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -168,7 +168,8 @@ def validate_services(config: Config, integration: Integration) -> None: # 2. Check if the service has an icon set in icons.json. # raise an error if not., for service_name, service_schema in services.items(): - if service_name not in service_icons: + if integration.core and service_name not in service_icons: + # This is enforced for Core integrations only integration.add_error( "services", f"Service {service_name} has no icon in icons.json.", From c81e9447f9ccd334936d1d0164fd3459d8e1cd80 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 28 Mar 2024 11:09:15 -0500 Subject: [PATCH 025/198] Filter preferred TTS format options if not supported (#114392) Filter preferred format options if not supported --- homeassistant/components/tts/__init__.py | 74 ++++++++---- tests/components/assist_pipeline/conftest.py | 3 +- tests/components/assist_pipeline/test_init.py | 105 ++++++++++++++++-- 3 files changed, 150 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index c88e0e83334..8ea4617bbf3 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -16,7 +16,7 @@ import os import re import subprocess import tempfile -from typing import Any, TypedDict, final +from typing import Any, Final, TypedDict, final from aiohttp import web import mutagen @@ -99,6 +99,13 @@ ATTR_PREFERRED_SAMPLE_CHANNELS = "preferred_sample_channels" ATTR_MEDIA_PLAYER_ENTITY_ID = "media_player_entity_id" ATTR_VOICE = "voice" +_DEFAULT_FORMAT = "mp3" +_PREFFERED_FORMAT_OPTIONS: Final[set[str]] = { + ATTR_PREFERRED_FORMAT, + ATTR_PREFERRED_SAMPLE_RATE, + ATTR_PREFERRED_SAMPLE_CHANNELS, +} + CONF_LANG = "language" SERVICE_CLEAR_CACHE = "clear_cache" @@ -569,25 +576,23 @@ class SpeechManager: ): raise HomeAssistantError(f"Language '{language}' not supported") + options = options or {} + supported_options = engine_instance.supported_options or [] + # Update default options with provided options + invalid_opts: list[str] = [] merged_options = dict(engine_instance.default_options or {}) - merged_options.update(options or {}) + for option_name, option_value in options.items(): + # Only count an option as invalid if it's not a "preferred format" + # option. These are used as hints to the TTS system if supported, + # and otherwise as parameters to ffmpeg conversion. + if (option_name in supported_options) or ( + option_name in _PREFFERED_FORMAT_OPTIONS + ): + merged_options[option_name] = option_value + else: + invalid_opts.append(option_name) - supported_options = list(engine_instance.supported_options or []) - - # ATTR_PREFERRED_* options are always "supported" since they're used to - # convert audio after the TTS has run (if necessary). - supported_options.extend( - ( - ATTR_PREFERRED_FORMAT, - ATTR_PREFERRED_SAMPLE_RATE, - ATTR_PREFERRED_SAMPLE_CHANNELS, - ) - ) - - invalid_opts = [ - opt_name for opt_name in merged_options if opt_name not in supported_options - ] if invalid_opts: raise HomeAssistantError(f"Invalid options found: {invalid_opts}") @@ -687,10 +692,31 @@ class SpeechManager: This method is a coroutine. """ - options = options or {} + options = dict(options or {}) + supported_options = engine_instance.supported_options or [] - # Default to MP3 unless a different format is preferred - final_extension = options.get(ATTR_PREFERRED_FORMAT, "mp3") + # Extract preferred format options. + # + # These options are used by Assist pipelines, etc. to get a format that + # the voice satellite will support. + # + # The TTS system ideally supports options directly so we won't have + # to convert with ffmpeg later. If not, we pop the options here and + # perform the conversation after receiving the audio. + if ATTR_PREFERRED_FORMAT in supported_options: + final_extension = options.get(ATTR_PREFERRED_FORMAT, _DEFAULT_FORMAT) + else: + final_extension = options.pop(ATTR_PREFERRED_FORMAT, _DEFAULT_FORMAT) + + if ATTR_PREFERRED_SAMPLE_RATE in supported_options: + sample_rate = options.get(ATTR_PREFERRED_SAMPLE_RATE) + else: + sample_rate = options.pop(ATTR_PREFERRED_SAMPLE_RATE, None) + + if ATTR_PREFERRED_SAMPLE_CHANNELS in supported_options: + sample_channels = options.get(ATTR_PREFERRED_SAMPLE_CHANNELS) + else: + sample_channels = options.pop(ATTR_PREFERRED_SAMPLE_CHANNELS, None) async def get_tts_data() -> str: """Handle data available.""" @@ -716,8 +742,8 @@ class SpeechManager: # rate/format/channel count is requested. needs_conversion = ( (final_extension != extension) - or (ATTR_PREFERRED_SAMPLE_RATE in options) - or (ATTR_PREFERRED_SAMPLE_CHANNELS in options) + or (sample_rate is not None) + or (sample_channels is not None) ) if needs_conversion: @@ -726,8 +752,8 @@ class SpeechManager: extension, data, to_extension=final_extension, - to_sample_rate=options.get(ATTR_PREFERRED_SAMPLE_RATE), - to_sample_channels=options.get(ATTR_PREFERRED_SAMPLE_CHANNELS), + to_sample_rate=sample_rate, + to_sample_channels=sample_channels, ) # Create file infos diff --git a/tests/components/assist_pipeline/conftest.py b/tests/components/assist_pipeline/conftest.py index 8c5cfe9d599..9f098150288 100644 --- a/tests/components/assist_pipeline/conftest.py +++ b/tests/components/assist_pipeline/conftest.py @@ -111,6 +111,7 @@ class MockTTSProvider(tts.Provider): tts.Voice("fran_drescher", "Fran Drescher"), ] } + _supported_options = ["voice", "age", tts.ATTR_AUDIO_OUTPUT] @property def default_language(self) -> str: @@ -130,7 +131,7 @@ class MockTTSProvider(tts.Provider): @property def supported_options(self) -> list[str]: """Return list of supported options like voice, emotions.""" - return ["voice", "age", tts.ATTR_AUDIO_OUTPUT] + return self._supported_options def get_tts_audio( self, message: str, language: str, options: dict[str, Any] diff --git a/tests/components/assist_pipeline/test_init.py b/tests/components/assist_pipeline/test_init.py index 81347e96235..c6f45044cb3 100644 --- a/tests/components/assist_pipeline/test_init.py +++ b/tests/components/assist_pipeline/test_init.py @@ -11,7 +11,7 @@ import wave import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.components import assist_pipeline, stt, tts +from homeassistant.components import assist_pipeline, media_source, stt, tts from homeassistant.components.assist_pipeline.const import ( CONF_DEBUG_RECORDING_DIR, DOMAIN, @@ -19,9 +19,14 @@ from homeassistant.components.assist_pipeline.const import ( from homeassistant.core import Context, HomeAssistant from homeassistant.setup import async_setup_component -from .conftest import MockSttProvider, MockSttProviderEntity, MockWakeWordEntity +from .conftest import ( + MockSttProvider, + MockSttProviderEntity, + MockTTSProvider, + MockWakeWordEntity, +) -from tests.typing import WebSocketGenerator +from tests.typing import ClientSessionGenerator, WebSocketGenerator BYTES_ONE_SECOND = 16000 * 2 @@ -729,15 +734,17 @@ def test_pipeline_run_equality(hass: HomeAssistant, init_components) -> None: async def test_tts_audio_output( hass: HomeAssistant, - mock_stt_provider: MockSttProvider, + hass_client: ClientSessionGenerator, + mock_tts_provider: MockTTSProvider, init_components, pipeline_data: assist_pipeline.pipeline.PipelineData, snapshot: SnapshotAssertion, ) -> None: """Test using tts_audio_output with wav sets options correctly.""" + client = await hass_client() + assert await async_setup_component(hass, media_source.DOMAIN, {}) - def event_callback(event): - pass + events: list[assist_pipeline.PipelineEvent] = [] pipeline_store = pipeline_data.pipeline_store pipeline_id = pipeline_store.async_get_preferred_item() @@ -753,7 +760,7 @@ async def test_tts_audio_output( pipeline=pipeline, start_stage=assist_pipeline.PipelineStage.TTS, end_stage=assist_pipeline.PipelineStage.TTS, - event_callback=event_callback, + event_callback=events.append, tts_audio_output="wav", ), ) @@ -764,3 +771,87 @@ async def test_tts_audio_output( assert pipeline_input.run.tts_options.get(tts.ATTR_PREFERRED_FORMAT) == "wav" assert pipeline_input.run.tts_options.get(tts.ATTR_PREFERRED_SAMPLE_RATE) == 16000 assert pipeline_input.run.tts_options.get(tts.ATTR_PREFERRED_SAMPLE_CHANNELS) == 1 + + with patch.object(mock_tts_provider, "get_tts_audio") as mock_get_tts_audio: + await pipeline_input.execute() + + for event in events: + if event.type == assist_pipeline.PipelineEventType.TTS_END: + # We must fetch the media URL to trigger the TTS + assert event.data + media_id = event.data["tts_output"]["media_id"] + resolved = await media_source.async_resolve_media(hass, media_id, None) + await client.get(resolved.url) + + # Ensure that no unsupported options were passed in + assert mock_get_tts_audio.called + options = mock_get_tts_audio.call_args_list[0].kwargs["options"] + extra_options = set(options).difference(mock_tts_provider.supported_options) + assert len(extra_options) == 0, extra_options + + +async def test_tts_supports_preferred_format( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + mock_tts_provider: MockTTSProvider, + init_components, + pipeline_data: assist_pipeline.pipeline.PipelineData, + snapshot: SnapshotAssertion, +) -> None: + """Test that preferred format options are given to the TTS system if supported.""" + client = await hass_client() + assert await async_setup_component(hass, media_source.DOMAIN, {}) + + events: list[assist_pipeline.PipelineEvent] = [] + + pipeline_store = pipeline_data.pipeline_store + pipeline_id = pipeline_store.async_get_preferred_item() + pipeline = assist_pipeline.pipeline.async_get_pipeline(hass, pipeline_id) + + pipeline_input = assist_pipeline.pipeline.PipelineInput( + tts_input="This is a test.", + conversation_id=None, + device_id=None, + run=assist_pipeline.pipeline.PipelineRun( + hass, + context=Context(), + pipeline=pipeline, + start_stage=assist_pipeline.PipelineStage.TTS, + end_stage=assist_pipeline.PipelineStage.TTS, + event_callback=events.append, + tts_audio_output="wav", + ), + ) + await pipeline_input.validate() + + # Make the TTS provider support preferred format options + supported_options = list(mock_tts_provider.supported_options or []) + supported_options.extend( + [ + tts.ATTR_PREFERRED_FORMAT, + tts.ATTR_PREFERRED_SAMPLE_RATE, + tts.ATTR_PREFERRED_SAMPLE_CHANNELS, + ] + ) + + with ( + patch.object(mock_tts_provider, "_supported_options", supported_options), + patch.object(mock_tts_provider, "get_tts_audio") as mock_get_tts_audio, + ): + await pipeline_input.execute() + + for event in events: + if event.type == assist_pipeline.PipelineEventType.TTS_END: + # We must fetch the media URL to trigger the TTS + assert event.data + media_id = event.data["tts_output"]["media_id"] + resolved = await media_source.async_resolve_media(hass, media_id, None) + await client.get(resolved.url) + + assert mock_get_tts_audio.called + options = mock_get_tts_audio.call_args_list[0].kwargs["options"] + + # We should have received preferred format options in get_tts_audio + assert tts.ATTR_PREFERRED_FORMAT in options + assert tts.ATTR_PREFERRED_SAMPLE_RATE in options + assert tts.ATTR_PREFERRED_SAMPLE_CHANNELS in options From 53ba732ed04fb51b05cb6f30cf01c865008b5ef8 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Mar 2024 16:57:29 +0100 Subject: [PATCH 026/198] Fix area search for entities of devices (#114394) --- homeassistant/components/search/__init__.py | 9 +++++--- tests/components/search/test_init.py | 24 ++++++++++++++++++--- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/search/__init__.py b/homeassistant/components/search/__init__.py index 71b51210a25..a85a21e8102 100644 --- a/homeassistant/components/search/__init__.py +++ b/homeassistant/components/search/__init__.py @@ -136,6 +136,9 @@ class Searcher: # Scripts referencing this area self._add(ItemType.SCRIPT, script.scripts_with_area(self.hass, area_id)) + # Entity in this area, will extend this with the entities of the devices in this area + entity_entries = er.async_entries_for_area(self._entity_registry, area_id) + # Devices in this area for device in dr.async_entries_for_area(self._device_registry, area_id): self._add(ItemType.DEVICE, device.id) @@ -160,10 +163,10 @@ class Searcher: # Skip the entity if it's in a different area if entity_entry.area_id is not None: continue - self._add(ItemType.ENTITY, entity_entry.entity_id) + entity_entries.append(entity_entry) - # Entities in this area - for entity_entry in er.async_entries_for_area(self._entity_registry, area_id): + # Process entities in this area + for entity_entry in entity_entries: self._add(ItemType.ENTITY, entity_entry.entity_id) # If this entity also exists as a resource, we add it. diff --git a/tests/components/search/test_init.py b/tests/components/search/test_init.py index ee7b60dc9ac..a817fbfc39e 100644 --- a/tests/components/search/test_init.py +++ b/tests/components/search/test_init.py @@ -496,11 +496,14 @@ async def test_search( ItemType.SCRIPT: {script_scene_entity.entity_id, "script.nested"}, } assert search(ItemType.AREA, living_room_area.id) == { - ItemType.AUTOMATION: {"automation.wled_device"}, + ItemType.AUTOMATION: {"automation.wled_device", "automation.wled_entity"}, ItemType.CONFIG_ENTRY: {wled_config_entry.entry_id}, ItemType.DEVICE: {wled_device.id}, ItemType.ENTITY: {wled_segment_1_entity.entity_id}, ItemType.FLOOR: {first_floor.floor_id}, + ItemType.GROUP: {"group.wled", "group.wled_hue"}, + ItemType.SCENE: {"scene.scene_wled_seg_1", scene_wled_hue_entity.entity_id}, + ItemType.SCRIPT: {"script.wled"}, } assert search(ItemType.AREA, kitchen_area.id) == { ItemType.AUTOMATION: {"automation.area"}, @@ -511,7 +514,9 @@ async def test_search( hue_segment_2_entity.entity_id, }, ItemType.FLOOR: {first_floor.floor_id}, - ItemType.SCRIPT: {"script.area", "script.device"}, + ItemType.GROUP: {"group.hue", "group.wled_hue"}, + ItemType.SCENE: {"scene.scene_hue_seg_1", scene_wled_hue_entity.entity_id}, + ItemType.SCRIPT: {"script.area", "script.device", "script.hue"}, } assert not search(ItemType.AUTOMATION, "automation.unknown") @@ -726,6 +731,7 @@ async def test_search( "automation.area", "automation.floor", "automation.wled_device", + "automation.wled_entity", }, ItemType.CONFIG_ENTRY: {hue_config_entry.entry_id, wled_config_entry.entry_id}, ItemType.DEVICE: {hue_device.id, wled_device.id}, @@ -734,7 +740,19 @@ async def test_search( hue_segment_1_entity.entity_id, hue_segment_2_entity.entity_id, }, - ItemType.SCRIPT: {"script.device", "script.area", "script.floor"}, + ItemType.GROUP: {"group.hue", "group.wled", "group.wled_hue"}, + ItemType.SCENE: { + "scene.scene_hue_seg_1", + "scene.scene_wled_seg_1", + scene_wled_hue_entity.entity_id, + }, + ItemType.SCRIPT: { + "script.device", + "script.area", + "script.floor", + "script.hue", + "script.wled", + }, } assert search(ItemType.FLOOR, second_floor.floor_id) == { ItemType.AREA: {bedroom_area.id}, From 8e4cf4e4a7fbcefa17b7b8861d4edaf6e550a6de Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 28 Mar 2024 20:38:12 +0100 Subject: [PATCH 027/198] Update frontend to 20240328.0 (#114396) --- 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 10917bb7f70..9e86436bd68 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==20240327.0"] + "requirements": ["home-assistant-frontend==20240328.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9af8c2f3e0a..b7db1514cba 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240327.0 +home-assistant-frontend==20240328.0 home-assistant-intents==2024.3.27 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 37fcf49a5e5..9c9e18a6ff3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1077,7 +1077,7 @@ hole==0.8.0 holidays==0.45 # homeassistant.components.frontend -home-assistant-frontend==20240327.0 +home-assistant-frontend==20240328.0 # homeassistant.components.conversation home-assistant-intents==2024.3.27 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 60ac93d37c5..e3cd33e994e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,7 +876,7 @@ hole==0.8.0 holidays==0.45 # homeassistant.components.frontend -home-assistant-frontend==20240327.0 +home-assistant-frontend==20240328.0 # homeassistant.components.conversation home-assistant-intents==2024.3.27 From 7a53ea4b92a7e810a73915f37c834af55cb1b72b Mon Sep 17 00:00:00 2001 From: Scott K Logan Date: Thu, 28 Mar 2024 12:52:17 -0500 Subject: [PATCH 028/198] Bump aioraven to 0.5.3 (#114397) --- 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 ad161d32201..a2717f0e886 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.2"], + "requirements": ["aioraven==0.5.3"], "usb": [ { "vid": "0403", diff --git a/requirements_all.txt b/requirements_all.txt index 9c9e18a6ff3..10c044009be 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.2 +aioraven==0.5.3 # homeassistant.components.recollect_waste aiorecollect==2023.09.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e3cd33e994e..dd555cfebc8 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.2 +aioraven==0.5.3 # homeassistant.components.recollect_waste aiorecollect==2023.09.0 From 612988cf3e07c56ebd67293335ebf3ee378b8b9e Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 28 Mar 2024 20:43:23 +0100 Subject: [PATCH 029/198] Bump version to 2024.4.0b1 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index d458a66b865..f9a9b6324f8 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -18,7 +18,7 @@ from .util.signal_type import SignalType APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 4 -PATCH_VERSION: Final = "0b0" +PATCH_VERSION: Final = "0b1" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 40b1f36a58b..48f520a878c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.4.0b0" +version = "2024.4.0b1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From b8a2c148131a3b1913871aea07d3cd4e43a9317a Mon Sep 17 00:00:00 2001 From: Jeremy TRUFIER Date: Fri, 29 Mar 2024 14:51:44 +0100 Subject: [PATCH 030/198] Follow real AtlanticPassAPCZoneControlZone physical mode on Overkiz (HEAT, COOL or HEAT_COOL) (#111830) * Support HEAT_COOL when mode is Auto on overkiz AtlanticPassAPCZoneControlZone * Refactor ZoneControlZone to simplify usic by only using a single hvac mode * Fix linting issues * Makes more sense to use halves there * Fix PR feedback --- homeassistant/components/overkiz/climate.py | 25 +- .../atlantic_pass_apc_heating_zone.py | 4 +- .../atlantic_pass_apc_zone_control_zone.py | 394 ++++++++++++++---- 3 files changed, 325 insertions(+), 98 deletions(-) diff --git a/homeassistant/components/overkiz/climate.py b/homeassistant/components/overkiz/climate.py index e23403c2162..b569d05d2d7 100644 --- a/homeassistant/components/overkiz/climate.py +++ b/homeassistant/components/overkiz/climate.py @@ -7,6 +7,7 @@ from typing import cast from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import HomeAssistantOverkizData @@ -27,15 +28,16 @@ async def async_setup_entry( """Set up the Overkiz climate from a config entry.""" data: HomeAssistantOverkizData = hass.data[DOMAIN][entry.entry_id] - async_add_entities( + # Match devices based on the widget. + entities_based_on_widget: list[Entity] = [ WIDGET_TO_CLIMATE_ENTITY[device.widget](device.device_url, data.coordinator) for device in data.platforms[Platform.CLIMATE] if device.widget in WIDGET_TO_CLIMATE_ENTITY - ) + ] - # Match devices based on the widget and controllableName - # This is for example used for Atlantic APC, where devices with different functionality share the same uiClass and widget. - async_add_entities( + # Match devices based on the widget and controllableName. + # ie Atlantic APC + entities_based_on_widget_and_controllable: list[Entity] = [ WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY[device.widget][ cast(Controllable, device.controllable_name) ](device.device_url, data.coordinator) @@ -43,14 +45,21 @@ async def async_setup_entry( if device.widget in WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY and device.controllable_name in WIDGET_AND_CONTROLLABLE_TO_CLIMATE_ENTITY[device.widget] - ) + ] - # Hitachi Air To Air Heat Pumps - async_add_entities( + # Match devices based on the widget and protocol. + # #ie Hitachi Air To Air Heat Pumps + entities_based_on_widget_and_protocol: list[Entity] = [ WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY[device.widget][device.protocol]( device.device_url, data.coordinator ) for device in data.platforms[Platform.CLIMATE] if device.widget in WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY and device.protocol in WIDGET_AND_PROTOCOL_TO_CLIMATE_ENTITY[device.widget] + ] + + async_add_entities( + entities_based_on_widget + + entities_based_on_widget_and_controllable + + entities_based_on_widget_and_protocol ) diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py index bf6aa43644e..3da2ccc922b 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_heating_zone.py @@ -159,7 +159,7 @@ class AtlanticPassAPCHeatingZone(OverkizEntity, ClimateEntity): await self.async_set_heating_mode(PRESET_MODES_TO_OVERKIZ[preset_mode]) @property - def preset_mode(self) -> str: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., home, away, temp.""" heating_mode = cast( str, self.executor.select_state(OverkizState.IO_PASS_APC_HEATING_MODE) @@ -179,7 +179,7 @@ class AtlanticPassAPCHeatingZone(OverkizEntity, ClimateEntity): return OVERKIZ_TO_PRESET_MODES[heating_mode] @property - def target_temperature(self) -> float: + def target_temperature(self) -> float | None: """Return hvac target temperature.""" current_heating_profile = self.current_heating_profile if current_heating_profile in OVERKIZ_TEMPERATURE_STATE_BY_PROFILE: diff --git a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control_zone.py b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control_zone.py index 261acc2838c..f18edd0cfe6 100644 --- a/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control_zone.py +++ b/homeassistant/components/overkiz/climate_entities/atlantic_pass_apc_zone_control_zone.py @@ -3,16 +3,24 @@ from __future__ import annotations from asyncio import sleep +from functools import cached_property from typing import Any, cast from pyoverkiz.enums import OverkizCommand, OverkizCommandParam, OverkizState -from homeassistant.components.climate import PRESET_NONE, HVACMode -from homeassistant.const import ATTR_TEMPERATURE +from homeassistant.components.climate import ( + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + PRESET_NONE, + ClimateEntityFeature, + HVACAction, + HVACMode, +) +from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES from ..coordinator import OverkizDataUpdateCoordinator +from ..executor import OverkizExecutor from .atlantic_pass_apc_heating_zone import AtlanticPassAPCHeatingZone -from .atlantic_pass_apc_zone_control import OVERKIZ_TO_HVAC_MODE PRESET_SCHEDULE = "schedule" PRESET_MANUAL = "manual" @@ -24,32 +32,127 @@ OVERKIZ_MODE_TO_PRESET_MODES: dict[str, str] = { PRESET_MODES_TO_OVERKIZ = {v: k for k, v in OVERKIZ_MODE_TO_PRESET_MODES.items()} -TEMPERATURE_ZONECONTROL_DEVICE_INDEX = 20 +# Maps the HVAC current ZoneControl system operating mode. +OVERKIZ_TO_HVAC_ACTION: dict[str, HVACAction] = { + OverkizCommandParam.COOLING: HVACAction.COOLING, + OverkizCommandParam.DRYING: HVACAction.DRYING, + OverkizCommandParam.HEATING: HVACAction.HEATING, + # There is no known way to differentiate OFF from Idle. + OverkizCommandParam.STOP: HVACAction.OFF, +} + +HVAC_ACTION_TO_OVERKIZ_PROFILE_STATE: dict[HVACAction, OverkizState] = { + HVACAction.COOLING: OverkizState.IO_PASS_APC_COOLING_PROFILE, + HVACAction.HEATING: OverkizState.IO_PASS_APC_HEATING_PROFILE, +} + +HVAC_ACTION_TO_OVERKIZ_MODE_STATE: dict[HVACAction, OverkizState] = { + HVACAction.COOLING: OverkizState.IO_PASS_APC_COOLING_MODE, + HVACAction.HEATING: OverkizState.IO_PASS_APC_HEATING_MODE, +} + +TEMPERATURE_ZONECONTROL_DEVICE_INDEX = 1 + +SUPPORTED_FEATURES: ClimateEntityFeature = ( + ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON +) + +OVERKIZ_THERMAL_CONFIGURATION_TO_HVAC_MODE: dict[ + OverkizCommandParam, tuple[HVACMode, ClimateEntityFeature] +] = { + OverkizCommandParam.COOLING: ( + HVACMode.COOL, + SUPPORTED_FEATURES | ClimateEntityFeature.TARGET_TEMPERATURE, + ), + OverkizCommandParam.HEATING: ( + HVACMode.HEAT, + SUPPORTED_FEATURES | ClimateEntityFeature.TARGET_TEMPERATURE, + ), + OverkizCommandParam.HEATING_AND_COOLING: ( + HVACMode.HEAT_COOL, + SUPPORTED_FEATURES | ClimateEntityFeature.TARGET_TEMPERATURE_RANGE, + ), +} -# Those device depends on a main probe that choose the operating mode (heating, cooling, ...) +# Those device depends on a main probe that choose the operating mode (heating, cooling, ...). class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): """Representation of Atlantic Pass APC Heating And Cooling Zone Control.""" + _attr_target_temperature_step = PRECISION_HALVES + def __init__( self, device_url: str, coordinator: OverkizDataUpdateCoordinator ) -> None: """Init method.""" super().__init__(device_url, coordinator) - # There is less supported functions, because they depend on the ZoneControl. - if not self.is_using_derogated_temperature_fallback: - # Modes are not configurable, they will follow current HVAC Mode of Zone Control. - self._attr_hvac_modes = [] + # When using derogated temperature, we fallback to legacy behavior. + if self.is_using_derogated_temperature_fallback: + return - # Those are available and tested presets on Shogun. - self._attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ] + self._attr_hvac_modes = [] + self._attr_supported_features = ClimateEntityFeature(0) + + # Modes depends on device capabilities. + if (thermal_configuration := self.thermal_configuration) is not None: + ( + device_hvac_mode, + climate_entity_feature, + ) = thermal_configuration + self._attr_hvac_modes = [device_hvac_mode, HVACMode.OFF] + self._attr_supported_features = climate_entity_feature + + # Those are available and tested presets on Shogun. + self._attr_preset_modes = [*PRESET_MODES_TO_OVERKIZ] # Those APC Heating and Cooling probes depends on the zone control device (main probe). # Only the base device (#1) can be used to get/set some states. # Like to retrieve and set the current operating mode (heating, cooling, drying, off). - self.zone_control_device = self.executor.linked_device( - TEMPERATURE_ZONECONTROL_DEVICE_INDEX + + self.zone_control_executor: OverkizExecutor | None = None + + if ( + zone_control_device := self.executor.linked_device( + TEMPERATURE_ZONECONTROL_DEVICE_INDEX + ) + ) is not None: + self.zone_control_executor = OverkizExecutor( + zone_control_device.device_url, + coordinator, + ) + + @cached_property + def thermal_configuration(self) -> tuple[HVACMode, ClimateEntityFeature] | None: + """Retrieve thermal configuration for this devices.""" + + if ( + ( + state_thermal_configuration := cast( + OverkizCommandParam | None, + self.executor.select_state(OverkizState.CORE_THERMAL_CONFIGURATION), + ) + ) + is not None + and state_thermal_configuration + in OVERKIZ_THERMAL_CONFIGURATION_TO_HVAC_MODE + ): + return OVERKIZ_THERMAL_CONFIGURATION_TO_HVAC_MODE[ + state_thermal_configuration + ] + + return None + + @cached_property + def device_hvac_mode(self) -> HVACMode | None: + """ZoneControlZone device has a single possible mode.""" + + return ( + None + if self.thermal_configuration is None + else self.thermal_configuration[0] ) @property @@ -61,21 +164,37 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): ) @property - def zone_control_hvac_mode(self) -> HVACMode: + def zone_control_hvac_action(self) -> HVACAction: """Return hvac operation ie. heat, cool, dry, off mode.""" - if ( - self.zone_control_device is not None - and ( - state := self.zone_control_device.states[ + if self.zone_control_executor is not None and ( + ( + state := self.zone_control_executor.select_state( OverkizState.IO_PASS_APC_OPERATING_MODE - ] + ) ) is not None - and (value := state.value_as_str) is not None ): - return OVERKIZ_TO_HVAC_MODE[value] - return HVACMode.OFF + return OVERKIZ_TO_HVAC_ACTION[cast(str, state)] + + return HVACAction.OFF + + @property + def hvac_action(self) -> HVACAction | None: + """Return the current running hvac operation.""" + + # When ZoneControl action is heating/cooling but Zone is stopped, means the zone is idle. + if ( + hvac_action := self.zone_control_hvac_action + ) in HVAC_ACTION_TO_OVERKIZ_PROFILE_STATE and cast( + str, + self.executor.select_state( + HVAC_ACTION_TO_OVERKIZ_PROFILE_STATE[hvac_action] + ), + ) == OverkizCommandParam.STOP: + return HVACAction.IDLE + + return hvac_action @property def hvac_mode(self) -> HVACMode: @@ -84,30 +203,32 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): if self.is_using_derogated_temperature_fallback: return super().hvac_mode - zone_control_hvac_mode = self.zone_control_hvac_mode + if (device_hvac_mode := self.device_hvac_mode) is None: + return HVACMode.OFF - # Should be same, because either thermostat or this integration change both. - on_off_state = cast( + cooling_is_off = cast( str, - self.executor.select_state( - OverkizState.CORE_COOLING_ON_OFF - if zone_control_hvac_mode == HVACMode.COOL - else OverkizState.CORE_HEATING_ON_OFF - ), - ) + self.executor.select_state(OverkizState.CORE_COOLING_ON_OFF), + ) in (OverkizCommandParam.OFF, None) + + heating_is_off = cast( + str, + self.executor.select_state(OverkizState.CORE_HEATING_ON_OFF), + ) in (OverkizCommandParam.OFF, None) # Device is Stopped, it means the air flux is flowing but its venting door is closed. - if on_off_state == OverkizCommandParam.OFF: - hvac_mode = HVACMode.OFF - else: - hvac_mode = zone_control_hvac_mode + if ( + (device_hvac_mode == HVACMode.COOL and cooling_is_off) + or (device_hvac_mode == HVACMode.HEAT and heating_is_off) + or ( + device_hvac_mode == HVACMode.HEAT_COOL + and cooling_is_off + and heating_is_off + ) + ): + return HVACMode.OFF - # It helps keep it consistent with the Zone Control, within the interface. - if self._attr_hvac_modes != [zone_control_hvac_mode, HVACMode.OFF]: - self._attr_hvac_modes = [zone_control_hvac_mode, HVACMode.OFF] - self.async_write_ha_state() - - return hvac_mode + return device_hvac_mode async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: """Set new target hvac mode.""" @@ -118,46 +239,49 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): # They are mainly managed by the Zone Control device # However, it make sense to map the OFF Mode to the Overkiz STOP Preset - if hvac_mode == HVACMode.OFF: - await self.executor.async_execute_command( - OverkizCommand.SET_COOLING_ON_OFF, - OverkizCommandParam.OFF, - ) - await self.executor.async_execute_command( - OverkizCommand.SET_HEATING_ON_OFF, - OverkizCommandParam.OFF, - ) - else: - await self.executor.async_execute_command( - OverkizCommand.SET_COOLING_ON_OFF, - OverkizCommandParam.ON, - ) - await self.executor.async_execute_command( - OverkizCommand.SET_HEATING_ON_OFF, - OverkizCommandParam.ON, - ) + on_off_target_command_param = ( + OverkizCommandParam.OFF + if hvac_mode == HVACMode.OFF + else OverkizCommandParam.ON + ) + + await self.executor.async_execute_command( + OverkizCommand.SET_COOLING_ON_OFF, + on_off_target_command_param, + ) + await self.executor.async_execute_command( + OverkizCommand.SET_HEATING_ON_OFF, + on_off_target_command_param, + ) await self.async_refresh_modes() @property - def preset_mode(self) -> str: + def preset_mode(self) -> str | None: """Return the current preset mode, e.g., schedule, manual.""" if self.is_using_derogated_temperature_fallback: return super().preset_mode - mode = OVERKIZ_MODE_TO_PRESET_MODES[ - cast( - str, - self.executor.select_state( - OverkizState.IO_PASS_APC_COOLING_MODE - if self.zone_control_hvac_mode == HVACMode.COOL - else OverkizState.IO_PASS_APC_HEATING_MODE - ), + if ( + self.zone_control_hvac_action in HVAC_ACTION_TO_OVERKIZ_MODE_STATE + and ( + mode_state := HVAC_ACTION_TO_OVERKIZ_MODE_STATE[ + self.zone_control_hvac_action + ] ) - ] + and ( + ( + mode := OVERKIZ_MODE_TO_PRESET_MODES[ + cast(str, self.executor.select_state(mode_state)) + ] + ) + is not None + ) + ): + return mode - return mode if mode is not None else PRESET_NONE + return PRESET_NONE async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" @@ -178,13 +302,18 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): await self.async_refresh_modes() @property - def target_temperature(self) -> float: + def target_temperature(self) -> float | None: """Return hvac target temperature.""" if self.is_using_derogated_temperature_fallback: return super().target_temperature - if self.zone_control_hvac_mode == HVACMode.COOL: + device_hvac_mode = self.device_hvac_mode + + if device_hvac_mode == HVACMode.HEAT_COOL: + return None + + if device_hvac_mode == HVACMode.COOL: return cast( float, self.executor.select_state( @@ -192,7 +321,7 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): ), ) - if self.zone_control_hvac_mode == HVACMode.HEAT: + if device_hvac_mode == HVACMode.HEAT: return cast( float, self.executor.select_state( @@ -204,32 +333,73 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): float, self.executor.select_state(OverkizState.CORE_TARGET_TEMPERATURE) ) + @property + def target_temperature_high(self) -> float | None: + """Return the highbound target temperature we try to reach (cooling).""" + + if self.device_hvac_mode != HVACMode.HEAT_COOL: + return None + + return cast( + float, + self.executor.select_state(OverkizState.CORE_COOLING_TARGET_TEMPERATURE), + ) + + @property + def target_temperature_low(self) -> float | None: + """Return the lowbound target temperature we try to reach (heating).""" + + if self.device_hvac_mode != HVACMode.HEAT_COOL: + return None + + return cast( + float, + self.executor.select_state(OverkizState.CORE_HEATING_TARGET_TEMPERATURE), + ) + async def async_set_temperature(self, **kwargs: Any) -> None: """Set new temperature.""" if self.is_using_derogated_temperature_fallback: return await super().async_set_temperature(**kwargs) - temperature = kwargs[ATTR_TEMPERATURE] + target_temperature = kwargs.get(ATTR_TEMPERATURE) + target_temp_low = kwargs.get(ATTR_TARGET_TEMP_LOW) + target_temp_high = kwargs.get(ATTR_TARGET_TEMP_HIGH) + hvac_mode = self.hvac_mode + + if hvac_mode == HVACMode.HEAT_COOL: + if target_temp_low is not None: + await self.executor.async_execute_command( + OverkizCommand.SET_HEATING_TARGET_TEMPERATURE, + target_temp_low, + ) + + if target_temp_high is not None: + await self.executor.async_execute_command( + OverkizCommand.SET_COOLING_TARGET_TEMPERATURE, + target_temp_high, + ) + + elif target_temperature is not None: + if hvac_mode == HVACMode.HEAT: + await self.executor.async_execute_command( + OverkizCommand.SET_HEATING_TARGET_TEMPERATURE, + target_temperature, + ) + + elif hvac_mode == HVACMode.COOL: + await self.executor.async_execute_command( + OverkizCommand.SET_COOLING_TARGET_TEMPERATURE, + target_temperature, + ) - # Change both (heating/cooling) temperature is a good way to have consistency - await self.executor.async_execute_command( - OverkizCommand.SET_HEATING_TARGET_TEMPERATURE, - temperature, - ) - await self.executor.async_execute_command( - OverkizCommand.SET_COOLING_TARGET_TEMPERATURE, - temperature, - ) await self.executor.async_execute_command( OverkizCommand.SET_DEROGATION_ON_OFF_STATE, - OverkizCommandParam.OFF, + OverkizCommandParam.ON, ) - # Target temperature may take up to 1 minute to get refreshed. - await self.executor.async_execute_command( - OverkizCommand.REFRESH_TARGET_TEMPERATURE - ) + await self.async_refresh_modes() async def async_refresh_modes(self) -> None: """Refresh the device modes to have new states.""" @@ -256,3 +426,51 @@ class AtlanticPassAPCZoneControlZone(AtlanticPassAPCHeatingZone): await self.executor.async_execute_command( OverkizCommand.REFRESH_TARGET_TEMPERATURE ) + + @property + def min_temp(self) -> float: + """Return Minimum Temperature for AC of this group.""" + + device_hvac_mode = self.device_hvac_mode + + if device_hvac_mode in (HVACMode.HEAT, HVACMode.HEAT_COOL): + return cast( + float, + self.executor.select_state( + OverkizState.CORE_MINIMUM_HEATING_TARGET_TEMPERATURE + ), + ) + + if device_hvac_mode == HVACMode.COOL: + return cast( + float, + self.executor.select_state( + OverkizState.CORE_MINIMUM_COOLING_TARGET_TEMPERATURE + ), + ) + + return super().min_temp + + @property + def max_temp(self) -> float: + """Return Max Temperature for AC of this group.""" + + device_hvac_mode = self.device_hvac_mode + + if device_hvac_mode == HVACMode.HEAT: + return cast( + float, + self.executor.select_state( + OverkizState.CORE_MAXIMUM_HEATING_TARGET_TEMPERATURE + ), + ) + + if device_hvac_mode in (HVACMode.COOL, HVACMode.HEAT_COOL): + return cast( + float, + self.executor.select_state( + OverkizState.CORE_MAXIMUM_COOLING_TARGET_TEMPERATURE + ), + ) + + return super().max_temp From 65d25bd7803619123eaabb6aeef6bf87ff894083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexey=20ALERT=20Rubash=D1=91ff?= Date: Fri, 29 Mar 2024 15:07:22 +0200 Subject: [PATCH 031/198] Add overkiz heating status, absence mode, and boost mode binary sensors for Atlantic Water Heater (#114184) * Adds heating status, absense mode, and boost mode binary sensors for Atlantic water heater * Renamed absence mode and boost mode binary sensors * Update homeassistant/components/overkiz/binary_sensor.py Co-authored-by: TheJulianJES * Update homeassistant/components/overkiz/binary_sensor.py Co-authored-by: TheJulianJES * Update homeassistant/components/overkiz/binary_sensor.py Co-authored-by: TheJulianJES --------- Co-authored-by: TheJulianJES --- .../components/overkiz/binary_sensor.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/homeassistant/components/overkiz/binary_sensor.py b/homeassistant/components/overkiz/binary_sensor.py index 871a70b3e0a..c37afc9cb0c 100644 --- a/homeassistant/components/overkiz/binary_sensor.py +++ b/homeassistant/components/overkiz/binary_sensor.py @@ -105,6 +105,22 @@ BINARY_SENSOR_DESCRIPTIONS: list[OverkizBinarySensorDescription] = [ ) == 1, ), + OverkizBinarySensorDescription( + key=OverkizState.CORE_HEATING_STATUS, + name="Heating status", + device_class=BinarySensorDeviceClass.HEAT, + value_fn=lambda state: state == OverkizCommandParam.ON, + ), + OverkizBinarySensorDescription( + key=OverkizState.MODBUSLINK_DHW_ABSENCE_MODE, + name="Absence mode", + value_fn=lambda state: state == OverkizCommandParam.ON, + ), + OverkizBinarySensorDescription( + key=OverkizState.MODBUSLINK_DHW_BOOST_MODE, + name="Boost mode", + value_fn=lambda state: state == OverkizCommandParam.ON, + ), ] SUPPORTED_STATES = { From 35e582a240edb66ed93d5c93d88a76bee9f6c383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexey=20ALERT=20Rubash=D1=91ff?= Date: Fri, 29 Mar 2024 20:33:13 +0200 Subject: [PATCH 032/198] Add overkiz water targets temperature numbers for Atlantic water heater (#114185) * Adds water targets temperature numbers for Atlantic water heater * Update homeassistant/components/overkiz/number.py Co-authored-by: Mick Vleeshouwer * Update homeassistant/components/overkiz/number.py Co-authored-by: Mick Vleeshouwer * ruff formatting reverted * Update homeassistant/components/overkiz/number.py Co-authored-by: TheJulianJES * Update homeassistant/components/overkiz/number.py Co-authored-by: TheJulianJES * changed command hardcode to a constant --------- Co-authored-by: Mick Vleeshouwer Co-authored-by: TheJulianJES --- homeassistant/components/overkiz/number.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/homeassistant/components/overkiz/number.py b/homeassistant/components/overkiz/number.py index f81ed82f7b1..494d430c393 100644 --- a/homeassistant/components/overkiz/number.py +++ b/homeassistant/components/overkiz/number.py @@ -97,6 +97,28 @@ NUMBER_DESCRIPTIONS: list[OverkizNumberDescription] = [ max_value_state_name=OverkizState.CORE_MAXIMAL_SHOWER_MANUAL_MODE, entity_category=EntityCategory.CONFIG, ), + OverkizNumberDescription( + key=OverkizState.CORE_TARGET_DWH_TEMPERATURE, + name="Target temperature", + device_class=NumberDeviceClass.TEMPERATURE, + command=OverkizCommand.SET_TARGET_DHW_TEMPERATURE, + native_min_value=50, + native_max_value=65, + min_value_state_name=OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE, + max_value_state_name=OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE, + entity_category=EntityCategory.CONFIG, + ), + OverkizNumberDescription( + key=OverkizState.CORE_WATER_TARGET_TEMPERATURE, + name="Water target temperature", + device_class=NumberDeviceClass.TEMPERATURE, + command=OverkizCommand.SET_WATER_TARGET_TEMPERATURE, + native_min_value=50, + native_max_value=65, + min_value_state_name=OverkizState.CORE_MINIMAL_TEMPERATURE_MANUAL_MODE, + max_value_state_name=OverkizState.CORE_MAXIMAL_TEMPERATURE_MANUAL_MODE, + entity_category=EntityCategory.CONFIG, + ), # SomfyHeatingTemperatureInterface OverkizNumberDescription( key=OverkizState.CORE_ECO_ROOM_TEMPERATURE, From bf4e527f4485e9b4b85103b4d0ed229dce51b7ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexey=20ALERT=20Rubash=D1=91ff?= Date: Fri, 29 Mar 2024 15:05:18 +0200 Subject: [PATCH 033/198] Add overkiz bottom tank water temperature and core control water temperature for Atlantic Water Heater (#114186) * Adds bottom tank water temperature and core conrol water temperature sensors for Atlantic water heater * Update homeassistant/components/overkiz/sensor.py Co-authored-by: TheJulianJES * Update homeassistant/components/overkiz/sensor.py Co-authored-by: TheJulianJES --------- Co-authored-by: TheJulianJES --- homeassistant/components/overkiz/sensor.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/homeassistant/components/overkiz/sensor.py b/homeassistant/components/overkiz/sensor.py index 2b0a222f96f..c62840eea97 100644 --- a/homeassistant/components/overkiz/sensor.py +++ b/homeassistant/components/overkiz/sensor.py @@ -399,6 +399,20 @@ SENSOR_DESCRIPTIONS: list[OverkizSensorDescription] = [ native_unit_of_measurement=UnitOfTime.SECONDS, entity_category=EntityCategory.DIAGNOSTIC, ), + OverkizSensorDescription( + key=OverkizState.CORE_BOTTOM_TANK_WATER_TEMPERATURE, + name="Bottom tank water temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), + OverkizSensorDescription( + key=OverkizState.CORE_CONTROL_WATER_TARGET_TEMPERATURE, + name="Control water target temperature", + device_class=SensorDeviceClass.TEMPERATURE, + state_class=SensorStateClass.MEASUREMENT, + native_unit_of_measurement=UnitOfTemperature.CELSIUS, + ), # Cover OverkizSensorDescription( key=OverkizState.CORE_TARGET_CLOSURE, From bc740f95c9fb64ec56bf04dfb2ff07ea4fa29f51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Mar 2024 05:20:26 -1000 Subject: [PATCH 034/198] Avoid concurrent radio operations with powerview hubs (#114399) Co-authored-by: kingy444 --- .../components/hunterdouglas_powerview/button.py | 3 ++- .../components/hunterdouglas_powerview/coordinator.py | 5 +++++ .../components/hunterdouglas_powerview/cover.py | 11 ++++++++--- .../components/hunterdouglas_powerview/select.py | 3 ++- .../components/hunterdouglas_powerview/sensor.py | 3 ++- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/hunterdouglas_powerview/button.py b/homeassistant/components/hunterdouglas_powerview/button.py index f7c90f3420b..ecb71f9653a 100644 --- a/homeassistant/components/hunterdouglas_powerview/button.py +++ b/homeassistant/components/hunterdouglas_powerview/button.py @@ -119,4 +119,5 @@ class PowerviewShadeButton(ShadeEntity, ButtonEntity): async def async_press(self) -> None: """Handle the button press.""" - await self.entity_description.press_action(self._shade) + async with self.coordinator.radio_operation_lock: + await self.entity_description.press_action(self._shade) diff --git a/homeassistant/components/hunterdouglas_powerview/coordinator.py b/homeassistant/components/hunterdouglas_powerview/coordinator.py index 1ea47ca9d1f..f074b06b2bc 100644 --- a/homeassistant/components/hunterdouglas_powerview/coordinator.py +++ b/homeassistant/components/hunterdouglas_powerview/coordinator.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from datetime import timedelta import logging @@ -25,6 +26,10 @@ class PowerviewShadeUpdateCoordinator(DataUpdateCoordinator[PowerviewShadeData]) """Initialize DataUpdateCoordinator to gather data for specific Powerview Hub.""" self.shades = shades self.hub = hub + # The hub tends to crash if there are multiple radio operations at the same time + # but it seems to handle all other requests that do not use RF without issue + # so we have a lock to prevent multiple radio operations at the same time + self.radio_operation_lock = asyncio.Lock() super().__init__( hass, _LOGGER, diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 453d5c4e920..57409f37ac9 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -67,7 +67,8 @@ async def async_setup_entry( for shade in pv_entry.shade_data.values(): _LOGGER.debug("Initial refresh of shade: %s", shade.name) - await shade.refresh(suppress_timeout=True) # default 15 second timeout + async with coordinator.radio_operation_lock: + await shade.refresh(suppress_timeout=True) # default 15 second timeout entities: list[ShadeEntity] = [] for shade in pv_entry.shade_data.values(): @@ -207,7 +208,8 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity): async def _async_execute_move(self, move: ShadePosition) -> None: """Execute a move that can affect multiple positions.""" _LOGGER.debug("Move request %s: %s", self.name, move) - response = await self._shade.move(move) + async with self.coordinator.radio_operation_lock: + response = await self._shade.move(move) _LOGGER.debug("Move response %s: %s", self.name, response) # Process the response from the hub (including new positions) @@ -318,7 +320,10 @@ class PowerViewShadeBase(ShadeEntity, CoverEntity): # error if are already have one in flight return # suppress timeouts caused by hub nightly reboot - await self._shade.refresh(suppress_timeout=True) # default 15 second timeout + async with self.coordinator.radio_operation_lock: + await self._shade.refresh( + suppress_timeout=True + ) # default 15 second timeout _LOGGER.debug("Process update %s: %s", self.name, self._shade.current_position) self._async_update_shade_data(self._shade.current_position) diff --git a/homeassistant/components/hunterdouglas_powerview/select.py b/homeassistant/components/hunterdouglas_powerview/select.py index 66207f6da7c..f1e9c491659 100644 --- a/homeassistant/components/hunterdouglas_powerview/select.py +++ b/homeassistant/components/hunterdouglas_powerview/select.py @@ -114,5 +114,6 @@ class PowerViewSelect(ShadeEntity, SelectEntity): """Change the selected option.""" await self.entity_description.select_fn(self._shade, option) # force update data to ensure new info is in coordinator - await self._shade.refresh() + async with self.coordinator.radio_operation_lock: + await self._shade.refresh(suppress_timeout=True) self.async_write_ha_state() diff --git a/homeassistant/components/hunterdouglas_powerview/sensor.py b/homeassistant/components/hunterdouglas_powerview/sensor.py index bca87189e56..b24193ac438 100644 --- a/homeassistant/components/hunterdouglas_powerview/sensor.py +++ b/homeassistant/components/hunterdouglas_powerview/sensor.py @@ -153,5 +153,6 @@ class PowerViewSensor(ShadeEntity, SensorEntity): async def async_update(self) -> None: """Refresh sensor entity.""" - await self.entity_description.update_fn(self._shade) + async with self.coordinator.radio_operation_lock: + await self.entity_description.update_fn(self._shade) self.async_write_ha_state() From 906febadef43171da7df58e570f2134073209fa0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 28 Mar 2024 20:36:33 -1000 Subject: [PATCH 035/198] Cleanup some plex tasks that delayed startup (#114418) --- homeassistant/components/plex/__init__.py | 19 ++++--------------- homeassistant/components/plex/const.py | 1 - homeassistant/components/plex/helpers.py | 2 -- homeassistant/components/plex/server.py | 1 + 4 files changed, 5 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 4e17e4032aa..eb57dc46727 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -42,7 +42,6 @@ from .const import ( DOMAIN, INVALID_TOKEN_MESSAGE, PLATFORMS, - PLATFORMS_COMPLETED, PLEX_SERVER_CONFIG, PLEX_UPDATE_LIBRARY_SIGNAL, PLEX_UPDATE_PLATFORMS_SIGNAL, @@ -94,18 +93,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: gdm.scan(scan_for_clients=True) debouncer = Debouncer[None]( - hass, - _LOGGER, - cooldown=10, - immediate=True, - function=gdm_scan, + hass, _LOGGER, cooldown=10, immediate=True, function=gdm_scan, background=True ).async_call hass_data = PlexData( servers={}, dispatchers={}, websockets={}, - platforms_completed={}, gdm_scanner=gdm, gdm_debouncer=debouncer, ) @@ -180,7 +174,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: server_id = plex_server.machine_identifier hass_data = get_plex_data(hass) hass_data[SERVERS][server_id] = plex_server - hass_data[PLATFORMS_COMPLETED][server_id] = set() entry.add_update_listener(async_options_updated) @@ -233,11 +226,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) hass_data[WEBSOCKETS][server_id] = websocket - def start_websocket_session(platform): - hass_data[PLATFORMS_COMPLETED][server_id].add(platform) - if hass_data[PLATFORMS_COMPLETED][server_id] == PLATFORMS: - hass.loop.create_task(websocket.listen()) - def close_websocket_session(_): websocket.close() @@ -248,8 +236,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) - for platform in PLATFORMS: - start_websocket_session(platform) + entry.async_create_background_task( + hass, websocket.listen(), f"plex websocket listener {entry.entry_id}" + ) async_cleanup_plex_devices(hass, entry) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 8dc75a447af..d5d70219471 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -24,7 +24,6 @@ GDM_SCANNER: Final = "gdm_scanner" PLATFORMS = frozenset( [Platform.BUTTON, Platform.MEDIA_PLAYER, Platform.SENSOR, Platform.UPDATE] ) -PLATFORMS_COMPLETED: Final = "platforms_completed" PLAYER_SOURCE = "player_source" SERVERS: Final = "servers" WEBSOCKETS: Final = "websockets" diff --git a/homeassistant/components/plex/helpers.py b/homeassistant/components/plex/helpers.py index f51350ac597..3c7ff8180c8 100644 --- a/homeassistant/components/plex/helpers.py +++ b/homeassistant/components/plex/helpers.py @@ -8,7 +8,6 @@ from typing import TYPE_CHECKING, Any, TypedDict from plexapi.gdm import GDM from plexwebsocket import PlexWebsocket -from homeassistant.const import Platform from homeassistant.core import CALLBACK_TYPE, HomeAssistant from .const import DOMAIN, SERVERS @@ -23,7 +22,6 @@ class PlexData(TypedDict): servers: dict[str, PlexServer] dispatchers: dict[str, list[CALLBACK_TYPE]] websockets: dict[str, PlexWebsocket] - platforms_completed: dict[str, set[Platform]] gdm_scanner: GDM gdm_debouncer: Callable[[], Coroutine[Any, Any, None]] diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 9e2bf63ce55..584378d51f9 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -97,6 +97,7 @@ class PlexServer: cooldown=DEBOUNCE_TIMEOUT, immediate=True, function=self._async_update_platforms, + background=True, ).async_call self.thumbnail_cache = {} From db7d0a0ee91e7dcf60ed31c95bd3907fc3ed4db9 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Fri, 29 Mar 2024 11:47:21 +0000 Subject: [PATCH 036/198] Bump python-ring-doorbell to 0.8.8 (#114431) * Bump ring_doorbell to 0.8.8 * Fix intercom history test for new library version --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ring/test_sensor.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 0390db640e5..764557a3a1d 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/ring", "iot_class": "cloud_polling", "loggers": ["ring_doorbell"], - "requirements": ["ring-doorbell[listen]==0.8.7"] + "requirements": ["ring-doorbell[listen]==0.8.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 10c044009be..60681ce093f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2444,7 +2444,7 @@ rfk101py==0.0.1 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell[listen]==0.8.7 +ring-doorbell[listen]==0.8.8 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dd555cfebc8..68e82ca22e1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1887,7 +1887,7 @@ reolink-aio==0.8.9 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell[listen]==0.8.7 +ring-doorbell[listen]==0.8.8 # homeassistant.components.roku rokuecp==0.19.2 diff --git a/tests/components/ring/test_sensor.py b/tests/components/ring/test_sensor.py index aadea6f0ba1..2c866586c6c 100644 --- a/tests/components/ring/test_sensor.py +++ b/tests/components/ring/test_sensor.py @@ -87,7 +87,7 @@ async def test_history( assert front_door_last_activity_state.state == "2017-03-05T15:03:40+00:00" ingress_last_activity_state = hass.states.get("sensor.ingress_last_activity") - assert ingress_last_activity_state.state == "unknown" + assert ingress_last_activity_state.state == "2024-02-02T11:21:24+00:00" async def test_only_chime_devices( From c7ce53cc4954190e9b7a6b11537190a565599416 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Fri, 29 Mar 2024 17:46:21 +0100 Subject: [PATCH 037/198] Bump pyoverkiz to 1.13.9 (#114442) --- homeassistant/components/overkiz/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/overkiz/manifest.json b/homeassistant/components/overkiz/manifest.json index db24a299f2a..2ef0f0ebef4 100644 --- a/homeassistant/components/overkiz/manifest.json +++ b/homeassistant/components/overkiz/manifest.json @@ -19,7 +19,7 @@ "integration_type": "hub", "iot_class": "local_polling", "loggers": ["boto3", "botocore", "pyhumps", "pyoverkiz", "s3transfer"], - "requirements": ["pyoverkiz==1.13.8"], + "requirements": ["pyoverkiz==1.13.9"], "zeroconf": [ { "type": "_kizbox._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 60681ce093f..bc52a8a1c54 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2035,7 +2035,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.13.8 +pyoverkiz==1.13.9 # homeassistant.components.openweathermap pyowm==3.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 68e82ca22e1..855eebb9912 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1583,7 +1583,7 @@ pyotgw==2.1.3 pyotp==2.8.0 # homeassistant.components.overkiz -pyoverkiz==1.13.8 +pyoverkiz==1.13.9 # homeassistant.components.openweathermap pyowm==3.2.0 From cdd7ce435ab71ddcae228199dcce42f1fe240da0 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Fri, 29 Mar 2024 16:20:43 +0100 Subject: [PATCH 038/198] Log warnings in Renault initialisation (#114445) --- homeassistant/components/renault/renault_vehicle.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/renault/renault_vehicle.py b/homeassistant/components/renault/renault_vehicle.py index 55a5574a444..59e1826ce1b 100644 --- a/homeassistant/components/renault/renault_vehicle.py +++ b/homeassistant/components/renault/renault_vehicle.py @@ -125,16 +125,16 @@ class RenaultVehicleProxy: coordinator = self.coordinators[key] if coordinator.not_supported: # Remove endpoint as it is not supported for this vehicle. - LOGGER.info( - "Ignoring endpoint %s as it is not supported for this vehicle: %s", + LOGGER.warning( + "Ignoring endpoint %s as it is not supported: %s", coordinator.name, coordinator.last_exception, ) del self.coordinators[key] elif coordinator.access_denied: # Remove endpoint as it is denied for this vehicle. - LOGGER.info( - "Ignoring endpoint %s as it is denied for this vehicle: %s", + LOGGER.warning( + "Ignoring endpoint %s as it is denied: %s", coordinator.name, coordinator.last_exception, ) From e4d973e8a2439f0fa23377eaa445f300b213418a Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Fri, 29 Mar 2024 19:08:07 +0100 Subject: [PATCH 039/198] Bump async-upnp-client to 0.38.3 (#114447) --- homeassistant/components/dlna_dmr/manifest.json | 2 +- homeassistant/components/dlna_dms/manifest.json | 2 +- homeassistant/components/samsungtv/manifest.json | 2 +- homeassistant/components/ssdp/manifest.json | 2 +- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/yeelight/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/dlna_dmr/manifest.json b/homeassistant/components/dlna_dmr/manifest.json index 128822cf289..41fa49f1a94 100644 --- a/homeassistant/components/dlna_dmr/manifest.json +++ b/homeassistant/components/dlna_dmr/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dmr", "iot_class": "local_push", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.38.2", "getmac==0.9.4"], + "requirements": ["async-upnp-client==0.38.3", "getmac==0.9.4"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1", diff --git a/homeassistant/components/dlna_dms/manifest.json b/homeassistant/components/dlna_dms/manifest.json index aaa6e1ee7de..c87e5e87779 100644 --- a/homeassistant/components/dlna_dms/manifest.json +++ b/homeassistant/components/dlna_dms/manifest.json @@ -8,7 +8,7 @@ "documentation": "https://www.home-assistant.io/integrations/dlna_dms", "iot_class": "local_polling", "quality_scale": "platinum", - "requirements": ["async-upnp-client==0.38.2"], + "requirements": ["async-upnp-client==0.38.3"], "ssdp": [ { "deviceType": "urn:schemas-upnp-org:device:MediaServer:1", diff --git a/homeassistant/components/samsungtv/manifest.json b/homeassistant/components/samsungtv/manifest.json index 00b8fec8e6a..460e191828e 100644 --- a/homeassistant/components/samsungtv/manifest.json +++ b/homeassistant/components/samsungtv/manifest.json @@ -39,7 +39,7 @@ "samsungctl[websocket]==0.7.1", "samsungtvws[async,encrypted]==2.6.0", "wakeonlan==2.1.0", - "async-upnp-client==0.38.2" + "async-upnp-client==0.38.3" ], "ssdp": [ { diff --git a/homeassistant/components/ssdp/manifest.json b/homeassistant/components/ssdp/manifest.json index a9ef8af8c90..5e549c31806 100644 --- a/homeassistant/components/ssdp/manifest.json +++ b/homeassistant/components/ssdp/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["async_upnp_client"], "quality_scale": "internal", - "requirements": ["async-upnp-client==0.38.2"] + "requirements": ["async-upnp-client==0.38.3"] } diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index edfde84a2ac..7d353a475c7 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -8,7 +8,7 @@ "integration_type": "device", "iot_class": "local_polling", "loggers": ["async_upnp_client"], - "requirements": ["async-upnp-client==0.38.2", "getmac==0.9.4"], + "requirements": ["async-upnp-client==0.38.3", "getmac==0.9.4"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 20f8ed3ed4d..e9f304d38cb 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -17,7 +17,7 @@ "iot_class": "local_push", "loggers": ["async_upnp_client", "yeelight"], "quality_scale": "platinum", - "requirements": ["yeelight==0.7.14", "async-upnp-client==0.38.2"], + "requirements": ["yeelight==0.7.14", "async-upnp-client==0.38.3"], "zeroconf": [ { "type": "_miio._udp.local.", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b7db1514cba..c757aceee3c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,7 +8,7 @@ aiohttp==3.9.3 aiohttp_cors==0.7.0 astral==2.2 async-interrupt==1.1.1 -async-upnp-client==0.38.2 +async-upnp-client==0.38.3 atomicwrites-homeassistant==1.4.1 attrs==23.2.0 awesomeversion==24.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index bc52a8a1c54..f28d2bf3e25 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -487,7 +487,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.38.2 +async-upnp-client==0.38.3 # homeassistant.components.keyboard_remote asyncinotify==4.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 855eebb9912..987f0be178b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -442,7 +442,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.ssdp # homeassistant.components.upnp # homeassistant.components.yeelight -async-upnp-client==0.38.2 +async-upnp-client==0.38.3 # homeassistant.components.sleepiq asyncsleepiq==1.5.2 From 84901f1983fa6158da1037d307e07031bf591c0a Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 29 Mar 2024 19:34:16 +0100 Subject: [PATCH 040/198] Update frontend to 20240329.0 (#114452) --- 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 9e86436bd68..a8f14187d48 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==20240328.0"] + "requirements": ["home-assistant-frontend==20240329.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c757aceee3c..2ebb82d2c75 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240328.0 +home-assistant-frontend==20240329.0 home-assistant-intents==2024.3.27 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index f28d2bf3e25..dd59129a7e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1077,7 +1077,7 @@ hole==0.8.0 holidays==0.45 # homeassistant.components.frontend -home-assistant-frontend==20240328.0 +home-assistant-frontend==20240329.0 # homeassistant.components.conversation home-assistant-intents==2024.3.27 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 987f0be178b..c46bf2e5a59 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,7 +876,7 @@ hole==0.8.0 holidays==0.45 # homeassistant.components.frontend -home-assistant-frontend==20240328.0 +home-assistant-frontend==20240329.0 # homeassistant.components.conversation home-assistant-intents==2024.3.27 From e53672250fe7a88d0139e383ca820fc825b88bc9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Mar 2024 19:35:52 +0100 Subject: [PATCH 041/198] Bump version to 2024.4.0b2 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f9a9b6324f8..4fca9fa50c3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -18,7 +18,7 @@ from .util.signal_type import SignalType APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 4 -PATCH_VERSION: Final = "0b1" +PATCH_VERSION: Final = "0b2" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 48f520a878c..790ee03d76b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.4.0b1" +version = "2024.4.0b2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 953ceb0d8d275b13ce3df6466f1e7f673aaecbac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Mar 2024 11:18:21 -1000 Subject: [PATCH 042/198] Avoid tracking import executor jobs (#114453) --- homeassistant/core.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 3b52b020957..2ed4de35925 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -783,11 +783,11 @@ class HomeAssistant: def async_add_import_executor_job( self, target: Callable[[*_Ts], _T], *args: *_Ts ) -> asyncio.Future[_T]: - """Add an import executor job from within the event loop.""" - task = self.loop.run_in_executor(self.import_executor, target, *args) - self._tasks.add(task) - task.add_done_callback(self._tasks.remove) - return task + """Add an import executor job from within the event loop. + + The future returned from this method must be awaited in the event loop. + """ + return self.loop.run_in_executor(self.import_executor, target, *args) @overload @callback From 4f761c25d83cf6a575835b3e6aac9b7b2b28a20a Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 29 Mar 2024 22:13:31 +0100 Subject: [PATCH 043/198] Update frontend to 20240329.1 (#114459) --- 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 a8f14187d48..7864801a986 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==20240329.0"] + "requirements": ["home-assistant-frontend==20240329.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 2ebb82d2c75..1d60b74f18f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240329.0 +home-assistant-frontend==20240329.1 home-assistant-intents==2024.3.27 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index dd59129a7e3..0a94eb110e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1077,7 +1077,7 @@ hole==0.8.0 holidays==0.45 # homeassistant.components.frontend -home-assistant-frontend==20240329.0 +home-assistant-frontend==20240329.1 # homeassistant.components.conversation home-assistant-intents==2024.3.27 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c46bf2e5a59..3a3ebbbb077 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,7 +876,7 @@ hole==0.8.0 holidays==0.45 # homeassistant.components.frontend -home-assistant-frontend==20240329.0 +home-assistant-frontend==20240329.1 # homeassistant.components.conversation home-assistant-intents==2024.3.27 From 11b8b01cde0424c3592dd22bd6f6ace14827bc6a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 29 Mar 2024 22:22:45 +0100 Subject: [PATCH 044/198] Bump version to 2024.4.0b3 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 4fca9fa50c3..f56ce656157 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -18,7 +18,7 @@ from .util.signal_type import SignalType APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 4 -PATCH_VERSION: Final = "0b2" +PATCH_VERSION: Final = "0b3" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 790ee03d76b..73bfdd6d5d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.4.0b2" +version = "2024.4.0b3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From e8ee2fd25cd705369f913db98259f7b44ed46584 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Sat, 30 Mar 2024 18:48:57 +0300 Subject: [PATCH 045/198] Cleanup Shelly RGBW light entities (#114410) --- homeassistant/components/shelly/const.py | 2 + homeassistant/components/shelly/light.py | 17 +++++ homeassistant/components/shelly/utils.py | 12 ++++ tests/components/shelly/__init__.py | 12 ++++ tests/components/shelly/conftest.py | 6 ++ tests/components/shelly/test_light.py | 88 +++++++++++++++++++++++- 6 files changed, 134 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 3580bcf9b38..2ac0416bb6c 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -234,3 +234,5 @@ DEVICES_WITHOUT_FIRMWARE_CHANGELOG = ( ) CONF_GEN = "gen" + +SHELLY_PLUS_RGBW_CHANNELS = 4 diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index 6c28023a5e3..d0590fc7c20 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -14,6 +14,7 @@ from homeassistant.components.light import ( ATTR_RGB_COLOR, ATTR_RGBW_COLOR, ATTR_TRANSITION, + DOMAIN as LIGHT_DOMAIN, ColorMode, LightEntity, LightEntityFeature, @@ -34,12 +35,14 @@ from .const import ( RGBW_MODELS, RPC_MIN_TRANSITION_TIME_SEC, SHBLB_1_RGB_EFFECTS, + SHELLY_PLUS_RGBW_CHANNELS, STANDARD_RGB_EFFECTS, ) from .coordinator import ShellyBlockCoordinator, ShellyRpcCoordinator, get_entry_data from .entity import ShellyBlockEntity, ShellyRpcEntity from .utils import ( async_remove_shelly_entity, + async_remove_shelly_rpc_entities, brightness_to_percentage, get_device_entry_gen, get_rpc_key_ids, @@ -118,14 +121,28 @@ def async_setup_rpc_entry( return if light_key_ids := get_rpc_key_ids(coordinator.device.status, "light"): + # Light mode remove RGB & RGBW entities, add light entities + async_remove_shelly_rpc_entities( + hass, LIGHT_DOMAIN, coordinator.mac, ["rgb:0", "rgbw:0"] + ) async_add_entities(RpcShellyLight(coordinator, id_) for id_ in light_key_ids) return + light_keys = [f"light:{i}" for i in range(SHELLY_PLUS_RGBW_CHANNELS)] + if rgb_key_ids := get_rpc_key_ids(coordinator.device.status, "rgb"): + # RGB mode remove light & RGBW entities, add RGB entity + async_remove_shelly_rpc_entities( + hass, LIGHT_DOMAIN, coordinator.mac, [*light_keys, "rgbw:0"] + ) async_add_entities(RpcShellyRgbLight(coordinator, id_) for id_ in rgb_key_ids) return if rgbw_key_ids := get_rpc_key_ids(coordinator.device.status, "rgbw"): + # RGBW mode remove light & RGB entities, add RGBW entity + async_remove_shelly_rpc_entities( + hass, LIGHT_DOMAIN, coordinator.mac, [*light_keys, "rgb:0"] + ) async_add_entities(RpcShellyRgbwLight(coordinator, id_) for id_ in rgbw_key_ids) diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index d26e3dc11f3..ce98e0d5c12 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -488,3 +488,15 @@ async def async_shutdown_device(device: BlockDevice | RpcDevice) -> None: await device.shutdown() if isinstance(device, BlockDevice): device.shutdown() + + +@callback +def async_remove_shelly_rpc_entities( + hass: HomeAssistant, domain: str, mac: str, keys: list[str] +) -> None: + """Remove RPC based Shelly entity.""" + entity_reg = er_async_get(hass) + for key in keys: + if entity_id := entity_reg.async_get_entity_id(domain, DOMAIN, f"{mac}-{key}"): + LOGGER.debug("Removing entity: %s", entity_id) + entity_reg.async_remove(entity_id) diff --git a/tests/components/shelly/__init__.py b/tests/components/shelly/__init__.py index 2dc9012d863..348b1115a6f 100644 --- a/tests/components/shelly/__init__.py +++ b/tests/components/shelly/__init__.py @@ -126,6 +126,18 @@ def register_entity( return f"{domain}.{object_id}" +def get_entity( + hass: HomeAssistant, + domain: str, + unique_id: str, +) -> str | None: + """Get Shelly entity.""" + entity_registry = async_get(hass) + return entity_registry.async_get_entity_id( + domain, DOMAIN, f"{MOCK_MAC}-{unique_id}" + ) + + def get_entity_state(hass: HomeAssistant, entity_id: str) -> str: """Return entity state.""" entity = hass.states.get(entity_id) diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 9a73252ca6c..3cd27101f76 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -169,6 +169,9 @@ MOCK_CONFIG = { "input:1": {"id": 1, "type": "analog", "enable": True}, "input:2": {"id": 2, "name": "Gas", "type": "count", "enable": True}, "light:0": {"name": "test light_0"}, + "light:1": {"name": "test light_1"}, + "light:2": {"name": "test light_2"}, + "light:3": {"name": "test light_3"}, "rgb:0": {"name": "test rgb_0"}, "rgbw:0": {"name": "test rgbw_0"}, "switch:0": {"name": "test switch_0"}, @@ -225,6 +228,9 @@ MOCK_STATUS_RPC = { "input:1": {"id": 1, "percent": 89, "xpercent": 8.9}, "input:2": {"id": 2, "counts": {"total": 56174, "xtotal": 561.74}}, "light:0": {"output": True, "brightness": 53.0}, + "light:1": {"output": True, "brightness": 53.0}, + "light:2": {"output": True, "brightness": 53.0}, + "light:3": {"output": True, "brightness": 53.0}, "rgb:0": {"output": True, "brightness": 53.0, "rgb": [45, 55, 65]}, "rgbw:0": {"output": True, "brightness": 53.0, "rgb": [21, 22, 23], "white": 120}, "cloud": {"connected": False}, diff --git a/tests/components/shelly/test_light.py b/tests/components/shelly/test_light.py index cca318c364d..2c464a8c39c 100644 --- a/tests/components/shelly/test_light.py +++ b/tests/components/shelly/test_light.py @@ -29,6 +29,7 @@ from homeassistant.components.light import ( ColorMode, LightEntityFeature, ) +from homeassistant.components.shelly.const import SHELLY_PLUS_RGBW_CHANNELS from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -38,7 +39,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_registry import EntityRegistry -from . import init_integration, mutate_rpc_device_status +from . import get_entity, init_integration, mutate_rpc_device_status, register_entity from .conftest import mock_white_light_set_state RELAY_BLOCK_ID = 0 @@ -587,7 +588,8 @@ async def test_rpc_device_rgb_profile( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC device in RGB profile.""" - monkeypatch.delitem(mock_rpc_device.status, "light:0") + for i in range(SHELLY_PLUS_RGBW_CHANNELS): + monkeypatch.delitem(mock_rpc_device.status, f"light:{i}") monkeypatch.delitem(mock_rpc_device.status, "rgbw:0") entity_id = "light.test_rgb_0" await init_integration(hass, 2) @@ -633,7 +635,8 @@ async def test_rpc_device_rgbw_profile( monkeypatch: pytest.MonkeyPatch, ) -> None: """Test RPC device in RGBW profile.""" - monkeypatch.delitem(mock_rpc_device.status, "light:0") + for i in range(SHELLY_PLUS_RGBW_CHANNELS): + monkeypatch.delitem(mock_rpc_device.status, f"light:{i}") monkeypatch.delitem(mock_rpc_device.status, "rgb:0") entity_id = "light.test_rgbw_0" await init_integration(hass, 2) @@ -673,3 +676,82 @@ async def test_rpc_device_rgbw_profile( entry = entity_registry.async_get(entity_id) assert entry assert entry.unique_id == "123456789ABC-rgbw:0" + + +async def test_rpc_rgbw_device_light_mode_remove_others( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Test Shelly RPC RGBW device in light mode removes RGB/RGBW entities.""" + # register lights + monkeypatch.delitem(mock_rpc_device.status, "rgb:0") + monkeypatch.delitem(mock_rpc_device.status, "rgbw:0") + register_entity(hass, LIGHT_DOMAIN, "test_rgb_0", "rgb:0") + register_entity(hass, LIGHT_DOMAIN, "test_rgbw_0", "rgbw:0") + + # verify RGB & RGBW entities created + assert get_entity(hass, LIGHT_DOMAIN, "rgb:0") is not None + assert get_entity(hass, LIGHT_DOMAIN, "rgbw:0") is not None + + # init to remove RGB & RGBW + await init_integration(hass, 2) + + # verify we have 4 lights + for i in range(SHELLY_PLUS_RGBW_CHANNELS): + entity_id = f"light.test_light_{i}" + assert hass.states.get(entity_id).state == STATE_ON + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == f"123456789ABC-light:{i}" + + # verify RGB & RGBW entities removed + assert get_entity(hass, LIGHT_DOMAIN, "rgb:0") is None + assert get_entity(hass, LIGHT_DOMAIN, "rgbw:0") is None + + +@pytest.mark.parametrize( + ("active_mode", "removed_mode"), + [ + ("rgb", "rgbw"), + ("rgbw", "rgb"), + ], +) +async def test_rpc_rgbw_device_rgb_w_modes_remove_others( + hass: HomeAssistant, + mock_rpc_device: Mock, + entity_registry: EntityRegistry, + monkeypatch: pytest.MonkeyPatch, + active_mode: str, + removed_mode: str, +) -> None: + """Test Shelly RPC RGBW device in RGB/W modes other lights.""" + removed_key = f"{removed_mode}:0" + + # register lights + for i in range(SHELLY_PLUS_RGBW_CHANNELS): + monkeypatch.delitem(mock_rpc_device.status, f"light:{i}") + entity_id = f"light.test_light_{i}" + register_entity(hass, LIGHT_DOMAIN, entity_id, f"light:{i}") + monkeypatch.delitem(mock_rpc_device.status, f"{removed_mode}:0") + register_entity(hass, LIGHT_DOMAIN, f"test_{removed_key}", removed_key) + + # verify lights entities created + for i in range(SHELLY_PLUS_RGBW_CHANNELS): + assert get_entity(hass, LIGHT_DOMAIN, f"light:{i}") is not None + assert get_entity(hass, LIGHT_DOMAIN, removed_key) is not None + + await init_integration(hass, 2) + + # verify we have RGB/w light + entity_id = f"light.test_{active_mode}_0" + assert hass.states.get(entity_id).state == STATE_ON + entry = entity_registry.async_get(entity_id) + assert entry + assert entry.unique_id == f"123456789ABC-{active_mode}:0" + + # verify light & RGB/W entities removed + for i in range(SHELLY_PLUS_RGBW_CHANNELS): + assert get_entity(hass, LIGHT_DOMAIN, f"light:{i}") is None + assert get_entity(hass, LIGHT_DOMAIN, removed_key) is None From 286a09d737ef3caacf7c9d9c8a317fb719968d0e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 29 Mar 2024 18:16:53 -1000 Subject: [PATCH 046/198] Mark executor jobs as background unless created from a tracked task (#114450) * Mark executor jobs as background unless created from a tracked task If the current task is not tracked the executor job should not be a background task to avoid delaying startup and shutdown. Currently any executor job created in a untracked task or background task would end up being tracked and delaying startup/shutdown * import exec has the same issue * Avoid tracking import executor jobs There is no reason to track these jobs as they are always awaited and we do not want to support fire and forget import executor jobs * fix xiaomi_miio * lots of fire time changed without background await * revert changes moved to other PR * more * more * more * m * m * p * fix fire and forget tests * scrape * sonos * system * more * capture callback before block * coverage * more * more races * more races * more * missed some * more fixes * missed some more * fix * remove unneeded * one more race * two --- homeassistant/core.py | 7 +++- .../aurora_abb_powerone/test_sensor.py | 8 ++-- tests/components/cast/test_config_flow.py | 4 +- tests/components/cast/test_media_player.py | 8 ++-- tests/components/fritz/test_image.py | 4 +- tests/components/fritz/test_sensor.py | 2 +- .../components/fritzbox/test_binary_sensor.py | 6 +-- tests/components/fritzbox/test_button.py | 2 +- tests/components/fritzbox/test_climate.py | 12 +++--- tests/components/fritzbox/test_cover.py | 2 +- tests/components/fritzbox/test_light.py | 6 +-- tests/components/fritzbox/test_sensor.py | 6 +-- tests/components/fritzbox/test_switch.py | 6 +-- .../components/geo_rss_events/test_sensor.py | 4 +- tests/components/google_mail/test_sensor.py | 4 +- .../maxcube/test_maxcube_binary_sensor.py | 6 +-- .../maxcube/test_maxcube_climate.py | 22 +++++----- tests/components/metoffice/test_weather.py | 10 ++--- .../mikrotik/test_device_tracker.py | 8 ++-- .../components/monoprice/test_media_player.py | 18 ++++----- .../panasonic_viera/test_media_player.py | 4 +- tests/components/pjlink/test_media_player.py | 4 +- tests/components/profiler/test_init.py | 10 ++--- tests/components/ps4/test_media_player.py | 2 + tests/components/python_script/test_init.py | 40 +++++++++---------- .../components/samsungtv/test_media_player.py | 10 ++--- .../components/schlage/test_binary_sensor.py | 4 +- tests/components/schlage/test_lock.py | 2 +- tests/components/scrape/test_sensor.py | 12 +++--- .../components/solaredge/test_coordinator.py | 10 ++--- tests/components/sonos/conftest.py | 3 +- tests/components/sonos/test_repairs.py | 5 ++- tests/components/sonos/test_sensor.py | 29 +++++++++----- tests/components/sonos/test_speaker.py | 16 +++++++- .../soundtouch/test_media_player.py | 2 +- tests/components/speedtestdotnet/test_init.py | 2 +- .../systemmonitor/test_binary_sensor.py | 2 +- tests/components/systemmonitor/test_sensor.py | 22 +++++----- tests/components/tcp/test_binary_sensor.py | 2 +- tests/components/temper/test_sensor.py | 2 +- .../totalconnect/test_alarm_control_panel.py | 10 ++--- tests/components/uvc/test_camera.py | 12 +++--- tests/components/ws66i/test_media_player.py | 20 +++++----- tests/components/xiaomi_miio/test_vacuum.py | 4 +- .../yale_smart_alarm/test_coordinator.py | 12 +++--- tests/test_core.py | 40 +++++++++++++++++++ 46 files changed, 246 insertions(+), 180 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 2ed4de35925..4794b284fd2 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -774,8 +774,11 @@ class HomeAssistant: ) -> asyncio.Future[_T]: """Add an executor job from within the event loop.""" task = self.loop.run_in_executor(None, target, *args) - self._tasks.add(task) - task.add_done_callback(self._tasks.remove) + + tracked = asyncio.current_task() in self._tasks + task_bucket = self._tasks if tracked else self._background_tasks + task_bucket.add(task) + task.add_done_callback(task_bucket.remove) return task diff --git a/tests/components/aurora_abb_powerone/test_sensor.py b/tests/components/aurora_abb_powerone/test_sensor.py index 178cf165f67..4bc5a5d3086 100644 --- a/tests/components/aurora_abb_powerone/test_sensor.py +++ b/tests/components/aurora_abb_powerone/test_sensor.py @@ -201,7 +201,7 @@ async def test_sensor_dark(hass: HomeAssistant, freezer: FrozenDateTimeFactory) ): freezer.tick(SCAN_INTERVAL * 2) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) power = hass.states.get("sensor.mydevicename_total_energy") assert power.state == "unknown" # sun rose again @@ -218,7 +218,7 @@ async def test_sensor_dark(hass: HomeAssistant, freezer: FrozenDateTimeFactory) ): freezer.tick(SCAN_INTERVAL * 4) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) power = hass.states.get("sensor.mydevicename_power_output") assert power is not None assert power.state == "45.7" @@ -237,7 +237,7 @@ async def test_sensor_dark(hass: HomeAssistant, freezer: FrozenDateTimeFactory) ): freezer.tick(SCAN_INTERVAL * 6) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) power = hass.states.get("sensor.mydevicename_power_output") assert power.state == "unknown" # should this be 'available'? @@ -277,7 +277,7 @@ async def test_sensor_unknown_error( ): freezer.tick(SCAN_INTERVAL * 2) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert ( "Exception: AuroraError('another error') occurred, 2 retries remaining" in caplog.text diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index 62c21fc95ee..a7b9311e88b 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -278,7 +278,7 @@ async def test_known_hosts(hass: HomeAssistant, castbrowser_mock) -> None: result["flow_id"], {"known_hosts": "192.168.0.1, 192.168.0.2"} ) assert result["type"] == "create_entry" - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) config_entry = hass.config_entries.async_entries("cast")[0] assert castbrowser_mock.return_value.start_discovery.call_count == 1 @@ -291,7 +291,7 @@ async def test_known_hosts(hass: HomeAssistant, castbrowser_mock) -> None: user_input={"known_hosts": "192.168.0.11, 192.168.0.12"}, ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) castbrowser_mock.return_value.start_discovery.assert_not_called() castbrowser_mock.assert_not_called() diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 9ef31457d5c..8381f27398a 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -137,8 +137,8 @@ async def async_setup_cast_internal_discovery(hass, config=None): return_value=browser, ) as cast_browser: add_entities = await async_setup_cast(hass, config) - await hass.async_block_till_done() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + await hass.async_block_till_done(wait_background_tasks=True) assert browser.start_discovery.call_count == 1 @@ -209,8 +209,8 @@ async def async_setup_media_player_cast(hass: HomeAssistant, info: ChromecastInf entry = MockConfigEntry(data=data, domain="cast") entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + await hass.async_block_till_done(wait_background_tasks=True) discovery_callback = cast_browser.call_args[0][0].add_cast diff --git a/tests/components/fritz/test_image.py b/tests/components/fritz/test_image.py index 85d02eff153..5d6b9265760 100644 --- a/tests/components/fritz/test_image.py +++ b/tests/components/fritz/test_image.py @@ -199,7 +199,7 @@ async def test_image_update_unavailable( # fritzbox becomes unavailable fc_class_mock().call_action_side_effect(ReadTimeout) async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("image.mock_title_guestwifi") assert state.state == STATE_UNKNOWN @@ -207,7 +207,7 @@ async def test_image_update_unavailable( # fritzbox is available again fc_class_mock().call_action_side_effect(None) async_fire_time_changed(hass, utcnow() + timedelta(seconds=60)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("image.mock_title_guestwifi") assert state.state != STATE_UNKNOWN diff --git a/tests/components/fritz/test_sensor.py b/tests/components/fritz/test_sensor.py index 4427fc6961e..37116e66719 100644 --- a/tests/components/fritz/test_sensor.py +++ b/tests/components/fritz/test_sensor.py @@ -134,7 +134,7 @@ async def test_sensor_update_fail( fc_class_mock().call_action_side_effect(FritzConnectionException) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=300)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) sensors = hass.states.async_all(SENSOR_DOMAIN) for sensor in sensors: diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index 3828cedc67f..3e1a2691f67 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -104,7 +104,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 1 @@ -123,7 +123,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 1 @@ -146,7 +146,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_device_alarm") assert state diff --git a/tests/components/fritzbox/test_button.py b/tests/components/fritzbox/test_button.py index f254b2e0710..89e8d8357dd 100644 --- a/tests/components/fritzbox/test_button.py +++ b/tests/components/fritzbox/test_button.py @@ -65,7 +65,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_template") assert state diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index a201eab3665..073a67f22c1 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -145,7 +145,7 @@ async def test_setup(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{SENSOR_DOMAIN}.{CONF_FAKE_NAME}_next_scheduled_preset") assert state @@ -203,7 +203,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert fritz().update_devices.call_count == 2 @@ -243,7 +243,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 4 assert fritz().login.call_count == 4 @@ -386,7 +386,7 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert fritz().update_devices.call_count == 2 @@ -397,7 +397,7 @@ async def test_preset_mode_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert fritz().update_devices.call_count == 3 @@ -422,7 +422,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_climate") assert state diff --git a/tests/components/fritzbox/test_cover.py b/tests/components/fritzbox/test_cover.py index b723ac97d06..6c301fc8f46 100644 --- a/tests/components/fritzbox/test_cover.py +++ b/tests/components/fritzbox/test_cover.py @@ -108,7 +108,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_climate") assert state diff --git a/tests/components/fritzbox/test_light.py b/tests/components/fritzbox/test_light.py index b750a2e9275..45920c7c3ee 100644 --- a/tests/components/fritzbox/test_light.py +++ b/tests/components/fritzbox/test_light.py @@ -237,7 +237,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 1 @@ -259,7 +259,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 4 assert fritz().login.call_count == 4 @@ -294,7 +294,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_light") assert state diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index 48b769eaac2..63d0b67d7f4 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -87,7 +87,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 1 @@ -105,7 +105,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 4 assert fritz().login.call_count == 4 @@ -128,7 +128,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_device_temperature") assert state diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 67393bc09a5..417b355b396 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -151,7 +151,7 @@ async def test_update(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 2 assert fritz().login.call_count == 1 @@ -169,7 +169,7 @@ async def test_update_error(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert fritz().update_devices.call_count == 4 assert fritz().login.call_count == 4 @@ -207,7 +207,7 @@ async def test_discover_new_device(hass: HomeAssistant, fritz: Mock) -> None: next_update = dt_util.utcnow() + timedelta(seconds=200) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(f"{DOMAIN}.new_switch") assert state diff --git a/tests/components/geo_rss_events/test_sensor.py b/tests/components/geo_rss_events/test_sensor.py index 76f1709bd75..d19262c3339 100644 --- a/tests/components/geo_rss_events/test_sensor.py +++ b/tests/components/geo_rss_events/test_sensor.py @@ -99,7 +99,7 @@ async def test_setup( # so no changes to entities. mock_feed.return_value.update.return_value = "OK_NO_DATA", None async_fire_time_changed(hass, utcnow + geo_rss_events.SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) all_states = hass.states.async_all() assert len(all_states) == 1 @@ -109,7 +109,7 @@ async def test_setup( # Simulate an update - empty data, removes all entities mock_feed.return_value.update.return_value = "ERROR", None async_fire_time_changed(hass, utcnow + 2 * geo_rss_events.SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) all_states = hass.states.async_all() assert len(all_states) == 1 diff --git a/tests/components/google_mail/test_sensor.py b/tests/components/google_mail/test_sensor.py index e0b072d4b7d..6f2f1a4ec32 100644 --- a/tests/components/google_mail/test_sensor.py +++ b/tests/components/google_mail/test_sensor.py @@ -46,7 +46,7 @@ async def test_sensors( ): next_update = dt_util.utcnow() + timedelta(minutes=15) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(SENSOR) assert state.state == result @@ -61,7 +61,7 @@ async def test_sensor_reauth_trigger( with patch(TOKEN, side_effect=RefreshError): next_update = dt_util.utcnow() + timedelta(minutes=15) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) flows = hass.config_entries.flow.async_progress() diff --git a/tests/components/maxcube/test_maxcube_binary_sensor.py b/tests/components/maxcube/test_maxcube_binary_sensor.py index cc86f389884..32ec4e92ee1 100644 --- a/tests/components/maxcube/test_maxcube_binary_sensor.py +++ b/tests/components/maxcube/test_maxcube_binary_sensor.py @@ -43,7 +43,7 @@ async def test_window_shuttler( windowshutter.is_open = False async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_OFF @@ -68,12 +68,12 @@ async def test_window_shuttler_battery( windowshutter.battery = 1 # maxcube-api MAX_DEVICE_BATTERY_LOW async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(BATTERY_ENTITY_ID) assert state.state == STATE_ON # on means low windowshutter.battery = 0 # maxcube-api MAX_DEVICE_BATTERY_OK async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(BATTERY_ENTITY_ID) assert state.state == STATE_OFF # off means normal diff --git a/tests/components/maxcube/test_maxcube_climate.py b/tests/components/maxcube/test_maxcube_climate.py index cb4dc510605..e1e7dc57c47 100644 --- a/tests/components/maxcube/test_maxcube_climate.py +++ b/tests/components/maxcube/test_maxcube_climate.py @@ -140,7 +140,7 @@ async def test_thermostat_set_hvac_mode_off( thermostat.valve_position = 0 async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.OFF @@ -168,8 +168,8 @@ async def test_thermostat_set_hvac_mode_heat( thermostat.mode = MAX_DEVICE_MODE_MANUAL async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.HEAT @@ -204,7 +204,7 @@ async def test_thermostat_set_temperature( thermostat.valve_position = 0 async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.AUTO @@ -248,7 +248,7 @@ async def test_thermostat_set_preset_on( thermostat.target_temperature = ON_TEMPERATURE async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.HEAT @@ -273,7 +273,7 @@ async def test_thermostat_set_preset_comfort( thermostat.target_temperature = thermostat.comfort_temperature async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.HEAT @@ -298,7 +298,7 @@ async def test_thermostat_set_preset_eco( thermostat.target_temperature = thermostat.eco_temperature async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.HEAT @@ -323,7 +323,7 @@ async def test_thermostat_set_preset_away( thermostat.target_temperature = thermostat.eco_temperature async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.HEAT @@ -348,7 +348,7 @@ async def test_thermostat_set_preset_boost( thermostat.target_temperature = thermostat.eco_temperature async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == HVACMode.AUTO @@ -401,7 +401,7 @@ async def test_wallthermostat_set_hvac_mode_heat( wallthermostat.target_temperature = MIN_TEMPERATURE async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(WALL_ENTITY_ID) assert state.state == HVACMode.HEAT @@ -425,7 +425,7 @@ async def test_wallthermostat_set_hvac_mode_auto( wallthermostat.target_temperature = 23.0 async_fire_time_changed(hass, utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(WALL_ENTITY_ID) assert state.state == HVACMode.AUTO diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index 2aa673d4010..64a85897738 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -125,7 +125,7 @@ async def test_site_cannot_update( future_time = utcnow() + timedelta(minutes=20) async_fire_time_changed(hass, future_time) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) weather = hass.states.get("weather.met_office_wavertree_daily") assert weather.state == STATE_UNAVAILABLE @@ -297,7 +297,7 @@ async def test_forecast_service( # Trigger data refetch freezer.tick(DEFAULT_SCAN_INTERVAL + timedelta(seconds=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert wavertree_data["wavertree_daily_mock"].call_count == 2 assert wavertree_data["wavertree_hourly_mock"].call_count == 1 @@ -324,7 +324,7 @@ async def test_forecast_service( freezer.tick(DEFAULT_SCAN_INTERVAL + timedelta(seconds=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) response = await hass.services.async_call( WEATHER_DOMAIN, @@ -412,7 +412,7 @@ async def test_forecast_subscription( freezer.tick(DEFAULT_SCAN_INTERVAL + timedelta(seconds=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) msg = await client.receive_json() assert msg["id"] == subscription_id @@ -430,6 +430,6 @@ async def test_forecast_subscription( ) freezer.tick(timedelta(seconds=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) msg = await client.receive_json() assert msg["success"] diff --git a/tests/components/mikrotik/test_device_tracker.py b/tests/components/mikrotik/test_device_tracker.py index 47ddc038f69..89dc37fd781 100644 --- a/tests/components/mikrotik/test_device_tracker.py +++ b/tests/components/mikrotik/test_device_tracker.py @@ -88,7 +88,7 @@ async def test_device_trackers( WIRELESS_DATA.append(DEVICE_2_WIRELESS) async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) device_2 = hass.states.get("device_tracker.device_2") assert device_2 @@ -101,7 +101,7 @@ async def test_device_trackers( del WIRELESS_DATA[1] # device 2 is removed from wireless list with freeze_time(utcnow() + timedelta(minutes=4)): async_fire_time_changed(hass, utcnow() + timedelta(minutes=4)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) device_2 = hass.states.get("device_tracker.device_2") assert device_2 @@ -110,7 +110,7 @@ async def test_device_trackers( # test state changes to away if last_seen past consider_home_interval with freeze_time(utcnow() + timedelta(minutes=6)): async_fire_time_changed(hass, utcnow() + timedelta(minutes=6)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) device_2 = hass.states.get("device_tracker.device_2") assert device_2 @@ -266,7 +266,7 @@ async def test_update_failed(hass: HomeAssistant, mock_device_registry_devices) mikrotik.hub.MikrotikData, "command", side_effect=mikrotik.errors.CannotConnect ): async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) device_1 = hass.states.get("device_tracker.device_1") assert device_1 diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index a0afd37f3b2..f7d88692cf5 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -183,7 +183,7 @@ async def test_service_calls_with_entity_id(hass: HomeAssistant) -> None: # Restoring other media player to its previous state # The zone should not be restored await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_2_ID}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Checking that values were not (!) restored state = hass.states.get(ZONE_1_ID) @@ -193,7 +193,7 @@ async def test_service_calls_with_entity_id(hass: HomeAssistant) -> None: # Restoring media player to its previous state await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -226,7 +226,7 @@ async def test_service_calls_with_all_entities(hass: HomeAssistant) -> None: # Restoring media player to its previous state await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": "all"}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -259,7 +259,7 @@ async def test_service_calls_without_relevant_entities(hass: HomeAssistant) -> N # Restoring media player to its previous state await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": "light.demo"}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -273,7 +273,7 @@ async def test_restore_without_snapshort(hass: HomeAssistant) -> None: with patch.object(MockMonoprice, "restore_zone") as method_call: await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert not method_call.called @@ -295,7 +295,7 @@ async def test_update(hass: HomeAssistant) -> None: monoprice.set_volume(11, 38) await async_update_entity(hass, ZONE_1_ID) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -321,7 +321,7 @@ async def test_failed_update(hass: HomeAssistant) -> None: with patch.object(MockMonoprice, "zone_status", side_effect=SerialException): await async_update_entity(hass, ZONE_1_ID) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -347,7 +347,7 @@ async def test_empty_update(hass: HomeAssistant) -> None: with patch.object(MockMonoprice, "zone_status", return_value=None): await async_update_entity(hass, ZONE_1_ID) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -418,7 +418,7 @@ async def test_unknown_source(hass: HomeAssistant) -> None: monoprice.set_source(11, 5) await async_update_entity(hass, ZONE_1_ID) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) diff --git a/tests/components/panasonic_viera/test_media_player.py b/tests/components/panasonic_viera/test_media_player.py index 1203bf1ed51..dab56542e6a 100644 --- a/tests/components/panasonic_viera/test_media_player.py +++ b/tests/components/panasonic_viera/test_media_player.py @@ -23,7 +23,7 @@ async def test_media_player_handle_URLerror( mock_remote.get_mute = Mock(side_effect=URLError(None, None)) async_fire_time_changed(hass, utcnow() + timedelta(minutes=2)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state_tv = hass.states.get("media_player.panasonic_viera_tv") assert state_tv.state == STATE_UNAVAILABLE @@ -41,7 +41,7 @@ async def test_media_player_handle_HTTPError( mock_remote.get_mute = Mock(side_effect=HTTPError(None, 400, None, None, None)) async_fire_time_changed(hass, utcnow() + timedelta(minutes=2)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state_tv = hass.states.get("media_player.panasonic_viera_tv") assert state_tv.state == STATE_OFF diff --git a/tests/components/pjlink/test_media_player.py b/tests/components/pjlink/test_media_player.py index a6d17233450..d44bc942290 100644 --- a/tests/components/pjlink/test_media_player.py +++ b/tests/components/pjlink/test_media_player.py @@ -208,7 +208,7 @@ async def test_update_unavailable(projector_from_address, hass: HomeAssistant) - projector_from_address.side_effect = socket.timeout async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("media_player.test") assert state.state == "unavailable" @@ -237,7 +237,7 @@ async def test_unavailable_time(mocked_projector, hass: HomeAssistant) -> None: mocked_projector.get_power.side_effect = ProjectorError("unavailable time") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=10)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("media_player.test") assert state.state == "off" diff --git a/tests/components/profiler/test_init.py b/tests/components/profiler/test_init.py index 1140dc74849..3cade465347 100644 --- a/tests/components/profiler/test_init.py +++ b/tests/components/profiler/test_init.py @@ -332,7 +332,7 @@ async def test_log_object_sources( caplog.clear() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=11)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "No new object growth found" in caplog.text fake_object2 = FakeObject() @@ -344,7 +344,7 @@ async def test_log_object_sources( caplog.clear() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=21)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "New object FakeObject (1/2)" in caplog.text many_objects = [FakeObject() for _ in range(30)] @@ -352,7 +352,7 @@ async def test_log_object_sources( caplog.clear() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "New object FakeObject (2/30)" in caplog.text assert "New objects overflowed by {'FakeObject': 25}" in caplog.text @@ -362,7 +362,7 @@ async def test_log_object_sources( caplog.clear() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=41)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "FakeObject" not in caplog.text assert "No new object growth found" not in caplog.text @@ -370,7 +370,7 @@ async def test_log_object_sources( await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=51)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "FakeObject" not in caplog.text assert "No new object growth found" not in caplog.text diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index 875b049d8c3..6adcad03016 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -234,6 +234,7 @@ async def test_media_attributes_are_fetched(hass: HomeAssistant) -> None: with patch(mock_func, return_value=mock_result) as mock_fetch: await mock_ddp_response(hass, MOCK_STATUS_PLAYING) + await hass.async_block_till_done(wait_background_tasks=True) mock_state = hass.states.get(mock_entity_id) mock_attrs = dict(mock_state.attributes) @@ -255,6 +256,7 @@ async def test_media_attributes_are_fetched(hass: HomeAssistant) -> None: with patch(mock_func, return_value=mock_result) as mock_fetch_app: await mock_ddp_response(hass, MOCK_STATUS_PLAYING) + await hass.async_block_till_done(wait_background_tasks=True) mock_state = hass.states.get(mock_entity_id) mock_attrs = dict(mock_state.attributes) diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index bec94db71f9..1c6fead6c4a 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -78,7 +78,7 @@ hass.states.set('test.entity', data.get('name', 'not set')) """ hass.async_add_executor_job(execute, hass, "test.py", source, {"name": "paulus"}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.is_state("test.entity", "paulus") @@ -96,7 +96,7 @@ print("This triggers warning.") """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "Don't use print() inside scripts." in caplog.text @@ -111,7 +111,7 @@ logger.info('Logging from inside script') """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "Logging from inside script" in caplog.text @@ -126,7 +126,7 @@ this is not valid Python """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "Error loading script test.py" in caplog.text @@ -140,8 +140,8 @@ async def test_execute_runtime_error( raise Exception('boom') """ - hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_add_executor_job(execute, hass, "test.py", source, {}) + await hass.async_block_till_done(wait_background_tasks=True) assert "Error executing script: boom" in caplog.text @@ -153,7 +153,7 @@ raise Exception('boom') """ task = hass.async_add_executor_job(execute, hass, "test.py", source, {}, True) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert type(task.exception()) == HomeAssistantError assert "Error executing script (Exception): boom" in str(task.exception()) @@ -168,7 +168,7 @@ async def test_accessing_async_methods( hass.async_stop() """ - hass.async_add_executor_job(execute, hass, "test.py", source, {}) + await hass.async_add_executor_job(execute, hass, "test.py", source, {}) await hass.async_block_till_done() assert "Not allowed to access async methods" in caplog.text @@ -181,7 +181,7 @@ hass.async_stop() """ task = hass.async_add_executor_job(execute, hass, "test.py", source, {}, True) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert type(task.exception()) == ServiceValidationError assert "Not allowed to access async methods" in str(task.exception()) @@ -198,7 +198,7 @@ mylist = [1, 2, 3, 4] logger.info('Logging from inside script: %s %s' % (mydict["a"], mylist[2])) """ - hass.async_add_executor_job(execute, hass, "test.py", source, {}) + await hass.async_add_executor_job(execute, hass, "test.py", source, {}) await hass.async_block_till_done() assert "Logging from inside script: 1 3" in caplog.text @@ -217,7 +217,7 @@ async def test_accessing_forbidden_methods( "time.tzset()": "TimeWrapper.tzset", }.items(): caplog.records.clear() - hass.async_add_executor_job(execute, hass, "test.py", source, {}) + await hass.async_add_executor_job(execute, hass, "test.py", source, {}) await hass.async_block_till_done() assert f"Not allowed to access {name}" in caplog.text @@ -231,7 +231,7 @@ async def test_accessing_forbidden_methods_with_response(hass: HomeAssistant) -> "time.tzset()": "TimeWrapper.tzset", }.items(): task = hass.async_add_executor_job(execute, hass, "test.py", source, {}, True) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert type(task.exception()) == ServiceValidationError assert f"Not allowed to access {name}" in str(task.exception()) @@ -244,7 +244,7 @@ for i in [1, 2]: hass.states.set('hello.{}'.format(i), 'world') """ - hass.async_add_executor_job(execute, hass, "test.py", source, {}) + await hass.async_add_executor_job(execute, hass, "test.py", source, {}) await hass.async_block_till_done() assert hass.states.is_state("hello.1", "world") @@ -279,7 +279,7 @@ hass.states.set('hello.ab_list', '{}'.format(ab_list)) """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.is_state("hello.a", "1") assert hass.states.is_state("hello.b", "2") @@ -302,7 +302,7 @@ hass.states.set('hello.b', a[1]) hass.states.set('hello.c', a[2]) """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.is_state("hello.a", "1") assert hass.states.is_state("hello.b", "2") @@ -325,7 +325,7 @@ hass.states.set('module.datetime', """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.is_state("module.time", "1986") assert hass.states.is_state("module.time_strptime", "12:34") @@ -351,7 +351,7 @@ def b(): b() """ hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.is_state("hello.a", "one") assert hass.states.is_state("hello.b", "two") @@ -517,7 +517,7 @@ time.sleep(5) with patch("homeassistant.components.python_script.time.sleep"): hass.async_add_executor_job(execute, hass, "test.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert caplog.text.count("time.sleep") == 1 @@ -664,7 +664,7 @@ hass.states.set('hello.c', c) """ hass.async_add_executor_job(execute, hass, "aug_assign.py", source, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get("hello.a").state == str(((10 + 20) * 5) - 8) assert hass.states.get("hello.b").state == ("foo" + "bar") * 2 @@ -686,5 +686,5 @@ async def test_prohibited_augmented_assignment_operations( ) -> None: """Test that prohibited augmented assignment operations raise an error.""" hass.async_add_executor_job(execute, hass, "aug_assign_prohibited.py", case, {}) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert error in caplog.text diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index f874b92305b..db4f3f0e41f 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -200,7 +200,7 @@ async def test_setup_websocket_2( next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(entity_id) assert state @@ -225,7 +225,7 @@ async def test_setup_encrypted_websocket( next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state @@ -242,7 +242,7 @@ async def test_update_on( next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_ON @@ -262,7 +262,7 @@ async def test_update_off( next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_UNAVAILABLE @@ -290,7 +290,7 @@ async def test_update_off_ws_no_power_state( next_update = mock_now + timedelta(minutes=5) freezer.move_to(next_update) async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ENTITY_ID) assert state.state == STATE_OFF diff --git a/tests/components/schlage/test_binary_sensor.py b/tests/components/schlage/test_binary_sensor.py index 4673f263c8c..97f11577b86 100644 --- a/tests/components/schlage/test_binary_sensor.py +++ b/tests/components/schlage/test_binary_sensor.py @@ -22,7 +22,7 @@ async def test_keypad_disabled_binary_sensor( # Make the coordinator refresh data. async_fire_time_changed(hass, utcnow() + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) keypad = hass.states.get("binary_sensor.vault_door_keypad_disabled") assert keypad is not None @@ -43,7 +43,7 @@ async def test_keypad_disabled_binary_sensor_use_previous_logs_on_failure( # Make the coordinator refresh data. async_fire_time_changed(hass, utcnow() + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) keypad = hass.states.get("binary_sensor.vault_door_keypad_disabled") assert keypad is not None diff --git a/tests/components/schlage/test_lock.py b/tests/components/schlage/test_lock.py index 0972aa97033..5b26da7b27e 100644 --- a/tests/components/schlage/test_lock.py +++ b/tests/components/schlage/test_lock.py @@ -59,7 +59,7 @@ async def test_changed_by( # Make the coordinator refresh data. async_fire_time_changed(hass, utcnow() + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) mock_lock.last_changed_by.assert_called_once_with() lock_device = hass.states.get("lock.vault_door") diff --git a/tests/components/scrape/test_sensor.py b/tests/components/scrape/test_sensor.py index 41da2eb9a79..4d9c2b732dc 100644 --- a/tests/components/scrape/test_sensor.py +++ b/tests/components/scrape/test_sensor.py @@ -261,7 +261,7 @@ async def test_scrape_sensor_no_data_refresh(hass: HomeAssistant) -> None: mocker.payload = "test_scrape_sensor_no_data" async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.ha_version") assert state is not None @@ -541,7 +541,7 @@ async def test_templates_with_yaml(hass: HomeAssistant) -> None: hass, dt_util.utcnow() + timedelta(minutes=10), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.get_values_with_template") assert state.state == "Current Version: 2021.12.10" @@ -555,7 +555,7 @@ async def test_templates_with_yaml(hass: HomeAssistant) -> None: hass, dt_util.utcnow() + timedelta(minutes=20), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.get_values_with_template") assert state.state == STATE_UNAVAILABLE @@ -568,7 +568,7 @@ async def test_templates_with_yaml(hass: HomeAssistant) -> None: hass, dt_util.utcnow() + timedelta(minutes=30), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.get_values_with_template") assert state.state == "Current Version: 2021.12.10" @@ -608,7 +608,7 @@ async def test_availability( hass.states.async_set("sensor.input1", "on") freezer.tick(timedelta(minutes=10)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.current_version") assert state.state == "2021.12.10" @@ -618,7 +618,7 @@ async def test_availability( freezer.tick(timedelta(minutes=10)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.current_version") assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/solaredge/test_coordinator.py b/tests/components/solaredge/test_coordinator.py index 4bd9dee930c..b1496d18d93 100644 --- a/tests/components/solaredge/test_coordinator.py +++ b/tests/components/solaredge/test_coordinator.py @@ -53,7 +53,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( mock_solaredge().get_overview.return_value = mock_overview_data freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.solaredge_lifetime_energy") assert state assert state.state == str(mock_overview_data["overview"]["lifeTimeData"]["energy"]) @@ -63,7 +63,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( mock_solaredge().get_overview.return_value = mock_overview_data freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.solaredge_lifetime_energy") assert state @@ -74,7 +74,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( mock_solaredge().get_overview.return_value = mock_overview_data freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.solaredge_lifetime_energy") assert state @@ -85,7 +85,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( mock_solaredge().get_overview.return_value = mock_overview_data freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.solaredge_energy_this_year") assert state @@ -103,7 +103,7 @@ async def test_solaredgeoverviewdataservice_energy_values_validity( mock_solaredge().get_overview.return_value = mock_overview_data freezer.tick(OVERVIEW_UPDATE_DELAY) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.solaredge_lifetime_energy") assert state diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 0b3834992d8..00858a180a3 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -94,8 +94,9 @@ def async_setup_sonos(hass, config_entry, fire_zgs_event): async def _wrapper(): config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) await fire_zgs_event() + await hass.async_block_till_done(wait_background_tasks=True) return _wrapper diff --git a/tests/components/sonos/test_repairs.py b/tests/components/sonos/test_repairs.py index cc1f59c5cd0..cf64912e498 100644 --- a/tests/components/sonos/test_repairs.py +++ b/tests/components/sonos/test_repairs.py @@ -28,10 +28,12 @@ async def test_subscription_repair_issues( config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + await hass.async_block_till_done() # Ensure an issue is registered on subscription failure + sub_callback = subscription.callback async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert issue_registry.async_get_issue(DOMAIN, SUB_FAIL_ISSUE_ID) # Ensure the issue still exists after reload @@ -42,7 +44,6 @@ async def test_subscription_repair_issues( # Ensure the issue has been removed after a successful subscription callback variables = {"ZoneGroupState": zgs_discovery} event = SonosMockEvent(soco, soco.zoneGroupTopology, variables) - sub_callback = subscription.callback sub_callback(event) await hass.async_block_till_done() assert not issue_registry.async_get_issue(DOMAIN, SUB_FAIL_ISSUE_ID) diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index 6e4461e5397..1f4ba8d22cd 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -26,6 +26,7 @@ async def test_entity_registry_unsupported( soco.get_battery_info.side_effect = NotSupportedException await async_setup_sonos() + await hass.async_block_till_done(wait_background_tasks=True) assert "media_player.zone_a" in entity_registry.entities assert "sensor.zone_a_battery" not in entity_registry.entities @@ -36,6 +37,8 @@ async def test_entity_registry_supported( hass: HomeAssistant, async_autosetup_sonos, soco, entity_registry: er.EntityRegistry ) -> None: """Test sonos device with battery registered in the device registry.""" + await hass.async_block_till_done(wait_background_tasks=True) + assert "media_player.zone_a" in entity_registry.entities assert "sensor.zone_a_battery" in entity_registry.entities assert "binary_sensor.zone_a_charging" in entity_registry.entities @@ -69,6 +72,7 @@ async def test_battery_on_s1( soco.get_battery_info.return_value = {} await async_setup_sonos() + await hass.async_block_till_done(wait_background_tasks=True) subscription = soco.deviceProperties.subscribe.return_value sub_callback = subscription.callback @@ -78,7 +82,7 @@ async def test_battery_on_s1( # Update the speaker with a callback event sub_callback(device_properties_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) battery = entity_registry.entities["sensor.zone_a_battery"] battery_state = hass.states.get(battery.entity_id) @@ -101,6 +105,7 @@ async def test_device_payload_without_battery( soco.get_battery_info.return_value = None await async_setup_sonos() + await hass.async_block_till_done(wait_background_tasks=True) subscription = soco.deviceProperties.subscribe.return_value sub_callback = subscription.callback @@ -109,7 +114,7 @@ async def test_device_payload_without_battery( device_properties_event.variables["more_info"] = bad_payload sub_callback(device_properties_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert bad_payload in caplog.text @@ -125,6 +130,7 @@ async def test_device_payload_without_battery_and_ignored_keys( soco.get_battery_info.return_value = None await async_setup_sonos() + await hass.async_block_till_done(wait_background_tasks=True) subscription = soco.deviceProperties.subscribe.return_value sub_callback = subscription.callback @@ -133,7 +139,7 @@ async def test_device_payload_without_battery_and_ignored_keys( device_properties_event.variables["more_info"] = ignored_payload sub_callback(device_properties_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert ignored_payload not in caplog.text @@ -150,7 +156,7 @@ async def test_audio_input_sensor( subscription = soco.avTransport.subscribe.return_value sub_callback = subscription.callback sub_callback(tv_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) audio_input_sensor = entity_registry.entities["sensor.zone_a_audio_input_format"] audio_input_state = hass.states.get(audio_input_sensor.entity_id) @@ -161,7 +167,7 @@ async def test_audio_input_sensor( type(soco).soundbar_audio_input_format = no_input_mock async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) no_input_mock.assert_called_once() audio_input_state = hass.states.get(audio_input_sensor.entity_id) @@ -169,13 +175,13 @@ async def test_audio_input_sensor( # Ensure state is not polled when source is not TV and state is already "No input" sub_callback(no_media_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) unpolled_mock = PropertyMock(return_value="Will not be polled") type(soco).soundbar_audio_input_format = unpolled_mock async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) unpolled_mock.assert_not_called() audio_input_state = hass.states.get(audio_input_sensor.entity_id) @@ -199,7 +205,7 @@ async def test_microphone_binary_sensor( # Update the speaker with a callback event subscription = soco.deviceProperties.subscribe.return_value subscription.callback(device_properties_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) mic_binary_sensor_state = hass.states.get(mic_binary_sensor.entity_id) assert mic_binary_sensor_state.state == STATE_ON @@ -225,17 +231,18 @@ async def test_favorites_sensor( empty_event = SonosMockEvent(soco, service, {}) subscription = service.subscribe.return_value subscription.callback(event=empty_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Reload the integration to enable the sensor async_fire_time_changed( hass, dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Trigger subscription callback for speaker discovery await fire_zgs_event() + await hass.async_block_till_done(wait_background_tasks=True) favorites_updated_event = SonosMockEvent( soco, service, {"favorites_update_id": "2", "container_update_i_ds": "FV:2,2"} @@ -245,4 +252,4 @@ async def test_favorites_sensor( return_value=True, ): subscription.callback(event=favorites_updated_event) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) diff --git a/tests/components/sonos/test_speaker.py b/tests/components/sonos/test_speaker.py index e0fc4c3baf9..2c4357060be 100644 --- a/tests/components/sonos/test_speaker.py +++ b/tests/components/sonos/test_speaker.py @@ -12,9 +12,20 @@ from tests.common import async_fire_time_changed async def test_fallback_to_polling( - hass: HomeAssistant, async_autosetup_sonos, soco, caplog: pytest.LogCaptureFixture + hass: HomeAssistant, + config_entry, + soco, + fire_zgs_event, + caplog: pytest.LogCaptureFixture, ) -> None: """Test that polling fallback works.""" + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + # Do not wait on background tasks here because the + # subscription callback will fire an unsub the polling check + await hass.async_block_till_done() + await fire_zgs_event() + speaker = list(hass.data[DATA_SONOS].discovered.values())[0] assert speaker.soco is soco assert speaker._subscriptions @@ -30,7 +41,7 @@ async def test_fallback_to_polling( ), ): async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert not speaker._subscriptions assert speaker.subscriptions_failed @@ -46,6 +57,7 @@ async def test_subscription_creation_fails( side_effect=ConnectionError("Took too long"), ): await async_setup_sonos() + await hass.async_block_till_done(wait_background_tasks=True) speaker = list(hass.data[DATA_SONOS].discovered.values())[0] assert not speaker._subscriptions diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index 94e6965a571..61d0c7b4ea5 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -665,7 +665,7 @@ async def test_zone_attributes( hass, dt_util.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) entity_1_state = hass.states.get(DEVICE_1_ENTITY_ID) assert entity_1_state.attributes[ATTR_SOUNDTOUCH_ZONE]["is_master"] diff --git a/tests/components/speedtestdotnet/test_init.py b/tests/components/speedtestdotnet/test_init.py index 5083f56a8e2..2b0f803eb6f 100644 --- a/tests/components/speedtestdotnet/test_init.py +++ b/tests/components/speedtestdotnet/test_init.py @@ -74,7 +74,7 @@ async def test_server_not_found(hass: HomeAssistant, mock_api: MagicMock) -> Non hass, dt_util.utcnow() + timedelta(minutes=61), ) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("sensor.speedtest_ping") assert state is not None assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/systemmonitor/test_binary_sensor.py b/tests/components/systemmonitor/test_binary_sensor.py index 51c8fc87a3a..e3fbdedc081 100644 --- a/tests/components/systemmonitor/test_binary_sensor.py +++ b/tests/components/systemmonitor/test_binary_sensor.py @@ -97,7 +97,7 @@ async def test_sensor_process_fails( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) process_sensor = hass.states.get("binary_sensor.system_monitor_process_python3") assert process_sensor is not None diff --git a/tests/components/systemmonitor/test_sensor.py b/tests/components/systemmonitor/test_sensor.py index 11dd002c2f7..a11112d8f86 100644 --- a/tests/components/systemmonitor/test_sensor.py +++ b/tests/components/systemmonitor/test_sensor.py @@ -232,7 +232,7 @@ async def test_sensor_updating( mock_psutil.virtual_memory.side_effect = Exception("Failed to update") freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) memory_sensor = hass.states.get("sensor.system_monitor_memory_free") assert memory_sensor is not None @@ -248,7 +248,7 @@ async def test_sensor_updating( ) freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) memory_sensor = hass.states.get("sensor.system_monitor_memory_free") assert memory_sensor is not None @@ -293,7 +293,7 @@ async def test_sensor_process_fails( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) process_sensor = hass.states.get("sensor.system_monitor_process_python3") assert process_sensor is not None @@ -330,7 +330,7 @@ async def test_sensor_network_sensors( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) network_out_sensor = hass.states.get("sensor.system_monitor_network_out_eth1") packets_out_sensor = hass.states.get("sensor.system_monitor_packets_out_eth1") @@ -362,7 +362,7 @@ async def test_sensor_network_sensors( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) network_out_sensor = hass.states.get("sensor.system_monitor_network_out_eth1") packets_out_sensor = hass.states.get("sensor.system_monitor_packets_out_eth1") @@ -470,7 +470,7 @@ async def test_exception_handling_disk_sensor( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "OS error for /" in caplog.text @@ -483,7 +483,7 @@ async def test_exception_handling_disk_sensor( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert "OS error for /" in caplog.text @@ -498,7 +498,7 @@ async def test_exception_handling_disk_sensor( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) disk_sensor = hass.states.get("sensor.system_monitor_disk_free") assert disk_sensor is not None @@ -528,7 +528,7 @@ async def test_cpu_percentage_is_zero_returns_unknown( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) cpu_sensor = hass.states.get("sensor.system_monitor_processor_use") assert cpu_sensor is not None @@ -538,7 +538,7 @@ async def test_cpu_percentage_is_zero_returns_unknown( freezer.tick(timedelta(minutes=1)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) cpu_sensor = hass.states.get("sensor.system_monitor_processor_use") assert cpu_sensor is not None @@ -573,7 +573,7 @@ async def test_remove_obsolete_entities( ) freezer.tick(timedelta(minutes=5)) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Fake an entity which should be removed as not supported and disabled entity_registry.async_get_or_create( diff --git a/tests/components/tcp/test_binary_sensor.py b/tests/components/tcp/test_binary_sensor.py index 959c1f050fd..05aa2a471db 100644 --- a/tests/components/tcp/test_binary_sensor.py +++ b/tests/components/tcp/test_binary_sensor.py @@ -79,7 +79,7 @@ async def test_state(hass: HomeAssistant, mock_socket, now) -> None: mock_socket.recv.return_value = b"on" async_fire_time_changed(hass, now + timedelta(seconds=45)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(TEST_ENTITY) diff --git a/tests/components/temper/test_sensor.py b/tests/components/temper/test_sensor.py index 94c44cc4296..d1e74f1ab0f 100644 --- a/tests/components/temper/test_sensor.py +++ b/tests/components/temper/test_sensor.py @@ -29,7 +29,7 @@ async def test_temperature_readback(hass: HomeAssistant) -> None: await hass.async_block_till_done() async_fire_time_changed(hass, utcnow + timedelta(seconds=70)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) temperature = hass.states.get("sensor.mydevicename") assert temperature diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py index 7ac6540f1ff..fa2e997756d 100644 --- a/tests/components/totalconnect/test_alarm_control_panel.py +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -548,30 +548,30 @@ async def test_other_update_failures(hass: HomeAssistant) -> None: # then an error: ServiceUnavailable --> UpdateFailed async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE assert mock_request.call_count == 2 # works again async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL * 2) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 3 # then an error: TotalConnectError --> UpdateFailed async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL * 3) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE assert mock_request.call_count == 4 # works again async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL * 4) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED assert mock_request.call_count == 5 # unknown TotalConnect status via ValueError async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL * 5) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(ENTITY_ID).state == STATE_UNAVAILABLE assert mock_request.call_count == 6 diff --git a/tests/components/uvc/test_camera.py b/tests/components/uvc/test_camera.py index 12203a3e222..522448ecfc4 100644 --- a/tests/components/uvc/test_camera.py +++ b/tests/components/uvc/test_camera.py @@ -278,7 +278,7 @@ async def test_setup_nvr_errors_during_indexing( mock_remote.return_value.index.side_effect = None async_fire_time_changed(hass, now + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) camera_states = hass.states.async_all("camera") @@ -313,7 +313,7 @@ async def test_setup_nvr_errors_during_initialization( mock_remote.side_effect = None async_fire_time_changed(hass, now + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) camera_states = hass.states.async_all("camera") @@ -362,7 +362,7 @@ async def test_motion_recording_mode_properties( ] = True async_fire_time_changed(hass, now + timedelta(seconds=31)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("camera.front") @@ -375,7 +375,7 @@ async def test_motion_recording_mode_properties( mock_remote.return_value.get_camera.return_value["recordingIndicator"] = "DISABLED" async_fire_time_changed(hass, now + timedelta(seconds=61)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("camera.front") @@ -387,7 +387,7 @@ async def test_motion_recording_mode_properties( ) async_fire_time_changed(hass, now + timedelta(seconds=91)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("camera.front") @@ -399,7 +399,7 @@ async def test_motion_recording_mode_properties( ) async_fire_time_changed(hass, now + timedelta(seconds=121)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get("camera.front") diff --git a/tests/components/ws66i/test_media_player.py b/tests/components/ws66i/test_media_player.py index eec6bf191f7..c13f6cbd738 100644 --- a/tests/components/ws66i/test_media_player.py +++ b/tests/components/ws66i/test_media_player.py @@ -195,7 +195,7 @@ async def test_update(hass: HomeAssistant, freezer: FrozenDateTimeFactory) -> No with patch.object(MockWs66i, "open") as method_call: freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert not method_call.called @@ -226,13 +226,13 @@ async def test_failed_update( freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # Failed update, close called with patch.object(MockWs66i, "zone_status", return_value=None): freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.is_state(ZONE_1_ID, STATE_UNAVAILABLE) @@ -240,12 +240,12 @@ async def test_failed_update( with patch.object(MockWs66i, "zone_status", return_value=None): freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # A connection re-attempt succeeds freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # confirm entity is back on state = hass.states.get(ZONE_1_ID) @@ -315,7 +315,7 @@ async def test_source_select( freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) state = hass.states.get(ZONE_1_ID) @@ -370,14 +370,14 @@ async def test_volume_up_down( ) freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # should not go below zero assert ws66i.zones[11].volume == 0 await _call_media_player_service(hass, SERVICE_VOLUME_UP, {"entity_id": ZONE_1_ID}) freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert ws66i.zones[11].volume == 1 await _call_media_player_service( @@ -385,14 +385,14 @@ async def test_volume_up_down( ) freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert ws66i.zones[11].volume == MAX_VOL await _call_media_player_service(hass, SERVICE_VOLUME_UP, {"entity_id": ZONE_1_ID}) freezer.tick(POLL_INTERVAL) async_fire_time_changed(hass) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) # should not go above 38 (MAX_VOL) assert ws66i.zones[11].volume == MAX_VOL diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index c5345386777..2cfc3a4f294 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -238,7 +238,7 @@ async def test_xiaomi_exceptions(hass: HomeAssistant, mock_mirobo_is_on) -> None mock_mirobo_is_on.status.side_effect = DeviceException("dummy exception") future = dt_util.utcnow() + timedelta(seconds=60) async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert not is_available() @@ -247,7 +247,7 @@ async def test_xiaomi_exceptions(hass: HomeAssistant, mock_mirobo_is_on) -> None mock_mirobo_is_on.status.reset_mock() future += timedelta(seconds=60) async_fire_time_changed(hass, future) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) assert not is_available() assert mock_mirobo_is_on.status.call_count == 1 diff --git a/tests/components/yale_smart_alarm/test_coordinator.py b/tests/components/yale_smart_alarm/test_coordinator.py index 5125c817567..6f1125fcf65 100644 --- a/tests/components/yale_smart_alarm/test_coordinator.py +++ b/tests/components/yale_smart_alarm/test_coordinator.py @@ -76,7 +76,7 @@ async def test_coordinator_setup_and_update_errors( client.get_all.side_effect = ConnectionError("Could not connect") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=1)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) client.get_all.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_UNAVAILABLE @@ -84,7 +84,7 @@ async def test_coordinator_setup_and_update_errors( client.get_all.side_effect = ConnectionError("Could not connect") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=2)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) client.get_all.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_UNAVAILABLE @@ -92,7 +92,7 @@ async def test_coordinator_setup_and_update_errors( client.get_all.side_effect = TimeoutError("Could not connect") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=3)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) client.get_all.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_UNAVAILABLE @@ -100,7 +100,7 @@ async def test_coordinator_setup_and_update_errors( client.get_all.side_effect = UnknownError("info") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=4)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) client.get_all.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_UNAVAILABLE @@ -110,7 +110,7 @@ async def test_coordinator_setup_and_update_errors( client.get_all.return_value = load_json client.get_armed_status.return_value = YALE_STATE_ARM_FULL async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) client.get_all.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_ALARM_ARMED_AWAY @@ -118,7 +118,7 @@ async def test_coordinator_setup_and_update_errors( client.get_all.side_effect = AuthenticationError("Can not authenticate") async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=6)) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) client.get_all.assert_called_once() state = hass.states.get("alarm_control_panel.yale_smart_alarm") assert state.state == STATE_UNAVAILABLE diff --git a/tests/test_core.py b/tests/test_core.py index 11fda50a180..a0a197096cd 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -588,6 +588,46 @@ async def test_async_get_hass_can_be_called(hass: HomeAssistant) -> None: my_job_create_task.join() +async def test_async_add_executor_job_background(hass: HomeAssistant) -> None: + """Test running an executor job in the background.""" + calls = [] + + def job(): + time.sleep(0.01) + calls.append(1) + + async def _async_add_executor_job(): + await hass.async_add_executor_job(job) + + task = hass.async_create_background_task( + _async_add_executor_job(), "background", eager_start=True + ) + await hass.async_block_till_done() + assert len(calls) == 0 + await hass.async_block_till_done(wait_background_tasks=True) + assert len(calls) == 1 + await task + + +async def test_async_add_executor_job(hass: HomeAssistant) -> None: + """Test running an executor job.""" + calls = [] + + def job(): + time.sleep(0.01) + calls.append(1) + + async def _async_add_executor_job(): + await hass.async_add_executor_job(job) + + task = hass.async_create_task( + _async_add_executor_job(), "background", eager_start=True + ) + await hass.async_block_till_done() + assert len(calls) == 1 + await task + + async def test_stage_shutdown(hass: HomeAssistant) -> None: """Simulate a shutdown, test calling stuff.""" test_stop = async_capture_events(hass, EVENT_HOMEASSISTANT_STOP) From f2edc156874511b8a921043a65249519c0bb74e9 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Sat, 30 Mar 2024 15:59:20 -0500 Subject: [PATCH 047/198] Add initial support for floors to intents (#114456) * Add initial support for floors to intents * Fix climate intent * More tests * No return value * Add requested changes * Reuse event handler --- homeassistant/components/climate/intent.py | 2 + .../components/conversation/default_agent.py | 46 +++++++- .../components/conversation/manifest.json | 2 +- homeassistant/helpers/intent.py | 110 ++++++++++++++---- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../conversation/test_default_agent.py | 73 +++++++++++- .../test_default_agent_intents.py | 105 ++++++++++++++++- tests/helpers/test_intent.py | 76 +++++++++++- 10 files changed, 384 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/climate/intent.py b/homeassistant/components/climate/intent.py index db263451f0b..3073d3e3c26 100644 --- a/homeassistant/components/climate/intent.py +++ b/homeassistant/components/climate/intent.py @@ -58,6 +58,7 @@ class GetTemperatureIntent(intent.IntentHandler): raise intent.NoStatesMatchedError( name=entity_text or entity_name, area=area_name or area_id, + floor=None, domains={DOMAIN}, device_classes=None, ) @@ -75,6 +76,7 @@ class GetTemperatureIntent(intent.IntentHandler): raise intent.NoStatesMatchedError( name=entity_name, area=None, + floor=None, domains={DOMAIN}, device_classes=None, ) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index 96b0565ebd3..c0307c68908 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -34,6 +34,7 @@ from homeassistant.helpers import ( area_registry as ar, device_registry as dr, entity_registry as er, + floor_registry as fr, intent, start, template, @@ -163,7 +164,12 @@ class DefaultAgent(AbstractConversationAgent): self.hass.bus.async_listen( ar.EVENT_AREA_REGISTRY_UPDATED, - self._async_handle_area_registry_changed, + self._async_handle_area_floor_registry_changed, + run_immediately=True, + ) + self.hass.bus.async_listen( + fr.EVENT_FLOOR_REGISTRY_UPDATED, + self._async_handle_area_floor_registry_changed, run_immediately=True, ) self.hass.bus.async_listen( @@ -696,10 +702,13 @@ class DefaultAgent(AbstractConversationAgent): return lang_intents @core.callback - def _async_handle_area_registry_changed( - self, event: core.Event[ar.EventAreaRegistryUpdatedData] + def _async_handle_area_floor_registry_changed( + self, + event: core.Event[ + ar.EventAreaRegistryUpdatedData | fr.EventFloorRegistryUpdatedData + ], ) -> None: - """Clear area area cache when the area registry has changed.""" + """Clear area/floor list cache when the area registry has changed.""" self._slot_lists = None @core.callback @@ -773,6 +782,8 @@ class DefaultAgent(AbstractConversationAgent): # Default name entity_names.append((state.name, state.name, context)) + _LOGGER.debug("Exposed entities: %s", entity_names) + # Expose all areas. # # We pass in area id here with the expectation that no two areas will @@ -788,11 +799,25 @@ class DefaultAgent(AbstractConversationAgent): area_names.append((alias, area.id)) - _LOGGER.debug("Exposed entities: %s", entity_names) + # Expose all floors. + # + # We pass in floor id here with the expectation that no two floors will + # share the same name or alias. + floors = fr.async_get(self.hass) + floor_names = [] + for floor in floors.async_list_floors(): + floor_names.append((floor.name, floor.floor_id)) + if floor.aliases: + for alias in floor.aliases: + if not alias.strip(): + continue + + floor_names.append((alias, floor.floor_id)) self._slot_lists = { "area": TextSlotList.from_tuples(area_names, allow_template=False), "name": TextSlotList.from_tuples(entity_names, allow_template=False), + "floor": TextSlotList.from_tuples(floor_names, allow_template=False), } return self._slot_lists @@ -953,6 +978,10 @@ def _get_unmatched_response(result: RecognizeResult) -> tuple[ErrorKey, dict[str # area only return ErrorKey.NO_AREA, {"area": unmatched_area} + if unmatched_floor := unmatched_text.get("floor"): + # floor only + return ErrorKey.NO_FLOOR, {"floor": unmatched_floor} + # Area may still have matched matched_area: str | None = None if matched_area_entity := result.entities.get("area"): @@ -1000,6 +1029,13 @@ def _get_no_states_matched_response( "area": no_states_error.area, } + if no_states_error.floor: + # domain in floor + return ErrorKey.NO_DOMAIN_IN_FLOOR, { + "domain": domain, + "floor": no_states_error.floor, + } + # domain only return ErrorKey.NO_DOMAIN, {"domain": domain} diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 7f3c4f5894e..7f463483bf9 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.27"] + "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.3.29"] } diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index 63214cb135b..fcebf91b854 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -24,7 +24,13 @@ from homeassistant.core import Context, HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.loader import bind_hass -from . import area_registry, config_validation as cv, device_registry, entity_registry +from . import ( + area_registry, + config_validation as cv, + device_registry, + entity_registry, + floor_registry, +) _LOGGER = logging.getLogger(__name__) _SlotsType = dict[str, Any] @@ -144,16 +150,18 @@ class NoStatesMatchedError(IntentError): def __init__( self, - name: str | None, - area: str | None, - domains: set[str] | None, - device_classes: set[str] | None, + name: str | None = None, + area: str | None = None, + floor: str | None = None, + domains: set[str] | None = None, + device_classes: set[str] | None = None, ) -> None: """Initialize error.""" super().__init__() self.name = name self.area = area + self.floor = floor self.domains = domains self.device_classes = device_classes @@ -220,12 +228,35 @@ def _find_area( return None -def _filter_by_area( +def _find_floor( + id_or_name: str, floors: floor_registry.FloorRegistry +) -> floor_registry.FloorEntry | None: + """Find an floor by id or name, checking aliases too.""" + floor = floors.async_get_floor(id_or_name) or floors.async_get_floor_by_name( + id_or_name + ) + if floor is not None: + return floor + + # Check floor aliases + for maybe_floor in floors.floors.values(): + if not maybe_floor.aliases: + continue + + for floor_alias in maybe_floor.aliases: + if id_or_name == floor_alias.casefold(): + return maybe_floor + + return None + + +def _filter_by_areas( states_and_entities: list[tuple[State, entity_registry.RegistryEntry | None]], - area: area_registry.AreaEntry, + areas: Iterable[area_registry.AreaEntry], devices: device_registry.DeviceRegistry, ) -> Iterable[tuple[State, entity_registry.RegistryEntry | None]]: """Filter state/entity pairs by an area.""" + filter_area_ids: set[str | None] = {a.id for a in areas} entity_area_ids: dict[str, str | None] = {} for _state, entity in states_and_entities: if entity is None: @@ -241,7 +272,7 @@ def _filter_by_area( entity_area_ids[entity.id] = device.area_id for state, entity in states_and_entities: - if (entity is not None) and (entity_area_ids.get(entity.id) == area.id): + if (entity is not None) and (entity_area_ids.get(entity.id) in filter_area_ids): yield (state, entity) @@ -252,11 +283,14 @@ def async_match_states( name: str | None = None, area_name: str | None = None, area: area_registry.AreaEntry | None = None, + floor_name: str | None = None, + floor: floor_registry.FloorEntry | None = None, domains: Collection[str] | None = None, device_classes: Collection[str] | None = None, states: Iterable[State] | None = None, entities: entity_registry.EntityRegistry | None = None, areas: area_registry.AreaRegistry | None = None, + floors: floor_registry.FloorRegistry | None = None, devices: device_registry.DeviceRegistry | None = None, assistant: str | None = None, ) -> Iterable[State]: @@ -268,6 +302,15 @@ def async_match_states( if entities is None: entities = entity_registry.async_get(hass) + if devices is None: + devices = device_registry.async_get(hass) + + if areas is None: + areas = area_registry.async_get(hass) + + if floors is None: + floors = floor_registry.async_get(hass) + # Gather entities states_and_entities: list[tuple[State, entity_registry.RegistryEntry | None]] = [] for state in states: @@ -294,20 +337,35 @@ def async_match_states( if _is_device_class(state, entity, device_classes) ] + filter_areas: list[area_registry.AreaEntry] = [] + + if (floor is None) and (floor_name is not None): + # Look up floor by name + floor = _find_floor(floor_name, floors) + if floor is None: + _LOGGER.warning("Floor not found: %s", floor_name) + return + + if floor is not None: + filter_areas = [ + a for a in areas.async_list_areas() if a.floor_id == floor.floor_id + ] + if (area is None) and (area_name is not None): # Look up area by name - if areas is None: - areas = area_registry.async_get(hass) - area = _find_area(area_name, areas) - assert area is not None, f"No area named {area_name}" + if area is None: + _LOGGER.warning("Area not found: %s", area_name) + return if area is not None: - # Filter by states/entities by area - if devices is None: - devices = device_registry.async_get(hass) + filter_areas = [area] - states_and_entities = list(_filter_by_area(states_and_entities, area, devices)) + if filter_areas: + # Filter by states/entities by area + states_and_entities = list( + _filter_by_areas(states_and_entities, filter_areas, devices) + ) if assistant is not None: # Filter by exposure @@ -318,9 +376,6 @@ def async_match_states( ] if name is not None: - if devices is None: - devices = device_registry.async_get(hass) - # Filter by name name = name.casefold() @@ -389,7 +444,7 @@ class DynamicServiceIntentHandler(IntentHandler): """ slot_schema = { - vol.Any("name", "area"): cv.string, + vol.Any("name", "area", "floor"): cv.string, vol.Optional("domain"): vol.All(cv.ensure_list, [cv.string]), vol.Optional("device_class"): vol.All(cv.ensure_list, [cv.string]), } @@ -453,7 +508,7 @@ class DynamicServiceIntentHandler(IntentHandler): # Don't match on name if targeting all entities entity_name = None - # Look up area first to fail early + # Look up area to fail early area_slot = slots.get("area", {}) area_id = area_slot.get("value") area_name = area_slot.get("text") @@ -464,6 +519,17 @@ class DynamicServiceIntentHandler(IntentHandler): if area is None: raise IntentHandleError(f"No area named {area_name}") + # Look up floor to fail early + floor_slot = slots.get("floor", {}) + floor_id = floor_slot.get("value") + floor_name = floor_slot.get("text") + floor: floor_registry.FloorEntry | None = None + if floor_id is not None: + floors = floor_registry.async_get(hass) + floor = floors.async_get_floor(floor_id) + if floor is None: + raise IntentHandleError(f"No floor named {floor_name}") + # Optional domain/device class filters. # Convert to sets for speed. domains: set[str] | None = None @@ -480,6 +546,7 @@ class DynamicServiceIntentHandler(IntentHandler): hass, name=entity_name, area=area, + floor=floor, domains=domains, device_classes=device_classes, assistant=intent_obj.assistant, @@ -491,6 +558,7 @@ class DynamicServiceIntentHandler(IntentHandler): raise NoStatesMatchedError( name=entity_text or entity_name, area=area_name or area_id, + floor=floor_name or floor_id, domains=domains, device_classes=device_classes, ) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 1d60b74f18f..bdfaa8fcf45 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240329.1 -home-assistant-intents==2024.3.27 +home-assistant-intents==2024.3.29 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.3 diff --git a/requirements_all.txt b/requirements_all.txt index 0a94eb110e1..da752f00279 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1080,7 +1080,7 @@ holidays==0.45 home-assistant-frontend==20240329.1 # homeassistant.components.conversation -home-assistant-intents==2024.3.27 +home-assistant-intents==2024.3.29 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3a3ebbbb077..4889e9de781 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -879,7 +879,7 @@ holidays==0.45 home-assistant-frontend==20240329.1 # homeassistant.components.conversation -home-assistant-intents==2024.3.27 +home-assistant-intents==2024.3.29 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/tests/components/conversation/test_default_agent.py b/tests/components/conversation/test_default_agent.py index aefb37f427e..8f38459a8da 100644 --- a/tests/components/conversation/test_default_agent.py +++ b/tests/components/conversation/test_default_agent.py @@ -17,6 +17,7 @@ from homeassistant.helpers import ( device_registry as dr, entity, entity_registry as er, + floor_registry as fr, intent, ) from homeassistant.setup import async_setup_component @@ -480,6 +481,20 @@ async def test_error_no_area(hass: HomeAssistant, init_components) -> None: ) +async def test_error_no_floor(hass: HomeAssistant, init_components) -> None: + """Test error message when floor is missing.""" + result = await conversation.async_converse( + hass, "turn on all the lights on missing floor", None, Context(), None + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR + assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS + assert ( + result.response.speech["plain"]["speech"] + == "Sorry, I am not aware of any floor called missing" + ) + + async def test_error_no_device_in_area( hass: HomeAssistant, init_components, area_registry: ar.AreaRegistry ) -> None: @@ -549,6 +564,48 @@ async def test_error_no_domain_in_area( ) +async def test_error_no_domain_in_floor( + hass: HomeAssistant, + init_components, + area_registry: ar.AreaRegistry, + floor_registry: fr.FloorRegistry, +) -> None: + """Test error message when no devices/entities for a domain exist on a floor.""" + floor_ground = floor_registry.async_create("ground") + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update( + area_kitchen.id, name="kitchen", floor_id=floor_ground.floor_id + ) + result = await conversation.async_converse( + hass, "turn on all lights on the ground floor", None, Context(), None + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR + assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS + assert ( + result.response.speech["plain"]["speech"] + == "Sorry, I am not aware of any light on the ground floor" + ) + + # Add a new floor/area to trigger registry event handlers + floor_upstairs = floor_registry.async_create("upstairs") + area_bedroom = area_registry.async_get_or_create("bedroom_id") + area_bedroom = area_registry.async_update( + area_bedroom.id, name="bedroom", floor_id=floor_upstairs.floor_id + ) + + result = await conversation.async_converse( + hass, "turn on all lights upstairs", None, Context(), None + ) + + assert result.response.response_type == intent.IntentResponseType.ERROR + assert result.response.error_code == intent.IntentResponseErrorCode.NO_VALID_TARGETS + assert ( + result.response.speech["plain"]["speech"] + == "Sorry, I am not aware of any light on the upstairs floor" + ) + + async def test_error_no_device_class(hass: HomeAssistant, init_components) -> None: """Test error message when no entities of a device class exist.""" @@ -736,7 +793,7 @@ async def test_no_states_matched_default_error( with patch( "homeassistant.components.conversation.default_agent.intent.async_handle", - side_effect=intent.NoStatesMatchedError(None, None, None, None), + side_effect=intent.NoStatesMatchedError(), ): result = await conversation.async_converse( hass, "turn on lights in the kitchen", None, Context(), None @@ -759,11 +816,16 @@ async def test_empty_aliases( area_registry: ar.AreaRegistry, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + floor_registry: fr.FloorRegistry, ) -> None: """Test that empty aliases are not added to slot lists.""" + floor_1 = floor_registry.async_create("first floor", aliases={" "}) + area_kitchen = area_registry.async_get_or_create("kitchen_id") area_kitchen = area_registry.async_update(area_kitchen.id, name="kitchen") - area_kitchen = area_registry.async_update(area_kitchen.id, aliases={" "}) + area_kitchen = area_registry.async_update( + area_kitchen.id, aliases={" "}, floor_id=floor_1 + ) entry = MockConfigEntry() entry.add_to_hass(hass) @@ -799,7 +861,7 @@ async def test_empty_aliases( slot_lists = mock_recognize_all.call_args[0][2] # Slot lists should only contain non-empty text - assert slot_lists.keys() == {"area", "name"} + assert slot_lists.keys() == {"area", "name", "floor"} areas = slot_lists["area"] assert len(areas.values) == 1 assert areas.values[0].value_out == area_kitchen.id @@ -810,6 +872,11 @@ async def test_empty_aliases( assert names.values[0].value_out == kitchen_light.name assert names.values[0].text_in.text == kitchen_light.name + floors = slot_lists["floor"] + assert len(floors.values) == 1 + assert floors.values[0].value_out == floor_1.floor_id + assert floors.values[0].text_in.text == floor_1.name + async def test_all_domains_loaded(hass: HomeAssistant, init_components) -> None: """Test that sentences for all domains are always loaded.""" diff --git a/tests/components/conversation/test_default_agent_intents.py b/tests/components/conversation/test_default_agent_intents.py index c57d93d8cef..9636ac07f63 100644 --- a/tests/components/conversation/test_default_agent_intents.py +++ b/tests/components/conversation/test_default_agent_intents.py @@ -2,14 +2,26 @@ import pytest -from homeassistant.components import conversation, cover, media_player, vacuum, valve +from homeassistant.components import ( + conversation, + cover, + light, + media_player, + vacuum, + valve, +) from homeassistant.components.cover import intent as cover_intent from homeassistant.components.homeassistant.exposed_entities import async_expose_entity from homeassistant.components.media_player import intent as media_player_intent from homeassistant.components.vacuum import intent as vaccum_intent from homeassistant.const import STATE_CLOSED from homeassistant.core import Context, HomeAssistant -from homeassistant.helpers import intent +from homeassistant.helpers import ( + area_registry as ar, + entity_registry as er, + floor_registry as fr, + intent, +) from homeassistant.setup import async_setup_component from tests.common import async_mock_service @@ -244,3 +256,92 @@ async def test_media_player_intents( "entity_id": entity_id, media_player.ATTR_MEDIA_VOLUME_LEVEL: 0.75, } + + +async def test_turn_floor_lights_on_off( + hass: HomeAssistant, + init_components, + entity_registry: er.EntityRegistry, + area_registry: ar.AreaRegistry, + floor_registry: fr.FloorRegistry, +) -> None: + """Test that we can turn lights on/off for an entire floor.""" + floor_ground = floor_registry.async_create("ground", aliases={"downstairs"}) + floor_upstairs = floor_registry.async_create("upstairs") + + # Kitchen and living room are on the ground floor + area_kitchen = area_registry.async_get_or_create("kitchen_id") + area_kitchen = area_registry.async_update( + area_kitchen.id, name="kitchen", floor_id=floor_ground.floor_id + ) + + area_living_room = area_registry.async_get_or_create("living_room_id") + area_living_room = area_registry.async_update( + area_living_room.id, name="living_room", floor_id=floor_ground.floor_id + ) + + # Bedroom is upstairs + area_bedroom = area_registry.async_get_or_create("bedroom_id") + area_bedroom = area_registry.async_update( + area_bedroom.id, name="bedroom", floor_id=floor_upstairs.floor_id + ) + + # One light per area + kitchen_light = entity_registry.async_get_or_create( + "light", "demo", "kitchen_light" + ) + kitchen_light = entity_registry.async_update_entity( + kitchen_light.entity_id, area_id=area_kitchen.id + ) + hass.states.async_set(kitchen_light.entity_id, "off") + + living_room_light = entity_registry.async_get_or_create( + "light", "demo", "living_room_light" + ) + living_room_light = entity_registry.async_update_entity( + living_room_light.entity_id, area_id=area_living_room.id + ) + hass.states.async_set(living_room_light.entity_id, "off") + + bedroom_light = entity_registry.async_get_or_create( + "light", "demo", "bedroom_light" + ) + bedroom_light = entity_registry.async_update_entity( + bedroom_light.entity_id, area_id=area_bedroom.id + ) + hass.states.async_set(bedroom_light.entity_id, "off") + + # Target by floor + on_calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_ON) + result = await conversation.async_converse( + hass, "turn on all lights downstairs", None, Context(), None + ) + + assert len(on_calls) == 2 + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert {s.entity_id for s in result.response.matched_states} == { + kitchen_light.entity_id, + living_room_light.entity_id, + } + + on_calls.clear() + result = await conversation.async_converse( + hass, "upstairs lights on", None, Context(), None + ) + + assert len(on_calls) == 1 + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert {s.entity_id for s in result.response.matched_states} == { + bedroom_light.entity_id + } + + off_calls = async_mock_service(hass, light.DOMAIN, light.SERVICE_TURN_OFF) + result = await conversation.async_converse( + hass, "turn upstairs lights off", None, Context(), None + ) + + assert len(off_calls) == 1 + assert result.response.response_type == intent.IntentResponseType.ACTION_DONE + assert {s.entity_id for s in result.response.matched_states} == { + bedroom_light.entity_id + } diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index 1bc01c28cf2..d77eb698205 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -15,6 +15,7 @@ from homeassistant.helpers import ( config_validation as cv, device_registry as dr, entity_registry as er, + floor_registry as fr, intent, ) from homeassistant.setup import async_setup_component @@ -34,12 +35,25 @@ async def test_async_match_states( hass: HomeAssistant, area_registry: ar.AreaRegistry, entity_registry: er.EntityRegistry, + floor_registry: fr.FloorRegistry, ) -> None: """Test async_match_state helper.""" area_kitchen = area_registry.async_get_or_create("kitchen") - area_registry.async_update(area_kitchen.id, aliases={"food room"}) + area_kitchen = area_registry.async_update(area_kitchen.id, aliases={"food room"}) area_bedroom = area_registry.async_get_or_create("bedroom") + # Kitchen is on the first floor + floor_1 = floor_registry.async_create("first floor", aliases={"ground floor"}) + area_kitchen = area_registry.async_update( + area_kitchen.id, floor_id=floor_1.floor_id + ) + + # Bedroom is on the second floor + floor_2 = floor_registry.async_create("second floor") + area_bedroom = area_registry.async_update( + area_bedroom.id, floor_id=floor_2.floor_id + ) + state1 = State( "light.kitchen", "on", attributes={ATTR_FRIENDLY_NAME: "kitchen light"} ) @@ -94,6 +108,13 @@ async def test_async_match_states( ) ) + # Invalid area + assert not list( + intent.async_match_states( + hass, area_name="invalid area", states=[state1, state2] + ) + ) + # Domain + area assert list( intent.async_match_states( @@ -111,6 +132,35 @@ async def test_async_match_states( ) ) == [state2] + # Floor + assert list( + intent.async_match_states( + hass, floor_name="first floor", states=[state1, state2] + ) + ) == [state1] + + assert list( + intent.async_match_states( + # Check alias + hass, + floor_name="ground floor", + states=[state1, state2], + ) + ) == [state1] + + assert list( + intent.async_match_states( + hass, floor_name="second floor", states=[state1, state2] + ) + ) == [state2] + + # Invalid floor + assert not list( + intent.async_match_states( + hass, floor_name="invalid floor", states=[state1, state2] + ) + ) + async def test_match_device_area( hass: HomeAssistant, @@ -300,3 +350,27 @@ async def test_validate_then_run_in_background(hass: HomeAssistant) -> None: assert len(calls) == 1 assert calls[0].data == {"entity_id": "light.kitchen"} + + +async def test_invalid_area_floor_names(hass: HomeAssistant) -> None: + """Test that we throw an intent handle error with invalid area/floor names.""" + handler = intent.ServiceIntentHandler( + "TestType", "light", "turn_on", "Turned {} on" + ) + intent.async_register(hass, handler) + + with pytest.raises(intent.IntentHandleError): + await intent.async_handle( + hass, + "test", + "TestType", + slots={"area": {"value": "invalid area"}}, + ) + + with pytest.raises(intent.IntentHandleError): + await intent.async_handle( + hass, + "test", + "TestType", + slots={"floor": {"value": "invalid floor"}}, + ) From bdf51553eff11b42eb2d06121b320471f9531d68 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Mar 2024 06:34:47 -1000 Subject: [PATCH 048/198] Improve sonos test synchronization (#114468) --- tests/components/sonos/conftest.py | 35 +++++++++++++++++++++++--- tests/components/sonos/test_repairs.py | 12 ++++----- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index 00858a180a3..576c9a80799 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -1,16 +1,20 @@ """Configuration for Sonos tests.""" +import asyncio +from collections.abc import Callable from copy import copy from ipaddress import ip_address from unittest.mock import AsyncMock, MagicMock, Mock, patch import pytest from soco import SoCo +from soco.events_base import Event as SonosEvent from homeassistant.components import ssdp, zeroconf from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sonos import DOMAIN from homeassistant.const import CONF_HOSTS +from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry, load_fixture @@ -30,6 +34,31 @@ class SonosMockSubscribe: """Initialize the mock subscriber.""" self.event_listener = SonosMockEventListener(ip_address) self.service = Mock() + self.callback_future: asyncio.Future[Callable[[SonosEvent], None]] = None + self._callback: Callable[[SonosEvent], None] | None = None + + @property + def callback(self) -> Callable[[SonosEvent], None] | None: + """Return the callback.""" + return self._callback + + @callback.setter + def callback(self, callback: Callable[[SonosEvent], None]) -> None: + """Set the callback.""" + self._callback = callback + future = self._get_callback_future() + if not future.done(): + future.set_result(callback) + + def _get_callback_future(self) -> asyncio.Future[Callable[[SonosEvent], None]]: + """Get the callback future.""" + if not self.callback_future: + self.callback_future = asyncio.get_running_loop().create_future() + return self.callback_future + + async def wait_for_callback_to_be_set(self) -> Callable[[SonosEvent], None]: + """Wait for the callback to be set.""" + return await self._get_callback_future() async def unsubscribe(self) -> None: """Unsubscribe mock.""" @@ -456,14 +485,14 @@ def zgs_discovery_fixture(): @pytest.fixture(name="fire_zgs_event") -def zgs_event_fixture(hass, soco, zgs_discovery): +def zgs_event_fixture(hass: HomeAssistant, soco: SoCo, zgs_discovery: str): """Create alarm_event fixture.""" variables = {"ZoneGroupState": zgs_discovery} async def _wrapper(): event = SonosMockEvent(soco, soco.zoneGroupTopology, variables) - subscription = soco.zoneGroupTopology.subscribe.return_value - sub_callback = subscription.callback + subscription: SonosMockSubscribe = soco.zoneGroupTopology.subscribe.return_value + sub_callback = await subscription.wait_for_callback_to_be_set() sub_callback(event) await hass.async_block_till_done() diff --git a/tests/components/sonos/test_repairs.py b/tests/components/sonos/test_repairs.py index cf64912e498..49b87b272d6 100644 --- a/tests/components/sonos/test_repairs.py +++ b/tests/components/sonos/test_repairs.py @@ -2,6 +2,8 @@ from unittest.mock import Mock +from soco import SoCo + from homeassistant.components.sonos.const import ( DOMAIN, SCAN_INTERVAL, @@ -11,27 +13,25 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.issue_registry import async_get as async_get_issue_registry from homeassistant.util import dt as dt_util -from .conftest import SonosMockEvent +from .conftest import SonosMockEvent, SonosMockSubscribe from tests.common import MockConfigEntry, async_fire_time_changed async def test_subscription_repair_issues( - hass: HomeAssistant, config_entry: MockConfigEntry, soco, zgs_discovery + hass: HomeAssistant, config_entry: MockConfigEntry, soco: SoCo, zgs_discovery ) -> None: """Test repair issues handling for failed subscriptions.""" issue_registry = async_get_issue_registry(hass) - subscription = soco.zoneGroupTopology.subscribe.return_value + subscription: SonosMockSubscribe = soco.zoneGroupTopology.subscribe.return_value subscription.event_listener = Mock(address=("192.168.4.2", 1400)) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - await hass.async_block_till_done() # Ensure an issue is registered on subscription failure - sub_callback = subscription.callback + sub_callback = await subscription.wait_for_callback_to_be_set() async_fire_time_changed(hass, dt_util.utcnow() + SCAN_INTERVAL) await hass.async_block_till_done(wait_background_tasks=True) assert issue_registry.async_get_issue(DOMAIN, SUB_FAIL_ISSUE_ID) From c373d40e34af020a9ef74b1949bee93f6a9ef173 Mon Sep 17 00:00:00 2001 From: dotvav Date: Sun, 31 Mar 2024 11:38:59 +0200 Subject: [PATCH 049/198] Fix Overkiz Hitachi OVP air-to-air heat pump (#114487) Unpack command parameters instead of passing a list --- .../climate_entities/hitachi_air_to_air_heat_pump_ovp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_ovp.py b/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_ovp.py index 86cde4fc4db..b4d6ab788a1 100644 --- a/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_ovp.py +++ b/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_ovp.py @@ -357,5 +357,5 @@ class HitachiAirToAirHeatPumpOVP(OverkizEntity, ClimateEntity): ] await self.executor.async_execute_command( - OverkizCommand.GLOBAL_CONTROL, command_data + OverkizCommand.GLOBAL_CONTROL, *command_data ) From 008e4413b5e579c426efb3cb2adcc8b400010049 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Mar 2024 08:56:01 -1000 Subject: [PATCH 050/198] Fix late load of anyio doing blocking I/O in the event loop (#114491) * Fix late load of anyio doing blocking I/O in the event loop httpx loads anyio which loads the asyncio backend in the event loop as soon as httpx makes the first request * tweak --- homeassistant/bootstrap.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 03c0de1ff62..5b805b6138e 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -93,6 +93,11 @@ from .util.async_ import create_eager_task from .util.logging import async_activate_log_queue_handler from .util.package import async_get_user_site, is_virtual_env +with contextlib.suppress(ImportError): + # Ensure anyio backend is imported to avoid it being imported in the event loop + from anyio._backends import _asyncio # noqa: F401 + + if TYPE_CHECKING: from .runner import RuntimeConfig From e8afdd67d0efff4d0d0752cf2baead5545036045 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 30 Mar 2024 08:05:14 -1000 Subject: [PATCH 051/198] Fix workday doing blocking I/O in the event loop (#114492) --- homeassistant/components/workday/__init__.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/workday/__init__.py b/homeassistant/components/workday/__init__.py index 195221ef088..077a6710b8d 100644 --- a/homeassistant/components/workday/__init__.py +++ b/homeassistant/components/workday/__init__.py @@ -2,6 +2,8 @@ from __future__ import annotations +from functools import partial + from holidays import HolidayBase, country_holidays from homeassistant.config_entries import ConfigEntry @@ -13,7 +15,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity, async_create_iss from .const import CONF_PROVINCE, DOMAIN, PLATFORMS -def _validate_country_and_province( +async def _async_validate_country_and_province( hass: HomeAssistant, entry: ConfigEntry, country: str | None, province: str | None ) -> None: """Validate country and province.""" @@ -21,7 +23,7 @@ def _validate_country_and_province( if not country: return try: - country_holidays(country) + await hass.async_add_executor_job(country_holidays, country) except NotImplementedError as ex: async_create_issue( hass, @@ -39,7 +41,9 @@ def _validate_country_and_province( if not province: return try: - country_holidays(country, subdiv=province) + await hass.async_add_executor_job( + partial(country_holidays, country, subdiv=province) + ) except NotImplementedError as ex: async_create_issue( hass, @@ -66,10 +70,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: country: str | None = entry.options.get(CONF_COUNTRY) province: str | None = entry.options.get(CONF_PROVINCE) - _validate_country_and_province(hass, entry, country, province) + await _async_validate_country_and_province(hass, entry, country, province) if country and CONF_LANGUAGE not in entry.options: - cls: HolidayBase = country_holidays(country, subdiv=province) + cls: HolidayBase = await hass.async_add_executor_job( + partial(country_holidays, country, subdiv=province) + ) default_language = cls.default_language new_options = entry.options.copy() new_options[CONF_LANGUAGE] = default_language From ef97255d9c0f67f4b79d02095f7eca7d44660705 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Sun, 31 Mar 2024 20:08:43 +0200 Subject: [PATCH 052/198] Fix server update from breaking setup in Speedtest.NET (#114524) --- homeassistant/components/speedtestdotnet/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/speedtestdotnet/__init__.py b/homeassistant/components/speedtestdotnet/__init__.py index 831e66d1c4e..3c15f2fb820 100644 --- a/homeassistant/components/speedtestdotnet/__init__.py +++ b/homeassistant/components/speedtestdotnet/__init__.py @@ -25,10 +25,11 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b partial(speedtest.Speedtest, secure=True) ) coordinator = SpeedTestDataCoordinator(hass, config_entry, api) - await hass.async_add_executor_job(coordinator.update_servers) except speedtest.SpeedtestException as err: raise ConfigEntryNotReady from err + hass.data[DOMAIN] = coordinator + async def _async_finish_startup(hass: HomeAssistant) -> None: """Run this only when HA has finished its startup.""" await coordinator.async_config_entry_first_refresh() @@ -36,8 +37,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b # Don't start a speedtest during startup async_at_started(hass, _async_finish_startup) - hass.data[DOMAIN] = coordinator - await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) config_entry.async_on_unload(config_entry.add_update_listener(update_listener)) From 14dfb6a2552587e2ad2c6059616b5d4c5750f0b5 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 1 Apr 2024 00:24:41 +0200 Subject: [PATCH 053/198] Bump axis to v60 (#114544) * Improve Axis MQTT support * Bump axis to v60 --- homeassistant/components/axis/hub/hub.py | 5 +++-- homeassistant/components/axis/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/axis/const.py | 1 + tests/components/axis/test_hub.py | 4 ++-- 6 files changed, 9 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/axis/hub/hub.py b/homeassistant/components/axis/hub/hub.py index 08eb816f6ab..4abd1358417 100644 --- a/homeassistant/components/axis/hub/hub.py +++ b/homeassistant/components/axis/hub/hub.py @@ -116,7 +116,7 @@ class AxisHub: if status.status.state == ClientState.ACTIVE: self.config.entry.async_on_unload( await mqtt.async_subscribe( - hass, f"{self.api.vapix.serial_number}/#", self.mqtt_message + hass, f"{status.config.device_topic_prefix}/#", self.mqtt_message ) ) @@ -124,7 +124,8 @@ class AxisHub: def mqtt_message(self, message: ReceiveMessage) -> None: """Receive Axis MQTT message.""" self.disconnect_from_stream() - + if message.topic.endswith("event/connection"): + return event = mqtt_json_to_event(message.payload) self.api.event.handler(event) diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index f47d10df484..1065783d957 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==59"], + "requirements": ["axis==60"], "ssdp": [ { "manufacturer": "AXIS" diff --git a/requirements_all.txt b/requirements_all.txt index da752f00279..1ef594ea50b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==59 +axis==60 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4889e9de781..fc19ead8e95 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==59 +axis==60 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 diff --git a/tests/components/axis/const.py b/tests/components/axis/const.py index 7b881ea55e5..16b9d17f99e 100644 --- a/tests/components/axis/const.py +++ b/tests/components/axis/const.py @@ -74,6 +74,7 @@ MQTT_CLIENT_RESPONSE = { "status": {"state": "active", "connectionStatus": "Connected"}, "config": { "server": {"protocol": "tcp", "host": "192.168.0.90", "port": 1883}, + "deviceTopicPrefix": f"axis/{MAC}", }, }, } diff --git a/tests/components/axis/test_hub.py b/tests/components/axis/test_hub.py index 3291f88d90a..1ae6db05427 100644 --- a/tests/components/axis/test_hub.py +++ b/tests/components/axis/test_hub.py @@ -91,9 +91,9 @@ async def test_device_support_mqtt( hass: HomeAssistant, mqtt_mock: MqttMockHAClient, setup_config_entry ) -> None: """Successful setup.""" - mqtt_mock.async_subscribe.assert_called_with(f"{MAC}/#", mock.ANY, 0, "utf-8") + mqtt_mock.async_subscribe.assert_called_with(f"axis/{MAC}/#", mock.ANY, 0, "utf-8") - topic = f"{MAC}/event/tns:onvif/Device/tns:axis/Sensor/PIR/$source/sensor/0" + topic = f"axis/{MAC}/event/tns:onvif/Device/tns:axis/Sensor/PIR/$source/sensor/0" message = ( b'{"timestamp": 1590258472044, "topic": "onvif:Device/axis:Sensor/PIR",' b' "message": {"source": {"sensor": "0"}, "key": {}, "data": {"state": "1"}}}' From 71588b5c22a9f3bd2191cb680fb57b4a0fa61c3c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 1 Apr 2024 15:53:14 +0200 Subject: [PATCH 054/198] Fix wrong icons (#114567) * Fix wrong icons * Fix wrong icons --- homeassistant/components/ffmpeg/icons.json | 2 +- homeassistant/components/input_select/icons.json | 2 +- homeassistant/components/media_player/icons.json | 2 +- homeassistant/components/synology_dsm/icons.json | 2 +- homeassistant/components/timer/icons.json | 2 +- homeassistant/components/xiaomi_miio/icons.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/ffmpeg/icons.json b/homeassistant/components/ffmpeg/icons.json index 3017b7dc0da..a23f024599c 100644 --- a/homeassistant/components/ffmpeg/icons.json +++ b/homeassistant/components/ffmpeg/icons.json @@ -1,7 +1,7 @@ { "services": { "restart": "mdi:restart", - "start": "mdi:start", + "start": "mdi:play", "stop": "mdi:stop" } } diff --git a/homeassistant/components/input_select/icons.json b/homeassistant/components/input_select/icons.json index 894b6be60dd..03b477ddb36 100644 --- a/homeassistant/components/input_select/icons.json +++ b/homeassistant/components/input_select/icons.json @@ -1,6 +1,6 @@ { "services": { - "select_next": "mdi:skip", + "select_next": "mdi:skip-next", "select_option": "mdi:check", "select_previous": "mdi:skip-previous", "select_first": "mdi:skip-backward", diff --git a/homeassistant/components/media_player/icons.json b/homeassistant/components/media_player/icons.json index e2769085833..847ce5989d6 100644 --- a/homeassistant/components/media_player/icons.json +++ b/homeassistant/components/media_player/icons.json @@ -52,7 +52,7 @@ "unjoin": "mdi:ungroup", "volume_down": "mdi:volume-minus", "volume_mute": "mdi:volume-mute", - "volume_set": "mdi:volume", + "volume_set": "mdi:volume-medium", "volume_up": "mdi:volume-plus" } } diff --git a/homeassistant/components/synology_dsm/icons.json b/homeassistant/components/synology_dsm/icons.json index bbdbc9d2c96..8b4fad457d5 100644 --- a/homeassistant/components/synology_dsm/icons.json +++ b/homeassistant/components/synology_dsm/icons.json @@ -75,7 +75,7 @@ } }, "services": { - "reboot": "mdi:reboot", + "reboot": "mdi:restart", "shutdown": "mdi:power" } } diff --git a/homeassistant/components/timer/icons.json b/homeassistant/components/timer/icons.json index 4cad5c119bd..1e352f7280b 100644 --- a/homeassistant/components/timer/icons.json +++ b/homeassistant/components/timer/icons.json @@ -1,6 +1,6 @@ { "services": { - "start": "mdi:start", + "start": "mdi:play", "pause": "mdi:pause", "cancel": "mdi:cancel", "finish": "mdi:check", diff --git a/homeassistant/components/xiaomi_miio/icons.json b/homeassistant/components/xiaomi_miio/icons.json index a9daaf9a61c..bbd3f6607d7 100644 --- a/homeassistant/components/xiaomi_miio/icons.json +++ b/homeassistant/components/xiaomi_miio/icons.json @@ -17,7 +17,7 @@ "switch_set_wifi_led_off": "mdi:wifi-off", "switch_set_power_price": "mdi:currency-usd", "switch_set_power_mode": "mdi:power", - "vacuum_remote_control_start": "mdi:start", + "vacuum_remote_control_start": "mdi:play", "vacuum_remote_control_stop": "mdi:stop", "vacuum_remote_control_move": "mdi:remote", "vacuum_remote_control_move_step": "mdi:remote", From fc24b61859cc6c1caa97e630364dfd1ff2c55d88 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Mon, 1 Apr 2024 14:49:14 +0200 Subject: [PATCH 055/198] Bump velbusaio to 2024.4.0 (#114569) Bump valbusaio to 2024.4.0 --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index c5f9ccd3563..1c51c58d238 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -13,7 +13,7 @@ "velbus-packet", "velbus-protocol" ], - "requirements": ["velbus-aio==2023.12.0"], + "requirements": ["velbus-aio==2024.4.0"], "usb": [ { "vid": "10CF", diff --git a/requirements_all.txt b/requirements_all.txt index 1ef594ea50b..3222dc2460d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2795,7 +2795,7 @@ vallox-websocket-api==5.1.1 vehicle==2.2.1 # homeassistant.components.velbus -velbus-aio==2023.12.0 +velbus-aio==2024.4.0 # homeassistant.components.venstar venstarcolortouch==0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fc19ead8e95..27e6f21027e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2151,7 +2151,7 @@ vallox-websocket-api==5.1.1 vehicle==2.2.1 # homeassistant.components.velbus -velbus-aio==2023.12.0 +velbus-aio==2024.4.0 # homeassistant.components.venstar venstarcolortouch==0.19 From 25c611ffc4e397cf5259eb81e6240d9d0f6c831b Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Mon, 1 Apr 2024 21:59:06 +0200 Subject: [PATCH 056/198] Reduce usage of executer threads in AVM Fritz!Tools (#114570) * call entity state update calls in one executer task * remove not needed wrapping * mark as "non-public" method * add guard against changes on _entity_update_functions --- homeassistant/components/fritz/common.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/fritz/common.py b/homeassistant/components/fritz/common.py index 8e773e74c75..5815f9abfc1 100644 --- a/homeassistant/components/fritz/common.py +++ b/homeassistant/components/fritz/common.py @@ -311,6 +311,17 @@ class FritzBoxTools( ) return unregister_entity_updates + def _entity_states_update(self) -> dict: + """Run registered entity update calls.""" + entity_states = {} + for key in list(self._entity_update_functions): + if (update_fn := self._entity_update_functions.get(key)) is not None: + _LOGGER.debug("update entity %s", key) + entity_states[key] = update_fn( + self.fritz_status, self.data["entity_states"].get(key) + ) + return entity_states + async def _async_update_data(self) -> UpdateCoordinatorDataType: """Update FritzboxTools data.""" entity_data: UpdateCoordinatorDataType = { @@ -319,15 +330,9 @@ class FritzBoxTools( } try: await self.async_scan_devices() - for key in list(self._entity_update_functions): - _LOGGER.debug("update entity %s", key) - entity_data["entity_states"][ - key - ] = await self.hass.async_add_executor_job( - self._entity_update_functions[key], - self.fritz_status, - self.data["entity_states"].get(key), - ) + entity_data["entity_states"] = await self.hass.async_add_executor_job( + self._entity_states_update + ) if self.has_call_deflections: entity_data[ "call_deflections" From bd6890ab83392f845b8b30ba2d26d3778b3c1f17 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Tue, 2 Apr 2024 03:48:26 +0200 Subject: [PATCH 057/198] Filter out ignored entries in ssdp step of AVM Fritz!SmartHome (#114574) filter out ignored entries in ssdp step --- homeassistant/components/fritzbox/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index 377d46eceff..e32f27969a1 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -141,7 +141,7 @@ class FritzboxConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_abort(reason="already_in_progress") # update old and user-configured config entries - for entry in self._async_current_entries(): + for entry in self._async_current_entries(include_ignore=False): if entry.data[CONF_HOST] == host: if uuid and not entry.unique_id: self.hass.config_entries.async_update_entry(entry, unique_id=uuid) From bb33725e7f966f63eb6595d825d2e028f55b8712 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 1 Apr 2024 13:28:39 -0400 Subject: [PATCH 058/198] Bump plexapi to 4.15.11 (#114581) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index e33cbc2e0c1..85362371715 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["plexapi", "plexwebsocket"], "requirements": [ - "PlexAPI==4.15.10", + "PlexAPI==4.15.11", "plexauth==0.0.6", "plexwebsocket==0.0.14" ], diff --git a/requirements_all.txt b/requirements_all.txt index 3222dc2460d..ced8c6dfec5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -45,7 +45,7 @@ Mastodon.py==1.8.1 Pillow==10.2.0 # homeassistant.components.plex -PlexAPI==4.15.10 +PlexAPI==4.15.11 # homeassistant.components.progettihwsw ProgettiHWSW==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 27e6f21027e..b94616bd07b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -39,7 +39,7 @@ HATasmota==0.8.0 Pillow==10.2.0 # homeassistant.components.plex -PlexAPI==4.15.10 +PlexAPI==4.15.11 # homeassistant.components.progettihwsw ProgettiHWSW==0.1.3 From ea13f102e01f35a662b0a03f207cf8d0d6f0f659 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Tue, 2 Apr 2024 00:14:13 +0200 Subject: [PATCH 059/198] Fix reolink media source data access (#114593) * Add test * Fix reolink media source data access --- homeassistant/components/reolink/media_source.py | 16 ++++++++++------ tests/components/reolink/test_media_source.py | 11 +++++++++++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/reolink/media_source.py b/homeassistant/components/reolink/media_source.py index 84c844a0f92..c22a0fc28e7 100644 --- a/homeassistant/components/reolink/media_source.py +++ b/homeassistant/components/reolink/media_source.py @@ -46,7 +46,6 @@ class ReolinkVODMediaSource(MediaSource): """Initialize ReolinkVODMediaSource.""" super().__init__(DOMAIN) self.hass = hass - self.data: dict[str, ReolinkData] = hass.data[DOMAIN] async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia: """Resolve media to a url.""" @@ -57,7 +56,8 @@ class ReolinkVODMediaSource(MediaSource): _, config_entry_id, channel_str, stream_res, filename = identifier channel = int(channel_str) - host = self.data[config_entry_id].host + data: dict[str, ReolinkData] = self.hass.data[DOMAIN] + host = data[config_entry_id].host vod_type = VodRequestType.RTMP if host.api.is_nvr: @@ -130,7 +130,8 @@ class ReolinkVODMediaSource(MediaSource): if config_entry.state != ConfigEntryState.LOADED: continue channels: list[str] = [] - host = self.data[config_entry.entry_id].host + data: dict[str, ReolinkData] = self.hass.data[DOMAIN] + host = data[config_entry.entry_id].host entities = er.async_entries_for_config_entry( entity_reg, config_entry.entry_id ) @@ -187,7 +188,8 @@ class ReolinkVODMediaSource(MediaSource): self, config_entry_id: str, channel: int ) -> BrowseMediaSource: """Allow the user to select the high or low playback resolution, (low loads faster).""" - host = self.data[config_entry_id].host + data: dict[str, ReolinkData] = self.hass.data[DOMAIN] + host = data[config_entry_id].host main_enc = await host.api.get_encoding(channel, "main") if main_enc == "h265": @@ -236,7 +238,8 @@ class ReolinkVODMediaSource(MediaSource): self, config_entry_id: str, channel: int, stream: str ) -> BrowseMediaSource: """Return all days on which recordings are available for a reolink camera.""" - host = self.data[config_entry_id].host + data: dict[str, ReolinkData] = self.hass.data[DOMAIN] + host = data[config_entry_id].host # We want today of the camera, not necessarily today of the server now = host.api.time() or await host.api.async_get_time() @@ -288,7 +291,8 @@ class ReolinkVODMediaSource(MediaSource): day: int, ) -> BrowseMediaSource: """Return all recording files on a specific day of a Reolink camera.""" - host = self.data[config_entry_id].host + data: dict[str, ReolinkData] = self.hass.data[DOMAIN] + host = data[config_entry_id].host start = dt.datetime(year, month, day, hour=0, minute=0, second=0) end = dt.datetime(year, month, day, hour=23, minute=59, second=59) diff --git a/tests/components/reolink/test_media_source.py b/tests/components/reolink/test_media_source.py index 9c5aebed222..1eb45945eee 100644 --- a/tests/components/reolink/test_media_source.py +++ b/tests/components/reolink/test_media_source.py @@ -65,6 +65,17 @@ async def setup_component(hass: HomeAssistant) -> None: assert await async_setup_component(hass, MEDIA_STREAM_DOMAIN, {}) +async def test_platform_loads_before_config_entry( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, +) -> None: + """Test that the platform can be loaded before the config entry.""" + # Fake that the config entry is not loaded before the media_source platform + assert await async_setup_component(hass, DOMAIN, {}) + await hass.async_block_till_done() + assert mock_setup_entry.call_count == 0 + + async def test_resolve( hass: HomeAssistant, reolink_connect: MagicMock, From 112aab47fb4b0657fee8c388631f4652aa67a864 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Apr 2024 11:18:26 -1000 Subject: [PATCH 060/198] Bump zeroconf to 0.132.0 (#114596) changelog: https://github.com/python-zeroconf/python-zeroconf/compare/0.131.0...0.132.0 --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index aecc88968f3..7c489517dd7 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.131.0"] + "requirements": ["zeroconf==0.132.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bdfaa8fcf45..9621137d855 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -60,7 +60,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtc-noise-gain==1.2.3 yarl==1.9.4 -zeroconf==0.131.0 +zeroconf==0.132.0 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index ced8c6dfec5..77c6247e856 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2925,7 +2925,7 @@ zamg==0.3.6 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.131.0 +zeroconf==0.132.0 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b94616bd07b..648fd38f1cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2260,7 +2260,7 @@ yt-dlp==2024.03.10 zamg==0.3.6 # homeassistant.components.zeroconf -zeroconf==0.131.0 +zeroconf==0.132.0 # homeassistant.components.zeversolar zeversolar==0.3.1 From 43631d5944ea13125eab8ec69710307adae7d2de Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Apr 2024 15:37:30 -1000 Subject: [PATCH 061/198] Add missing platforms_exist guard to check_config (#114600) * Add missing platforms_exist guard to check_config related issue #112811 When the exception hits, the config will end up being saved in the traceback so the memory is never released. This matches the check_config code to homeassistant.config to avoid having the exception thrown. * patch * merge branch --- homeassistant/helpers/check_config.py | 19 ++++++++++--------- tests/helpers/test_check_config.py | 2 ++ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index 8537f442595..78dddb12381 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -198,15 +198,16 @@ async def async_check_ha_config_file( # noqa: C901 # Check if the integration has a custom config validator config_validator = None - try: - config_validator = await integration.async_get_platform("config") - except ImportError as err: - # Filter out import error of the config platform. - # If the config platform contains bad imports, make sure - # that still fails. - if err.name != f"{integration.pkg_path}.config": - result.add_error(f"Error importing config platform {domain}: {err}") - continue + if integration.platforms_exists(("config",)): + try: + config_validator = await integration.async_get_platform("config") + except ImportError as err: + # Filter out import error of the config platform. + # If the config platform contains bad imports, make sure + # that still fails. + if err.name != f"{integration.pkg_path}.config": + result.add_error(f"Error importing config platform {domain}: {err}") + continue if config_validator is not None and hasattr( config_validator, "async_validate_config" diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index fd94c453e51..de7edf42dc2 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -350,6 +350,7 @@ async def test_config_platform_import_error(hass: HomeAssistant) -> None: side_effect=ImportError("blablabla"), ), patch("os.path.isfile", return_value=True), + patch("homeassistant.loader.Integration.platforms_exists", return_value=True), patch_yaml_files(files), ): res = await async_check_ha_config_file(hass) @@ -373,6 +374,7 @@ async def test_platform_import_error(hass: HomeAssistant) -> None: "homeassistant.loader.Integration.async_get_platform", side_effect=[None, ImportError("blablabla")], ), + patch("homeassistant.loader.Integration.platforms_exists", return_value=True), patch("os.path.isfile", return_value=True), patch_yaml_files(files), ): From 623d85ecaac3e5a8955c9719d101d5c92931a164 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Apr 2024 15:36:48 -1000 Subject: [PATCH 062/198] Fix memory leak when importing a platform fails (#114602) * Fix memory leak when importing a platform fails re-raising ImportError would trigger a memory leak * fixes, coverage * Apply suggestions from code review --- homeassistant/loader.py | 31 ++++++------ tests/test_loader.py | 107 +++++++++++++++++++++++++++++++--------- 2 files changed, 98 insertions(+), 40 deletions(-) diff --git a/homeassistant/loader.py b/homeassistant/loader.py index f462ea16886..48fd3cd54c2 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -750,9 +750,7 @@ class Integration: self._import_futures: dict[str, asyncio.Future[ModuleType]] = {} cache: dict[str, ModuleType | ComponentProtocol] = hass.data[DATA_COMPONENTS] self._cache = cache - missing_platforms_cache: dict[str, ImportError] = hass.data[ - DATA_MISSING_PLATFORMS - ] + missing_platforms_cache: dict[str, bool] = hass.data[DATA_MISSING_PLATFORMS] self._missing_platforms_cache = missing_platforms_cache self._top_level_files = top_level_files or set() _LOGGER.info("Loaded %s from %s", self.domain, pkg_path) @@ -1085,8 +1083,7 @@ class Integration: import_futures: list[tuple[str, asyncio.Future[ModuleType]]] = [] for platform_name in platform_names: - full_name = f"{domain}.{platform_name}" - if platform := self._get_platform_cached_or_raise(full_name): + if platform := self._get_platform_cached_or_raise(platform_name): platforms[platform_name] = platform continue @@ -1095,6 +1092,7 @@ class Integration: in_progress_imports[platform_name] = future continue + full_name = f"{domain}.{platform_name}" if ( self.import_executor and full_name not in self.hass.config.components @@ -1166,14 +1164,18 @@ class Integration: return platforms - def _get_platform_cached_or_raise(self, full_name: str) -> ModuleType | None: + def _get_platform_cached_or_raise(self, platform_name: str) -> ModuleType | None: """Return a platform for an integration from cache.""" + full_name = f"{self.domain}.{platform_name}" if full_name in self._cache: # the cache is either a ModuleType or a ComponentProtocol # but we only care about the ModuleType here return self._cache[full_name] # type: ignore[return-value] if full_name in self._missing_platforms_cache: - raise self._missing_platforms_cache[full_name] + raise ModuleNotFoundError( + f"Platform {full_name} not found", + name=f"{self.pkg_path}.{platform_name}", + ) return None def platforms_are_loaded(self, platform_names: Iterable[str]) -> bool: @@ -1189,9 +1191,7 @@ class Integration: def get_platform(self, platform_name: str) -> ModuleType: """Return a platform for an integration.""" - if platform := self._get_platform_cached_or_raise( - f"{self.domain}.{platform_name}" - ): + if platform := self._get_platform_cached_or_raise(platform_name): return platform return self._load_platform(platform_name) @@ -1212,10 +1212,7 @@ class Integration: ): existing_platforms.append(platform_name) continue - missing_platforms[full_name] = ModuleNotFoundError( - f"Platform {full_name} not found", - name=f"{self.pkg_path}.{platform_name}", - ) + missing_platforms[full_name] = True return existing_platforms @@ -1233,11 +1230,13 @@ class Integration: cache: dict[str, ModuleType] = self.hass.data[DATA_COMPONENTS] try: cache[full_name] = self._import_platform(platform_name) - except ImportError as ex: + except ModuleNotFoundError: if self.domain in cache: # If the domain is loaded, cache that the platform # does not exist so we do not try to load it again - self._missing_platforms_cache[full_name] = ex + self._missing_platforms_cache[full_name] = True + raise + except ImportError: raise except RuntimeError as err: # _DeadlockError inherits from RuntimeError diff --git a/tests/test_loader.py b/tests/test_loader.py index 9e191ee9e00..6685bb4f2ac 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -274,7 +274,61 @@ async def test_get_integration_exceptions(hass: HomeAssistant) -> None: async def test_get_platform_caches_failures_when_component_loaded( hass: HomeAssistant, ) -> None: - """Test get_platform cache failures only when the component is loaded.""" + """Test get_platform caches failures only when the component is loaded. + + Only ModuleNotFoundError is cached, ImportError is not cached. + """ + integration = await loader.async_get_integration(hass, "hue") + + with ( + pytest.raises(ModuleNotFoundError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ModuleNotFoundError("Boom"), + ), + ): + assert integration.get_component() == hue + + with ( + pytest.raises(ModuleNotFoundError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ModuleNotFoundError("Boom"), + ), + ): + assert integration.get_platform("light") == hue_light + + # Hue is not loaded so we should still hit the import_module path + with ( + pytest.raises(ModuleNotFoundError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ModuleNotFoundError("Boom"), + ), + ): + assert integration.get_platform("light") == hue_light + + assert integration.get_component() == hue + + # Hue is loaded so we should cache the import_module failure now + with ( + pytest.raises(ModuleNotFoundError), + patch( + "homeassistant.loader.importlib.import_module", + side_effect=ModuleNotFoundError("Boom"), + ), + ): + assert integration.get_platform("light") == hue_light + + # Hue is loaded and the last call should have cached the import_module failure + with pytest.raises(ModuleNotFoundError): + assert integration.get_platform("light") == hue_light + + +async def test_get_platform_only_cached_module_not_found_when_component_loaded( + hass: HomeAssistant, +) -> None: + """Test get_platform cache only cache module not found when the component is loaded.""" integration = await loader.async_get_integration(hass, "hue") with ( @@ -317,41 +371,43 @@ async def test_get_platform_caches_failures_when_component_loaded( ): assert integration.get_platform("light") == hue_light - # Hue is loaded and the last call should have cached the import_module failure - with pytest.raises(ImportError): - assert integration.get_platform("light") == hue_light + # ImportError is not cached because we only cache ModuleNotFoundError + assert integration.get_platform("light") == hue_light async def test_async_get_platform_caches_failures_when_component_loaded( hass: HomeAssistant, ) -> None: - """Test async_get_platform cache failures only when the component is loaded.""" + """Test async_get_platform caches failures only when the component is loaded. + + Only ModuleNotFoundError is cached, ImportError is not cached. + """ integration = await loader.async_get_integration(hass, "hue") with ( - pytest.raises(ImportError), + pytest.raises(ModuleNotFoundError), patch( "homeassistant.loader.importlib.import_module", - side_effect=ImportError("Boom"), + side_effect=ModuleNotFoundError("Boom"), ), ): assert integration.get_component() == hue with ( - pytest.raises(ImportError), + pytest.raises(ModuleNotFoundError), patch( "homeassistant.loader.importlib.import_module", - side_effect=ImportError("Boom"), + side_effect=ModuleNotFoundError("Boom"), ), ): assert await integration.async_get_platform("light") == hue_light # Hue is not loaded so we should still hit the import_module path with ( - pytest.raises(ImportError), + pytest.raises(ModuleNotFoundError), patch( "homeassistant.loader.importlib.import_module", - side_effect=ImportError("Boom"), + side_effect=ModuleNotFoundError("Boom"), ), ): assert await integration.async_get_platform("light") == hue_light @@ -360,16 +416,16 @@ async def test_async_get_platform_caches_failures_when_component_loaded( # Hue is loaded so we should cache the import_module failure now with ( - pytest.raises(ImportError), + pytest.raises(ModuleNotFoundError), patch( "homeassistant.loader.importlib.import_module", - side_effect=ImportError("Boom"), + side_effect=ModuleNotFoundError("Boom"), ), ): assert await integration.async_get_platform("light") == hue_light # Hue is loaded and the last call should have cached the import_module failure - with pytest.raises(ImportError): + with pytest.raises(ModuleNotFoundError): assert await integration.async_get_platform("light") == hue_light # The cache should never be filled because the import error is remembered @@ -379,33 +435,36 @@ async def test_async_get_platform_caches_failures_when_component_loaded( async def test_async_get_platforms_caches_failures_when_component_loaded( hass: HomeAssistant, ) -> None: - """Test async_get_platforms cache failures only when the component is loaded.""" + """Test async_get_platforms cache failures only when the component is loaded. + + Only ModuleNotFoundError is cached, ImportError is not cached. + """ integration = await loader.async_get_integration(hass, "hue") with ( - pytest.raises(ImportError), + pytest.raises(ModuleNotFoundError), patch( "homeassistant.loader.importlib.import_module", - side_effect=ImportError("Boom"), + side_effect=ModuleNotFoundError("Boom"), ), ): assert integration.get_component() == hue with ( - pytest.raises(ImportError), + pytest.raises(ModuleNotFoundError), patch( "homeassistant.loader.importlib.import_module", - side_effect=ImportError("Boom"), + side_effect=ModuleNotFoundError("Boom"), ), ): assert await integration.async_get_platforms(["light"]) == {"light": hue_light} # Hue is not loaded so we should still hit the import_module path with ( - pytest.raises(ImportError), + pytest.raises(ModuleNotFoundError), patch( "homeassistant.loader.importlib.import_module", - side_effect=ImportError("Boom"), + side_effect=ModuleNotFoundError("Boom"), ), ): assert await integration.async_get_platforms(["light"]) == {"light": hue_light} @@ -414,16 +473,16 @@ async def test_async_get_platforms_caches_failures_when_component_loaded( # Hue is loaded so we should cache the import_module failure now with ( - pytest.raises(ImportError), + pytest.raises(ModuleNotFoundError), patch( "homeassistant.loader.importlib.import_module", - side_effect=ImportError("Boom"), + side_effect=ModuleNotFoundError("Boom"), ), ): assert await integration.async_get_platforms(["light"]) == {"light": hue_light} # Hue is loaded and the last call should have cached the import_module failure - with pytest.raises(ImportError): + with pytest.raises(ModuleNotFoundError): assert await integration.async_get_platforms(["light"]) == {"light": hue_light} # The cache should never be filled because the import error is remembered From 52612b10fdd6cf32e7f06a04ce5dd1fbfc66512d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 1 Apr 2024 15:35:38 -1000 Subject: [PATCH 063/198] Avoid storing raw extracted traceback in system_log (#114603) This is never actually used and takes up quite a bit of ram --- homeassistant/components/system_log/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 77f0b095a30..423f5c6f5d8 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -166,7 +166,6 @@ class LogEntry: "level", "message", "exception", - "extracted_tb", "root_cause", "source", "count", @@ -200,7 +199,6 @@ class LogEntry: else: self.source = (record.pathname, record.lineno) self.count = 1 - self.extracted_tb = extracted_tb self.key = (self.name, self.source, self.root_cause) def to_dict(self) -> dict[str, Any]: From bc21836e7eeadcb593cb3b37107e60912cf16b7a Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 1 Apr 2024 21:47:30 -0400 Subject: [PATCH 064/198] Bump whirlpool-sixth-sense to 0.18.7 (#114606) Bump sixth-sense to 0.18.7 --- homeassistant/components/whirlpool/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/whirlpool/manifest.json b/homeassistant/components/whirlpool/manifest.json index 0c46580ceeb..ee7861588ed 100644 --- a/homeassistant/components/whirlpool/manifest.json +++ b/homeassistant/components/whirlpool/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["whirlpool"], - "requirements": ["whirlpool-sixth-sense==0.18.6"] + "requirements": ["whirlpool-sixth-sense==0.18.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 77c6247e856..8fb57f0834a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2847,7 +2847,7 @@ webmin-xmlrpc==0.0.2 webrtc-noise-gain==1.2.3 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.18.6 +whirlpool-sixth-sense==0.18.7 # homeassistant.components.whois whois==0.9.27 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 648fd38f1cb..6eb3c89e09b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2194,7 +2194,7 @@ webmin-xmlrpc==0.0.2 webrtc-noise-gain==1.2.3 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.18.6 +whirlpool-sixth-sense==0.18.7 # homeassistant.components.whois whois==0.9.27 From 7164993562d0f4bd008da234bc9954915a7af158 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 2 Apr 2024 01:51:42 +0000 Subject: [PATCH 065/198] Bump version to 2024.4.0b4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index f56ce656157..a69c4c84e27 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -18,7 +18,7 @@ from .util.signal_type import SignalType APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 4 -PATCH_VERSION: Final = "0b3" +PATCH_VERSION: Final = "0b4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 73bfdd6d5d7..11007e624dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.4.0b3" +version = "2024.4.0b4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From a6076a0d33326f467f59c017a8f4d9d0d452ff7e Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Tue, 2 Apr 2024 04:11:45 -0400 Subject: [PATCH 066/198] Display sonos album title with URL encoding (#113693) * unescape the title When extracting the title from the item_id, it needs to be unescaped. * sort imports --- .../components/sonos/media_browser.py | 2 +- tests/components/sonos/test_media_browser.py | 96 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 tests/components/sonos/test_media_browser.py diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index 17327bf4be1..9d3ef5d353b 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -201,7 +201,7 @@ def build_item_response( if not title: try: - title = payload["idstring"].split("/")[1] + title = urllib.parse.unquote(payload["idstring"].split("/")[1]) except IndexError: title = LIBRARY_TITLES_MAPPING[payload["idstring"]] diff --git a/tests/components/sonos/test_media_browser.py b/tests/components/sonos/test_media_browser.py new file mode 100644 index 00000000000..cb6303c800d --- /dev/null +++ b/tests/components/sonos/test_media_browser.py @@ -0,0 +1,96 @@ +"""Tests for the Sonos Media Browser.""" + +from functools import partial + +from homeassistant.components.media_player.browse_media import BrowseMedia +from homeassistant.components.media_player.const import MediaClass, MediaType +from homeassistant.components.sonos.media_browser import ( + build_item_response, + get_thumbnail_url_full, +) +from homeassistant.core import HomeAssistant + +from .conftest import SoCoMockFactory + + +class MockMusicServiceItem: + """Mocks a Soco MusicServiceItem.""" + + def __init__( + self, + title: str, + item_id: str, + parent_id: str, + item_class: str, + ) -> None: + """Initialize the mock item.""" + self.title = title + self.item_id = item_id + self.item_class = item_class + self.parent_id = parent_id + + def get_uri(self) -> str: + """Return URI.""" + return self.item_id.replace("S://", "x-file-cifs://") + + +def mock_browse_by_idstring( + search_type: str, idstring: str, start=0, max_items=100, full_album_art_uri=False +) -> list[MockMusicServiceItem]: + """Mock the call to browse_by_id_string.""" + if search_type == "albums" and ( + idstring == "A:ALBUM/Abbey%20Road" or idstring == "A:ALBUM/Abbey Road" + ): + return [ + MockMusicServiceItem( + "Come Together", + "S://192.168.42.10/music/The%20Beatles/Abbey%20Road/01%20Come%20Together.mp3", + "A:ALBUM/Abbey%20Road", + "object.item.audioItem.musicTrack", + ), + MockMusicServiceItem( + "Something", + "S://192.168.42.10/music/The%20Beatles/Abbey%20Road/03%20Something.mp3", + "A:ALBUM/Abbey%20Road", + "object.item.audioItem.musicTrack", + ), + ] + return None + + +async def test_build_item_response( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + soco, + discover, +) -> None: + """Test building a browse item response.""" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + soco_mock.music_library.browse_by_idstring = mock_browse_by_idstring + browse_item: BrowseMedia = build_item_response( + soco_mock.music_library, + {"search_type": MediaType.ALBUM, "idstring": "A:ALBUM/Abbey%20Road"}, + partial( + get_thumbnail_url_full, + soco_mock.music_library, + True, + None, + ), + ) + assert browse_item.title == "Abbey Road" + assert browse_item.media_class == MediaClass.ALBUM + assert browse_item.media_content_id == "A:ALBUM/Abbey%20Road" + assert len(browse_item.children) == 2 + assert browse_item.children[0].media_class == MediaClass.TRACK + assert browse_item.children[0].title == "Come Together" + assert ( + browse_item.children[0].media_content_id + == "x-file-cifs://192.168.42.10/music/The%20Beatles/Abbey%20Road/01%20Come%20Together.mp3" + ) + assert browse_item.children[1].media_class == MediaClass.TRACK + assert browse_item.children[1].title == "Something" + assert ( + browse_item.children[1].media_content_id + == "x-file-cifs://192.168.42.10/music/The%20Beatles/Abbey%20Road/03%20Something.mp3" + ) From 18b6de567d4f481c65368452c49e483117eec721 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 2 Apr 2024 11:15:52 +0200 Subject: [PATCH 067/198] Bump roombapy to 1.8.1 (#114478) * Bump roombapy to 1.7.0 * Bump * Bump * Fix --- homeassistant/components/roomba/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/roomba/test_config_flow.py | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index ae08d8f6a1f..a697680b379 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -24,7 +24,7 @@ "documentation": "https://www.home-assistant.io/integrations/roomba", "iot_class": "local_push", "loggers": ["paho_mqtt", "roombapy"], - "requirements": ["roombapy==1.6.13"], + "requirements": ["roombapy==1.8.1"], "zeroconf": [ { "type": "_amzn-alexa._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index 8fb57f0834a..a98a76af9b0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2462,7 +2462,7 @@ rokuecp==0.19.2 romy==0.0.7 # homeassistant.components.roomba -roombapy==1.6.13 +roombapy==1.8.1 # homeassistant.components.roon roonapi==0.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6eb3c89e09b..3937eae1e53 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1896,7 +1896,7 @@ rokuecp==0.19.2 romy==0.0.7 # homeassistant.components.roomba -roombapy==1.6.13 +roombapy==1.8.1 # homeassistant.components.roon roonapi==0.1.6 diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index 2eaf3b14e38..282884c0be3 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -99,12 +99,12 @@ def _mocked_discovery(*_): roomba = RoombaInfo( hostname="irobot-BLID", - robotname="robot_name", + robot_name="robot_name", ip=MOCK_IP, mac="mac", - sw="firmware", + firmware="firmware", sku="sku", - cap={"cap": 1}, + capabilities={"cap": 1}, ) roomba_discovery.get_all = MagicMock(return_value=[roomba]) From 7b84e86f898efa14dca4e74323d3aecff1f0e703 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 2 Apr 2024 10:15:58 +0200 Subject: [PATCH 068/198] Improve Shelly RPC device update progress (#114566) Co-authored-by: Shay Levy Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/shelly/update.py | 16 ++++++++++------ tests/components/shelly/test_update.py | 19 +++++++++++-------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/shelly/update.py b/homeassistant/components/shelly/update.py index f6a89c5381b..56ad1f2ef67 100644 --- a/homeassistant/components/shelly/update.py +++ b/homeassistant/components/shelly/update.py @@ -222,7 +222,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): ) -> None: """Initialize update entity.""" super().__init__(coordinator, key, attribute, description) - self._ota_in_progress: bool = False + self._ota_in_progress: bool | int = False self._attr_release_url = get_release_url( coordinator.device.gen, coordinator.model, description.beta ) @@ -237,14 +237,13 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): @callback def _ota_progress_callback(self, event: dict[str, Any]) -> None: """Handle device OTA progress.""" - if self._ota_in_progress: + if self.in_progress is not False: event_type = event["event"] if event_type == OTA_BEGIN: - self._attr_in_progress = 0 + self._ota_in_progress = 0 elif event_type == OTA_PROGRESS: - self._attr_in_progress = event["progress_percent"] + self._ota_in_progress = event["progress_percent"] elif event_type in (OTA_ERROR, OTA_SUCCESS): - self._attr_in_progress = False self._ota_in_progress = False self.async_write_ha_state() @@ -262,6 +261,11 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): return self.installed_version + @property + def in_progress(self) -> bool | int: + """Update installation in progress.""" + return self._ota_in_progress + async def async_install( self, version: str | None, backup: bool, **kwargs: Any ) -> None: @@ -292,7 +296,7 @@ class RpcUpdateEntity(ShellyRpcAttributeEntity, UpdateEntity): await self.coordinator.async_shutdown_device_and_start_reauth() else: self._ota_in_progress = True - LOGGER.debug("OTA update call successful") + LOGGER.info("OTA update call for %s successful", self.coordinator.name) class RpcSleepingUpdateEntity( diff --git a/tests/components/shelly/test_update.py b/tests/components/shelly/test_update.py index 387dc93e33e..f3960620a21 100644 --- a/tests/components/shelly/test_update.py +++ b/tests/components/shelly/test_update.py @@ -255,6 +255,16 @@ async def test_rpc_update( {ATTR_ENTITY_ID: entity_id}, blocking=True, ) + + assert mock_rpc_device.trigger_ota_update.call_count == 1 + + state = hass.states.get(entity_id) + assert state.state == STATE_ON + assert state.attributes[ATTR_INSTALLED_VERSION] == "1" + assert state.attributes[ATTR_LATEST_VERSION] == "2" + assert state.attributes[ATTR_IN_PROGRESS] is True + assert state.attributes[ATTR_RELEASE_URL] == GEN2_RELEASE_URL + inject_rpc_device_event( monkeypatch, mock_rpc_device, @@ -270,14 +280,7 @@ async def test_rpc_update( }, ) - assert mock_rpc_device.trigger_ota_update.call_count == 1 - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - assert state.attributes[ATTR_INSTALLED_VERSION] == "1" - assert state.attributes[ATTR_LATEST_VERSION] == "2" - assert state.attributes[ATTR_IN_PROGRESS] == 0 - assert state.attributes[ATTR_RELEASE_URL] == GEN2_RELEASE_URL + assert hass.states.get(entity_id).attributes[ATTR_IN_PROGRESS] == 0 inject_rpc_device_event( monkeypatch, From e5a620545c49a9181627f606a0a431dc4df9dd46 Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Tue, 2 Apr 2024 20:23:08 +1000 Subject: [PATCH 069/198] Fix battery heater in Tessie (#114568) --- homeassistant/components/tessie/binary_sensor.py | 2 +- homeassistant/components/tessie/strings.json | 2 +- tests/components/tessie/snapshots/test_binary_sensors.ambr | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/tessie/binary_sensor.py b/homeassistant/components/tessie/binary_sensor.py index 015fa63736f..9b7d6861dfb 100644 --- a/homeassistant/components/tessie/binary_sensor.py +++ b/homeassistant/components/tessie/binary_sensor.py @@ -34,7 +34,7 @@ DESCRIPTIONS: tuple[TessieBinarySensorEntityDescription, ...] = ( is_on=lambda x: x == TessieState.ONLINE, ), TessieBinarySensorEntityDescription( - key="charge_state_battery_heater_on", + key="climate_state_battery_heater", device_class=BinarySensorDeviceClass.HEAT, entity_category=EntityCategory.DIAGNOSTIC, ), diff --git a/homeassistant/components/tessie/strings.json b/homeassistant/components/tessie/strings.json index 62de4f276f4..8e1e47f934f 100644 --- a/homeassistant/components/tessie/strings.json +++ b/homeassistant/components/tessie/strings.json @@ -252,7 +252,7 @@ "state": { "name": "Status" }, - "charge_state_battery_heater_on": { + "climate_state_battery_heater": { "name": "Battery heater" }, "charge_state_charge_enable_request": { diff --git a/tests/components/tessie/snapshots/test_binary_sensors.ambr b/tests/components/tessie/snapshots/test_binary_sensors.ambr index 854e1350234..7bc191de6ed 100644 --- a/tests/components/tessie/snapshots/test_binary_sensors.ambr +++ b/tests/components/tessie/snapshots/test_binary_sensors.ambr @@ -165,8 +165,8 @@ 'platform': 'tessie', 'previous_unique_id': None, 'supported_features': 0, - 'translation_key': 'charge_state_battery_heater_on', - 'unique_id': 'VINVINVIN-charge_state_battery_heater_on', + 'translation_key': 'climate_state_battery_heater', + 'unique_id': 'VINVINVIN-climate_state_battery_heater', 'unit_of_measurement': None, }) # --- From 384d10a51d298f7bddbd6c16dfc07bc3c4bef165 Mon Sep 17 00:00:00 2001 From: mkmer Date: Tue, 2 Apr 2024 03:41:40 -0400 Subject: [PATCH 070/198] Add diagnostic platform to Whirlpool (#114578) * Add diagnostic platform and tests * lowercase variable * Correc doc string --- .../components/whirlpool/diagnostics.py | 49 +++++++++++++++++++ .../whirlpool/snapshots/test_diagnostics.ambr | 44 +++++++++++++++++ .../components/whirlpool/test_diagnostics.py | 32 ++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 homeassistant/components/whirlpool/diagnostics.py create mode 100644 tests/components/whirlpool/snapshots/test_diagnostics.ambr create mode 100644 tests/components/whirlpool/test_diagnostics.py diff --git a/homeassistant/components/whirlpool/diagnostics.py b/homeassistant/components/whirlpool/diagnostics.py new file mode 100644 index 00000000000..9b1dd00e7bd --- /dev/null +++ b/homeassistant/components/whirlpool/diagnostics.py @@ -0,0 +1,49 @@ +"""Diagnostics support for Whirlpool.""" + +from __future__ import annotations + +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import WhirlpoolData +from .const import DOMAIN + +TO_REDACT = { + "SERIAL_NUMBER", + "macaddress", + "username", + "password", + "token", + "unique_id", + "SAID", +} + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, + config_entry: ConfigEntry, +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + + whirlpool: WhirlpoolData = hass.data[DOMAIN][config_entry.entry_id] + diagnostics_data = { + "Washer_dryers": { + wd["NAME"]: dict(wd.items()) + for wd in whirlpool.appliances_manager.washer_dryers + }, + "aircons": { + ac["NAME"]: dict(ac.items()) for ac in whirlpool.appliances_manager.aircons + }, + "ovens": { + oven["NAME"]: dict(oven.items()) + for oven in whirlpool.appliances_manager.ovens + }, + } + + return { + "config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT), + "appliances": async_redact_data(diagnostics_data, TO_REDACT), + } diff --git a/tests/components/whirlpool/snapshots/test_diagnostics.ambr b/tests/components/whirlpool/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..5a0beb112e6 --- /dev/null +++ b/tests/components/whirlpool/snapshots/test_diagnostics.ambr @@ -0,0 +1,44 @@ +# serializer version: 1 +# name: test_entry_diagnostics + dict({ + 'appliances': dict({ + 'Washer_dryers': dict({ + 'dryer': dict({ + 'NAME': 'dryer', + 'SAID': '**REDACTED**', + }), + 'washer': dict({ + 'NAME': 'washer', + 'SAID': '**REDACTED**', + }), + }), + 'aircons': dict({ + 'TestZone': dict({ + 'NAME': 'TestZone', + 'SAID': '**REDACTED**', + }), + }), + 'ovens': dict({ + }), + }), + 'config_entry': dict({ + 'data': dict({ + 'brand': 'Whirlpool', + 'password': '**REDACTED**', + 'region': 'EU', + 'username': '**REDACTED**', + }), + 'disabled_by': None, + 'domain': 'whirlpool', + 'minor_version': 1, + 'options': dict({ + }), + 'pref_disable_new_entities': False, + 'pref_disable_polling': False, + 'source': 'user', + 'title': 'Mock Title', + 'unique_id': None, + 'version': 1, + }), + }) +# --- diff --git a/tests/components/whirlpool/test_diagnostics.py b/tests/components/whirlpool/test_diagnostics.py new file mode 100644 index 00000000000..6cfc1b76e38 --- /dev/null +++ b/tests/components/whirlpool/test_diagnostics.py @@ -0,0 +1,32 @@ +"""Test Blink diagnostics.""" + +from unittest.mock import MagicMock + +from syrupy import SnapshotAssertion +from syrupy.filters import props + +from homeassistant.core import HomeAssistant + +from . import init_integration + +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + +YAML_CONFIG = {"username": "test-user", "password": "test-password"} + + +async def test_entry_diagnostics( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + snapshot: SnapshotAssertion, + mock_appliances_manager_api: MagicMock, + mock_aircon1_api: MagicMock, + mock_aircon_api_instances: MagicMock, +) -> None: + """Test config entry diagnostics.""" + + mock_entry = await init_integration(hass) + + result = await get_diagnostics_for_config_entry(hass, hass_client, mock_entry) + + assert result == snapshot(exclude=props("entry_id")) From 559fe65471fa29339b005f610c1133fa50c3259a Mon Sep 17 00:00:00 2001 From: Jack Boswell Date: Tue, 2 Apr 2024 21:45:46 +1300 Subject: [PATCH 071/198] Catch potential ValueError when getting or setting Starlink sleep values (#114607) --- homeassistant/components/starlink/time.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/starlink/time.py b/homeassistant/components/starlink/time.py index 4d9e2d06675..6475610564d 100644 --- a/homeassistant/components/starlink/time.py +++ b/homeassistant/components/starlink/time.py @@ -10,6 +10,7 @@ import math from homeassistant.components.time import TimeEntity, TimeEntityDescription from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DOMAIN @@ -62,14 +63,22 @@ class StarlinkTimeEntity(StarlinkEntity, TimeEntity): def _utc_minutes_to_time(utc_minutes: int, timezone: tzinfo) -> time: hour = math.floor(utc_minutes / 60) minute = utc_minutes % 60 - utc = datetime.now(UTC).replace(hour=hour, minute=minute, second=0, microsecond=0) + try: + utc = datetime.now(UTC).replace( + hour=hour, minute=minute, second=0, microsecond=0 + ) + except ValueError as exc: + raise HomeAssistantError from exc return utc.astimezone(timezone).time() def _time_to_utc_minutes(t: time, timezone: tzinfo) -> int: - zoned_time = datetime.now(timezone).replace( - hour=t.hour, minute=t.minute, second=0, microsecond=0 - ) + try: + zoned_time = datetime.now(timezone).replace( + hour=t.hour, minute=t.minute, second=0, microsecond=0 + ) + except ValueError as exc: + raise HomeAssistantError from exc utc_time = zoned_time.astimezone(UTC).time() return (utc_time.hour * 60) + utc_time.minute From 230c29edbed885a39ba7fd38d098b40e84c74ded Mon Sep 17 00:00:00 2001 From: max2697 <143563471+max2697@users.noreply.github.com> Date: Tue, 2 Apr 2024 02:04:28 -0500 Subject: [PATCH 072/198] Bump opower to 0.4.2 (#114608) --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index bc6f8796d50..879aeb0327b 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.4.1"] + "requirements": ["opower==0.4.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index a98a76af9b0..a6f4d51d89b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1482,7 +1482,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.4.1 +opower==0.4.2 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3937eae1e53..fdb4c92e442 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1176,7 +1176,7 @@ openerz-api==0.3.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.4.1 +opower==0.4.2 # homeassistant.components.oralb oralb-ble==0.17.6 From 92dfec3c98359d1ecabc7d42871812c56eebb537 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Apr 2024 10:43:14 +0200 Subject: [PATCH 073/198] Add floor selector (#114614) --- homeassistant/helpers/selector.py | 42 +++++++++++++++++++ tests/helpers/test_selector.py | 67 +++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 938cc6a9246..c4db601fac6 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -844,6 +844,48 @@ class EntitySelector(Selector[EntitySelectorConfig]): return cast(list, vol.Schema([validate])(data)) # Output is a list +class FloorSelectorConfig(TypedDict, total=False): + """Class to represent an floor selector config.""" + + entity: EntityFilterSelectorConfig | list[EntityFilterSelectorConfig] + device: DeviceFilterSelectorConfig | list[DeviceFilterSelectorConfig] + multiple: bool + + +@SELECTORS.register("floor") +class FloorSelector(Selector[AreaSelectorConfig]): + """Selector of a single or list of floors.""" + + selector_type = "floor" + + CONFIG_SCHEMA = vol.Schema( + { + vol.Optional("entity"): vol.All( + cv.ensure_list, + [ENTITY_FILTER_SELECTOR_CONFIG_SCHEMA], + ), + vol.Optional("device"): vol.All( + cv.ensure_list, + [DEVICE_FILTER_SELECTOR_CONFIG_SCHEMA], + ), + vol.Optional("multiple", default=False): cv.boolean, + } + ) + + def __init__(self, config: FloorSelectorConfig | None = None) -> None: + """Instantiate a selector.""" + super().__init__(config) + + def __call__(self, data: Any) -> str | list[str]: + """Validate the passed selection.""" + if not self.config["multiple"]: + floor_id: str = vol.Schema(str)(data) + return floor_id + if not isinstance(data, list): + raise vol.Invalid("Value should be a list") + return [vol.Schema(str)(val) for val in data] + + class IconSelectorConfig(TypedDict, total=False): """Class to represent an icon selector config.""" diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 0dc7e570fc5..8864edc7386 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -1158,3 +1158,70 @@ def test_qr_code_selector_schema(schema, valid_selections, invalid_selections) - def test_label_selector_schema(schema, valid_selections, invalid_selections) -> None: """Test label selector.""" _test_selector("label", schema, valid_selections, invalid_selections) + + +@pytest.mark.parametrize( + ("schema", "valid_selections", "invalid_selections"), + [ + ({}, ("abc123",), (None,)), + ({"entity": {}}, ("abc123",), (None,)), + ({"entity": {"domain": "light"}}, ("abc123",), (None,)), + ( + {"entity": {"domain": "binary_sensor", "device_class": "motion"}}, + ("abc123",), + (None,), + ), + ( + { + "entity": { + "domain": "binary_sensor", + "device_class": "motion", + "integration": "demo", + } + }, + ("abc123",), + (None,), + ), + ( + { + "entity": [ + {"domain": "light"}, + {"domain": "binary_sensor", "device_class": "motion"}, + ] + }, + ("abc123",), + (None,), + ), + ( + {"device": {"integration": "demo", "model": "mock-model"}}, + ("abc123",), + (None,), + ), + ( + { + "device": [ + {"integration": "demo", "model": "mock-model"}, + {"integration": "other-demo", "model": "other-mock-model"}, + ] + }, + ("abc123",), + (None,), + ), + ( + { + "entity": {"domain": "binary_sensor", "device_class": "motion"}, + "device": {"integration": "demo", "model": "mock-model"}, + }, + ("abc123",), + (None,), + ), + ( + {"multiple": True}, + ((["abc123", "def456"],)), + (None, "abc123", ["abc123", None]), + ), + ], +) +def test_floor_selector_schema(schema, valid_selections, invalid_selections) -> None: + """Test floor selector.""" + _test_selector("floor", schema, valid_selections, invalid_selections) From ca31479d298490d9a19e55dace29648e3e92dba4 Mon Sep 17 00:00:00 2001 From: Fexiven <48439988+Fexiven@users.noreply.github.com> Date: Tue, 2 Apr 2024 12:04:07 +0200 Subject: [PATCH 074/198] Fix Starlink integration startup issue (#114615) --- homeassistant/components/starlink/coordinator.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/starlink/coordinator.py b/homeassistant/components/starlink/coordinator.py index 9c597fbb033..ff33b3ecc41 100644 --- a/homeassistant/components/starlink/coordinator.py +++ b/homeassistant/components/starlink/coordinator.py @@ -58,14 +58,14 @@ class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]): async def _async_update_data(self) -> StarlinkData: async with asyncio.timeout(4): try: - status, location, sleep = await asyncio.gather( - self.hass.async_add_executor_job(status_data, self.channel_context), - self.hass.async_add_executor_job( - location_data, self.channel_context - ), - self.hass.async_add_executor_job( - get_sleep_config, self.channel_context - ), + status = await self.hass.async_add_executor_job( + status_data, self.channel_context + ) + location = await self.hass.async_add_executor_job( + location_data, self.channel_context + ) + sleep = await self.hass.async_add_executor_job( + get_sleep_config, self.channel_context ) return StarlinkData(location, sleep, *status) except GrpcError as exc: From b539b25682754f9d4abb7ef4361184a7054c52ac Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 2 Apr 2024 12:17:52 +0200 Subject: [PATCH 075/198] Update frontend to 20240402.0 (#114627) --- 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 7864801a986..5eaa6e94769 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==20240329.1"] + "requirements": ["home-assistant-frontend==20240402.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9621137d855..eb6d347a479 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240329.1 +home-assistant-frontend==20240402.0 home-assistant-intents==2024.3.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index a6f4d51d89b..adfcc1322e3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1077,7 +1077,7 @@ hole==0.8.0 holidays==0.45 # homeassistant.components.frontend -home-assistant-frontend==20240329.1 +home-assistant-frontend==20240402.0 # homeassistant.components.conversation home-assistant-intents==2024.3.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fdb4c92e442..85fb1c70471 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,7 +876,7 @@ hole==0.8.0 holidays==0.45 # homeassistant.components.frontend -home-assistant-frontend==20240329.1 +home-assistant-frontend==20240402.0 # homeassistant.components.conversation home-assistant-intents==2024.3.29 From 5af5f3694ef2816e34c585f04ff9862f929b7278 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Apr 2024 12:28:20 +0200 Subject: [PATCH 076/198] Bump version to 2024.4.0b5 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index a69c4c84e27..0cac1ae45a3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -18,7 +18,7 @@ from .util.signal_type import SignalType APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 4 -PATCH_VERSION: Final = "0b4" +PATCH_VERSION: Final = "0b5" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 11007e624dc..62c9166cd22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.4.0b4" +version = "2024.4.0b5" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 4e0d6f287ed716edcf9a39267ce08dce4b4cd1da Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Tue, 2 Apr 2024 08:16:59 -0700 Subject: [PATCH 077/198] Reduce ZHA OTA logbook entries and extraneous updates (#114591) --- .../components/zha/core/cluster_handlers/general.py | 7 +++++++ homeassistant/components/zha/update.py | 5 ----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/zha/core/cluster_handlers/general.py b/homeassistant/components/zha/core/cluster_handlers/general.py index 478f41da3b7..438fc6b1723 100644 --- a/homeassistant/components/zha/core/cluster_handlers/general.py +++ b/homeassistant/components/zha/core/cluster_handlers/general.py @@ -553,6 +553,13 @@ class OtaClientClusterHandler(ClientClusterHandler): Ota.AttributeDefs.current_file_version.name: True, } + @callback + def attribute_updated(self, attrid: int, value: Any, timestamp: Any) -> None: + """Handle an attribute updated on this cluster.""" + # We intentionally avoid the `ClientClusterHandler` attribute update handler: + # it emits a logbook event on every update, which pollutes the logbook + ClusterHandler.attribute_updated(self, attrid, value, timestamp) + @property def current_file_version(self) -> int | None: """Return cached value of current_file_version attribute.""" diff --git a/homeassistant/components/zha/update.py b/homeassistant/components/zha/update.py index 7ceba4fc924..0cb80d13119 100644 --- a/homeassistant/components/zha/update.py +++ b/homeassistant/components/zha/update.py @@ -130,14 +130,9 @@ class ZHAFirmwareUpdateEntity( def _get_cluster_version(self) -> str | None: """Synchronize current file version with the cluster.""" - device = self._ota_cluster_handler._endpoint.device # pylint: disable=protected-access - if self._ota_cluster_handler.current_file_version is not None: return f"0x{self._ota_cluster_handler.current_file_version:08x}" - if device.sw_version is not None: - return device.sw_version - return None @callback From d53848aae44d4c25849fdaea29ad22b408a77400 Mon Sep 17 00:00:00 2001 From: dotvav Date: Tue, 2 Apr 2024 13:08:53 +0200 Subject: [PATCH 078/198] Fix Overkiz Hitachi OVP air-to-air heat pump (#114611) --- .../climate_entities/hitachi_air_to_air_heat_pump_ovp.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_ovp.py b/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_ovp.py index b4d6ab788a1..b31ecf91ec0 100644 --- a/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_ovp.py +++ b/homeassistant/components/overkiz/climate_entities/hitachi_air_to_air_heat_pump_ovp.py @@ -298,6 +298,11 @@ class HitachiAirToAirHeatPumpOVP(OverkizEntity, ClimateEntity): OverkizState.OVP_FAN_SPEED, OverkizCommandParam.AUTO, ) + # Sanitize fan mode: Overkiz is sometimes providing a state that + # cannot be used as a command. Convert it to HA space and back to Overkiz + if fan_mode not in FAN_MODES_TO_OVERKIZ.values(): + fan_mode = FAN_MODES_TO_OVERKIZ[OVERKIZ_TO_FAN_MODES[fan_mode]] + hvac_mode = self._control_backfill( hvac_mode, OverkizState.OVP_MODE_CHANGE, From 5bd52da13a595f9bb41690bfe2096a01d427afce Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Tue, 2 Apr 2024 12:17:47 +0100 Subject: [PATCH 079/198] Bump ring_doorbell integration to 0.8.9 (#114631) --- homeassistant/components/ring/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 764557a3a1d..67e2cfcdc78 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -13,5 +13,5 @@ "documentation": "https://www.home-assistant.io/integrations/ring", "iot_class": "cloud_polling", "loggers": ["ring_doorbell"], - "requirements": ["ring-doorbell[listen]==0.8.8"] + "requirements": ["ring-doorbell[listen]==0.8.9"] } diff --git a/requirements_all.txt b/requirements_all.txt index adfcc1322e3..fd44076ef24 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2444,7 +2444,7 @@ rfk101py==0.0.1 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell[listen]==0.8.8 +ring-doorbell[listen]==0.8.9 # homeassistant.components.fleetgo ritassist==0.9.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 85fb1c70471..70c27940562 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1887,7 +1887,7 @@ reolink-aio==0.8.9 rflink==0.0.66 # homeassistant.components.ring -ring-doorbell[listen]==0.8.8 +ring-doorbell[listen]==0.8.9 # homeassistant.components.roku rokuecp==0.19.2 From 8cbedbe26b6e588f652c566073716ef76d081aa6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 2 Apr 2024 17:15:24 +0200 Subject: [PATCH 080/198] Update frontend to 20240402.1 (#114646) --- 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 5eaa6e94769..2010a9985b3 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==20240402.0"] + "requirements": ["home-assistant-frontend==20240402.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index eb6d347a479..cf0aa0848af 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240402.0 +home-assistant-frontend==20240402.1 home-assistant-intents==2024.3.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index fd44076ef24..5bb02a079fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1077,7 +1077,7 @@ hole==0.8.0 holidays==0.45 # homeassistant.components.frontend -home-assistant-frontend==20240402.0 +home-assistant-frontend==20240402.1 # homeassistant.components.conversation home-assistant-intents==2024.3.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 70c27940562..e25784596a3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,7 +876,7 @@ hole==0.8.0 holidays==0.45 # homeassistant.components.frontend -home-assistant-frontend==20240402.0 +home-assistant-frontend==20240402.1 # homeassistant.components.conversation home-assistant-intents==2024.3.29 From 85fb4a27a3a728d1bf178b300ed4055f956dd28c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 2 Apr 2024 17:35:01 +0200 Subject: [PATCH 081/198] Bump version to 2024.4.0b6 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0cac1ae45a3..fd3b1257df2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -18,7 +18,7 @@ from .util.signal_type import SignalType APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 4 -PATCH_VERSION: Final = "0b5" +PATCH_VERSION: Final = "0b6" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 62c9166cd22..73d03f0f92c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.4.0b5" +version = "2024.4.0b6" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 2ce784105d40890e36eceb6a12d2eb28df663668 Mon Sep 17 00:00:00 2001 From: Pete Sage <76050312+PeteRager@users.noreply.github.com> Date: Tue, 2 Apr 2024 18:10:15 -0400 Subject: [PATCH 082/198] Fix Sonos play imported playlists (#113934) --- .../components/sonos/media_browser.py | 14 +++ .../components/sonos/media_player.py | 12 +- tests/components/sonos/test_media_player.py | 117 ++++++++++++++++++ 3 files changed, 137 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index 9d3ef5d353b..87ee3ed3b4d 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -493,6 +493,20 @@ def get_media( """Fetch media/album.""" search_type = MEDIA_TYPES_TO_SONOS.get(search_type, search_type) + if search_type == "playlists": + # Format is S:TITLE or S:ITEM_ID + splits = item_id.split(":") + title = splits[1] if len(splits) > 1 else None + playlist = next( + ( + p + for p in media_library.get_playlists() + if (item_id == p.item_id or title == p.title) + ), + None, + ) + return playlist + if not item_id.startswith("A:ALBUM") and search_type == SONOS_ALBUM: item_id = "A:ALBUMARTIST/" + "/".join(item_id.split("/")[2:]) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 12e8b44652a..581bdaad37d 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -626,13 +626,13 @@ class SonosMediaPlayerEntity(SonosEntity, MediaPlayerEntity): soco.play_uri(media_id, force_radio=is_radio) elif media_type == MediaType.PLAYLIST: if media_id.startswith("S:"): - item = media_browser.get_media(self.media.library, media_id, media_type) - soco.play_uri(item.get_uri()) - return - try: + playlist = media_browser.get_media( + self.media.library, media_id, media_type + ) + else: playlists = soco.get_sonos_playlists(complete_result=True) - playlist = next(p for p in playlists if p.title == media_id) - except StopIteration: + playlist = next((p for p in playlists if p.title == media_id), None) + if not playlist: _LOGGER.error('Could not find a Sonos playlist named "%s"', media_id) else: soco.clear_queue() diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index d89a1076db3..c181520b85d 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -1,5 +1,13 @@ """Tests for the Sonos Media Player platform.""" +import logging + +import pytest + +from homeassistant.components.media_player import ( + DOMAIN as MP_DOMAIN, + SERVICE_PLAY_MEDIA, +) from homeassistant.const import STATE_IDLE from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import ( @@ -8,6 +16,8 @@ from homeassistant.helpers.device_registry import ( DeviceRegistry, ) +from .conftest import SoCoMockFactory + async def test_device_registry( hass: HomeAssistant, device_registry: DeviceRegistry, async_autosetup_sonos, soco @@ -53,3 +63,110 @@ async def test_entity_basic( assert attributes["friendly_name"] == "Zone A" assert attributes["is_volume_muted"] is False assert attributes["volume_level"] == 0.19 + + +class _MockMusicServiceItem: + """Mocks a Soco MusicServiceItem.""" + + def __init__( + self, + title: str, + item_id: str, + parent_id: str, + item_class: str, + ) -> None: + """Initialize the mock item.""" + self.title = title + self.item_id = item_id + self.item_class = item_class + self.parent_id = parent_id + + def get_uri(self) -> str: + """Return URI.""" + return self.item_id.replace("S://", "x-file-cifs://") + + +_mock_playlists = [ + _MockMusicServiceItem( + "playlist1", + "S://192.168.1.68/music/iTunes/iTunes%20Music%20Library.xml#GUID_1", + "A:PLAYLISTS", + "object.container.playlistContainer", + ), + _MockMusicServiceItem( + "playlist2", + "S://192.168.1.68/music/iTunes/iTunes%20Music%20Library.xml#GUID_2", + "A:PLAYLISTS", + "object.container.playlistContainer", + ), +] + + +@pytest.mark.parametrize( + ("media_content_id", "expected_item_id"), + [ + ( + _mock_playlists[0].item_id, + _mock_playlists[0].item_id, + ), + ( + f"S:{_mock_playlists[1].title}", + _mock_playlists[1].item_id, + ), + ], +) +async def test_play_media_music_library_playlist( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + discover, + media_content_id, + expected_item_id, +) -> None: + """Test that playlists can be found by id or title.""" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + soco_mock.music_library.get_playlists.return_value = _mock_playlists + + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + "entity_id": "media_player.zone_a", + "media_content_type": "playlist", + "media_content_id": media_content_id, + }, + blocking=True, + ) + + assert soco_mock.clear_queue.call_count == 1 + assert soco_mock.add_to_queue.call_count == 1 + assert soco_mock.add_to_queue.call_args_list[0].args[0].item_id == expected_item_id + assert soco_mock.play_from_queue.call_count == 1 + + +async def test_play_media_music_library_playlist_dne( + hass: HomeAssistant, + soco_factory: SoCoMockFactory, + async_autosetup_sonos, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test error handling when attempting to play a non-existent playlist .""" + media_content_id = "S:nonexistent" + soco_mock = soco_factory.mock_list.get("192.168.42.2") + soco_mock.music_library.get_playlists.return_value = _mock_playlists + + with caplog.at_level(logging.ERROR): + caplog.clear() + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + "entity_id": "media_player.zone_a", + "media_content_type": "playlist", + "media_content_id": media_content_id, + }, + blocking=True, + ) + assert soco_mock.play_uri.call_count == 0 + assert media_content_id in caplog.text + assert "playlist" in caplog.text From fa2f49693c206e2cec938bfb8763cfda21ddef01 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 2 Apr 2024 18:33:12 +0200 Subject: [PATCH 083/198] Bump aiounifi to v74 (#114649) --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 63f9f67605e..05dc2189908 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==73"], + "requirements": ["aiounifi==74"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 5bb02a079fd..4bc0c15d06d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -392,7 +392,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==73 +aiounifi==74 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e25784596a3..0c363f73167 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -365,7 +365,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==73 +aiounifi==74 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From 4e0290ce0e91286c54769b02eada309ce30017dd Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Tue, 2 Apr 2024 18:28:52 +0200 Subject: [PATCH 084/198] Add missing state to the Tractive tracker state sensor (#114654) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/tractive/sensor.py | 1 + homeassistant/components/tractive/strings.json | 1 + 2 files changed, 2 insertions(+) diff --git a/homeassistant/components/tractive/sensor.py b/homeassistant/components/tractive/sensor.py index 5e2f3288f57..1edee71467b 100644 --- a/homeassistant/components/tractive/sensor.py +++ b/homeassistant/components/tractive/sensor.py @@ -107,6 +107,7 @@ SENSOR_TYPES: tuple[TractiveSensorEntityDescription, ...] = ( entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.ENUM, options=[ + "inaccurate_position", "not_reporting", "operational", "system_shutdown_user", diff --git a/homeassistant/components/tractive/strings.json b/homeassistant/components/tractive/strings.json index 82b7ecc295c..0690328c99c 100644 --- a/homeassistant/components/tractive/strings.json +++ b/homeassistant/components/tractive/strings.json @@ -70,6 +70,7 @@ "tracker_state": { "name": "Tracker state", "state": { + "inaccurate_position": "Inaccurate position", "not_reporting": "Not reporting", "operational": "Operational", "system_shutdown_user": "System shutdown user", From 02dee343380fe2b78fd10971e3eb28bfd62c621c Mon Sep 17 00:00:00 2001 From: G Johansson Date: Tue, 2 Apr 2024 20:58:18 +0300 Subject: [PATCH 085/198] Bump holidays to 0.46 (#114657) --- homeassistant/components/holiday/manifest.json | 2 +- homeassistant/components/workday/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/holiday/manifest.json b/homeassistant/components/holiday/manifest.json index f1bc60dece4..5a1edcd3c3f 100644 --- a/homeassistant/components/holiday/manifest.json +++ b/homeassistant/components/holiday/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/holiday", "iot_class": "local_polling", - "requirements": ["holidays==0.45", "babel==2.13.1"] + "requirements": ["holidays==0.46", "babel==2.13.1"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 6b17a980870..314f4c6bcf4 100644 --- a/homeassistant/components/workday/manifest.json +++ b/homeassistant/components/workday/manifest.json @@ -7,5 +7,5 @@ "iot_class": "local_polling", "loggers": ["holidays"], "quality_scale": "internal", - "requirements": ["holidays==0.45"] + "requirements": ["holidays==0.46"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4bc0c15d06d..6776b8daa0b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1074,7 +1074,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.45 +holidays==0.46 # homeassistant.components.frontend home-assistant-frontend==20240402.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c363f73167..5140cbaa5e4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -873,7 +873,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.45 +holidays==0.46 # homeassistant.components.frontend home-assistant-frontend==20240402.1 From 639c4a843bd3b4926c05d83e9ebc22b330ad59c6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 2 Apr 2024 11:22:40 -1000 Subject: [PATCH 086/198] Avoid trying to load platform that are known to not exist in async_prepare_setup_platform (#114659) --- homeassistant/setup.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 178ee6425e3..2e64fefee77 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -504,6 +504,12 @@ async def async_prepare_setup_platform( log_error(f"Unable to import the component ({exc}).") return None + if not integration.platforms_exists((domain,)): + log_error( + f"Platform not found (No module named '{integration.pkg_path}.{domain}')" + ) + return None + try: platform = await integration.async_get_platform(domain) except ImportError as exc: From f676448f27141ba21af81e05b747ef94b19e013c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Apr 2024 00:12:31 +0200 Subject: [PATCH 087/198] Update frontend to 20240402.2 (#114683) --- 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 2010a9985b3..3ac7efa9fab 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==20240402.1"] + "requirements": ["home-assistant-frontend==20240402.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cf0aa0848af..80cea24b817 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240402.1 +home-assistant-frontend==20240402.2 home-assistant-intents==2024.3.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6776b8daa0b..2be277f422a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1077,7 +1077,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240402.1 +home-assistant-frontend==20240402.2 # homeassistant.components.conversation home-assistant-intents==2024.3.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5140cbaa5e4..b742a3fa589 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,7 +876,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240402.1 +home-assistant-frontend==20240402.2 # homeassistant.components.conversation home-assistant-intents==2024.3.29 From 8bdb27c88b7e1a837000116e5cb9231ef8f152a2 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Apr 2024 00:14:07 +0200 Subject: [PATCH 088/198] Bump version to 2024.4.0b7 --- 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 fd3b1257df2..7dc1177c62f 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -18,7 +18,7 @@ from .util.signal_type import SignalType APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 4 -PATCH_VERSION: Final = "0b6" +PATCH_VERSION: Final = "0b7" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 73d03f0f92c..7bf5f806dd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.4.0b6" +version = "2024.4.0b7" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 79fa7caa41c54162c431ed6ae42f54df5e328b32 Mon Sep 17 00:00:00 2001 From: Lenn <78048721+LennP@users.noreply.github.com> Date: Wed, 3 Apr 2024 12:11:46 +0200 Subject: [PATCH 089/198] Rename Motionblinds BLE integration to Motionblinds Bluetooth (#114584) --- homeassistant/components/motionblinds_ble/__init__.py | 10 +++++----- homeassistant/components/motionblinds_ble/button.py | 2 +- .../components/motionblinds_ble/config_flow.py | 4 ++-- homeassistant/components/motionblinds_ble/const.py | 2 +- homeassistant/components/motionblinds_ble/cover.py | 2 +- homeassistant/components/motionblinds_ble/entity.py | 4 ++-- .../components/motionblinds_ble/manifest.json | 2 +- homeassistant/components/motionblinds_ble/select.py | 2 +- homeassistant/generated/integrations.json | 2 +- tests/components/motionblinds_ble/__init__.py | 2 +- tests/components/motionblinds_ble/conftest.py | 2 +- tests/components/motionblinds_ble/test_config_flow.py | 2 +- 12 files changed, 18 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/motionblinds_ble/__init__.py b/homeassistant/components/motionblinds_ble/__init__.py index f70625cd36d..3c6df12e878 100644 --- a/homeassistant/components/motionblinds_ble/__init__.py +++ b/homeassistant/components/motionblinds_ble/__init__.py @@ -1,4 +1,4 @@ -"""Motionblinds BLE integration.""" +"""Motionblinds Bluetooth integration.""" from __future__ import annotations @@ -34,9 +34,9 @@ CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: - """Set up Motionblinds BLE integration.""" + """Set up Motionblinds Bluetooth integration.""" - _LOGGER.debug("Setting up Motionblinds BLE integration") + _LOGGER.debug("Setting up Motionblinds Bluetooth integration") # The correct time is needed for encryption _LOGGER.debug("Setting timezone for encryption: %s", hass.config.time_zone) @@ -46,7 +46,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Motionblinds BLE device from a config entry.""" + """Set up Motionblinds Bluetooth device from a config entry.""" _LOGGER.debug("(%s) Setting up device", entry.data[CONF_MAC_CODE]) @@ -94,7 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Unload Motionblinds BLE device from a config entry.""" + """Unload Motionblinds Bluetooth device from a config entry.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/motionblinds_ble/button.py b/homeassistant/components/motionblinds_ble/button.py index d3bd22e9276..a099276cd85 100644 --- a/homeassistant/components/motionblinds_ble/button.py +++ b/homeassistant/components/motionblinds_ble/button.py @@ -1,4 +1,4 @@ -"""Button entities for the Motionblinds BLE integration.""" +"""Button entities for the Motionblinds Bluetooth integration.""" from __future__ import annotations diff --git a/homeassistant/components/motionblinds_ble/config_flow.py b/homeassistant/components/motionblinds_ble/config_flow.py index 0282c4d5584..23302ae9624 100644 --- a/homeassistant/components/motionblinds_ble/config_flow.py +++ b/homeassistant/components/motionblinds_ble/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for Motionblinds BLE integration.""" +"""Config flow for Motionblinds Bluetooth integration.""" from __future__ import annotations @@ -38,7 +38,7 @@ CONFIG_SCHEMA = vol.Schema({vol.Required(CONF_MAC_CODE): str}) class FlowHandler(ConfigFlow, domain=DOMAIN): - """Handle a config flow for Motionblinds BLE.""" + """Handle a config flow for Motionblinds Bluetooth.""" def __init__(self) -> None: """Initialize a ConfigFlow.""" diff --git a/homeassistant/components/motionblinds_ble/const.py b/homeassistant/components/motionblinds_ble/const.py index d2eb5821b9f..bd88927559e 100644 --- a/homeassistant/components/motionblinds_ble/const.py +++ b/homeassistant/components/motionblinds_ble/const.py @@ -1,4 +1,4 @@ -"""Constants for the Motionblinds BLE integration.""" +"""Constants for the Motionblinds Bluetooth integration.""" ATTR_CONNECT = "connect" ATTR_DISCONNECT = "disconnect" diff --git a/homeassistant/components/motionblinds_ble/cover.py b/homeassistant/components/motionblinds_ble/cover.py index c4f14dc5605..afeeb5b0d70 100644 --- a/homeassistant/components/motionblinds_ble/cover.py +++ b/homeassistant/components/motionblinds_ble/cover.py @@ -1,4 +1,4 @@ -"""Cover entities for the Motionblinds BLE integration.""" +"""Cover entities for the Motionblinds Bluetooth integration.""" from __future__ import annotations diff --git a/homeassistant/components/motionblinds_ble/entity.py b/homeassistant/components/motionblinds_ble/entity.py index 5c2b3ae9afb..0b8171e7acd 100644 --- a/homeassistant/components/motionblinds_ble/entity.py +++ b/homeassistant/components/motionblinds_ble/entity.py @@ -1,4 +1,4 @@ -"""Base entities for the Motionblinds BLE integration.""" +"""Base entities for the Motionblinds Bluetooth integration.""" import logging @@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__) class MotionblindsBLEEntity(Entity): - """Base class for Motionblinds BLE entities.""" + """Base class for Motionblinds Bluetooth entities.""" _attr_has_entity_name = True _attr_should_poll = False diff --git a/homeassistant/components/motionblinds_ble/manifest.json b/homeassistant/components/motionblinds_ble/manifest.json index 2a24dd67483..aa727be13f8 100644 --- a/homeassistant/components/motionblinds_ble/manifest.json +++ b/homeassistant/components/motionblinds_ble/manifest.json @@ -1,6 +1,6 @@ { "domain": "motionblinds_ble", - "name": "Motionblinds BLE", + "name": "Motionblinds Bluetooth", "bluetooth": [ { "local_name": "MOTION_*", diff --git a/homeassistant/components/motionblinds_ble/select.py b/homeassistant/components/motionblinds_ble/select.py index 2ba2b8df2d4..c297c887910 100644 --- a/homeassistant/components/motionblinds_ble/select.py +++ b/homeassistant/components/motionblinds_ble/select.py @@ -1,4 +1,4 @@ -"""Select entities for the Motionblinds BLE integration.""" +"""Select entities for the Motionblinds Bluetooth integration.""" from __future__ import annotations diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 53b885ea853..b8abac5145b 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3741,7 +3741,7 @@ "integration_type": "device", "config_flow": true, "iot_class": "assumed_state", - "name": "Motionblinds BLE" + "name": "Motionblinds Bluetooth" } } }, diff --git a/tests/components/motionblinds_ble/__init__.py b/tests/components/motionblinds_ble/__init__.py index 302c3266ea1..c2385555dbf 100644 --- a/tests/components/motionblinds_ble/__init__.py +++ b/tests/components/motionblinds_ble/__init__.py @@ -1 +1 @@ -"""Tests for the Motionblinds BLE integration.""" +"""Tests for the Motionblinds Bluetooth integration.""" diff --git a/tests/components/motionblinds_ble/conftest.py b/tests/components/motionblinds_ble/conftest.py index 8cd1adb1c0e..ae487957302 100644 --- a/tests/components/motionblinds_ble/conftest.py +++ b/tests/components/motionblinds_ble/conftest.py @@ -1,4 +1,4 @@ -"""Setup the MotionBlinds BLE tests.""" +"""Setup the Motionblinds Bluetooth tests.""" from unittest.mock import AsyncMock, Mock, patch diff --git a/tests/components/motionblinds_ble/test_config_flow.py b/tests/components/motionblinds_ble/test_config_flow.py index 9451e04830a..f540fdf421c 100644 --- a/tests/components/motionblinds_ble/test_config_flow.py +++ b/tests/components/motionblinds_ble/test_config_flow.py @@ -1,4 +1,4 @@ -"""Test the MotionBlinds BLE config flow.""" +"""Test the Motionblinds Bluetooth config flow.""" from unittest.mock import patch From 43562289e401af6881539a2ae646be4afd3d6f63 Mon Sep 17 00:00:00 2001 From: Jonas Fors Lellky Date: Wed, 3 Apr 2024 09:23:06 +0200 Subject: [PATCH 090/198] Bump flexit_bacnet to 2.2.1 (#114641) --- homeassistant/components/flexit_bacnet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/flexit_bacnet/manifest.json b/homeassistant/components/flexit_bacnet/manifest.json index d230e4ebb7a..40390162ce6 100644 --- a/homeassistant/components/flexit_bacnet/manifest.json +++ b/homeassistant/components/flexit_bacnet/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/flexit_bacnet", "integration_type": "device", "iot_class": "local_polling", - "requirements": ["flexit_bacnet==2.1.0"] + "requirements": ["flexit_bacnet==2.2.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2be277f422a..792e8b53e96 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -867,7 +867,7 @@ fixerio==1.0.0a0 fjaraskupan==2.3.0 # homeassistant.components.flexit_bacnet -flexit_bacnet==2.1.0 +flexit_bacnet==2.2.1 # homeassistant.components.flipr flipr-api==1.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b742a3fa589..c771a0e4b54 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -705,7 +705,7 @@ fivem-api==0.1.2 fjaraskupan==2.3.0 # homeassistant.components.flexit_bacnet -flexit_bacnet==2.1.0 +flexit_bacnet==2.2.1 # homeassistant.components.flipr flipr-api==1.5.1 From 7cb603a2268ba66990f91f2cc7f07bed5db4a183 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 3 Apr 2024 03:12:00 -0400 Subject: [PATCH 091/198] Import zha quirks in the executor (#114685) --- homeassistant/components/zha/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index ef603a4ea71..de761138ce1 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -124,8 +124,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b zha_data = get_zha_data(hass) if zha_data.yaml_config.get(CONF_ENABLE_QUIRKS, True): - setup_quirks( - custom_quirks_path=zha_data.yaml_config.get(CONF_CUSTOM_QUIRKS_PATH) + await hass.async_add_import_executor_job( + setup_quirks, zha_data.yaml_config.get(CONF_CUSTOM_QUIRKS_PATH) ) # Load and cache device trigger information early From 7a2f6ce4305f0b26db8619143ec24002de5653de Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 3 Apr 2024 09:56:19 +0200 Subject: [PATCH 092/198] Fix Downloader config flow (#114718) --- homeassistant/components/downloader/config_flow.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/downloader/config_flow.py b/homeassistant/components/downloader/config_flow.py index 69393c04985..635c241edc4 100644 --- a/homeassistant/components/downloader/config_flow.py +++ b/homeassistant/components/downloader/config_flow.py @@ -55,8 +55,9 @@ class DownloaderConfigFlow(ConfigFlow, domain=DOMAIN): async def _validate_input(self, user_input: dict[str, Any]) -> None: """Validate the user input if the directory exists.""" - if not os.path.isabs(user_input[CONF_DOWNLOAD_DIR]): - download_path = self.hass.config.path(user_input[CONF_DOWNLOAD_DIR]) + download_path = user_input[CONF_DOWNLOAD_DIR] + if not os.path.isabs(download_path): + download_path = self.hass.config.path(download_path) if not os.path.isdir(download_path): _LOGGER.error( From 35ff633d999096ffbbbdf7e4012acc8aac730f0f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 3 Apr 2024 13:50:34 +0200 Subject: [PATCH 093/198] Avoid blocking IO in downloader config flow (#114741) --- homeassistant/components/downloader/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/downloader/config_flow.py b/homeassistant/components/downloader/config_flow.py index 635c241edc4..15af8b56163 100644 --- a/homeassistant/components/downloader/config_flow.py +++ b/homeassistant/components/downloader/config_flow.py @@ -59,7 +59,7 @@ class DownloaderConfigFlow(ConfigFlow, domain=DOMAIN): if not os.path.isabs(download_path): download_path = self.hass.config.path(download_path) - if not os.path.isdir(download_path): + if not await self.hass.async_add_executor_job(os.path.isdir, download_path): _LOGGER.error( "Download path %s does not exist. File Downloader not active", download_path, From 0ca3700c16a58502d8a68be4a61d21df3598a2e2 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Apr 2024 15:19:49 +0200 Subject: [PATCH 094/198] Update frontend to 20240403.0 (#114747) --- 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 3ac7efa9fab..e2826fdb185 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==20240402.2"] + "requirements": ["home-assistant-frontend==20240403.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 80cea24b817..07b3aa76ebd 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240402.2 +home-assistant-frontend==20240403.0 home-assistant-intents==2024.3.29 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 792e8b53e96..143e9e1fa53 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1077,7 +1077,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240402.2 +home-assistant-frontend==20240403.0 # homeassistant.components.conversation home-assistant-intents==2024.3.29 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c771a0e4b54..2ea289f7bd3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,7 +876,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240402.2 +home-assistant-frontend==20240403.0 # homeassistant.components.conversation home-assistant-intents==2024.3.29 From 0aa134459be89790358fc2bd73f00863495d88ab Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Apr 2024 15:35:53 +0200 Subject: [PATCH 095/198] Bump version to 2024.4.0b8 --- 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 7dc1177c62f..95fab13bb5e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -18,7 +18,7 @@ from .util.signal_type import SignalType APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 4 -PATCH_VERSION: Final = "0b7" +PATCH_VERSION: Final = "0b8" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 7bf5f806dd2..031afa09704 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.4.0b7" +version = "2024.4.0b8" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From b2df1b1c03069b6f20a026aae31acf40d737679c Mon Sep 17 00:00:00 2001 From: Robert Resch Date: Wed, 3 Apr 2024 16:33:58 +0200 Subject: [PATCH 096/198] Allow passing area/device/entity IDs to floor_id and floor_name (#114748) --- homeassistant/helpers/template.py | 16 ++++ tests/helpers/test_template.py | 125 +++++++++++++++++++++++------- 2 files changed, 115 insertions(+), 26 deletions(-) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index a48f0133e84..5f692e0de89 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1408,6 +1408,12 @@ def floor_id(hass: HomeAssistant, lookup_value: Any) -> str | None: floor_registry = fr.async_get(hass) if floor := floor_registry.async_get_floor_by_name(str(lookup_value)): return floor.floor_id + + if aid := area_id(hass, lookup_value): + area_reg = area_registry.async_get(hass) + if area := area_reg.async_get_area(aid): + return area.floor_id + return None @@ -1416,6 +1422,16 @@ def floor_name(hass: HomeAssistant, lookup_value: str) -> str | None: floor_registry = fr.async_get(hass) if floor := floor_registry.async_get_floor(lookup_value): return floor.name + + if aid := area_id(hass, lookup_value): + area_reg = area_registry.async_get(hass) + if ( + (area := area_reg.async_get_area(aid)) + and area.floor_id + and (floor := floor_registry.async_get_floor(area.floor_id)) + ): + return floor.name + return None diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 6f455c3dda4..54fdf0368eb 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -5198,17 +5198,23 @@ async def test_floors( async def test_floor_id( hass: HomeAssistant, floor_registry: fr.FloorRegistry, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test floor_id function.""" - # Test non existing floor name - info = render_to_info(hass, "{{ floor_id('Third floor') }}") - assert_result_info(info, None) - assert info.rate_limit is None + def test(value: str, expected: str | None) -> None: + info = render_to_info(hass, f"{{{{ floor_id('{value}') }}}}") + assert_result_info(info, expected) + assert info.rate_limit is None - info = render_to_info(hass, "{{ 'Third floor' | floor_id }}") - assert_result_info(info, None) - assert info.rate_limit is None + info = render_to_info(hass, f"{{{{ '{value}' | floor_id }}}}") + assert_result_info(info, expected) + assert info.rate_limit is None + + # Test non existing floor name + test("Third floor", None) # Test wrong value type info = render_to_info(hass, "{{ floor_id(42) }}") @@ -5221,28 +5227,65 @@ async def test_floor_id( # Test with an actual floor floor = floor_registry.async_create("First floor") - info = render_to_info(hass, "{{ floor_id('First floor') }}") - assert_result_info(info, floor.floor_id) - assert info.rate_limit is None + test("First floor", floor.floor_id) - info = render_to_info(hass, "{{ 'First floor' | floor_id }}") - assert_result_info(info, floor.floor_id) - assert info.rate_limit is None + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + area_entry_hex = area_registry.async_get_or_create("123abc") + + # Create area, device, entity and assign area to device and entity + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + device_id=device_entry.id, + ) + device_entry = device_registry.async_update_device( + device_entry.id, area_id=area_entry_hex.id + ) + entity_entry = entity_registry.async_update_entity( + entity_entry.entity_id, area_id=area_entry_hex.id + ) + + test(area_entry_hex.id, None) + test(device_entry.id, None) + test(entity_entry.entity_id, None) + + # Add floor to area + area_entry_hex = area_registry.async_update( + area_entry_hex.id, floor_id=floor.floor_id + ) + + test(area_entry_hex.id, floor.floor_id) + test(device_entry.id, floor.floor_id) + test(entity_entry.entity_id, floor.floor_id) async def test_floor_name( hass: HomeAssistant, floor_registry: fr.FloorRegistry, + area_registry: ar.AreaRegistry, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, ) -> None: """Test floor_name function.""" - # Test non existing floor ID - info = render_to_info(hass, "{{ floor_name('third_floor') }}") - assert_result_info(info, None) - assert info.rate_limit is None - info = render_to_info(hass, "{{ 'third_floor' | floor_name }}") - assert_result_info(info, None) - assert info.rate_limit is None + def test(value: str, expected: str | None) -> None: + info = render_to_info(hass, f"{{{{ floor_name('{value}') }}}}") + assert_result_info(info, expected) + assert info.rate_limit is None + + info = render_to_info(hass, f"{{{{ '{value}' | floor_name }}}}") + assert_result_info(info, expected) + assert info.rate_limit is None + + # Test non existing floor name + test("Third floor", None) # Test wrong value type info = render_to_info(hass, "{{ floor_name(42) }}") @@ -5255,13 +5298,43 @@ async def test_floor_name( # Test existing floor ID floor = floor_registry.async_create("First floor") - info = render_to_info(hass, f"{{{{ floor_name('{floor.floor_id}') }}}}") - assert_result_info(info, floor.name) - assert info.rate_limit is None + test(floor.floor_id, floor.name) - info = render_to_info(hass, f"{{{{ '{floor.floor_id}' | floor_name }}}}") - assert_result_info(info, floor.name) - assert info.rate_limit is None + config_entry = MockConfigEntry(domain="light") + config_entry.add_to_hass(hass) + area_entry_hex = area_registry.async_get_or_create("123abc") + + # Create area, device, entity and assign area to device and entity + device_entry = device_registry.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_entry = entity_registry.async_get_or_create( + "light", + "hue", + "5678", + config_entry=config_entry, + device_id=device_entry.id, + ) + device_entry = device_registry.async_update_device( + device_entry.id, area_id=area_entry_hex.id + ) + entity_entry = entity_registry.async_update_entity( + entity_entry.entity_id, area_id=area_entry_hex.id + ) + + test(area_entry_hex.id, None) + test(device_entry.id, None) + test(entity_entry.entity_id, None) + + # Add floor to area + area_entry_hex = area_registry.async_update( + area_entry_hex.id, floor_id=floor.floor_id + ) + + test(area_entry_hex.id, floor.name) + test(device_entry.id, floor.name) + test(entity_entry.entity_id, floor.name) async def test_floor_areas( From 4302c5c273e56ee12487c49f1bccfee48a5733d6 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Wed, 3 Apr 2024 10:27:26 -0500 Subject: [PATCH 097/198] Bump intents (#114755) --- 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 7f463483bf9..612e9b25c06 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.29"] + "requirements": ["hassil==1.6.1", "home-assistant-intents==2024.4.3"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 07b3aa76ebd..4f5bc0f5a63 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -31,7 +31,7 @@ hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240403.0 -home-assistant-intents==2024.3.29 +home-assistant-intents==2024.4.3 httpx==0.27.0 ifaddr==0.2.0 Jinja2==3.1.3 diff --git a/requirements_all.txt b/requirements_all.txt index 143e9e1fa53..7980e71d5d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1080,7 +1080,7 @@ holidays==0.46 home-assistant-frontend==20240403.0 # homeassistant.components.conversation -home-assistant-intents==2024.3.29 +home-assistant-intents==2024.4.3 # homeassistant.components.home_connect homeconnect==0.7.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ea289f7bd3..2097ed21f55 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -879,7 +879,7 @@ holidays==0.46 home-assistant-frontend==20240403.0 # homeassistant.components.conversation -home-assistant-intents==2024.3.29 +home-assistant-intents==2024.4.3 # homeassistant.components.home_connect homeconnect==0.7.2 From 33f07ce035f01484857181086a7dd39d952966e1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 3 Apr 2024 17:32:26 +0200 Subject: [PATCH 098/198] Update frontend to 20240403.1 (#114756) --- 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 e2826fdb185..1890572bf5a 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==20240403.0"] + "requirements": ["home-assistant-frontend==20240403.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4f5bc0f5a63..6bb6bd4d2d3 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240403.0 +home-assistant-frontend==20240403.1 home-assistant-intents==2024.4.3 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 7980e71d5d2..76dc587d6b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1077,7 +1077,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240403.0 +home-assistant-frontend==20240403.1 # homeassistant.components.conversation home-assistant-intents==2024.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2097ed21f55..6f329b782aa 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,7 +876,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240403.0 +home-assistant-frontend==20240403.1 # homeassistant.components.conversation home-assistant-intents==2024.4.3 From 6a7fad0228a94191ad46fad807dfde330fe4ee1c Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:53:44 +0200 Subject: [PATCH 099/198] Fix Synology DSM setup in case no Surveillance Station permission (#114757) --- homeassistant/components/synology_dsm/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index a0c3a10774f..ec93c92a698 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -105,6 +105,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if ( SynoSurveillanceStation.INFO_API_KEY in available_apis and SynoSurveillanceStation.HOME_MODE_API_KEY in available_apis + and api.surveillance_station is not None ): coordinator_switches = SynologyDSMSwitchUpdateCoordinator(hass, entry, api) await coordinator_switches.async_config_entry_first_refresh() From 3845523a27d245ee686aeeaec5c427ba2086bfdf Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Apr 2024 17:55:24 +0200 Subject: [PATCH 100/198] Bump version to 2024.4.0b9 --- 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 95fab13bb5e..514124e8b2e 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -18,7 +18,7 @@ from .util.signal_type import SignalType APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 4 -PATCH_VERSION: Final = "0b8" +PATCH_VERSION: Final = "0b9" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 031afa09704..ff96331c1e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.4.0b8" +version = "2024.4.0b9" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From aa33da546df167da6157442f73c934fa5f409242 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 3 Apr 2024 19:09:39 +0200 Subject: [PATCH 101/198] Bump version to 2024.4.0 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 514124e8b2e..6e08c49f970 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -18,7 +18,7 @@ from .util.signal_type import SignalType APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 4 -PATCH_VERSION: Final = "0b9" +PATCH_VERSION: Final = "0" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index ff96331c1e1..e0f07fac6b6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.4.0b9" +version = "2024.4.0" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 9ba4d26abd7d082ddb55af4a2ac456307ddaf09e Mon Sep 17 00:00:00 2001 From: IngoK1 <45150614+IngoK1@users.noreply.github.com> Date: Tue, 2 Apr 2024 00:07:02 +0200 Subject: [PATCH 102/198] Fix for Sonos URL encoding problem #102557 (#109518) * Fix for URL encoding problem #102557 Fixes the problem "Cannot play media with spaces in folder names to Sonos #102557" removing the encoding of the strings in the music library. * Fix type casting problem * Update media_browser.py to fix pr check findings Added required casting for all unquote statements to avoid further casting findings in the pr checks * Update media_browser.py Checked on linting, lets give it another try * Update media_browser.py Updated ruff run * Update media_browser.py - added version run through ruff * Update media_browser.py - ruff changes * Apply ruff formatting * Update homeassistant/components/sonos/media_browser.py Co-authored-by: jjlawren * Update homeassistant/components/sonos/media_browser.py Co-authored-by: jjlawren * Update homeassistant/components/sonos/media_browser.py Co-authored-by: jjlawren * Update homeassistant/components/sonos/media_browser.py Co-authored-by: jjlawren --------- Co-authored-by: computeq-admin <51021172+computeq-admin@users.noreply.github.com> Co-authored-by: Jason Lawrence --- homeassistant/components/sonos/media_browser.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sonos/media_browser.py b/homeassistant/components/sonos/media_browser.py index 87ee3ed3b4d..6e6f388ed50 100644 --- a/homeassistant/components/sonos/media_browser.py +++ b/homeassistant/components/sonos/media_browser.py @@ -7,6 +7,7 @@ from contextlib import suppress from functools import partial import logging from typing import cast +import urllib.parse from soco.data_structures import DidlObject from soco.ms_data_structures import MusicServiceItem @@ -60,12 +61,14 @@ def get_thumbnail_url_full( media_content_id, media_content_type, ) - return getattr(item, "album_art_uri", None) + return urllib.parse.unquote(getattr(item, "album_art_uri", "")) - return get_browse_image_url( - media_content_type, - media_content_id, - media_image_id, + return urllib.parse.unquote( + get_browse_image_url( + media_content_type, + media_content_id, + media_image_id, + ) ) @@ -166,6 +169,7 @@ def build_item_response( payload["idstring"] = "A:ALBUMARTIST/" + "/".join( payload["idstring"].split("/")[2:] ) + payload["idstring"] = urllib.parse.unquote(payload["idstring"]) try: search_type = MEDIA_TYPES_TO_SONOS[payload["search_type"]] @@ -510,7 +514,7 @@ def get_media( if not item_id.startswith("A:ALBUM") and search_type == SONOS_ALBUM: item_id = "A:ALBUMARTIST/" + "/".join(item_id.split("/")[2:]) - search_term = item_id.split("/")[-1] + search_term = urllib.parse.unquote(item_id.split("/")[-1]) matches = media_library.get_music_library_information( search_type, search_term=search_term, full_album_art_uri=True ) From 590546a9a56eb817739c789ed19c3ceec6ca3653 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Thu, 28 Mar 2024 12:07:55 +0100 Subject: [PATCH 103/198] Use `setup_test_component_platform` helper for sensor entity component tests instead of `hass.components` (#114316) * Use `setup_test_component_platform` helper for sensor entity component tests instead of `hass.components` * Missing file * Fix import * Remove invalid device class --- tests/common.py | 14 +- tests/components/conftest.py | 9 + tests/components/mqtt/test_init.py | 14 +- .../sensor.py => components/sensor/common.py} | 49 +-- .../sensor/test_device_condition.py | 32 +- .../components/sensor/test_device_trigger.py | 32 +- tests/components/sensor/test_init.py | 360 +++++++----------- tests/components/sensor/test_recorder.py | 15 +- 8 files changed, 219 insertions(+), 306 deletions(-) rename tests/{testing_config/custom_components/test/sensor.py => components/sensor/common.py} (84%) diff --git a/tests/common.py b/tests/common.py index a7d4cf6b83a..0ac0ee4556b 100644 --- a/tests/common.py +++ b/tests/common.py @@ -1461,7 +1461,10 @@ def mock_integration( def mock_platform( - hass: HomeAssistant, platform_path: str, module: Mock | MockPlatform | None = None + hass: HomeAssistant, + platform_path: str, + module: Mock | MockPlatform | None = None, + built_in=True, ) -> None: """Mock a platform. @@ -1472,7 +1475,7 @@ def mock_platform( module_cache = hass.data[loader.DATA_COMPONENTS] if domain not in integration_cache: - mock_integration(hass, MockModule(domain)) + mock_integration(hass, MockModule(domain), built_in=built_in) integration_cache[domain]._top_level_files.add(f"{platform_name}.py") _LOGGER.info("Adding mock integration platform: %s", platform_path) @@ -1665,6 +1668,7 @@ def setup_test_component_platform( domain: str, entities: Sequence[Entity], from_config_entry: bool = False, + built_in: bool = True, ) -> MockPlatform: """Mock a test component platform for tests.""" @@ -1695,9 +1699,5 @@ def setup_test_component_platform( platform.async_setup_entry = _async_setup_entry platform.async_setup_platform = None - mock_platform( - hass, - f"test.{domain}", - platform, - ) + mock_platform(hass, f"test.{domain}", platform, built_in=built_in) return platform diff --git a/tests/components/conftest.py b/tests/components/conftest.py index 4669e17c8e7..d84fb3600ab 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -10,6 +10,7 @@ from homeassistant.const import STATE_OFF, STATE_ON if TYPE_CHECKING: from tests.components.light.common import MockLight + from tests.components.sensor.common import MockSensor @pytest.fixture(scope="session", autouse=True) @@ -118,3 +119,11 @@ def mock_light_entities() -> list["MockLight"]: MockLight("Ceiling", STATE_OFF), MockLight(None, STATE_OFF), ] + + +@pytest.fixture +def mock_sensor_entities() -> dict[str, "MockSensor"]: + """Return mocked sensor entities.""" + from tests.components.sensor.common import get_mock_sensor_entities + + return get_mock_sensor_entities() diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 3459e6fc058..a9f2ba4354b 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -23,6 +23,7 @@ from homeassistant.components.mqtt.models import ( MqttValueTemplateException, ReceiveMessage, ) +from homeassistant.components.sensor import SensorDeviceClass from homeassistant.config_entries import ConfigEntryDisabler, ConfigEntryState from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -52,10 +53,9 @@ from tests.common import ( async_fire_mqtt_message, async_fire_time_changed, mock_restore_cache, + setup_test_component_platform, ) -from tests.testing_config.custom_components.test.sensor import ( # type: ignore[attr-defined] - DEVICE_CLASSES, -) +from tests.components.sensor.common import MockSensor from tests.typing import ( MqttMockHAClient, MqttMockHAClientGenerator, @@ -3142,12 +3142,12 @@ async def test_debug_info_non_mqtt( device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, mqtt_mock_entry: MqttMockHAClientGenerator, + mock_sensor_entities: dict[str, MockSensor], ) -> None: """Test we get empty debug_info for a device with non MQTT entities.""" await mqtt_mock_entry() domain = "sensor" - platform = getattr(hass.components, f"test.{domain}") - platform.init() + setup_test_component_platform(hass, domain, mock_sensor_entities) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -3155,11 +3155,11 @@ async def test_debug_info_non_mqtt( config_entry_id=config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) - for device_class in DEVICE_CLASSES: + for device_class in SensorDeviceClass: entity_registry.async_get_or_create( domain, "test", - platform.ENTITIES[device_class].unique_id, + mock_sensor_entities[device_class].unique_id, device_id=device_entry.id, ) diff --git a/tests/testing_config/custom_components/test/sensor.py b/tests/components/sensor/common.py similarity index 84% rename from tests/testing_config/custom_components/test/sensor.py rename to tests/components/sensor/common.py index 9ebf16b9dcd..53a93b73da3 100644 --- a/tests/testing_config/custom_components/test/sensor.py +++ b/tests/components/sensor/common.py @@ -1,10 +1,6 @@ -"""Provide a mock sensor platform. - -Call init before using it in your tests to ensure clean test data. -""" +"""Common test utilities for sensor entity component tests.""" from homeassistant.components.sensor import ( - DEVICE_CLASSES, RestoreSensor, SensorDeviceClass, SensorEntity, @@ -24,8 +20,6 @@ from homeassistant.const import ( from tests.common import MockEntity -DEVICE_CLASSES.append("none") - UNITS_OF_MEASUREMENT = { SensorDeviceClass.APPARENT_POWER: UnitOfApparentPower.VOLT_AMPERE, # apparent power (VA) SensorDeviceClass.BATTERY: PERCENTAGE, # % of battery that is left @@ -56,34 +50,6 @@ UNITS_OF_MEASUREMENT = { SensorDeviceClass.GAS: UnitOfVolume.CUBIC_METERS, # gas (m³) } -ENTITIES = {} - - -def init(empty=False): - """Initialize the platform with entities.""" - global ENTITIES - - ENTITIES = ( - {} - if empty - else { - device_class: MockSensor( - name=f"{device_class} sensor", - unique_id=f"unique_{device_class}", - device_class=device_class, - native_unit_of_measurement=UNITS_OF_MEASUREMENT.get(device_class), - ) - for device_class in DEVICE_CLASSES - } - ) - - -async def async_setup_platform( - hass, config, async_add_entities_callback, discovery_info=None -): - """Return mock entities.""" - async_add_entities_callback(list(ENTITIES.values())) - class MockSensor(MockEntity, SensorEntity): """Mock Sensor class.""" @@ -141,3 +107,16 @@ class MockRestoreSensor(MockSensor, RestoreSensor): self._values["native_unit_of_measurement"] = ( last_sensor_data.native_unit_of_measurement ) + + +def get_mock_sensor_entities() -> dict[str, MockSensor]: + """Get mock sensor entities.""" + return { + device_class: MockSensor( + name=f"{device_class} sensor", + unique_id=f"unique_{device_class}", + device_class=device_class, + native_unit_of_measurement=UNITS_OF_MEASUREMENT.get(device_class), + ) + for device_class in SensorDeviceClass + } diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index 7263154c1dc..b633c744205 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -26,8 +26,9 @@ from tests.common import ( async_get_device_automation_capabilities, async_get_device_automations, async_mock_service, + setup_test_component_platform, ) -from tests.testing_config.custom_components.test.sensor import UNITS_OF_MEASUREMENT +from tests.components.sensor.common import UNITS_OF_MEASUREMENT, MockSensor @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -85,11 +86,10 @@ async def test_get_conditions( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_sensor_entities: dict[str, MockSensor], ) -> None: """Test we get the expected conditions from a sensor.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_sensor_entities.values()) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() sensor_entries = {} @@ -104,7 +104,7 @@ async def test_get_conditions( sensor_entries[device_class] = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES[device_class].unique_id, + mock_sensor_entities[device_class].unique_id, device_id=device_entry.id, ) @@ -284,6 +284,7 @@ async def test_get_condition_capabilities( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + mock_sensor_entities: dict[str, MockSensor], set_state, device_class_reg, device_class_state, @@ -291,8 +292,7 @@ async def test_get_condition_capabilities( unit_state, ) -> None: """Test we get the expected capabilities from a sensor condition.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_sensor_entities.values()) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -303,7 +303,7 @@ async def test_get_condition_capabilities( entity_id = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["battery"].unique_id, + mock_sensor_entities["battery"].unique_id, device_id=device_entry.id, original_device_class=device_class_reg, unit_of_measurement=unit_reg, @@ -353,6 +353,7 @@ async def test_get_condition_capabilities_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + mock_sensor_entities: dict[str, MockSensor], set_state, device_class_reg, device_class_state, @@ -360,8 +361,7 @@ async def test_get_condition_capabilities_legacy( unit_state, ) -> None: """Test we get the expected capabilities from a sensor condition.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_sensor_entities.values()) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -372,7 +372,7 @@ async def test_get_condition_capabilities_legacy( entity_id = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["battery"].unique_id, + mock_sensor_entities["battery"].unique_id, device_id=device_entry.id, original_device_class=device_class_reg, unit_of_measurement=unit_reg, @@ -417,11 +417,13 @@ async def test_get_condition_capabilities_legacy( async def test_get_condition_capabilities_none( hass: HomeAssistant, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, ) -> None: """Test we get the expected capabilities from a sensor condition.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + entity = MockSensor( + name="none sensor", + unique_id="unique_none", + ) + setup_test_component_platform(hass, DOMAIN, [entity]) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -429,7 +431,7 @@ async def test_get_condition_capabilities_none( entry_none = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["none"].unique_id, + entity.unique_id, ) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 4193adc9299..98bea960fcc 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -30,8 +30,9 @@ from tests.common import ( async_get_device_automation_capabilities, async_get_device_automations, async_mock_service, + setup_test_component_platform, ) -from tests.testing_config.custom_components.test.sensor import UNITS_OF_MEASUREMENT +from tests.components.sensor.common import UNITS_OF_MEASUREMENT, MockSensor @pytest.fixture(autouse=True, name="stub_blueprint_populate") @@ -87,11 +88,10 @@ async def test_get_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, + mock_sensor_entities: dict[str, MockSensor], ) -> None: """Test we get the expected triggers from a sensor.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_sensor_entities.values()) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) await hass.async_block_till_done() sensor_entries: dict[SensorDeviceClass, er.RegistryEntry] = {} @@ -106,7 +106,7 @@ async def test_get_triggers( sensor_entries[device_class] = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES[device_class].unique_id, + mock_sensor_entities[device_class].unique_id, device_id=device_entry.id, ) @@ -241,6 +241,7 @@ async def test_get_trigger_capabilities( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + mock_sensor_entities: dict[str, MockSensor], set_state, device_class_reg, device_class_state, @@ -248,8 +249,7 @@ async def test_get_trigger_capabilities( unit_state, ) -> None: """Test we get the expected capabilities from a sensor trigger.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_sensor_entities) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -260,7 +260,7 @@ async def test_get_trigger_capabilities( entity_id = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["battery"].unique_id, + mock_sensor_entities["battery"].unique_id, device_id=device_entry.id, original_device_class=device_class_reg, unit_of_measurement=unit_reg, @@ -311,6 +311,7 @@ async def test_get_trigger_capabilities_legacy( hass: HomeAssistant, device_registry: dr.DeviceRegistry, entity_registry: er.EntityRegistry, + mock_sensor_entities: dict[str, MockSensor], set_state, device_class_reg, device_class_state, @@ -318,8 +319,7 @@ async def test_get_trigger_capabilities_legacy( unit_state, ) -> None: """Test we get the expected capabilities from a sensor trigger.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + setup_test_component_platform(hass, DOMAIN, mock_sensor_entities) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -330,7 +330,7 @@ async def test_get_trigger_capabilities_legacy( entity_id = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["battery"].unique_id, + mock_sensor_entities["battery"].unique_id, device_id=device_entry.id, original_device_class=device_class_reg, unit_of_measurement=unit_reg, @@ -374,11 +374,13 @@ async def test_get_trigger_capabilities_legacy( async def test_get_trigger_capabilities_none( hass: HomeAssistant, entity_registry: er.EntityRegistry, - enable_custom_integrations: None, ) -> None: """Test we get the expected capabilities from a sensor trigger.""" - platform = getattr(hass.components, f"test.{DOMAIN}") - platform.init() + entity = MockSensor( + name="none sensor", + unique_id="unique_none", + ) + setup_test_component_platform(hass, DOMAIN, [entity]) config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) @@ -386,7 +388,7 @@ async def test_get_trigger_capabilities_none( entry_none = entity_registry.async_get_or_create( DOMAIN, "test", - platform.ENTITIES["none"].unique_id, + entity.unique_id, ) assert await async_setup_component(hass, DOMAIN, {DOMAIN: {CONF_PLATFORM: "test"}}) diff --git a/tests/components/sensor/test_init.py b/tests/components/sensor/test_init.py index 59df07bb0b9..0ecb4b9c60f 100644 --- a/tests/components/sensor/test_init.py +++ b/tests/components/sensor/test_init.py @@ -63,7 +63,9 @@ from tests.common import ( mock_integration, mock_platform, mock_restore_cache_with_extra_data, + setup_test_component_platform, ) +from tests.components.sensor.common import MockRestoreSensor, MockSensor TEST_DOMAIN = "test" @@ -103,7 +105,6 @@ TEST_DOMAIN = "test" ) async def test_temperature_conversion( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, state_unit, @@ -112,16 +113,14 @@ async def test_temperature_conversion( ) -> None: """Test temperature conversion.""" hass.config.units = unit_system - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=str(native_value), native_unit_of_measurement=native_unit, device_class=SensorDeviceClass.TEMPERATURE, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) - entity0 = platform.ENTITIES["0"] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -132,19 +131,17 @@ async def test_temperature_conversion( @pytest.mark.parametrize("device_class", [None, SensorDeviceClass.PRESSURE]) async def test_temperature_conversion_wrong_device_class( - hass: HomeAssistant, device_class, enable_custom_integrations: None + hass: HomeAssistant, device_class ) -> None: """Test temperatures are not converted if the sensor has wrong device class.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value="0.0", native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT, device_class=device_class, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) - entity0 = platform.ENTITIES["0"] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -158,21 +155,19 @@ async def test_temperature_conversion_wrong_device_class( async def test_deprecated_last_reset( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, state_class, ) -> None: """Test warning on deprecated last reset.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", state_class=state_class, last_reset=dt_util.utc_from_timestamp(0) ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() assert ( - "Entity sensor.test () " + "Entity sensor.test () " f"with state_class {state_class} has set last_reset. Setting last_reset for " "entities with state_class other than 'total' is not supported. Please update " "your configuration if state_class is manually configured." @@ -185,7 +180,6 @@ async def test_deprecated_last_reset( async def test_datetime_conversion( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test conversion of datetime.""" test_timestamp = datetime(2017, 12, 19, 18, 29, 42, tzinfo=UTC) @@ -193,51 +187,49 @@ async def test_datetime_conversion( dt_util.get_time_zone("Europe/Amsterdam") ) test_date = date(2017, 12, 19) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( - name="Test", - native_value=test_timestamp, - device_class=SensorDeviceClass.TIMESTAMP, - ) - platform.ENTITIES["1"] = platform.MockSensor( - name="Test", native_value=test_date, device_class=SensorDeviceClass.DATE - ) - platform.ENTITIES["2"] = platform.MockSensor( - name="Test", native_value=None, device_class=SensorDeviceClass.TIMESTAMP - ) - platform.ENTITIES["3"] = platform.MockSensor( - name="Test", native_value=None, device_class=SensorDeviceClass.DATE - ) - platform.ENTITIES["4"] = platform.MockSensor( - name="Test", - native_value=test_local_timestamp, - device_class=SensorDeviceClass.TIMESTAMP, - ) + entities = [ + MockSensor( + name="Test", + native_value=test_timestamp, + device_class=SensorDeviceClass.TIMESTAMP, + ), + MockSensor( + name="Test", native_value=test_date, device_class=SensorDeviceClass.DATE + ), + MockSensor( + name="Test", native_value=None, device_class=SensorDeviceClass.TIMESTAMP + ), + MockSensor(name="Test", native_value=None, device_class=SensorDeviceClass.DATE), + MockSensor( + name="Test", + native_value=test_local_timestamp, + device_class=SensorDeviceClass.TIMESTAMP, + ), + ] + setup_test_component_platform(hass, sensor.DOMAIN, entities) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() - state = hass.states.get(platform.ENTITIES["0"].entity_id) + state = hass.states.get(entities[0].entity_id) assert state.state == test_timestamp.isoformat() - state = hass.states.get(platform.ENTITIES["1"].entity_id) + state = hass.states.get(entities[1].entity_id) assert state.state == test_date.isoformat() - state = hass.states.get(platform.ENTITIES["2"].entity_id) + state = hass.states.get(entities[2].entity_id) assert state.state == STATE_UNKNOWN - state = hass.states.get(platform.ENTITIES["3"].entity_id) + state = hass.states.get(entities[3].entity_id) assert state.state == STATE_UNKNOWN - state = hass.states.get(platform.ENTITIES["4"].entity_id) + state = hass.states.get(entities[4].entity_id) assert state.state == test_timestamp.isoformat() async def test_a_sensor_with_a_non_numeric_device_class( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test that a sensor with a non numeric device class will be non numeric. @@ -249,29 +241,29 @@ async def test_a_sensor_with_a_non_numeric_device_class( dt_util.get_time_zone("Europe/Amsterdam") ) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( - name="Test", - native_value=test_local_timestamp, - native_unit_of_measurement="", - device_class=SensorDeviceClass.TIMESTAMP, - ) - - platform.ENTITIES["1"] = platform.MockSensor( - name="Test", - native_value=test_local_timestamp, - state_class="", - device_class=SensorDeviceClass.TIMESTAMP, - ) + entities = [ + MockSensor( + name="Test", + native_value=test_local_timestamp, + native_unit_of_measurement="", + device_class=SensorDeviceClass.TIMESTAMP, + ), + MockSensor( + name="Test", + native_value=test_local_timestamp, + state_class="", + device_class=SensorDeviceClass.TIMESTAMP, + ), + ] + setup_test_component_platform(hass, sensor.DOMAIN, entities) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() - state = hass.states.get(platform.ENTITIES["0"].entity_id) + state = hass.states.get(entities[0].entity_id) assert state.state == test_timestamp.isoformat() - state = hass.states.get(platform.ENTITIES["1"].entity_id) + state = hass.states.get(entities[1].entity_id) assert state.state == test_timestamp.isoformat() @@ -285,17 +277,15 @@ async def test_a_sensor_with_a_non_numeric_device_class( async def test_deprecated_datetime_str( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, device_class, state_value, provides, ) -> None: """Test warning on deprecated str for a date(time) value.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=state_value, device_class=device_class ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -309,17 +299,15 @@ async def test_deprecated_datetime_str( async def test_reject_timezoneless_datetime_str( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test rejection of timezone-less datetime objects as timestamp.""" test_timestamp = datetime(2017, 12, 19, 18, 29, 42, tzinfo=None) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=test_timestamp, device_class=SensorDeviceClass.TIMESTAMP, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -403,7 +391,6 @@ RESTORE_DATA = { ) async def test_restore_sensor_save_state( hass: HomeAssistant, - enable_custom_integrations: None, hass_storage: dict[str, Any], native_value, native_value_type, @@ -412,16 +399,14 @@ async def test_restore_sensor_save_state( uom, ) -> None: """Test RestoreSensor.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockRestoreSensor( + entity0 = MockRestoreSensor( name="Test", native_value=native_value, native_unit_of_measurement=uom, device_class=device_class, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) - entity0 = platform.ENTITIES["0"] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -472,7 +457,6 @@ async def test_restore_sensor_save_state( ) async def test_restore_sensor_restore_state( hass: HomeAssistant, - enable_custom_integrations: None, hass_storage: dict[str, Any], native_value, native_value_type, @@ -483,14 +467,12 @@ async def test_restore_sensor_restore_state( """Test RestoreSensor.""" mock_restore_cache_with_extra_data(hass, ((State("sensor.test", ""), extra_data),)) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockRestoreSensor( + entity0 = MockRestoreSensor( name="Test", device_class=device_class, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) - entity0 = platform.ENTITIES["0"] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -621,7 +603,6 @@ async def test_restore_sensor_restore_state( ) async def test_custom_unit( hass: HomeAssistant, - enable_custom_integrations: None, device_class, native_unit, custom_unit, @@ -638,17 +619,15 @@ async def test_custom_unit( ) await hass.async_block_till_done() - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=str(native_value), native_unit_of_measurement=native_unit, device_class=device_class, unique_id="very_unique", ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) - entity0 = platform.ENTITIES["0"] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -884,7 +863,6 @@ async def test_custom_unit( ) async def test_custom_unit_change( hass: HomeAssistant, - enable_custom_integrations: None, native_unit, custom_unit, state_unit, @@ -895,17 +873,15 @@ async def test_custom_unit_change( ) -> None: """Test custom unit changes are picked up.""" entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=str(native_value), native_unit_of_measurement=native_unit, device_class=device_class, unique_id="very_unique", ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) - entity0 = platform.ENTITIES["0"] assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -972,7 +948,6 @@ async def test_custom_unit_change( ) async def test_unit_conversion_priority( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, automatic_unit, @@ -990,27 +965,21 @@ async def test_unit_conversion_priority( hass.config.units = unit_system entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] - - platform.ENTITIES["1"] = platform.MockSensor( + entity1 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), ) - entity1 = platform.ENTITIES["1"] - - platform.ENTITIES["2"] = platform.MockSensor( + entity2 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1018,16 +987,23 @@ async def test_unit_conversion_priority( suggested_unit_of_measurement=suggested_unit, unique_id="very_unique_2", ) - entity2 = platform.ENTITIES["2"] - - platform.ENTITIES["3"] = platform.MockSensor( + entity3 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), suggested_unit_of_measurement=suggested_unit, ) - entity3 = platform.ENTITIES["3"] + setup_test_component_platform( + hass, + sensor.DOMAIN, + [ + entity0, + entity1, + entity2, + entity3, + ], + ) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1119,7 +1095,6 @@ async def test_unit_conversion_priority( ) async def test_unit_conversion_priority_precision( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, automatic_unit, @@ -1138,10 +1113,8 @@ async def test_unit_conversion_priority_precision( hass.config.units = unit_system entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1149,18 +1122,14 @@ async def test_unit_conversion_priority_precision( suggested_display_precision=suggested_precision, unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] - - platform.ENTITIES["1"] = platform.MockSensor( + entity1 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), suggested_display_precision=suggested_precision, ) - entity1 = platform.ENTITIES["1"] - - platform.ENTITIES["2"] = platform.MockSensor( + entity2 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1169,9 +1138,7 @@ async def test_unit_conversion_priority_precision( suggested_unit_of_measurement=suggested_unit, unique_id="very_unique_2", ) - entity2 = platform.ENTITIES["2"] - - platform.ENTITIES["3"] = platform.MockSensor( + entity3 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1179,7 +1146,16 @@ async def test_unit_conversion_priority_precision( suggested_display_precision=suggested_precision, suggested_unit_of_measurement=suggested_unit, ) - entity3 = platform.ENTITIES["3"] + setup_test_component_platform( + hass, + sensor.DOMAIN, + [ + entity0, + entity1, + entity2, + entity3, + ], + ) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1280,7 +1256,6 @@ async def test_unit_conversion_priority_precision( ) async def test_unit_conversion_priority_suggested_unit_change( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, original_unit, @@ -1294,8 +1269,6 @@ async def test_unit_conversion_priority_suggested_unit_change( hass.config.units = unit_system entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) # Pre-register entities entry = entity_registry.async_get_or_create( @@ -1315,16 +1288,14 @@ async def test_unit_conversion_priority_suggested_unit_change( {"suggested_unit_of_measurement": original_unit}, ) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] - - platform.ENTITIES["1"] = platform.MockSensor( + entity1 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1332,7 +1303,7 @@ async def test_unit_conversion_priority_suggested_unit_change( suggested_unit_of_measurement=suggested_unit, unique_id="very_unique_2", ) - entity1 = platform.ENTITIES["1"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0, entity1]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1392,7 +1363,6 @@ async def test_unit_conversion_priority_suggested_unit_change( ) async def test_unit_conversion_priority_suggested_unit_change_2( hass: HomeAssistant, - enable_custom_integrations: None, native_unit_1, native_unit_2, suggested_unit, @@ -1405,8 +1375,6 @@ async def test_unit_conversion_priority_suggested_unit_change_2( hass.config.units = METRIC_SYSTEM entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) # Pre-register entities entity_registry.async_get_or_create( @@ -1416,16 +1384,14 @@ async def test_unit_conversion_priority_suggested_unit_change_2( "sensor", "test", "very_unique_2", unit_of_measurement=native_unit_1 ) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit_2, native_value=str(native_value), unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] - - platform.ENTITIES["1"] = platform.MockSensor( + entity1 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit_2, @@ -1433,7 +1399,7 @@ async def test_unit_conversion_priority_suggested_unit_change_2( suggested_unit_of_measurement=suggested_unit, unique_id="very_unique_2", ) - entity1 = platform.ENTITIES["1"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0, entity1]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1496,7 +1462,6 @@ async def test_unit_conversion_priority_suggested_unit_change_2( ) async def test_suggested_precision_option( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, integration_suggested_precision, @@ -1510,10 +1475,7 @@ async def test_suggested_precision_option( hass.config.units = unit_system entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1521,7 +1483,7 @@ async def test_suggested_precision_option( suggested_display_precision=integration_suggested_precision, unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1574,7 +1536,6 @@ async def test_suggested_precision_option( ) async def test_suggested_precision_option_update( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, suggested_unit, @@ -1590,8 +1551,6 @@ async def test_suggested_precision_option_update( hass.config.units = unit_system entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) # Pre-register entities entry = entity_registry.async_get_or_create("sensor", "test", "very_unique") @@ -1610,7 +1569,7 @@ async def test_suggested_precision_option_update( }, ) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, @@ -1618,7 +1577,7 @@ async def test_suggested_precision_option_update( suggested_display_precision=new_precision, unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1666,7 +1625,6 @@ async def test_suggested_precision_option_update( ) async def test_unit_conversion_priority_legacy_conversion_removed( hass: HomeAssistant, - enable_custom_integrations: None, unit_system, native_unit, original_unit, @@ -1679,22 +1637,20 @@ async def test_unit_conversion_priority_legacy_conversion_removed( hass.config.units = unit_system entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) # Pre-register entities entity_registry.async_get_or_create( "sensor", "test", "very_unique", unit_of_measurement=original_unit ) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=native_unit, native_value=str(native_value), unique_id="very_unique", ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1715,17 +1671,15 @@ def test_device_classes_aligned() -> None: async def test_value_unknown_in_enumeration( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test warning on invalid enum value.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value="invalid_option", device_class=SensorDeviceClass.ENUM, options=["option1", "option2"], ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1739,17 +1693,15 @@ async def test_value_unknown_in_enumeration( async def test_invalid_enumeration_entity_with_device_class( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test warning on entities that provide an enum with a device class.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=21, device_class=SensorDeviceClass.POWER, options=["option1", "option2"], ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1763,16 +1715,14 @@ async def test_invalid_enumeration_entity_with_device_class( async def test_invalid_enumeration_entity_without_device_class( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test warning on entities that provide an enum without a device class.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=21, options=["option1", "option2"], ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1794,19 +1744,17 @@ async def test_invalid_enumeration_entity_without_device_class( async def test_non_numeric_device_class_with_unit_of_measurement( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, device_class: SensorDeviceClass, ) -> None: """Test error on numeric entities that provide an unit of measurement.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=None, device_class=device_class, native_unit_of_measurement=UnitOfTemperature.CELSIUS, options=["option1", "option2"], ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1869,18 +1817,16 @@ async def test_non_numeric_device_class_with_unit_of_measurement( async def test_device_classes_with_invalid_unit_of_measurement( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, device_class: SensorDeviceClass, ) -> None: """Test error when unit of measurement is not valid for used device class.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value="1.0", device_class=device_class, native_unit_of_measurement="INVALID!", ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) units = [ str(unit) if unit else "no unit of measurement" for unit in DEVICE_CLASS_UNITS.get(device_class, set()) @@ -1920,7 +1866,6 @@ async def test_device_classes_with_invalid_unit_of_measurement( async def test_non_numeric_validation_error( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, native_value: Any, problem: str, device_class: SensorDeviceClass | None, @@ -1928,16 +1873,14 @@ async def test_non_numeric_validation_error( unit: str | None, ) -> None: """Test error on expected numeric entities.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=native_value, device_class=device_class, native_unit_of_measurement=unit, state_class=state_class, ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -1966,7 +1909,6 @@ async def test_non_numeric_validation_error( async def test_non_numeric_validation_raise( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, native_value: Any, expected: str, device_class: SensorDeviceClass | None, @@ -1975,9 +1917,7 @@ async def test_non_numeric_validation_raise( precision, ) -> None: """Test error on expected numeric entities.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", device_class=device_class, native_unit_of_measurement=unit, @@ -1985,7 +1925,7 @@ async def test_non_numeric_validation_raise( state_class=state_class, suggested_display_precision=precision, ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -2018,7 +1958,6 @@ async def test_non_numeric_validation_raise( async def test_numeric_validation( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, native_value: Any, expected: str, device_class: SensorDeviceClass | None, @@ -2026,16 +1965,14 @@ async def test_numeric_validation( unit: str | None, ) -> None: """Test does not error on expected numeric entities.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=native_value, device_class=device_class, native_unit_of_measurement=unit, state_class=state_class, ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -2052,18 +1989,15 @@ async def test_numeric_validation( async def test_numeric_validation_ignores_custom_device_class( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, ) -> None: """Test does not error on expected numeric entities.""" native_value = "Three elephants" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=native_value, device_class="custom__deviceclass", ) - entity0 = platform.ENTITIES["0"] + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -2084,18 +2018,16 @@ async def test_numeric_validation_ignores_custom_device_class( async def test_device_classes_with_invalid_state_class( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, device_class: SensorDeviceClass, ) -> None: """Test error when unit of measurement is not valid for used device class.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=None, state_class="INVALID!", device_class=device_class, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -2133,7 +2065,6 @@ async def test_device_classes_with_invalid_state_class( async def test_numeric_state_expected_helper( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - enable_custom_integrations: None, device_class: SensorDeviceClass | None, state_class: SensorStateClass | None, native_unit_of_measurement: str | None, @@ -2141,9 +2072,7 @@ async def test_numeric_state_expected_helper( is_numeric: bool, ) -> None: """Test numeric_state_expected helper.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( name="Test", native_value=None, device_class=device_class, @@ -2151,11 +2080,11 @@ async def test_numeric_state_expected_helper( native_unit_of_measurement=native_unit_of_measurement, suggested_display_precision=suggested_precision, ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() - entity0 = platform.ENTITIES["0"] state = hass.states.get(entity0.entity_id) assert state is not None @@ -2199,7 +2128,6 @@ async def test_numeric_state_expected_helper( ) async def test_unit_conversion_update( hass: HomeAssistant, - enable_custom_integrations: None, unit_system_1, unit_system_2, native_unit, @@ -2219,9 +2147,8 @@ async def test_unit_conversion_update( hass.config.units = unit_system_1 entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - entity0 = platform.MockSensor( + entity0 = MockSensor( name="Test 0", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2229,7 +2156,7 @@ async def test_unit_conversion_update( unique_id="very_unique", ) - entity1 = platform.MockSensor( + entity1 = MockSensor( name="Test 1", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2237,7 +2164,7 @@ async def test_unit_conversion_update( unique_id="very_unique_1", ) - entity2 = platform.MockSensor( + entity2 = MockSensor( name="Test 2", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2246,7 +2173,7 @@ async def test_unit_conversion_update( unique_id="very_unique_2", ) - entity3 = platform.MockSensor( + entity3 = MockSensor( name="Test 3", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2255,7 +2182,7 @@ async def test_unit_conversion_update( unique_id="very_unique_3", ) - entity4 = platform.MockSensor( + entity4 = MockSensor( name="Test 4", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2544,11 +2471,8 @@ async def test_entity_category_config_raises_error( caplog: pytest.LogCaptureFixture, ) -> None: """Test error is raised when entity category is set to config.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( - name="Test", entity_category=EntityCategory.CONFIG - ) + entity0 = MockSensor(name="Test", entity_category=EntityCategory.CONFIG) + setup_test_component_platform(hass, sensor.DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -2644,13 +2568,11 @@ async def test_suggested_unit_guard_invalid_unit( An invalid suggested unit creates a log entry and the suggested unit will be ignored. """ entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) state_value = 10 invalid_suggested_unit = "invalid_unit" - entity = platform.ENTITIES["0"] = platform.MockSensor( + entity = MockSensor( name="Invalid", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2658,6 +2580,7 @@ async def test_suggested_unit_guard_invalid_unit( native_value=str(state_value), unique_id="invalid", ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() @@ -2674,10 +2597,10 @@ async def test_suggested_unit_guard_invalid_unit( "homeassistant.components.sensor", logging.WARNING, ( - " sets an" - " invalid suggested_unit_of_measurement. Please report it to the author" - " of the 'test' custom integration. This warning will become an error in" - " Home Assistant Core 2024.5" + " sets an" + " invalid suggested_unit_of_measurement. Please create a bug report at " + "https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+test%22." + " This warning will become an error in Home Assistant Core 2024.5" ), ) in caplog.record_tuples @@ -2715,10 +2638,8 @@ async def test_suggested_unit_guard_valid_unit( in the entity registry. """ entity_registry = er.async_get(hass) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - entity = platform.ENTITIES["0"] = platform.MockSensor( + entity = MockSensor( name="Valid", device_class=device_class, native_unit_of_measurement=native_unit, @@ -2726,6 +2647,7 @@ async def test_suggested_unit_guard_valid_unit( suggested_unit_of_measurement=suggested_unit, unique_id="valid", ) + setup_test_component_platform(hass, sensor.DOMAIN, [entity]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py index 40b38b2e57a..8084fe69e89 100644 --- a/tests/components/sensor/test_recorder.py +++ b/tests/components/sensor/test_recorder.py @@ -33,13 +33,14 @@ from homeassistant.components.recorder.statistics import ( list_statistic_ids, ) from homeassistant.components.recorder.util import get_instance, session_scope -from homeassistant.components.sensor import ATTR_OPTIONS, SensorDeviceClass +from homeassistant.components.sensor import ATTR_OPTIONS, DOMAIN, SensorDeviceClass from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant, State from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM +from tests.common import setup_test_component_platform from tests.components.recorder.common import ( assert_dict_of_states_equal_without_context_and_last_changed, assert_multiple_states_equal_without_context_and_last_changed, @@ -49,6 +50,7 @@ from tests.components.recorder.common import ( statistics_during_period, wait_recording_done, ) +from tests.components.sensor.common import MockSensor from tests.typing import WebSocketGenerator BATTERY_SENSOR_ATTRIBUTES = { @@ -1363,11 +1365,9 @@ def test_compile_hourly_sum_statistics_negative_state( hass = hass_recorder() hass.data.pop(loader.DATA_CUSTOM_COMPONENTS) - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - mocksensor = platform.MockSensor(name="custom_sensor") + mocksensor = MockSensor(name="custom_sensor") mocksensor._attr_should_poll = False - platform.ENTITIES["custom_sensor"] = mocksensor + setup_test_component_platform(hass, DOMAIN, [mocksensor], built_in=False) setup_component(hass, "homeassistant", {}) setup_component( @@ -5178,9 +5178,7 @@ async def test_exclude_attributes( recorder_mock: Recorder, hass: HomeAssistant, enable_custom_integrations: None ) -> None: """Test sensor attributes to be excluded.""" - platform = getattr(hass.components, "test.sensor") - platform.init(empty=True) - platform.ENTITIES["0"] = platform.MockSensor( + entity0 = MockSensor( has_entity_name=True, unique_id="test", name="Test", @@ -5188,6 +5186,7 @@ async def test_exclude_attributes( device_class=SensorDeviceClass.ENUM, options=["option1", "option2"], ) + setup_test_component_platform(hass, DOMAIN, [entity0]) assert await async_setup_component(hass, "sensor", {"sensor": {"platform": "test"}}) await hass.async_block_till_done() await async_wait_recording_done(hass) From d983fa6da7fd32aa64cbf633d0f70de0da11202a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Fern=C3=A1ndez=20Rojas?= Date: Thu, 4 Apr 2024 09:05:08 +0200 Subject: [PATCH 104/198] Update aioairzone-cloud to v0.4.7 (#114761) --- homeassistant/components/airzone_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/airzone_cloud/manifest.json b/homeassistant/components/airzone_cloud/manifest.json index 14f02620c91..b4445f6fe45 100644 --- a/homeassistant/components/airzone_cloud/manifest.json +++ b/homeassistant/components/airzone_cloud/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/airzone_cloud", "iot_class": "cloud_push", "loggers": ["aioairzone_cloud"], - "requirements": ["aioairzone-cloud==0.4.6"] + "requirements": ["aioairzone-cloud==0.4.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 76dc587d6b9..d7b500abdd3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -185,7 +185,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.4.6 +aioairzone-cloud==0.4.7 # homeassistant.components.airzone aioairzone==0.7.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6f329b782aa..82aae896b59 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -164,7 +164,7 @@ aio-georss-gdacs==0.9 aioairq==0.3.2 # homeassistant.components.airzone_cloud -aioairzone-cloud==0.4.6 +aioairzone-cloud==0.4.7 # homeassistant.components.airzone aioairzone==0.7.6 From 25289e0ca19c9a9382dda561854d5978b244e399 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Fri, 5 Apr 2024 02:55:39 +0200 Subject: [PATCH 105/198] Bump myuplink dependency to 0.6.0 (#114767) --- homeassistant/components/myuplink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/myuplink/manifest.json b/homeassistant/components/myuplink/manifest.json index a76f596ade3..0e638a72715 100644 --- a/homeassistant/components/myuplink/manifest.json +++ b/homeassistant/components/myuplink/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/myuplink", "iot_class": "cloud_polling", - "requirements": ["myuplink==0.5.0"] + "requirements": ["myuplink==0.6.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index d7b500abdd3..257dea984e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1349,7 +1349,7 @@ mutesync==0.0.1 mypermobil==0.1.8 # homeassistant.components.myuplink -myuplink==0.5.0 +myuplink==0.6.0 # homeassistant.components.nad nad-receiver==0.3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 82aae896b59..4dcea7c582d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1088,7 +1088,7 @@ mutesync==0.0.1 mypermobil==0.1.8 # homeassistant.components.myuplink -myuplink==0.5.0 +myuplink==0.6.0 # homeassistant.components.keenetic_ndms2 ndms2-client==0.1.2 From 450be674069530904862168ca2078ed78f8c6c29 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 28 Mar 2024 13:56:08 +0100 Subject: [PATCH 106/198] Update romy to 0.0.9 (#114360) --- homeassistant/components/romy/manifest.json | 2 +- pyproject.toml | 2 -- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/romy/manifest.json b/homeassistant/components/romy/manifest.json index 1257c2d1d60..7e30c418599 100644 --- a/homeassistant/components/romy/manifest.json +++ b/homeassistant/components/romy/manifest.json @@ -5,6 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/romy", "iot_class": "local_polling", - "requirements": ["romy==0.0.7"], + "requirements": ["romy==0.0.9"], "zeroconf": ["_aicu-http._tcp.local."] } diff --git a/pyproject.toml b/pyproject.toml index e0f07fac6b6..ac890603ac3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -513,8 +513,6 @@ filterwarnings = [ "ignore:invalid escape sequence:SyntaxWarning:.*stringcase", # https://github.com/pyudev/pyudev/pull/466 - >=0.24.0 "ignore:invalid escape sequence:SyntaxWarning:.*pyudev.monitor", - # https://github.com/xeniter/romy/pull/1 - >=0.0.8 - "ignore:with timeout\\(\\) is deprecated, use async with timeout\\(\\) instead:DeprecationWarning:romy.utils", # https://github.com/grahamwetzler/smart-meter-texas/pull/143 - >0.5.3 "ignore:ssl.OP_NO_SSL\\*/ssl.OP_NO_TLS\\* options are deprecated:DeprecationWarning:smart_meter_texas", # https://github.com/mvantellingen/python-zeep/pull/1364 - >4.2.1 diff --git a/requirements_all.txt b/requirements_all.txt index 257dea984e6..a8f9e5877d8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2459,7 +2459,7 @@ rocketchat-API==0.6.1 rokuecp==0.19.2 # homeassistant.components.romy -romy==0.0.7 +romy==0.0.9 # homeassistant.components.roomba roombapy==1.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4dcea7c582d..34d4b8e8644 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1893,7 +1893,7 @@ ring-doorbell[listen]==0.8.9 rokuecp==0.19.2 # homeassistant.components.romy -romy==0.0.7 +romy==0.0.9 # homeassistant.components.roomba roombapy==1.8.1 From a39e1a6428a09d331b32d64360f12d423367ed5e Mon Sep 17 00:00:00 2001 From: Manuel Dipolt Date: Thu, 4 Apr 2024 00:48:35 +0200 Subject: [PATCH 107/198] Update romy to 0.0.10 (#114785) --- homeassistant/components/romy/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/romy/manifest.json b/homeassistant/components/romy/manifest.json index 7e30c418599..efb8072ebbc 100644 --- a/homeassistant/components/romy/manifest.json +++ b/homeassistant/components/romy/manifest.json @@ -5,6 +5,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/romy", "iot_class": "local_polling", - "requirements": ["romy==0.0.9"], + "requirements": ["romy==0.0.10"], "zeroconf": ["_aicu-http._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index a8f9e5877d8..9a28576d836 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2459,7 +2459,7 @@ rocketchat-API==0.6.1 rokuecp==0.19.2 # homeassistant.components.romy -romy==0.0.9 +romy==0.0.10 # homeassistant.components.roomba roombapy==1.8.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 34d4b8e8644..673d47d02a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1893,7 +1893,7 @@ ring-doorbell[listen]==0.8.9 rokuecp==0.19.2 # homeassistant.components.romy -romy==0.0.9 +romy==0.0.10 # homeassistant.components.roomba roombapy==1.8.1 From ef8e54877fb6283f8104e7c4f917c3fcf5090ba5 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 3 Apr 2024 18:20:20 -0600 Subject: [PATCH 108/198] Fix unhandled `KeyError` during Notion setup (#114787) --- homeassistant/components/notion/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index ca45e3a6d16..1793a0cfd47 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -108,7 +108,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: (CONF_REFRESH_TOKEN, client.refresh_token), (CONF_USER_UUID, client.user_uuid), ): - if entry.data[key] == value: + if entry.data.get(key) == value: continue entry_updates["data"][key] = value From 3d0bafbdc95b2514ffff276b5fc5354a3e54d772 Mon Sep 17 00:00:00 2001 From: cdheiser <10488026+cdheiser@users.noreply.github.com> Date: Thu, 4 Apr 2024 02:24:02 -0700 Subject: [PATCH 109/198] Fix Lutron light brightness values (#114794) Fix brightness values in light.py Bugfix to set the brightness to 0-100 which is what Lutron expects. --- homeassistant/components/lutron/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lutron/light.py b/homeassistant/components/lutron/light.py index 18b5edd1039..eb003fd431a 100644 --- a/homeassistant/components/lutron/light.py +++ b/homeassistant/components/lutron/light.py @@ -141,7 +141,7 @@ class LutronLight(LutronDevice, LightEntity): else: brightness = self._prev_brightness self._prev_brightness = brightness - args = {"new_level": brightness} + args = {"new_level": to_lutron_level(brightness)} if ATTR_TRANSITION in kwargs: args["fade_time_seconds"] = kwargs[ATTR_TRANSITION] self._lutron_device.set_level(**args) From d8ae7d6955ae199589d57de6fda9e2199e6c7c38 Mon Sep 17 00:00:00 2001 From: Lex Li <425130+lextm@users.noreply.github.com> Date: Fri, 5 Apr 2024 02:41:15 -0400 Subject: [PATCH 110/198] Fix type cast in snmp (#114795) --- homeassistant/components/snmp/sensor.py | 2 +- tests/components/snmp/test_negative_sensor.py | 79 +++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 tests/components/snmp/test_negative_sensor.py diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index f55cd07effb..972b9131935 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -270,7 +270,7 @@ class SnmpData: "SNMP OID %s received type=%s and data %s", self._baseoid, type(value), - bytes(value), + value, ) if isinstance(value, NoSuchObject): _LOGGER.error( diff --git a/tests/components/snmp/test_negative_sensor.py b/tests/components/snmp/test_negative_sensor.py new file mode 100644 index 00000000000..c5ac6460841 --- /dev/null +++ b/tests/components/snmp/test_negative_sensor.py @@ -0,0 +1,79 @@ +"""SNMP sensor tests.""" + +from unittest.mock import patch + +from pysnmp.hlapi import Integer32 +import pytest + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + + +@pytest.fixture(autouse=True) +def hlapi_mock(): + """Mock out 3rd party API.""" + mock_data = Integer32(-13) + with patch( + "homeassistant.components.snmp.sensor.getCmd", + return_value=(None, None, None, [[mock_data]]), + ): + yield + + +async def test_basic_config(hass: HomeAssistant) -> None: + """Test basic entity configuration.""" + + config = { + SENSOR_DOMAIN: { + "platform": "snmp", + "host": "192.168.1.32", + "baseoid": "1.3.6.1.4.1.2021.10.1.3.1", + }, + } + + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("sensor.snmp") + assert state.state == "-13" + assert state.attributes == {"friendly_name": "SNMP"} + + +async def test_entity_config(hass: HomeAssistant) -> None: + """Test entity configuration.""" + + config = { + SENSOR_DOMAIN: { + # SNMP configuration + "platform": "snmp", + "host": "192.168.1.32", + "baseoid": "1.3.6.1.4.1.2021.10.1.3.1", + # Entity configuration + "icon": "{{'mdi:one_two_three'}}", + "picture": "{{'blabla.png'}}", + "device_class": "temperature", + "name": "{{'SNMP' + ' ' + 'Sensor'}}", + "state_class": "measurement", + "unique_id": "very_unique", + "unit_of_measurement": "°C", + }, + } + + assert await async_setup_component(hass, SENSOR_DOMAIN, config) + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + assert entity_registry.async_get("sensor.snmp_sensor").unique_id == "very_unique" + + state = hass.states.get("sensor.snmp_sensor") + assert state.state == "-13" + assert state.attributes == { + "device_class": "temperature", + "entity_picture": "blabla.png", + "friendly_name": "SNMP Sensor", + "icon": "mdi:one_two_three", + "state_class": "measurement", + "unit_of_measurement": "°C", + } From 530725bbfa0274a04b2b0aed72e2592843e0da02 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Apr 2024 20:42:57 -1000 Subject: [PATCH 111/198] Handle ambiguous script actions by using action map order (#114825) --- homeassistant/helpers/config_validation.py | 6 ++++++ tests/helpers/test_config_validation.py | 22 ++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index f7245607be7..70de144d5c8 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -1855,6 +1855,12 @@ def determine_script_action(action: dict[str, Any]) -> str: """Determine action type.""" if not (actions := ACTIONS_SET.intersection(action)): raise ValueError("Unable to determine action") + if len(actions) > 1: + # Ambiguous action, select the first one in the + # order of the ACTIONS_MAP + for action_key, _script_action in ACTIONS_MAP.items(): + if action_key in actions: + return _script_action return ACTIONS_MAP[actions.pop()] diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 133e5e80442..9816dc38189 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -1672,3 +1672,25 @@ def test_color_hex() -> None: with pytest.raises(vol.Invalid, match=msg): cv.color_hex(123456) + + +def test_determine_script_action_ambiguous(): + """Test determine script action with ambiguous actions.""" + assert ( + cv.determine_script_action( + { + "type": "is_power", + "condition": "device", + "device_id": "9c2bda81bc7997c981f811c32cafdb22", + "entity_id": "2ee287ec70dd0c6db187b539bee429b7", + "domain": "sensor", + "below": "15", + } + ) + == "condition" + ) + + +def test_determine_script_action_non_ambiguous(): + """Test determine script action with a non ambiguous action.""" + assert cv.determine_script_action({"delay": "00:00:05"}) == "delay" From 319f76cdc844cb369778f7e3a55527ba5253aa1a Mon Sep 17 00:00:00 2001 From: tronikos Date: Thu, 4 Apr 2024 13:06:15 -0700 Subject: [PATCH 112/198] Bump opower to 0.4.3 (#114826) Co-authored-by: Joost Lekkerkerker --- homeassistant/components/opower/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opower/manifest.json b/homeassistant/components/opower/manifest.json index 879aeb0327b..51ad669733b 100644 --- a/homeassistant/components/opower/manifest.json +++ b/homeassistant/components/opower/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/opower", "iot_class": "cloud_polling", "loggers": ["opower"], - "requirements": ["opower==0.4.2"] + "requirements": ["opower==0.4.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9a28576d836..ff3b0a8068e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1482,7 +1482,7 @@ openwrt-luci-rpc==1.1.17 openwrt-ubus-rpc==0.0.2 # homeassistant.components.opower -opower==0.4.2 +opower==0.4.3 # homeassistant.components.oralb oralb-ble==0.17.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 673d47d02a5..56703105b7d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1176,7 +1176,7 @@ openerz-api==0.3.0 openhomedevice==2.2.0 # homeassistant.components.opower -opower==0.4.2 +opower==0.4.3 # homeassistant.components.oralb oralb-ble==0.17.6 From 0191d3e41b3b655d937aeac92a5c7f9a534f2111 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Apr 2024 09:30:10 -1000 Subject: [PATCH 113/198] Refactor ConfigStore to avoid needing to pass config_dir (#114827) Co-authored-by: Erik --- homeassistant/core.py | 16 +++++++++++----- homeassistant/helpers/storage.py | 13 +++++-------- tests/test_core.py | 3 +++ 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index 4794b284fd2..d4510e970f9 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -401,6 +401,7 @@ class HomeAssistant: self.services = ServiceRegistry(self) self.states = StateMachine(self.bus, self.loop) self.config = Config(self, config_dir) + self.config.async_initialize() self.components = loader.Components(self) self.helpers = loader.Helpers(self) self.state: CoreState = CoreState.not_running @@ -2589,12 +2590,12 @@ class ServiceRegistry: class Config: """Configuration settings for Home Assistant.""" + _store: Config._ConfigStore + def __init__(self, hass: HomeAssistant, config_dir: str) -> None: """Initialize a new config object.""" self.hass = hass - self._store = self._ConfigStore(self.hass, config_dir) - self.latitude: float = 0 self.longitude: float = 0 @@ -2645,6 +2646,13 @@ class Config: # If Home Assistant is running in safe mode self.safe_mode: bool = False + def async_initialize(self) -> None: + """Finish initializing a config object. + + This must be called before the config object is used. + """ + self._store = self._ConfigStore(self.hass) + def distance(self, lat: float, lon: float) -> float | None: """Calculate distance from Home Assistant. @@ -2850,7 +2858,6 @@ class Config: "country": self.country, "language": self.language, } - await self._store.async_save(data) # Circular dependency prevents us from generating the class at top level @@ -2860,7 +2867,7 @@ class Config: class _ConfigStore(Store[dict[str, Any]]): """Class to help storing Config data.""" - def __init__(self, hass: HomeAssistant, config_dir: str) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize storage class.""" super().__init__( hass, @@ -2869,7 +2876,6 @@ class Config: private=True, atomic_writes=True, minor_version=CORE_STORAGE_MINOR_VERSION, - config_dir=config_dir, ) self._original_unit_system: str | None = None # from old store 1.1 diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 2413a53e605..92a31ae9345 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -95,9 +95,7 @@ async def async_migrator( return config -def get_internal_store_manager( - hass: HomeAssistant, config_dir: str | None = None -) -> _StoreManager: +def get_internal_store_manager(hass: HomeAssistant) -> _StoreManager: """Get the store manager. This function is not part of the API and should only be @@ -105,7 +103,7 @@ def get_internal_store_manager( guaranteed to be stable. """ if STORAGE_MANAGER not in hass.data: - manager = _StoreManager(hass, config_dir or hass.config.config_dir) + manager = _StoreManager(hass) hass.data[STORAGE_MANAGER] = manager return hass.data[STORAGE_MANAGER] @@ -116,13 +114,13 @@ class _StoreManager: The store manager is used to cache and manage storage files. """ - def __init__(self, hass: HomeAssistant, config_dir: str) -> None: + def __init__(self, hass: HomeAssistant) -> None: """Initialize storage manager class.""" self._hass = hass self._invalidated: set[str] = set() self._files: set[str] | None = None self._data_preload: dict[str, json_util.JsonValueType] = {} - self._storage_path: Path = Path(config_dir).joinpath(STORAGE_DIR) + self._storage_path: Path = Path(hass.config.config_dir).joinpath(STORAGE_DIR) self._cancel_cleanup: asyncio.TimerHandle | None = None async def async_initialize(self) -> None: @@ -251,7 +249,6 @@ class Store(Generic[_T]): encoder: type[JSONEncoder] | None = None, minor_version: int = 1, read_only: bool = False, - config_dir: str | None = None, ) -> None: """Initialize storage class.""" self.version = version @@ -268,7 +265,7 @@ class Store(Generic[_T]): self._atomic_writes = atomic_writes self._read_only = read_only self._next_write_time = 0.0 - self._manager = get_internal_store_manager(hass, config_dir) + self._manager = get_internal_store_manager(hass) @cached_property def path(self): diff --git a/tests/test_core.py b/tests/test_core.py index a0a197096cd..905d8efe6de 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -2288,6 +2288,7 @@ async def test_additional_data_in_core_config( ) -> None: """Test that we can handle additional data in core configuration.""" config = ha.Config(hass, "/test/ha-config") + config.async_initialize() hass_storage[ha.CORE_STORAGE_KEY] = { "version": 1, "data": {"location_name": "Test Name", "additional_valid_key": "value"}, @@ -2301,6 +2302,7 @@ async def test_incorrect_internal_external_url( ) -> None: """Test that we warn when detecting invalid internal/external url.""" config = ha.Config(hass, "/test/ha-config") + config.async_initialize() hass_storage[ha.CORE_STORAGE_KEY] = { "version": 1, @@ -2314,6 +2316,7 @@ async def test_incorrect_internal_external_url( assert "Invalid internal_url set" not in caplog.text config = ha.Config(hass, "/test/ha-config") + config.async_initialize() hass_storage[ha.CORE_STORAGE_KEY] = { "version": 1, From aa14793479a4f481de3508ddd49189fee1d8cfe9 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Thu, 4 Apr 2024 13:45:44 +0200 Subject: [PATCH 114/198] Avoid blocking IO in downloader initialization (#114841) * Avoid blocking IO in downloader initialization * Avoid blocking IO in downloader initialization --- homeassistant/components/downloader/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index 94d243e2cf2..3ca503a2167 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -83,7 +83,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if not os.path.isabs(download_path): download_path = hass.config.path(download_path) - if not os.path.isdir(download_path): + if not await hass.async_add_executor_job(os.path.isdir, download_path): _LOGGER.error( "Download path %s does not exist. File Downloader not active", download_path ) From 58533f02af7c612abdc51ce7ebcda20093ffe267 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 5 Apr 2024 11:32:59 +0200 Subject: [PATCH 115/198] Fix Downloader YAML import (#114844) --- .../components/downloader/__init__.py | 10 +++- tests/components/downloader/test_init.py | 51 +++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 tests/components/downloader/test_init.py diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index 3ca503a2167..d110c28785a 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -43,6 +43,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if DOMAIN not in config: return True + hass.async_create_task(_async_import_config(hass, config), eager_start=True) + return True + + +async def _async_import_config(hass: HomeAssistant, config: ConfigType) -> None: + """Import the Downloader component from the YAML file.""" + import_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, @@ -62,7 +69,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: hass, DOMAIN, f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.9.0", + breaks_in_ha_version="2024.10.0", is_fixable=False, issue_domain=DOMAIN, severity=IssueSeverity.WARNING, @@ -72,7 +79,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: "integration_title": "Downloader", }, ) - return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/tests/components/downloader/test_init.py b/tests/components/downloader/test_init.py new file mode 100644 index 00000000000..8cd0d00b1ab --- /dev/null +++ b/tests/components/downloader/test_init.py @@ -0,0 +1,51 @@ +"""Tests for the downloader component init.""" + +from unittest.mock import patch + +from homeassistant.components.downloader import ( + CONF_DOWNLOAD_DIR, + DOMAIN, + SERVICE_DOWNLOAD_FILE, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_initialization(hass: HomeAssistant) -> None: + """Test the initialization of the downloader component.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_DOWNLOAD_DIR: "/test_dir", + }, + ) + config_entry.add_to_hass(hass) + with patch("os.path.isdir", return_value=True): + assert await hass.config_entries.async_setup(config_entry.entry_id) + + assert hass.services.has_service(DOMAIN, SERVICE_DOWNLOAD_FILE) + assert config_entry.state is ConfigEntryState.LOADED + + +async def test_import(hass: HomeAssistant) -> None: + """Test the import of the downloader component.""" + with patch("os.path.isdir", return_value=True): + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DOWNLOAD_DIR: "/test_dir", + }, + }, + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + config_entry = hass.config_entries.async_entries(DOMAIN)[0] + assert config_entry.data == {CONF_DOWNLOAD_DIR: "/test_dir"} + assert config_entry.state is ConfigEntryState.LOADED + assert hass.services.has_service(DOMAIN, SERVICE_DOWNLOAD_FILE) From 411e55d0596dac418215e297fd7ec9365bc5b6ef Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 4 Apr 2024 21:01:15 +0200 Subject: [PATCH 116/198] Update frontend to 20240404.0 (#114859) --- 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 1890572bf5a..75c630b4471 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==20240403.1"] + "requirements": ["home-assistant-frontend==20240404.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 6bb6bd4d2d3..d520d7f8f76 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240403.1 +home-assistant-frontend==20240404.0 home-assistant-intents==2024.4.3 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index ff3b0a8068e..51de44eaca6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1077,7 +1077,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240403.1 +home-assistant-frontend==20240404.0 # homeassistant.components.conversation home-assistant-intents==2024.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 56703105b7d..eb1fb0583cb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,7 +876,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240403.1 +home-assistant-frontend==20240404.0 # homeassistant.components.conversation home-assistant-intents==2024.4.3 From 96003e3562c0cdc91d06556fd1ae06c062bcd237 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 5 Apr 2024 08:28:35 +0200 Subject: [PATCH 117/198] Fix Axis camera platform support HTTPS (#114886) --- homeassistant/components/axis/camera.py | 16 ++++++++-------- homeassistant/components/axis/hub/config.py | 3 +++ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index 769be676a78..025244fb675 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -56,6 +56,7 @@ class AxisCamera(AxisEntity, MjpegCamera): mjpeg_url=self.mjpeg_source, still_image_url=self.image_source, authentication=HTTP_DIGEST_AUTHENTICATION, + verify_ssl=False, unique_id=f"{hub.unique_id}-camera", ) @@ -74,16 +75,18 @@ class AxisCamera(AxisEntity, MjpegCamera): Additionally used when device change IP address. """ + proto = self.hub.config.protocol + host = self.hub.config.host + port = self.hub.config.port + image_options = self.generate_options(skip_stream_profile=True) self._still_image_url = ( - f"http://{self.hub.config.host}:{self.hub.config.port}/axis-cgi" - f"/jpg/image.cgi{image_options}" + f"{proto}://{host}:{port}/axis-cgi/jpg/image.cgi{image_options}" ) mjpeg_options = self.generate_options() self._mjpeg_url = ( - f"http://{self.hub.config.host}:{self.hub.config.port}/axis-cgi" - f"/mjpg/video.cgi{mjpeg_options}" + f"{proto}://{host}:{port}/axis-cgi/mjpg/video.cgi{mjpeg_options}" ) stream_options = self.generate_options(add_video_codec_h264=True) @@ -95,10 +98,7 @@ class AxisCamera(AxisEntity, MjpegCamera): self.hub.additional_diagnostics["camera_sources"] = { "Image": self._still_image_url, "MJPEG": self._mjpeg_url, - "Stream": ( - f"rtsp://user:pass@{self.hub.config.host}/axis-media" - f"/media.amp{stream_options}" - ), + "Stream": (f"rtsp://user:pass@{host}/axis-media/media.amp{stream_options}"), } @property diff --git a/homeassistant/components/axis/hub/config.py b/homeassistant/components/axis/hub/config.py index e6d8378b45c..eba706edc83 100644 --- a/homeassistant/components/axis/hub/config.py +++ b/homeassistant/components/axis/hub/config.py @@ -12,6 +12,7 @@ from homeassistant.const import ( CONF_NAME, CONF_PASSWORD, CONF_PORT, + CONF_PROTOCOL, CONF_TRIGGER_TIME, CONF_USERNAME, ) @@ -31,6 +32,7 @@ class AxisConfig: entry: ConfigEntry + protocol: str host: str port: int username: str @@ -54,6 +56,7 @@ class AxisConfig: options = config_entry.options return cls( entry=config_entry, + protocol=config.get(CONF_PROTOCOL, "http"), host=config[CONF_HOST], username=config[CONF_USERNAME], password=config[CONF_PASSWORD], From 618fa08ab2db104c768b772d611f517aaa349c3e Mon Sep 17 00:00:00 2001 From: Jeef Date: Thu, 4 Apr 2024 19:37:54 -0600 Subject: [PATCH 118/198] Bump weatherflow4py to 0.2.20 (#114888) --- 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 8376bd1b50d..361349dcbe8 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.2.17"] + "requirements": ["weatherflow4py==0.2.20"] } diff --git a/requirements_all.txt b/requirements_all.txt index 51de44eaca6..ca23e4beaf7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2838,7 +2838,7 @@ watchdog==2.3.1 waterfurnace==1.1.0 # homeassistant.components.weatherflow_cloud -weatherflow4py==0.2.17 +weatherflow4py==0.2.20 # homeassistant.components.webmin webmin-xmlrpc==0.0.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index eb1fb0583cb..a3b058f2029 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2185,7 +2185,7 @@ wallbox==0.6.0 watchdog==2.3.1 # homeassistant.components.weatherflow_cloud -weatherflow4py==0.2.17 +weatherflow4py==0.2.20 # homeassistant.components.webmin webmin-xmlrpc==0.0.2 From 2434a22e4e487c9a654b6f23a365fbc84f83ab36 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 5 Apr 2024 09:47:49 +0200 Subject: [PATCH 119/198] Fix Axis reconfigure step not providing protocols as alternatives but as string (#114889) --- homeassistant/components/axis/config_flow.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 30bc653c202..80872fc9be4 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -168,16 +168,13 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN): self, entry_data: Mapping[str, Any], keep_password: bool ) -> ConfigFlowResult: """Re-run configuration step.""" + protocol = entry_data.get(CONF_PROTOCOL, "http") + password = entry_data[CONF_PASSWORD] if keep_password else "" self.discovery_schema = { - vol.Required( - CONF_PROTOCOL, default=entry_data.get(CONF_PROTOCOL, "http") - ): str, + vol.Required(CONF_PROTOCOL, default=protocol): vol.In(PROTOCOL_CHOICES), vol.Required(CONF_HOST, default=entry_data[CONF_HOST]): str, vol.Required(CONF_USERNAME, default=entry_data[CONF_USERNAME]): str, - vol.Required( - CONF_PASSWORD, - default=entry_data[CONF_PASSWORD] if keep_password else "", - ): str, + vol.Required(CONF_PASSWORD, default=password): str, vol.Required(CONF_PORT, default=entry_data[CONF_PORT]): int, } From 71877fdeda060229db38b40a27cad375144d6c0c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 5 Apr 2024 00:26:07 +0200 Subject: [PATCH 120/198] Update frontend to 20240404.1 (#114890) --- 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 75c630b4471..028fb28f01b 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==20240404.0"] + "requirements": ["home-assistant-frontend==20240404.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d520d7f8f76..bd35403340f 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -30,7 +30,7 @@ habluetooth==2.4.2 hass-nabucasa==0.79.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 -home-assistant-frontend==20240404.0 +home-assistant-frontend==20240404.1 home-assistant-intents==2024.4.3 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index ca23e4beaf7..9bbb5293660 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1077,7 +1077,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240404.0 +home-assistant-frontend==20240404.1 # homeassistant.components.conversation home-assistant-intents==2024.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a3b058f2029..2125f7e13d0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,7 +876,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240404.0 +home-assistant-frontend==20240404.1 # homeassistant.components.conversation home-assistant-intents==2024.4.3 From 87ffd5ac56750798d222b06c3122f4099e7ffced Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Apr 2024 20:22:00 -1000 Subject: [PATCH 121/198] Ensure all tables have the default table args in the db_schema (#114895) --- homeassistant/components/recorder/db_schema.py | 12 +++++++++++- tests/components/recorder/test_init.py | 7 +++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/recorder/db_schema.py b/homeassistant/components/recorder/db_schema.py index 5b24448211d..eac743c3d75 100644 --- a/homeassistant/components/recorder/db_schema.py +++ b/homeassistant/components/recorder/db_schema.py @@ -715,6 +715,7 @@ class Statistics(Base, StatisticsBase): "start_ts", unique=True, ), + _DEFAULT_TABLE_ARGS, ) __tablename__ = TABLE_STATISTICS @@ -732,6 +733,7 @@ class StatisticsShortTerm(Base, StatisticsBase): "start_ts", unique=True, ), + _DEFAULT_TABLE_ARGS, ) __tablename__ = TABLE_STATISTICS_SHORT_TERM @@ -760,7 +762,10 @@ class StatisticsMeta(Base): class RecorderRuns(Base): """Representation of recorder run.""" - __table_args__ = (Index("ix_recorder_runs_start_end", "start", "end"),) + __table_args__ = ( + Index("ix_recorder_runs_start_end", "start", "end"), + _DEFAULT_TABLE_ARGS, + ) __tablename__ = TABLE_RECORDER_RUNS run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) start: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow) @@ -789,6 +794,7 @@ class MigrationChanges(Base): """Representation of migration changes.""" __tablename__ = TABLE_MIGRATION_CHANGES + __table_args__ = (_DEFAULT_TABLE_ARGS,) migration_id: Mapped[str] = mapped_column(String(255), primary_key=True) version: Mapped[int] = mapped_column(SmallInteger) @@ -798,6 +804,8 @@ class SchemaChanges(Base): """Representation of schema version changes.""" __tablename__ = TABLE_SCHEMA_CHANGES + __table_args__ = (_DEFAULT_TABLE_ARGS,) + change_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) schema_version: Mapped[int | None] = mapped_column(Integer) changed: Mapped[datetime] = mapped_column(DATETIME_TYPE, default=dt_util.utcnow) @@ -816,6 +824,8 @@ class StatisticsRuns(Base): """Representation of statistics run.""" __tablename__ = TABLE_STATISTICS_RUNS + __table_args__ = (_DEFAULT_TABLE_ARGS,) + run_id: Mapped[int] = mapped_column(Integer, Identity(), primary_key=True) start: Mapped[datetime] = mapped_column(DATETIME_TYPE, index=True) diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index cde2da3cc83..206c356bad8 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -27,6 +27,7 @@ from homeassistant.components.recorder import ( DOMAIN, SQLITE_URL_PREFIX, Recorder, + db_schema, get_instance, migration, pool, @@ -2598,3 +2599,9 @@ async def test_commit_before_commits_pending_writes( await verify_states_in_queue_future await verify_session_commit_future + + +def test_all_tables_use_default_table_args(hass: HomeAssistant) -> None: + """Test that all tables use the default table args.""" + for table in db_schema.Base.metadata.tables.values(): + assert table.kwargs.items() >= db_schema._DEFAULT_TABLE_ARGS.items() From c39d6f07300d3def56be05cc8a8239c71a9b5a14 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 4 Apr 2024 15:28:36 -1000 Subject: [PATCH 122/198] Reduce august polling frequency (#114904) Co-authored-by: TheJulianJES --- homeassistant/components/august/activity.py | 21 +++++++++++- homeassistant/components/august/const.py | 2 +- homeassistant/components/august/subscriber.py | 33 +++++++++---------- tests/components/august/test_lock.py | 23 ++++++++++++- 4 files changed, 58 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/august/activity.py b/homeassistant/components/august/activity.py index ae920383e40..ee180ab5480 100644 --- a/homeassistant/components/august/activity.py +++ b/homeassistant/components/august/activity.py @@ -5,6 +5,7 @@ from __future__ import annotations from datetime import datetime from functools import partial import logging +from time import monotonic from aiohttp import ClientError from yalexs.activity import Activity, ActivityType @@ -26,9 +27,11 @@ _LOGGER = logging.getLogger(__name__) ACTIVITY_STREAM_FETCH_LIMIT = 10 ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500 +INITIAL_LOCK_RESYNC_TIME = 60 + # If there is a storm of activity (ie lock, unlock, door open, door close, etc) # we want to debounce the updates so we don't hammer the activity api too much. -ACTIVITY_DEBOUNCE_COOLDOWN = 3 +ACTIVITY_DEBOUNCE_COOLDOWN = 4 @callback @@ -62,6 +65,7 @@ class ActivityStream(AugustSubscriberMixin): self.pubnub = pubnub self._update_debounce: dict[str, Debouncer] = {} self._update_debounce_jobs: dict[str, HassJob] = {} + self._start_time: float | None = None @callback def _async_update_house_id_later(self, debouncer: Debouncer, _: datetime) -> None: @@ -70,6 +74,7 @@ class ActivityStream(AugustSubscriberMixin): async def async_setup(self) -> None: """Token refresh check and catch up the activity stream.""" + self._start_time = monotonic() update_debounce = self._update_debounce update_debounce_jobs = self._update_debounce_jobs for house_id in self._house_ids: @@ -140,11 +145,25 @@ class ActivityStream(AugustSubscriberMixin): debouncer = self._update_debounce[house_id] debouncer.async_schedule_call() + # Schedule two updates past the debounce time # to ensure we catch the case where the activity # api does not update right away and we need to poll # it again. Sometimes the lock operator or a doorbell # will not show up in the activity stream right away. + # Only do additional polls if we are past + # the initial lock resync time to avoid a storm + # of activity at setup. + if ( + not self._start_time + or monotonic() - self._start_time < INITIAL_LOCK_RESYNC_TIME + ): + _LOGGER.debug( + "Skipping additional updates due to ongoing initial lock resync time" + ) + return + + _LOGGER.debug("Scheduling additional updates for house id %s", house_id) job = self._update_debounce_jobs[house_id] for step in (1, 2): future_updates.append( diff --git a/homeassistant/components/august/const.py b/homeassistant/components/august/const.py index 0cbd21f397e..6aa033c62b2 100644 --- a/homeassistant/components/august/const.py +++ b/homeassistant/components/august/const.py @@ -40,7 +40,7 @@ ATTR_OPERATION_TAG = "tag" # Limit battery, online, and hardware updates to hourly # in order to reduce the number of api requests and # avoid hitting rate limits -MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=1) +MIN_TIME_BETWEEN_DETAIL_UPDATES = timedelta(hours=24) # Activity needs to be checked more frequently as the # doorbell motion and rings are included here diff --git a/homeassistant/components/august/subscriber.py b/homeassistant/components/august/subscriber.py index e800b5cb604..9332080d9ad 100644 --- a/homeassistant/components/august/subscriber.py +++ b/homeassistant/components/august/subscriber.py @@ -49,9 +49,17 @@ class AugustSubscriberMixin: """Call the refresh method.""" self._hass.async_create_task(self._async_refresh(now), eager_start=True) + @callback + def _async_cancel_update_interval(self, _: Event | None = None) -> None: + """Cancel the scheduled update.""" + if self._unsub_interval: + self._unsub_interval() + self._unsub_interval = None + @callback def _async_setup_listeners(self) -> None: """Create interval and stop listeners.""" + self._async_cancel_update_interval() self._unsub_interval = async_track_time_interval( self._hass, self._async_scheduled_refresh, @@ -59,17 +67,12 @@ class AugustSubscriberMixin: name="august refresh", ) - @callback - def _async_cancel_update_interval(_: Event) -> None: - self._stop_interval = None - if self._unsub_interval: - self._unsub_interval() - - self._stop_interval = self._hass.bus.async_listen( - EVENT_HOMEASSISTANT_STOP, - _async_cancel_update_interval, - run_immediately=True, - ) + if not self._stop_interval: + self._stop_interval = self._hass.bus.async_listen( + EVENT_HOMEASSISTANT_STOP, + self._async_cancel_update_interval, + run_immediately=True, + ) @callback def async_unsubscribe_device_id( @@ -82,13 +85,7 @@ class AugustSubscriberMixin: if self._subscriptions: return - - if self._unsub_interval: - self._unsub_interval() - self._unsub_interval = None - if self._stop_interval: - self._stop_interval() - self._stop_interval = None + self._async_cancel_update_interval() @callback def async_signal_device_id_update(self, device_id: str) -> None: diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index 39c1745d967..4de931e6979 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -4,9 +4,11 @@ import datetime from unittest.mock import Mock from aiohttp import ClientResponseError +from freezegun.api import FrozenDateTimeFactory import pytest from yalexs.pubnub_async import AugustPubNub +from homeassistant.components.august.activity import INITIAL_LOCK_RESYNC_TIME from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, STATE_JAMMED, @@ -155,7 +157,9 @@ async def test_one_lock_operation( async def test_one_lock_operation_pubnub_connected( - hass: HomeAssistant, entity_registry: er.EntityRegistry + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + freezer: FrozenDateTimeFactory, ) -> None: """Test lock and unlock operations are async when pubnub is connected.""" lock_one = await _mock_doorsense_enabled_august_lock_detail(hass) @@ -230,6 +234,23 @@ async def test_one_lock_operation_pubnub_connected( == STATE_UNKNOWN ) + freezer.tick(INITIAL_LOCK_RESYNC_TIME) + + pubnub.message( + pubnub, + Mock( + channel=lock_one.pubsub_channel, + timetoken=(dt_util.utcnow().timestamp() + 2) * 10000000, + message={ + "status": "kAugLockState_Unlocked", + }, + ), + ) + await hass.async_block_till_done() + + lock_online_with_doorsense_name = hass.states.get("lock.online_with_doorsense_name") + assert lock_online_with_doorsense_name.state == STATE_UNLOCKED + async def test_lock_jammed(hass: HomeAssistant) -> None: """Test lock gets jammed on unlock.""" From 5d5dc24b3339c9d45f958521ceaf8c8e4def526e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=85ke=20Strandberg?= Date: Fri, 5 Apr 2024 10:01:51 +0200 Subject: [PATCH 123/198] Show correct model string in myuplink (#114921) --- homeassistant/components/myuplink/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/myuplink/__init__.py b/homeassistant/components/myuplink/__init__.py index 5dee46b24cf..42bb9007789 100644 --- a/homeassistant/components/myuplink/__init__.py +++ b/homeassistant/components/myuplink/__init__.py @@ -5,7 +5,7 @@ from __future__ import annotations from http import HTTPStatus from aiohttp import ClientError, ClientResponseError -from myuplink import MyUplinkAPI, get_manufacturer, get_system_name +from myuplink import MyUplinkAPI, get_manufacturer, get_model, get_system_name from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform @@ -92,7 +92,7 @@ def create_devices( identifiers={(DOMAIN, device_id)}, name=get_system_name(system), manufacturer=get_manufacturer(device), - model=device.productName, + model=get_model(device), sw_version=device.firmwareCurrent, serial_number=device.product_serial_number, ) From ed3daed86935b6aaf2fb625788fd6599c0273471 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 5 Apr 2024 12:32:09 +0200 Subject: [PATCH 124/198] Create right import issues in Downloader (#114922) * Create right import issues in Downloader * Create right import issues in Downloader * Create right import issues in Downloader * Create right import issues in Downloader * Fix * Fix * Fix * Fix * Apply suggestions from code review Co-authored-by: Martin Hjelmare * Fix --------- Co-authored-by: Martin Hjelmare --- .../components/downloader/__init__.py | 57 +++++++++++------ .../components/downloader/config_flow.py | 12 ++-- .../components/downloader/strings.json | 8 +-- .../components/downloader/test_config_flow.py | 16 +++++ tests/components/downloader/test_init.py | 64 ++++++++++++++++++- 5 files changed, 125 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index d110c28785a..3fded1215c4 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -11,7 +11,11 @@ import requests import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.core import HomeAssistant, ServiceCall +from homeassistant.core import ( + DOMAIN as HOMEASSISTANT_DOMAIN, + HomeAssistant, + ServiceCall, +) from homeassistant.data_entry_flow import FlowResultType import homeassistant.helpers.config_validation as cv from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue @@ -43,7 +47,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: if DOMAIN not in config: return True - hass.async_create_task(_async_import_config(hass, config), eager_start=True) + hass.async_create_task(_async_import_config(hass, config)) return True @@ -58,27 +62,40 @@ async def _async_import_config(hass: HomeAssistant, config: ConfigType) -> None: }, ) - translation_key = "deprecated_yaml" if ( import_result["type"] == FlowResultType.ABORT - and import_result["reason"] == "import_failed" + and import_result["reason"] != "single_instance_allowed" ): - translation_key = "import_failed" - - async_create_issue( - hass, - DOMAIN, - f"deprecated_yaml_{DOMAIN}", - breaks_in_ha_version="2024.10.0", - is_fixable=False, - issue_domain=DOMAIN, - severity=IssueSeverity.WARNING, - translation_key=translation_key, - translation_placeholders={ - "domain": DOMAIN, - "integration_title": "Downloader", - }, - ) + async_create_issue( + hass, + DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.10.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="directory_does_not_exist", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Downloader", + "url": "/config/integrations/dashboard/add?domain=downloader", + }, + ) + else: + async_create_issue( + hass, + HOMEASSISTANT_DOMAIN, + f"deprecated_yaml_{DOMAIN}", + breaks_in_ha_version="2024.10.0", + is_fixable=False, + issue_domain=DOMAIN, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + translation_placeholders={ + "domain": DOMAIN, + "integration_title": "Downloader", + }, + ) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/downloader/config_flow.py b/homeassistant/components/downloader/config_flow.py index 15af8b56163..94b33f4e93f 100644 --- a/homeassistant/components/downloader/config_flow.py +++ b/homeassistant/components/downloader/config_flow.py @@ -46,12 +46,16 @@ class DownloaderConfigFlow(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import( - self, user_input: dict[str, Any] | None = None - ) -> ConfigFlowResult: + async def async_step_import(self, user_input: dict[str, Any]) -> ConfigFlowResult: """Handle a flow initiated by configuration file.""" + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") - return await self.async_step_user(user_input) + try: + await self._validate_input(user_input) + except DirectoryDoesNotExist: + return self.async_abort(reason="directory_does_not_exist") + return self.async_create_entry(title=DEFAULT_NAME, data=user_input) async def _validate_input(self, user_input: dict[str, Any]) -> None: """Validate the user input if the directory exists.""" diff --git a/homeassistant/components/downloader/strings.json b/homeassistant/components/downloader/strings.json index 77dd0abd9d3..4cadabf96c6 100644 --- a/homeassistant/components/downloader/strings.json +++ b/homeassistant/components/downloader/strings.json @@ -37,13 +37,9 @@ } }, "issues": { - "deprecated_yaml": { - "title": "The {integration_title} YAML configuration is being removed", - "description": "Configuring {integration_title} using YAML is being removed.\n\nYour configuration is already imported.\n\nRemove the `{domain}` configuration from your configuration.yaml file and restart Home Assistant to fix this issue." - }, - "import_failed": { + "directory_does_not_exist": { "title": "The {integration_title} failed to import", - "description": "The {integration_title} integration failed to import.\n\nPlease check the logs for more details." + "description": "The {integration_title} integration failed to import because the configured directory does not exist.\n\nEnsure the directory exists and restart Home Assistant to try again or remove the {integration_title} configuration from your configuration.yaml file and continue to [set up the integration]({url}) manually." } } } diff --git a/tests/components/downloader/test_config_flow.py b/tests/components/downloader/test_config_flow.py index 5e75a9b33ba..897fbba0c59 100644 --- a/tests/components/downloader/test_config_flow.py +++ b/tests/components/downloader/test_config_flow.py @@ -99,3 +99,19 @@ async def test_import_flow_success(hass: HomeAssistant) -> None: assert result["title"] == "Downloader" assert result["data"] == {} assert result["options"] == {} + + +async def test_import_flow_directory_not_found(hass: HomeAssistant) -> None: + """Test import flow.""" + with patch("os.path.isdir", return_value=False): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_DOWNLOAD_DIR: "download_dir", + }, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "directory_does_not_exist" diff --git a/tests/components/downloader/test_init.py b/tests/components/downloader/test_init.py index 8cd0d00b1ab..5832c0402b4 100644 --- a/tests/components/downloader/test_init.py +++ b/tests/components/downloader/test_init.py @@ -8,7 +8,8 @@ from homeassistant.components.downloader import ( SERVICE_DOWNLOAD_FILE, ) from homeassistant.config_entries import ConfigEntryState -from homeassistant.core import HomeAssistant +from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant +from homeassistant.helpers import issue_registry as ir from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -30,7 +31,7 @@ async def test_initialization(hass: HomeAssistant) -> None: assert config_entry.state is ConfigEntryState.LOADED -async def test_import(hass: HomeAssistant) -> None: +async def test_import(hass: HomeAssistant, issue_registry: ir.IssueRegistry) -> None: """Test the import of the downloader component.""" with patch("os.path.isdir", return_value=True): assert await async_setup_component( @@ -49,3 +50,62 @@ async def test_import(hass: HomeAssistant) -> None: assert config_entry.data == {CONF_DOWNLOAD_DIR: "/test_dir"} assert config_entry.state is ConfigEntryState.LOADED assert hass.services.has_service(DOMAIN, SERVICE_DOWNLOAD_FILE) + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue( + issue_id="deprecated_yaml_downloader", domain=HOMEASSISTANT_DOMAIN + ) + assert issue + + +async def test_import_directory_missing( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test the import of the downloader component.""" + with patch("os.path.isdir", return_value=False): + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DOWNLOAD_DIR: "/test_dir", + }, + }, + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue( + issue_id="deprecated_yaml_downloader", domain=DOMAIN + ) + assert issue + + +async def test_import_already_exists( + hass: HomeAssistant, issue_registry: ir.IssueRegistry +) -> None: + """Test the import of the downloader component.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_DOWNLOAD_DIR: "/test_dir", + }, + ) + config_entry.add_to_hass(hass) + with patch("os.path.isdir", return_value=True): + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_DOWNLOAD_DIR: "/test_dir", + }, + }, + ) + await hass.async_block_till_done() + + assert len(issue_registry.issues) == 1 + issue = issue_registry.async_get_issue( + issue_id="deprecated_yaml_downloader", domain=HOMEASSISTANT_DOMAIN + ) + assert issue From 9937743863c9135b0865f6548aed4d1c88698567 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 5 Apr 2024 11:44:51 +0200 Subject: [PATCH 125/198] Fix cast dashboard in media browser (#114924) --- homeassistant/components/lovelace/cast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/lovelace/cast.py b/homeassistant/components/lovelace/cast.py index 02f5d0c0478..82a92b94ae5 100644 --- a/homeassistant/components/lovelace/cast.py +++ b/homeassistant/components/lovelace/cast.py @@ -179,7 +179,7 @@ async def _get_dashboard_info(hass, url_path): "views": views, } - if config is None: + if config is None or "views" not in config: return data for idx, view in enumerate(config["views"]): From e3c111b1dd0fd41e3d15a6c4fe300bdadfa60885 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 5 Apr 2024 12:34:07 +0200 Subject: [PATCH 126/198] Bump version to 2024.4.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 6e08c49f970..b642ce6ce8c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -18,7 +18,7 @@ from .util.signal_type import SignalType APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 4 -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, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index ac890603ac3..2dd3a9632c6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.4.0" +version = "2024.4.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 47d9879c0c1da274d9939d530441aff9661d2fb5 Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Thu, 4 Apr 2024 13:25:35 +0100 Subject: [PATCH 127/198] Pin systembridgemodels to 4.0.4 (#114842) --- homeassistant/components/system_bridge/manifest.json | 2 +- requirements_all.txt | 3 +++ requirements_test_all.txt | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/system_bridge/manifest.json b/homeassistant/components/system_bridge/manifest.json index b4365fda778..aea66d22f62 100644 --- a/homeassistant/components/system_bridge/manifest.json +++ b/homeassistant/components/system_bridge/manifest.json @@ -10,6 +10,6 @@ "iot_class": "local_push", "loggers": ["systembridgeconnector"], "quality_scale": "silver", - "requirements": ["systembridgeconnector==4.0.3"], + "requirements": ["systembridgeconnector==4.0.3", "systembridgemodels==4.0.4"], "zeroconf": ["_system-bridge._tcp.local."] } diff --git a/requirements_all.txt b/requirements_all.txt index 9bbb5293660..a87df9614d1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2654,6 +2654,9 @@ synology-srm==0.2.0 # homeassistant.components.system_bridge systembridgeconnector==4.0.3 +# homeassistant.components.system_bridge +systembridgemodels==4.0.4 + # homeassistant.components.tailscale tailscale==0.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2125f7e13d0..1f5d01eb46c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2049,6 +2049,9 @@ switchbot-api==2.0.0 # homeassistant.components.system_bridge systembridgeconnector==4.0.3 +# homeassistant.components.system_bridge +systembridgemodels==4.0.4 + # homeassistant.components.tailscale tailscale==0.6.0 From 95606135a629fd698a26266354a5246bb4713c54 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 5 Apr 2024 14:21:24 +0200 Subject: [PATCH 128/198] Fix ROVA validation (#114938) * Fix ROVA validation * Fix ROVA validation --- homeassistant/components/rova/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rova/sensor.py b/homeassistant/components/rova/sensor.py index e510bcf0caf..f63b9893c02 100644 --- a/homeassistant/components/rova/sensor.py +++ b/homeassistant/components/rova/sensor.py @@ -54,7 +54,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_HOUSE_NUMBER_SUFFIX, default=""): cv.string, vol.Optional(CONF_NAME, default="Rova"): cv.string, vol.Optional(CONF_MONITORED_CONDITIONS, default=["bio"]): vol.All( - cv.ensure_list, [vol.In(SENSOR_TYPES)] + cv.ensure_list, [vol.In(["bio", "paper", "plastic", "residual"])] ), } ) From 61a359e4d21140b7147abd006e07ee70689110b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccol=C3=B2=20Maggioni?= Date: Mon, 8 Apr 2024 10:04:59 +0200 Subject: [PATCH 129/198] Fix hang in SNMP device_tracker implementation (#112815) Co-authored-by: J. Nick Koston --- CODEOWNERS | 2 + .../components/snmp/device_tracker.py | 154 ++++++++++++------ homeassistant/components/snmp/manifest.json | 2 +- 3 files changed, 110 insertions(+), 48 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 85603250b7c..c6cee80ea80 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1249,6 +1249,8 @@ build.json @home-assistant/supervisor /homeassistant/components/sms/ @ocalvo /homeassistant/components/snapcast/ @luar123 /tests/components/snapcast/ @luar123 +/homeassistant/components/snmp/ @nmaggioni +/tests/components/snmp/ @nmaggioni /homeassistant/components/snooz/ @AustinBrunkhorst /tests/components/snooz/ @AustinBrunkhorst /homeassistant/components/solaredge/ @frenck diff --git a/homeassistant/components/snmp/device_tracker.py b/homeassistant/components/snmp/device_tracker.py index 4b8ab073b9c..a1a91116f0f 100644 --- a/homeassistant/components/snmp/device_tracker.py +++ b/homeassistant/components/snmp/device_tracker.py @@ -5,8 +5,19 @@ from __future__ import annotations import binascii import logging -from pysnmp.entity import config as cfg -from pysnmp.entity.rfc3413.oneliner import cmdgen +from pysnmp.error import PySnmpError +from pysnmp.hlapi.asyncio import ( + CommunityData, + ContextData, + ObjectIdentity, + ObjectType, + SnmpEngine, + Udp6TransportTarget, + UdpTransportTarget, + UsmUserData, + bulkWalkCmd, + isEndOfMib, +) import voluptuous as vol from homeassistant.components.device_tracker import ( @@ -24,7 +35,13 @@ from .const import ( CONF_BASEOID, CONF_COMMUNITY, CONF_PRIV_KEY, + DEFAULT_AUTH_PROTOCOL, DEFAULT_COMMUNITY, + DEFAULT_PORT, + DEFAULT_PRIV_PROTOCOL, + DEFAULT_TIMEOUT, + DEFAULT_VERSION, + SNMP_VERSIONS, ) _LOGGER = logging.getLogger(__name__) @@ -40,9 +57,12 @@ PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( ) -def get_scanner(hass: HomeAssistant, config: ConfigType) -> SnmpScanner | None: +async def async_get_scanner( + hass: HomeAssistant, config: ConfigType +) -> SnmpScanner | None: """Validate the configuration and return an SNMP scanner.""" scanner = SnmpScanner(config[DOMAIN]) + await scanner.async_init() return scanner if scanner.success_init else None @@ -51,39 +71,75 @@ class SnmpScanner(DeviceScanner): """Queries any SNMP capable Access Point for connected devices.""" def __init__(self, config): - """Initialize the scanner.""" + """Initialize the scanner and test the target device.""" + host = config[CONF_HOST] + community = config[CONF_COMMUNITY] + baseoid = config[CONF_BASEOID] + authkey = config.get(CONF_AUTH_KEY) + authproto = DEFAULT_AUTH_PROTOCOL + privkey = config.get(CONF_PRIV_KEY) + privproto = DEFAULT_PRIV_PROTOCOL - self.snmp = cmdgen.CommandGenerator() + try: + # Try IPv4 first. + target = UdpTransportTarget((host, DEFAULT_PORT), timeout=DEFAULT_TIMEOUT) + except PySnmpError: + # Then try IPv6. + try: + target = Udp6TransportTarget( + (host, DEFAULT_PORT), timeout=DEFAULT_TIMEOUT + ) + except PySnmpError as err: + _LOGGER.error("Invalid SNMP host: %s", err) + return - self.host = cmdgen.UdpTransportTarget((config[CONF_HOST], 161)) - if CONF_AUTH_KEY not in config or CONF_PRIV_KEY not in config: - self.auth = cmdgen.CommunityData(config[CONF_COMMUNITY]) + if authkey is not None or privkey is not None: + if not authkey: + authproto = "none" + if not privkey: + privproto = "none" + + request_args = [ + SnmpEngine(), + UsmUserData( + community, + authKey=authkey or None, + privKey=privkey or None, + authProtocol=authproto, + privProtocol=privproto, + ), + target, + ContextData(), + ] else: - self.auth = cmdgen.UsmUserData( - config[CONF_COMMUNITY], - config[CONF_AUTH_KEY], - config[CONF_PRIV_KEY], - authProtocol=cfg.usmHMACSHAAuthProtocol, - privProtocol=cfg.usmAesCfb128Protocol, - ) - self.baseoid = cmdgen.MibVariable(config[CONF_BASEOID]) - self.last_results = [] + request_args = [ + SnmpEngine(), + CommunityData(community, mpModel=SNMP_VERSIONS[DEFAULT_VERSION]), + target, + ContextData(), + ] - # Test the router is accessible - data = self.get_snmp_data() + self.request_args = request_args + self.baseoid = baseoid + self.last_results = [] + self.success_init = False + + async def async_init(self): + """Make a one-off read to check if the target device is reachable and readable.""" + data = await self.async_get_snmp_data() self.success_init = data is not None - def scan_devices(self): + async def async_scan_devices(self): """Scan for new devices and return a list with found device IDs.""" - self._update_info() + await self._async_update_info() return [client["mac"] for client in self.last_results if client.get("mac")] - def get_device_name(self, device): + async def async_get_device_name(self, device): """Return the name of the given device or None if we don't know.""" # We have no names return None - def _update_info(self): + async def _async_update_info(self): """Ensure the information from the device is up to date. Return boolean if scanning successful. @@ -91,38 +147,42 @@ class SnmpScanner(DeviceScanner): if not self.success_init: return False - if not (data := self.get_snmp_data()): + if not (data := await self.async_get_snmp_data()): return False self.last_results = data return True - def get_snmp_data(self): + async def async_get_snmp_data(self): """Fetch MAC addresses from access point via SNMP.""" devices = [] - errindication, errstatus, errindex, restable = self.snmp.nextCmd( - self.auth, self.host, self.baseoid + walker = bulkWalkCmd( + *self.request_args, + 0, + 50, + ObjectType(ObjectIdentity(self.baseoid)), + lexicographicMode=False, ) + async for errindication, errstatus, errindex, res in walker: + if errindication: + _LOGGER.error("SNMPLIB error: %s", errindication) + return + if errstatus: + _LOGGER.error( + "SNMP error: %s at %s", + errstatus.prettyPrint(), + errindex and res[int(errindex) - 1][0] or "?", + ) + return - if errindication: - _LOGGER.error("SNMPLIB error: %s", errindication) - return - if errstatus: - _LOGGER.error( - "SNMP error: %s at %s", - errstatus.prettyPrint(), - errindex and restable[int(errindex) - 1][0] or "?", - ) - return - - for resrow in restable: - for _, val in resrow: - try: - mac = binascii.hexlify(val.asOctets()).decode("utf-8") - except AttributeError: - continue - _LOGGER.debug("Found MAC address: %s", mac) - mac = ":".join([mac[i : i + 2] for i in range(0, len(mac), 2)]) - devices.append({"mac": mac}) + for _oid, value in res: + if not isEndOfMib(res): + try: + mac = binascii.hexlify(value.asOctets()).decode("utf-8") + except AttributeError: + continue + _LOGGER.debug("Found MAC address: %s", mac) + mac = ":".join([mac[i : i + 2] for i in range(0, len(mac), 2)]) + devices.append({"mac": mac}) return devices diff --git a/homeassistant/components/snmp/manifest.json b/homeassistant/components/snmp/manifest.json index c4aa82f2a74..d79910c44cd 100644 --- a/homeassistant/components/snmp/manifest.json +++ b/homeassistant/components/snmp/manifest.json @@ -1,7 +1,7 @@ { "domain": "snmp", "name": "SNMP", - "codeowners": [], + "codeowners": ["@nmaggioni"], "documentation": "https://www.home-assistant.io/integrations/snmp", "iot_class": "local_polling", "loggers": ["pyasn1", "pysmi", "pysnmp"], From 93569e38278ddfc46ee55545e8b9c24de54b0f53 Mon Sep 17 00:00:00 2001 From: Benjamin <46243805+bbr111@users.noreply.github.com> Date: Thu, 4 Apr 2024 11:45:01 +0200 Subject: [PATCH 130/198] Fix missing if statement in homematic (#114832) * homematic fix issue #114807 Update climate.py * Update homeassistant/components/homematic/climate.py --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/homematic/climate.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematic/climate.py b/homeassistant/components/homematic/climate.py index efdb9324f76..16c345c5635 100644 --- a/homeassistant/components/homematic/climate.py +++ b/homeassistant/components/homematic/climate.py @@ -113,7 +113,11 @@ class HMThermostat(HMDevice, ClimateEntity): @property def preset_modes(self): """Return a list of available preset modes.""" - return [HM_PRESET_MAP[mode] for mode in self._hmdevice.ACTIONNODE] + return [ + HM_PRESET_MAP[mode] + for mode in self._hmdevice.ACTIONNODE + if mode in HM_PRESET_MAP + ] @property def current_humidity(self): From d1b1d6388f99419667f0a7b6898a993618c9c439 Mon Sep 17 00:00:00 2001 From: Nathan Spencer Date: Sat, 6 Apr 2024 03:16:00 -0700 Subject: [PATCH 131/198] Bump pylitterbot to 2023.4.11 (#114918) --- .../components/litterrobot/manifest.json | 2 +- .../components/litterrobot/vacuum.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/litterrobot/common.py | 3 ++- tests/components/litterrobot/test_sensor.py | 2 +- tests/components/litterrobot/test_vacuum.py | 26 +++++++++++++++++++ 7 files changed, 33 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/litterrobot/manifest.json b/homeassistant/components/litterrobot/manifest.json index ea096a908fc..66ade5f356c 100644 --- a/homeassistant/components/litterrobot/manifest.json +++ b/homeassistant/components/litterrobot/manifest.json @@ -12,5 +12,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["pylitterbot"], - "requirements": ["pylitterbot==2023.4.9"] + "requirements": ["pylitterbot==2023.4.11"] } diff --git a/homeassistant/components/litterrobot/vacuum.py b/homeassistant/components/litterrobot/vacuum.py index 4f9efa2dff7..d752609d7de 100644 --- a/homeassistant/components/litterrobot/vacuum.py +++ b/homeassistant/components/litterrobot/vacuum.py @@ -35,6 +35,7 @@ LITTER_BOX_STATUS_STATE_MAP = { LitterBoxStatus.CLEAN_CYCLE: STATE_CLEANING, LitterBoxStatus.EMPTY_CYCLE: STATE_CLEANING, LitterBoxStatus.CLEAN_CYCLE_COMPLETE: STATE_DOCKED, + LitterBoxStatus.CAT_DETECTED: STATE_DOCKED, LitterBoxStatus.CAT_SENSOR_TIMING: STATE_DOCKED, LitterBoxStatus.DRAWER_FULL_1: STATE_DOCKED, LitterBoxStatus.DRAWER_FULL_2: STATE_DOCKED, diff --git a/requirements_all.txt b/requirements_all.txt index a87df9614d1..61e91226c78 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1943,7 +1943,7 @@ pylibrespot-java==0.1.1 pylitejet==0.6.2 # homeassistant.components.litterrobot -pylitterbot==2023.4.9 +pylitterbot==2023.4.11 # homeassistant.components.lutron_caseta pylutron-caseta==0.20.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f5d01eb46c..ac619efc85a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1509,7 +1509,7 @@ pylibrespot-java==0.1.1 pylitejet==0.6.2 # homeassistant.components.litterrobot -pylitterbot==2023.4.9 +pylitterbot==2023.4.11 # homeassistant.components.lutron_caseta pylutron-caseta==0.20.0 diff --git a/tests/components/litterrobot/common.py b/tests/components/litterrobot/common.py index fe6202edc47..cac81aad4ef 100644 --- a/tests/components/litterrobot/common.py +++ b/tests/components/litterrobot/common.py @@ -33,6 +33,7 @@ ROBOT_4_DATA = { "wifiRssi": -53.0, "unitPowerType": "AC", "catWeight": 12.0, + "displayCode": "DC_MODE_IDLE", "unitTimezone": "America/New_York", "unitTime": None, "cleanCycleWaitTime": 15, @@ -66,7 +67,7 @@ ROBOT_4_DATA = { "isDFIResetPending": False, "DFINumberOfCycles": 104, "DFILevelPercent": 76, - "isDFIFull": True, + "isDFIFull": False, "DFIFullCounter": 3, "DFITriggerCount": 42, "litterLevel": 460, diff --git a/tests/components/litterrobot/test_sensor.py b/tests/components/litterrobot/test_sensor.py index 9002894d0ab..8d1f2b68e05 100644 --- a/tests/components/litterrobot/test_sensor.py +++ b/tests/components/litterrobot/test_sensor.py @@ -86,7 +86,7 @@ async def test_litter_robot_sensor( assert sensor.state == "2022-09-17T12:06:37+00:00" assert sensor.attributes["device_class"] == SensorDeviceClass.TIMESTAMP sensor = hass.states.get("sensor.test_status_code") - assert sensor.state == "dfs" + assert sensor.state == "rdy" assert sensor.attributes["device_class"] == SensorDeviceClass.ENUM sensor = hass.states.get("sensor.test_litter_level") assert sensor.state == "70.0" diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index 9013d6e83eb..68ebae1e239 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -5,6 +5,7 @@ from __future__ import annotations from typing import Any from unittest.mock import MagicMock +from pylitterbot import Robot import pytest from homeassistant.components.litterrobot import DOMAIN @@ -16,6 +17,7 @@ from homeassistant.components.vacuum import ( SERVICE_STOP, STATE_DOCKED, STATE_ERROR, + STATE_PAUSED, ) from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant @@ -96,6 +98,30 @@ async def test_vacuum_with_error( assert vacuum.state == STATE_ERROR +@pytest.mark.parametrize( + ("robot_data", "expected_state"), + [ + ({"displayCode": "DC_CAT_DETECT"}, STATE_DOCKED), + ({"isDFIFull": True}, STATE_ERROR), + ({"robotCycleState": "CYCLE_STATE_CAT_DETECT"}, STATE_PAUSED), + ], +) +async def test_vacuum_states( + hass: HomeAssistant, + mock_account_with_litterrobot_4: MagicMock, + robot_data: dict[str, str | bool], + expected_state: str, +) -> None: + """Test sending commands to the switch.""" + await setup_integration(hass, mock_account_with_litterrobot_4, PLATFORM_DOMAIN) + robot: Robot = mock_account_with_litterrobot_4.robots[0] + robot._update_data(robot_data, partial=True) + + vacuum = hass.states.get(VACUUM_ENTITY_ID) + assert vacuum + assert vacuum.state == expected_state + + @pytest.mark.parametrize( ("service", "command", "extra"), [ From 0a2d79f63ef16c645af7a208497bff704424b897 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 5 Apr 2024 19:45:24 +0200 Subject: [PATCH 132/198] Fix Snapcast Config flow (#114952) --- homeassistant/components/snapcast/config_flow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/snapcast/config_flow.py b/homeassistant/components/snapcast/config_flow.py index c9f69c48ab5..b37921fd374 100644 --- a/homeassistant/components/snapcast/config_flow.py +++ b/homeassistant/components/snapcast/config_flow.py @@ -45,7 +45,7 @@ class SnapcastConfigFlow(ConfigFlow, domain=DOMAIN): except OSError: errors["base"] = "cannot_connect" else: - await client.stop() + client.stop() return self.async_create_entry(title=DEFAULT_TITLE, data=user_input) return self.async_show_form( step_id="user", data_schema=SNAPCAST_SCHEMA, errors=errors From e26ea405705f659b283a3c04c1f9917f43750dbf Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Sat, 6 Apr 2024 09:01:55 +0200 Subject: [PATCH 133/198] Bump axis to v61 (#114964) --- 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 1065783d957..b3898b7aab8 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==60"], + "requirements": ["axis==61"], "ssdp": [ { "manufacturer": "AXIS" diff --git a/requirements_all.txt b/requirements_all.txt index 61e91226c78..3df6e7b6b28 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -514,7 +514,7 @@ aurorapy==0.2.7 # avion==0.10 # homeassistant.components.axis -axis==60 +axis==61 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ac619efc85a..77faf484a1c 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==60 +axis==61 # homeassistant.components.azure_event_hub azure-eventhub==5.11.1 From dcd9d987a7e9897cd3111e4d6175d2f0f225f4b1 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Mon, 8 Apr 2024 10:04:16 +0200 Subject: [PATCH 134/198] Filter out fuzzy translations from Lokalise (#114968) --- script/translations/download.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script/translations/download.py b/script/translations/download.py index 958a4b35a7b..8f7327c07ec 100755 --- a/script/translations/download.py +++ b/script/translations/download.py @@ -39,6 +39,8 @@ def run_download_docker(): CORE_PROJECT_ID, "--original-filenames=false", "--replace-breaks=false", + "--filter-data", + "nonfuzzy", "--export-empty-as", "skip", "--format", From b29eb317bd7e3382d8b5194310ef0c379494d58b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=98yvind=20Matheson=20Wergeland?= Date: Sat, 6 Apr 2024 09:38:14 +0200 Subject: [PATCH 135/198] Upgrade to pynobo 1.8.1 (#114982) pynobo 1.8.1 --- homeassistant/components/nobo_hub/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nobo_hub/manifest.json b/homeassistant/components/nobo_hub/manifest.json index 4741eb39e29..ce32244e1ce 100644 --- a/homeassistant/components/nobo_hub/manifest.json +++ b/homeassistant/components/nobo_hub/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/nobo_hub", "integration_type": "hub", "iot_class": "local_push", - "requirements": ["pynobo==1.8.0"] + "requirements": ["pynobo==1.8.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3df6e7b6b28..d65a9a592c0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1991,7 +1991,7 @@ pynetgear==0.10.10 pynetio==0.1.9.1 # homeassistant.components.nobo_hub -pynobo==1.8.0 +pynobo==1.8.1 # homeassistant.components.nuki pynuki==1.6.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 77faf484a1c..93ebf7a8857 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1545,7 +1545,7 @@ pymysensors==0.24.0 pynetgear==0.10.10 # homeassistant.components.nobo_hub -pynobo==1.8.0 +pynobo==1.8.1 # homeassistant.components.nuki pynuki==1.6.3 From 90bc21b7f652feca610b4a8ead1fd2cfeea7449c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 5 Apr 2024 23:50:29 -1000 Subject: [PATCH 136/198] Fix dictionary changed size during iteration in prometheus (#115005) Fixes #104803 --- homeassistant/components/prometheus/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/prometheus/__init__.py b/homeassistant/components/prometheus/__init__.py index d3a307a6616..0ec931ceade 100644 --- a/homeassistant/components/prometheus/__init__.py +++ b/homeassistant/components/prometheus/__init__.py @@ -258,7 +258,7 @@ class PrometheusMetrics: self, entity_id: str, friendly_name: str | None = None ) -> None: """Remove labelsets matching the given entity id from all metrics.""" - for metric in self._metrics.values(): + for metric in list(self._metrics.values()): for sample in cast(list[prometheus_client.Metric], metric.collect())[ 0 ].samples: From 2f9f1008a55afaeabbf6dedcc638859e41d5146e Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sat, 6 Apr 2024 23:09:46 +0200 Subject: [PATCH 137/198] Bump `brother` to version 4.1.0 (#115021) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/brother/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 9ca18a95a1e..3bbaf40f686 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.2"], + "requirements": ["brother==4.1.0"], "zeroconf": [ { "type": "_printer._tcp.local.", diff --git a/requirements_all.txt b/requirements_all.txt index d65a9a592c0..5c06b9828e9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -609,7 +609,7 @@ bring-api==0.5.7 broadlink==0.18.3 # homeassistant.components.brother -brother==4.0.2 +brother==4.1.0 # homeassistant.components.brottsplatskartan brottsplatskartan==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 93ebf7a8857..8744119c082 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -520,7 +520,7 @@ bring-api==0.5.7 broadlink==0.18.3 # homeassistant.components.brother -brother==4.0.2 +brother==4.1.0 # homeassistant.components.brottsplatskartan brottsplatskartan==1.0.5 From 20e88255dfb9ad447b4e6ada480da869641e8164 Mon Sep 17 00:00:00 2001 From: Matrix Date: Mon, 8 Apr 2024 05:31:52 +0800 Subject: [PATCH 138/198] Bump yolink-api to 0.4.2 (#115026) --- homeassistant/components/yolink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/yolink/manifest.json b/homeassistant/components/yolink/manifest.json index 8b3b071161c..cd6759b5864 100644 --- a/homeassistant/components/yolink/manifest.json +++ b/homeassistant/components/yolink/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["auth", "application_credentials"], "documentation": "https://www.home-assistant.io/integrations/yolink", "iot_class": "cloud_push", - "requirements": ["yolink-api==0.4.1"] + "requirements": ["yolink-api==0.4.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5c06b9828e9..ff6662ca0f6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2910,7 +2910,7 @@ yeelight==0.7.14 yeelightsunflower==0.0.10 # homeassistant.components.yolink -yolink-api==0.4.1 +yolink-api==0.4.2 # homeassistant.components.youless youless-api==1.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8744119c082..8ebdb549cb7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2248,7 +2248,7 @@ yalexs==2.0.0 yeelight==0.7.14 # homeassistant.components.yolink -yolink-api==0.4.1 +yolink-api==0.4.2 # homeassistant.components.youless youless-api==1.0.1 From d6793a756f6288fb980504ef105a870ff1df6571 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Sat, 6 Apr 2024 23:18:52 +0200 Subject: [PATCH 139/198] Update xknxproject to 3.7.1 (#115053) --- homeassistant/components/knx/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 99c150a8346..af0c6b8d01c 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -12,7 +12,7 @@ "quality_scale": "platinum", "requirements": [ "xknx==2.12.2", - "xknxproject==3.7.0", + "xknxproject==3.7.1", "knx-frontend==2024.1.20.105944" ], "single_config_entry": true diff --git a/requirements_all.txt b/requirements_all.txt index ff6662ca0f6..d48ee5bdaab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2880,7 +2880,7 @@ xiaomi-ble==0.28.0 xknx==2.12.2 # homeassistant.components.knx -xknxproject==3.7.0 +xknxproject==3.7.1 # homeassistant.components.bluesound # homeassistant.components.fritz diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ebdb549cb7..8ee44387a02 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2224,7 +2224,7 @@ xiaomi-ble==0.28.0 xknx==2.12.2 # homeassistant.components.knx -xknxproject==3.7.0 +xknxproject==3.7.1 # homeassistant.components.bluesound # homeassistant.components.fritz From 75127105b906d2d993bd4ae72a3b81d61e7db54c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 6 Apr 2024 22:54:40 -1000 Subject: [PATCH 140/198] Fix synology_dsm availablity (#115073) * Remove reload on update failure from synology_dsm fixes #115062 The coordinator will retry on its own later, there is no reason to reload here. This was added in #42697 * fix available checks --- .../components/synology_dsm/binary_sensor.py | 2 +- homeassistant/components/synology_dsm/camera.py | 2 +- homeassistant/components/synology_dsm/common.py | 13 +------------ homeassistant/components/synology_dsm/sensor.py | 2 +- homeassistant/components/synology_dsm/switch.py | 2 +- homeassistant/components/synology_dsm/update.py | 2 +- 6 files changed, 6 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/synology_dsm/binary_sensor.py b/homeassistant/components/synology_dsm/binary_sensor.py index 7579f350774..28dc750bc91 100644 --- a/homeassistant/components/synology_dsm/binary_sensor.py +++ b/homeassistant/components/synology_dsm/binary_sensor.py @@ -116,7 +116,7 @@ class SynoDSMSecurityBinarySensor(SynoDSMBinarySensor): @property def available(self) -> bool: """Return True if entity is available.""" - return bool(self._api.security) + return bool(self._api.security) and super().available @property def extra_state_attributes(self) -> dict[str, str]: diff --git a/homeassistant/components/synology_dsm/camera.py b/homeassistant/components/synology_dsm/camera.py index 19f95c710d0..82d15138f05 100644 --- a/homeassistant/components/synology_dsm/camera.py +++ b/homeassistant/components/synology_dsm/camera.py @@ -108,7 +108,7 @@ class SynoDSMCamera(SynologyDSMBaseEntity[SynologyDSMCameraUpdateCoordinator], C @property def available(self) -> bool: """Return the availability of the camera.""" - return self.camera_data.is_enabled and self.coordinator.last_update_success + return self.camera_data.is_enabled and super().available @property def is_recording(self) -> bool: diff --git a/homeassistant/components/synology_dsm/common.py b/homeassistant/components/synology_dsm/common.py index 4bb52383148..4a7018119be 100644 --- a/homeassistant/components/synology_dsm/common.py +++ b/homeassistant/components/synology_dsm/common.py @@ -286,18 +286,7 @@ class SynoApi: async def async_update(self) -> None: """Update function for updating API information.""" - try: - await self._update() - except SYNOLOGY_CONNECTION_EXCEPTIONS as err: - LOGGER.debug( - "Connection error during update of '%s' with exception: %s", - self._entry.unique_id, - err, - ) - LOGGER.warning( - "Connection error during update, fallback by reloading the entry" - ) - await self._hass.config_entries.async_reload(self._entry.entry_id) + await self._update() async def _update(self) -> None: """Update function for updating API information.""" diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 47483ee4a63..4f20a6233f3 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -366,7 +366,7 @@ class SynoDSMUtilSensor(SynoDSMSensor): @property def available(self) -> bool: """Return True if entity is available.""" - return bool(self._api.utilisation) + return bool(self._api.utilisation) and super().available class SynoDSMStorageSensor(SynologyDSMDeviceEntity, SynoDSMSensor): diff --git a/homeassistant/components/synology_dsm/switch.py b/homeassistant/components/synology_dsm/switch.py index 6e1e38675a0..c19cdb8c815 100644 --- a/homeassistant/components/synology_dsm/switch.py +++ b/homeassistant/components/synology_dsm/switch.py @@ -98,7 +98,7 @@ class SynoDSMSurveillanceHomeModeToggle( @property def available(self) -> bool: """Return True if entity is available.""" - return bool(self._api.surveillance_station) + return bool(self._api.surveillance_station) and super().available @property def device_info(self) -> DeviceInfo: diff --git a/homeassistant/components/synology_dsm/update.py b/homeassistant/components/synology_dsm/update.py index 7b1a36c57b3..c7bcff48cea 100644 --- a/homeassistant/components/synology_dsm/update.py +++ b/homeassistant/components/synology_dsm/update.py @@ -59,7 +59,7 @@ class SynoDSMUpdateEntity( @property def available(self) -> bool: """Return True if entity is available.""" - return bool(self._api.upgrade) + return bool(self._api.upgrade) and super().available @property def installed_version(self) -> str | None: From 1322f3891122cdef335a35e9d0871caf82e242f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joakim=20S=C3=B8rensen?= Date: Sun, 7 Apr 2024 14:20:58 +0200 Subject: [PATCH 141/198] Downgrade hass-nabucasa from 0.80.0 to 0.78.0 (#115078) --- homeassistant/components/cloud/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index eed2bda421b..49a3fc0bf5c 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -8,5 +8,5 @@ "integration_type": "system", "iot_class": "cloud_push", "loggers": ["hass_nabucasa"], - "requirements": ["hass-nabucasa==0.79.0"] + "requirements": ["hass-nabucasa==0.78.0"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index bd35403340f..4ba42672c4d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -27,7 +27,7 @@ fnv-hash-fast==0.5.0 ha-av==10.1.1 ha-ffmpeg==3.2.0 habluetooth==2.4.2 -hass-nabucasa==0.79.0 +hass-nabucasa==0.78.0 hassil==1.6.1 home-assistant-bluetooth==1.12.0 home-assistant-frontend==20240404.1 diff --git a/pyproject.toml b/pyproject.toml index 2dd3a9632c6..ff848d37b8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ dependencies = [ "fnv-hash-fast==0.5.0", # hass-nabucasa is imported by helpers which don't depend on the cloud # integration - "hass-nabucasa==0.79.0", + "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 1dd9b1811d3..05d66a79873 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ bcrypt==4.1.2 certifi>=2021.5.30 ciso8601==2.3.1 fnv-hash-fast==0.5.0 -hass-nabucasa==0.79.0 +hass-nabucasa==0.78.0 httpx==0.27.0 home-assistant-bluetooth==1.12.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index d48ee5bdaab..0741ee85dab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1037,7 +1037,7 @@ habitipy==0.2.0 habluetooth==2.4.2 # homeassistant.components.cloud -hass-nabucasa==0.79.0 +hass-nabucasa==0.78.0 # homeassistant.components.splunk hass-splunk==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8ee44387a02..b14195e88e1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -848,7 +848,7 @@ habitipy==0.2.0 habluetooth==2.4.2 # homeassistant.components.cloud -hass-nabucasa==0.79.0 +hass-nabucasa==0.78.0 # homeassistant.components.conversation hassil==1.6.1 From fa88975055590304ccebffdb7b9af34fffbb6398 Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Sun, 7 Apr 2024 19:32:15 +0200 Subject: [PATCH 142/198] Bump velbus-aio to 2024.4.1 (#115109) bump velbusaio to 2024.4.1 --- homeassistant/components/velbus/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index 1c51c58d238..6f817a23325 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -13,7 +13,7 @@ "velbus-packet", "velbus-protocol" ], - "requirements": ["velbus-aio==2024.4.0"], + "requirements": ["velbus-aio==2024.4.1"], "usb": [ { "vid": "10CF", diff --git a/requirements_all.txt b/requirements_all.txt index 0741ee85dab..e64621e3726 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2798,7 +2798,7 @@ vallox-websocket-api==5.1.1 vehicle==2.2.1 # homeassistant.components.velbus -velbus-aio==2024.4.0 +velbus-aio==2024.4.1 # homeassistant.components.venstar venstarcolortouch==0.19 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b14195e88e1..40386b1be2e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2154,7 +2154,7 @@ vallox-websocket-api==5.1.1 vehicle==2.2.1 # homeassistant.components.velbus -velbus-aio==2024.4.0 +velbus-aio==2024.4.1 # homeassistant.components.venstar venstarcolortouch==0.19 From 19f3ef763dd9a035ef7ff9da54013aec7cd0a6d0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Apr 2024 11:02:53 -1000 Subject: [PATCH 143/198] Terminate scripts with until and while conditions that execute more than 10000 times (#115110) --- homeassistant/helpers/script.py | 63 +++++++++++++++++++++++++++++++++ tests/helpers/test_script.py | 52 +++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index a86df259f11..b4e02e0e4ad 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -286,6 +286,9 @@ STATIC_VALIDATION_ACTION_TYPES = ( cv.SCRIPT_ACTION_WAIT_TEMPLATE, ) +REPEAT_WARN_ITERATIONS = 5000 +REPEAT_TERMINATE_ITERATIONS = 10000 + async def async_validate_actions_config( hass: HomeAssistant, actions: list[ConfigType] @@ -846,6 +849,7 @@ class _ScriptRun: # pylint: disable-next=protected-access script = self._script._get_repeat_script(self._step) + warned_too_many_loops = False async def async_run_sequence(iteration, extra_msg=""): self._log("Repeating %s: Iteration %i%s", description, iteration, extra_msg) @@ -916,6 +920,36 @@ class _ScriptRun: _LOGGER.warning("Error in 'while' evaluation:\n%s", ex) break + if iteration > 1: + if iteration > REPEAT_WARN_ITERATIONS: + if not warned_too_many_loops: + warned_too_many_loops = True + _LOGGER.warning( + "While condition %s in script `%s` looped %s times", + repeat[CONF_WHILE], + self._script.name, + REPEAT_WARN_ITERATIONS, + ) + + if iteration > REPEAT_TERMINATE_ITERATIONS: + _LOGGER.critical( + "While condition %s in script `%s` " + "terminated because it looped %s times", + repeat[CONF_WHILE], + self._script.name, + REPEAT_TERMINATE_ITERATIONS, + ) + raise _AbortScript( + f"While condition {repeat[CONF_WHILE]} " + "terminated because it looped " + f" {REPEAT_TERMINATE_ITERATIONS} times" + ) + + # If the user creates a script with a tight loop, + # yield to the event loop so the system stays + # responsive while all the cpu time is consumed. + await asyncio.sleep(0) + await async_run_sequence(iteration) elif CONF_UNTIL in repeat: @@ -934,6 +968,35 @@ class _ScriptRun: _LOGGER.warning("Error in 'until' evaluation:\n%s", ex) break + if iteration >= REPEAT_WARN_ITERATIONS: + if not warned_too_many_loops: + warned_too_many_loops = True + _LOGGER.warning( + "Until condition %s in script `%s` looped %s times", + repeat[CONF_UNTIL], + self._script.name, + REPEAT_WARN_ITERATIONS, + ) + + if iteration >= REPEAT_TERMINATE_ITERATIONS: + _LOGGER.critical( + "Until condition %s in script `%s` " + "terminated because it looped %s times", + repeat[CONF_UNTIL], + self._script.name, + REPEAT_TERMINATE_ITERATIONS, + ) + raise _AbortScript( + f"Until condition {repeat[CONF_UNTIL]} " + "terminated because it looped " + f"{REPEAT_TERMINATE_ITERATIONS} times" + ) + + # If the user creates a script with a tight loop, + # yield to the event loop so the system stays responsive + # while all the cpu time is consumed. + await asyncio.sleep(0) + if saved_repeat_vars: self._variables["repeat"] = saved_repeat_vars else: diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 86fb84eb582..409b3639d43 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -2837,6 +2837,58 @@ async def test_repeat_nested( assert_action_trace(expected_trace) +@pytest.mark.parametrize( + ("condition", "check"), [("while", "above"), ("until", "below")] +) +async def test_repeat_limits( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, condition: str, check: str +) -> None: + """Test limits on repeats prevent the system from hanging.""" + event = "test_event" + events = async_capture_events(hass, event) + hass.states.async_set("sensor.test", "0.5") + + sequence = { + "repeat": { + "sequence": [ + { + "event": event, + }, + ], + } + } + sequence["repeat"][condition] = { + "condition": "numeric_state", + "entity_id": "sensor.test", + check: "0", + } + + with ( + patch.object(script, "REPEAT_WARN_ITERATIONS", 5), + patch.object(script, "REPEAT_TERMINATE_ITERATIONS", 10), + ): + script_obj = script.Script( + hass, cv.SCRIPT_SCHEMA(sequence), f"Test {condition}", "test_domain" + ) + + caplog.clear() + caplog.set_level(logging.WARNING) + + hass.async_create_task(script_obj.async_run(context=Context())) + await asyncio.wait_for(hass.async_block_till_done(), 1) + + title_condition = condition.title() + + assert f"{title_condition} condition" in caplog.text + assert f"in script `Test {condition}` looped 5 times" in caplog.text + assert ( + f"script `Test {condition}` terminated because it looped 10 times" + in caplog.text + ) + + assert len(events) == 10 + + async def test_choose_warning( hass: HomeAssistant, caplog: pytest.LogCaptureFixture ) -> None: From 7bea6eface6f2aa8ae2dbf103cbce74669fa7721 Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Sun, 7 Apr 2024 21:07:51 +0200 Subject: [PATCH 144/198] improve handling of incorrect values in fyta integration (#115134) * improve handling of incorrect values * Changes based on review comment * Apply suggestions from code review Co-authored-by: Joost Lekkerkerker * update value_fn * ruff --------- Co-authored-by: Joost Lekkerkerker --- homeassistant/components/fyta/sensor.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/fyta/sensor.py b/homeassistant/components/fyta/sensor.py index 0643c69981e..2b9e8e3de07 100644 --- a/homeassistant/components/fyta/sensor.py +++ b/homeassistant/components/fyta/sensor.py @@ -46,35 +46,35 @@ SENSORS: Final[list[FytaSensorEntityDescription]] = [ translation_key="plant_status", device_class=SensorDeviceClass.ENUM, options=PLANT_STATUS_LIST, - value_fn=lambda value: PLANT_STATUS[value], + value_fn=PLANT_STATUS.get, ), FytaSensorEntityDescription( key="temperature_status", translation_key="temperature_status", device_class=SensorDeviceClass.ENUM, options=PLANT_STATUS_LIST, - value_fn=lambda value: PLANT_STATUS[value], + value_fn=PLANT_STATUS.get, ), FytaSensorEntityDescription( key="light_status", translation_key="light_status", device_class=SensorDeviceClass.ENUM, options=PLANT_STATUS_LIST, - value_fn=lambda value: PLANT_STATUS[value], + value_fn=PLANT_STATUS.get, ), FytaSensorEntityDescription( key="moisture_status", translation_key="moisture_status", device_class=SensorDeviceClass.ENUM, options=PLANT_STATUS_LIST, - value_fn=lambda value: PLANT_STATUS[value], + value_fn=PLANT_STATUS.get, ), FytaSensorEntityDescription( key="salinity_status", translation_key="salinity_status", device_class=SensorDeviceClass.ENUM, options=PLANT_STATUS_LIST, - value_fn=lambda value: PLANT_STATUS[value], + value_fn=PLANT_STATUS.get, ), FytaSensorEntityDescription( key="temperature", From fc9653581a49f0eb91264b6a09f908fd5e64fbfe Mon Sep 17 00:00:00 2001 From: dontinelli <73341522+dontinelli@users.noreply.github.com> Date: Sun, 7 Apr 2024 21:38:13 +0200 Subject: [PATCH 145/198] Bump fyta_cli to 0.3.5 (#115143) bump fyta_cli to 0.3.5 --- homeassistant/components/fyta/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/fyta/manifest.json b/homeassistant/components/fyta/manifest.json index a93a76a9e1d..55255777994 100644 --- a/homeassistant/components/fyta/manifest.json +++ b/homeassistant/components/fyta/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/fyta", "integration_type": "hub", "iot_class": "cloud_polling", - "requirements": ["fyta_cli==0.3.3"] + "requirements": ["fyta_cli==0.3.5"] } diff --git a/requirements_all.txt b/requirements_all.txt index e64621e3726..cbcae805bd0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -899,7 +899,7 @@ freesms==0.2.0 fritzconnection[qr]==1.13.2 # homeassistant.components.fyta -fyta_cli==0.3.3 +fyta_cli==0.3.5 # homeassistant.components.google_translate gTTS==2.2.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 40386b1be2e..77dbd53a73e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -731,7 +731,7 @@ freebox-api==1.1.0 fritzconnection[qr]==1.13.2 # homeassistant.components.fyta -fyta_cli==0.3.3 +fyta_cli==0.3.5 # homeassistant.components.google_translate gTTS==2.2.4 From 265d04c593cacd305e060a194a1643eb21b808c8 Mon Sep 17 00:00:00 2001 From: rappenze Date: Sun, 7 Apr 2024 22:53:30 +0200 Subject: [PATCH 146/198] Fix fibaro sensor additional sensor lookup (#115148) --- homeassistant/components/fibaro/sensor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/fibaro/sensor.py b/homeassistant/components/fibaro/sensor.py index 6e672e9cc97..fd6ec74050d 100644 --- a/homeassistant/components/fibaro/sensor.py +++ b/homeassistant/components/fibaro/sensor.py @@ -121,6 +121,7 @@ async def async_setup_entry( Platform.COVER, Platform.LIGHT, Platform.LOCK, + Platform.SENSOR, Platform.SWITCH, ) for device in controller.fibaro_devices[platform] From d062ef357b76f11dcc977ebc84a2148dafcfe8fe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Apr 2024 12:51:59 -1000 Subject: [PATCH 147/198] Write timer entity state before firing events (#115151) --- homeassistant/components/timer/__init__.py | 12 ++++++------ tests/components/timer/test_init.py | 17 ++++++++++++----- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/timer/__init__.py b/homeassistant/components/timer/__init__.py index 72e93f5655a..5da68d99dd6 100644 --- a/homeassistant/components/timer/__init__.py +++ b/homeassistant/components/timer/__init__.py @@ -325,12 +325,12 @@ class Timer(collection.CollectionEntity, RestoreEntity): self._end = start + self._remaining + self.async_write_ha_state() self.hass.bus.async_fire(event, {ATTR_ENTITY_ID: self.entity_id}) self._listener = async_track_point_in_utc_time( self.hass, self._async_finished, self._end ) - self.async_write_ha_state() @callback def async_change(self, duration: timedelta) -> None: @@ -351,11 +351,11 @@ class Timer(collection.CollectionEntity, RestoreEntity): self._listener() self._end += duration self._remaining = self._end - dt_util.utcnow().replace(microsecond=0) + self.async_write_ha_state() self.hass.bus.async_fire(EVENT_TIMER_CHANGED, {ATTR_ENTITY_ID: self.entity_id}) self._listener = async_track_point_in_utc_time( self.hass, self._async_finished, self._end ) - self.async_write_ha_state() @callback def async_pause(self) -> None: @@ -368,8 +368,8 @@ class Timer(collection.CollectionEntity, RestoreEntity): self._remaining = self._end - dt_util.utcnow().replace(microsecond=0) self._state = STATUS_PAUSED self._end = None - self.hass.bus.async_fire(EVENT_TIMER_PAUSED, {ATTR_ENTITY_ID: self.entity_id}) self.async_write_ha_state() + self.hass.bus.async_fire(EVENT_TIMER_PAUSED, {ATTR_ENTITY_ID: self.entity_id}) @callback def async_cancel(self) -> None: @@ -381,10 +381,10 @@ class Timer(collection.CollectionEntity, RestoreEntity): self._end = None self._remaining = None self._running_duration = self._configured_duration + self.async_write_ha_state() self.hass.bus.async_fire( EVENT_TIMER_CANCELLED, {ATTR_ENTITY_ID: self.entity_id} ) - self.async_write_ha_state() @callback def async_finish(self) -> None: @@ -400,11 +400,11 @@ class Timer(collection.CollectionEntity, RestoreEntity): self._end = None self._remaining = None self._running_duration = self._configured_duration + self.async_write_ha_state() self.hass.bus.async_fire( EVENT_TIMER_FINISHED, {ATTR_ENTITY_ID: self.entity_id, ATTR_FINISHED_AT: end.isoformat()}, ) - self.async_write_ha_state() @callback def _async_finished(self, time: datetime) -> None: @@ -418,11 +418,11 @@ class Timer(collection.CollectionEntity, RestoreEntity): self._end = None self._remaining = None self._running_duration = self._configured_duration + self.async_write_ha_state() self.hass.bus.async_fire( EVENT_TIMER_FINISHED, {ATTR_ENTITY_ID: self.entity_id, ATTR_FINISHED_AT: end.isoformat()}, ) - self.async_write_ha_state() async def async_update_config(self, config: ConfigType) -> None: """Handle when the config is updated.""" diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 5aca1625d1f..c1c9f56094b 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -45,7 +45,7 @@ from homeassistant.const import ( EVENT_STATE_CHANGED, SERVICE_RELOAD, ) -from homeassistant.core import Context, CoreState, HomeAssistant, State +from homeassistant.core import Context, CoreState, Event, HomeAssistant, State, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import config_validation as cv, entity_registry as er from homeassistant.helpers.restore_state import StoredState, async_get @@ -156,11 +156,12 @@ async def test_methods_and_events(hass: HomeAssistant) -> None: assert state assert state.state == STATUS_IDLE - results = [] + results: list[tuple[Event, str]] = [] - def fake_event_listener(event): + @callback + def fake_event_listener(event: Event): """Fake event listener for trigger.""" - results.append(event) + results.append((event, hass.states.get("timer.test1").state)) hass.bus.async_listen(EVENT_TIMER_STARTED, fake_event_listener) hass.bus.async_listen(EVENT_TIMER_RESTARTED, fake_event_listener) @@ -262,7 +263,10 @@ async def test_methods_and_events(hass: HomeAssistant) -> None: if step["event"] is not None: expected_events += 1 - assert results[-1].event_type == step["event"] + last_result = results[-1] + event, state = last_result + assert event.event_type == step["event"] + assert state == step["state"] assert len(results) == expected_events @@ -404,6 +408,7 @@ async def test_wait_till_timer_expires(hass: HomeAssistant) -> None: results = [] + @callback def fake_event_listener(event): """Fake event listener for trigger.""" results.append(event) @@ -580,6 +585,7 @@ async def test_timer_restarted_event(hass: HomeAssistant) -> None: results = [] + @callback def fake_event_listener(event): """Fake event listener for trigger.""" results.append(event) @@ -647,6 +653,7 @@ async def test_state_changed_when_timer_restarted(hass: HomeAssistant) -> None: results = [] + @callback def fake_event_listener(event): """Fake event listener for trigger.""" results.append(event) From 9a342f87c0b78ceccc733e27fe53e7f2ef6e20a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 7 Apr 2024 15:25:55 -1000 Subject: [PATCH 148/198] Avoid checking for polling if an entity fails to add (#115159) * Avoid checking for polling if an entity fails to add * no need to do protected access * no need to do protected access * no need to do protected access * no need to do protected access * coverage * fix test * fix * broken one must be first --- homeassistant/helpers/entity_platform.py | 11 +++++++- tests/helpers/test_entity_platform.py | 36 +++++++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 1cff472af72..6d7ed7ed1b8 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -631,7 +631,16 @@ class EntityPlatform: if ( (self.config_entry and self.config_entry.pref_disable_polling) or self._async_unsub_polling is not None - or not any(entity.should_poll for entity in entities) + or not any( + # Entity may have failed to add or called `add_to_platform_abort` + # so we check if the entity is in self.entities before + # checking `entity.should_poll` since `should_poll` may need to + # check `self.hass` which will be `None` if the entity did not add + entity.entity_id + and entity.entity_id in self.entities + and entity.should_poll + for entity in entities + ) ): return diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 31c6f8e6e30..59c4f7357f3 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -5,7 +5,7 @@ from collections.abc import Iterable from datetime import timedelta import logging from typing import Any -from unittest.mock import ANY, Mock, patch +from unittest.mock import ANY, AsyncMock, Mock, patch import pytest @@ -78,6 +78,40 @@ async def test_polling_only_updates_entities_it_should_poll( assert poll_ent.async_update.called +async def test_polling_check_works_if_entity_add_fails( + hass: HomeAssistant, +) -> None: + """Test the polling check works if an entity add fails.""" + component = EntityComponent(_LOGGER, DOMAIN, hass, timedelta(seconds=20)) + await component.async_setup({}) + + class MockEntityNeedsSelfHassInShouldPoll(MockEntity): + """Mock entity that needs self.hass in should_poll.""" + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled.""" + return self.hass.data is not None + + working_poll_ent = MockEntityNeedsSelfHassInShouldPoll(should_poll=True) + working_poll_ent.async_update = AsyncMock() + broken_poll_ent = MockEntityNeedsSelfHassInShouldPoll(should_poll=True) + broken_poll_ent.async_update = AsyncMock(side_effect=Exception("Broken")) + + await component.async_add_entities( + [broken_poll_ent, working_poll_ent], update_before_add=True + ) + + working_poll_ent.async_update.reset_mock() + broken_poll_ent.async_update.reset_mock() + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=20)) + await hass.async_block_till_done(wait_background_tasks=True) + + assert not broken_poll_ent.async_update.called + assert working_poll_ent.async_update.called + + async def test_polling_disabled_by_config_entry(hass: HomeAssistant) -> None: """Test the polling of only updated entities.""" entity_platform = MockEntityPlatform(hass) From 8c0b44d6d52259a1c284adc3a1df77b0c07ab7c6 Mon Sep 17 00:00:00 2001 From: gibwar Date: Mon, 8 Apr 2024 02:05:46 -0600 Subject: [PATCH 149/198] Only reset requested utility meter with no tariff (#115170) --- .../components/utility_meter/sensor.py | 8 +- tests/components/utility_meter/test_sensor.py | 133 ++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 26582df1b44..014cd93b53b 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -578,7 +578,13 @@ class UtilityMeterSensor(RestoreSensor): async def async_reset_meter(self, entity_id): """Reset meter.""" - if self._tariff is not None and self._tariff_entity != entity_id: + if self._tariff_entity is not None and self._tariff_entity != entity_id: + return + if ( + self._tariff_entity is None + and entity_id is not None + and self.entity_id != entity_id + ): return _LOGGER.debug("Reset utility meter <%s>", self.entity_id) self._last_reset = dt_util.utcnow() diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 99a63809329..43a71eca85e 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -983,6 +983,139 @@ async def test_service_reset_no_tariffs( assert state.attributes.get("last_period") == "3" +@pytest.mark.parametrize( + ("yaml_config", "config_entry_configs"), + [ + ( + { + "utility_meter": { + "energy_bill": { + "source": "sensor.energy", + }, + "water_bill": { + "source": "sensor.water", + }, + }, + }, + None, + ), + ( + None, + [ + { + "cycle": "none", + "delta_values": False, + "name": "Energy bill", + "net_consumption": False, + "offset": 0, + "periodically_resetting": True, + "source": "sensor.energy", + "tariffs": [], + }, + { + "cycle": "none", + "delta_values": False, + "name": "Water bill", + "net_consumption": False, + "offset": 0, + "periodically_resetting": True, + "source": "sensor.water", + "tariffs": [], + }, + ], + ), + ], +) +async def test_service_reset_no_tariffs_correct_with_multi( + hass: HomeAssistant, yaml_config, config_entry_configs +) -> None: + """Test complex utility sensor service reset for multiple sensors with no tarrifs. + + See GitHub issue #114864: Service "utility_meter.reset" affects all meters. + """ + + # Home assistant is not runnit yet + hass.state = CoreState.not_running + last_reset = "2023-10-01T00:00:00+00:00" + + mock_restore_cache_with_extra_data( + hass, + [ + ( + State( + "sensor.energy_bill", + "3", + attributes={ + ATTR_LAST_RESET: last_reset, + }, + ), + {}, + ), + ( + State( + "sensor.water_bill", + "6", + attributes={ + ATTR_LAST_RESET: last_reset, + }, + ), + {}, + ), + ], + ) + + if yaml_config: + assert await async_setup_component(hass, DOMAIN, yaml_config) + await hass.async_block_till_done() + else: + for entry in config_entry_configs: + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options=entry, + title=entry["name"], + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_bill") + assert state + assert state.state == "3" + assert state.attributes.get("last_reset") == last_reset + assert state.attributes.get("last_period") == "0" + + state = hass.states.get("sensor.water_bill") + assert state + assert state.state == "6" + assert state.attributes.get("last_reset") == last_reset + assert state.attributes.get("last_period") == "0" + + now = dt_util.utcnow() + with freeze_time(now): + await hass.services.async_call( + domain=DOMAIN, + service=SERVICE_RESET, + service_data={}, + target={"entity_id": "sensor.energy_bill"}, + blocking=True, + ) + + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_bill") + assert state + assert state.state == "0" + assert state.attributes.get("last_reset") == now.isoformat() + assert state.attributes.get("last_period") == "3" + + state = hass.states.get("sensor.water_bill") + assert state + assert state.state == "6" + assert state.attributes.get("last_reset") == last_reset + assert state.attributes.get("last_period") == "0" + + @pytest.mark.parametrize( ("yaml_config", "config_entry_config"), [ From 05082fcceb9293e078131fb7206ac3cdb288a342 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 8 Apr 2024 10:41:50 +0200 Subject: [PATCH 150/198] Bump version to 2024.4.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 b642ce6ce8c..e4359f5bbfb 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -18,7 +18,7 @@ from .util.signal_type import SignalType APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 4 -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, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index ff848d37b8a..a6484fa3349 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.4.1" +version = "2024.4.2" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 733e2ec57aed3a196acfa50b2d3a3da019ed7e2e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 11 Apr 2024 11:58:56 -1000 Subject: [PATCH 151/198] Bump aiohttp to 3.9.4 (#110730) * Bump aiohttp to 3.9.4 This is rc0 for now but will be updated when the full release it out * cleanup cruft * regen * fix tests (these changes are fine) * chunk size is too small to read since boundry is now enforced * chunk size is too small to read since boundry is now enforced --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- tests/components/file_upload/test_init.py | 8 ++++---- tests/components/websocket_api/test_auth.py | 2 +- tests/components/websocket_api/test_http.py | 6 +++--- tests/components/websocket_api/test_init.py | 2 +- 7 files changed, 12 insertions(+), 12 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 4ba42672c4d..b8c8b0fcb64 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.0 aiodiscover==2.0.0 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.3.1 -aiohttp==3.9.3 +aiohttp==3.9.4 aiohttp_cors==0.7.0 astral==2.2 async-interrupt==1.1.1 diff --git a/pyproject.toml b/pyproject.toml index a6484fa3349..9993c8e9cb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ ] requires-python = ">=3.12.0" dependencies = [ - "aiohttp==3.9.3", + "aiohttp==3.9.4", "aiohttp_cors==0.7.0", "aiohttp-fast-url-dispatcher==0.3.0", "aiohttp-zlib-ng==0.3.1", diff --git a/requirements.txt b/requirements.txt index 05d66a79873..519a8287d18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ -c homeassistant/package_constraints.txt # Home Assistant Core -aiohttp==3.9.3 +aiohttp==3.9.4 aiohttp_cors==0.7.0 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.3.1 diff --git a/tests/components/file_upload/test_init.py b/tests/components/file_upload/test_init.py index 1ef238cafd0..fa77f6e55f5 100644 --- a/tests/components/file_upload/test_init.py +++ b/tests/components/file_upload/test_init.py @@ -90,9 +90,9 @@ async def test_upload_large_file( file_upload.TEMP_DIR_NAME + f"-{getrandbits(10):03x}", ), patch( - # Patch one megabyte to 8 bytes to prevent having to use big files in tests + # Patch one megabyte to 50 bytes to prevent having to use big files in tests "homeassistant.components.file_upload.ONE_MEGABYTE", - 8, + 50, ), ): res = await client.post("/api/file_upload", data={"file": large_file_io}) @@ -152,9 +152,9 @@ async def test_upload_large_file_fails( file_upload.TEMP_DIR_NAME + f"-{getrandbits(10):03x}", ), patch( - # Patch one megabyte to 8 bytes to prevent having to use big files in tests + # Patch one megabyte to 50 bytes to prevent having to use big files in tests "homeassistant.components.file_upload.ONE_MEGABYTE", - 8, + 50, ), patch( "homeassistant.components.file_upload.Path.open", return_value=_mock_open() diff --git a/tests/components/websocket_api/test_auth.py b/tests/components/websocket_api/test_auth.py index 35bf2402b6c..595dc7dcc32 100644 --- a/tests/components/websocket_api/test_auth.py +++ b/tests/components/websocket_api/test_auth.py @@ -221,7 +221,7 @@ async def test_auth_close_after_revoke( hass.auth.async_remove_refresh_token(refresh_token) msg = await websocket_client.receive() - assert msg.type == aiohttp.WSMsgType.CLOSED + assert msg.type is aiohttp.WSMsgType.CLOSE assert websocket_client.closed diff --git a/tests/components/websocket_api/test_http.py b/tests/components/websocket_api/test_http.py index db186e4811b..6ce46a5d9fe 100644 --- a/tests/components/websocket_api/test_http.py +++ b/tests/components/websocket_api/test_http.py @@ -43,7 +43,7 @@ async def test_pending_msg_overflow( for idx in range(10): await websocket_client.send_json({"id": idx + 1, "type": "ping"}) msg = await websocket_client.receive() - assert msg.type == WSMsgType.CLOSED + assert msg.type is WSMsgType.CLOSE async def test_cleanup_on_cancellation( @@ -249,7 +249,7 @@ async def test_pending_msg_peak( ) msg = await websocket_client.receive() - assert msg.type == WSMsgType.CLOSED + assert msg.type is WSMsgType.CLOSE assert "Client unable to keep up with pending messages" in caplog.text assert "Stayed over 5 for 5 seconds" in caplog.text assert "overload" in caplog.text @@ -297,7 +297,7 @@ async def test_pending_msg_peak_recovery( msg = await websocket_client.receive() assert msg.type == WSMsgType.TEXT msg = await websocket_client.receive() - assert msg.type == WSMsgType.CLOSED + assert msg.type is WSMsgType.CLOSE assert "Client unable to keep up with pending messages" not in caplog.text diff --git a/tests/components/websocket_api/test_init.py b/tests/components/websocket_api/test_init.py index 9360ff4ef8a..b20fd1c2f7e 100644 --- a/tests/components/websocket_api/test_init.py +++ b/tests/components/websocket_api/test_init.py @@ -41,7 +41,7 @@ async def test_quiting_hass(hass: HomeAssistant, websocket_client) -> None: msg = await websocket_client.receive() - assert msg.type == WSMsgType.CLOSED + assert msg.type is WSMsgType.CLOSE async def test_unknown_command(websocket_client) -> None: From 4c6fad8dc3d1c2a8e2e19f273d57b2193b2057aa Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Thu, 11 Apr 2024 05:23:10 -0400 Subject: [PATCH 152/198] Add support for adopt data disk repair (#114891) --- homeassistant/components/hassio/repairs.py | 2 +- homeassistant/components/hassio/strings.json | 11 +- tests/components/hassio/test_repairs.py | 113 +++++++++++++++++++ 3 files changed, 123 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hassio/repairs.py b/homeassistant/components/hassio/repairs.py index 8458d7eaac2..63ed3d5c8a3 100644 --- a/homeassistant/components/hassio/repairs.py +++ b/homeassistant/components/hassio/repairs.py @@ -22,7 +22,7 @@ from .const import ( from .handler import async_apply_suggestion from .issues import Issue, Suggestion -SUGGESTION_CONFIRMATION_REQUIRED = {"system_execute_reboot"} +SUGGESTION_CONFIRMATION_REQUIRED = {"system_adopt_data_disk", "system_execute_reboot"} EXTRA_PLACEHOLDERS = { "issue_mount_mount_failed": { diff --git a/homeassistant/components/hassio/strings.json b/homeassistant/components/hassio/strings.json index 77ef408cafe..63c1da4bfd8 100644 --- a/homeassistant/components/hassio/strings.json +++ b/homeassistant/components/hassio/strings.json @@ -51,8 +51,15 @@ "title": "Multiple data disks detected", "fix_flow": { "step": { - "system_rename_data_disk": { - "description": "`{reference}` is a filesystem with the name hassos-data and is not the active data disk. This can cause Home Assistant to choose the wrong data disk at system reboot.\n\nUse the fix option to rename the filesystem to prevent this. Alternatively you can move the data disk to the drive (overwriting its contents) or remove the drive from the system." + "fix_menu": { + "description": "`{reference}` is a filesystem with the name hassos-data and is not the active data disk. This can cause Home Assistant to choose the wrong data disk at system reboot.\n\nUse the 'Rename' option to rename the filesystem to prevent this. Use the 'Adopt' option to make that your data disk and rename the existing one. Alternatively you can move the data disk to the drive (overwriting its contents) or remove the drive from the system.", + "menu_options": { + "system_rename_data_disk": "Rename", + "system_adopt_data_disk": "Adopt" + } + }, + "system_adopt_data_disk": { + "description": "This fix will initiate a system reboot which will make Home Assistant and all the Add-ons inaccessible for a brief period. After the reboot `{reference}` will be the data disk of Home Assistant and your existing data disk will be renamed and ignored." } }, "abort": { diff --git a/tests/components/hassio/test_repairs.py b/tests/components/hassio/test_repairs.py index d387968da46..2dffba74fef 100644 --- a/tests/components/hassio/test_repairs.py +++ b/tests/components/hassio/test_repairs.py @@ -674,3 +674,116 @@ async def test_supervisor_issue_docker_config_repair_flow( str(aioclient_mock.mock_calls[-1][1]) == "http://127.0.0.1/resolution/suggestion/1235" ) + + +async def test_supervisor_issue_repair_flow_multiple_data_disks( + hass: HomeAssistant, + aioclient_mock: AiohttpClientMocker, + hass_client: ClientSessionGenerator, + issue_registry: ir.IssueRegistry, + all_setup_requests, +) -> None: + """Test fix flow for multiple data disks supervisor issue.""" + mock_resolution_info( + aioclient_mock, + issues=[ + { + "uuid": "1234", + "type": "multiple_data_disks", + "context": "system", + "reference": "/dev/sda1", + "suggestions": [ + { + "uuid": "1235", + "type": "rename_data_disk", + "context": "system", + "reference": "/dev/sda1", + }, + { + "uuid": "1236", + "type": "adopt_data_disk", + "context": "system", + "reference": "/dev/sda1", + }, + ], + }, + ], + ) + + 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"] + assert data == { + "type": "menu", + "flow_id": flow_id, + "handler": "hassio", + "step_id": "fix_menu", + "data_schema": [ + { + "type": "select", + "options": [ + ["system_rename_data_disk", "system_rename_data_disk"], + ["system_adopt_data_disk", "system_adopt_data_disk"], + ], + "name": "next_step_id", + } + ], + "menu_options": ["system_rename_data_disk", "system_adopt_data_disk"], + "description_placeholders": {"reference": "/dev/sda1"}, + } + + resp = await client.post( + f"/api/repairs/issues/fix/{flow_id}", + json={"next_step_id": "system_adopt_data_disk"}, + ) + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "type": "form", + "flow_id": flow_id, + "handler": "hassio", + "step_id": "system_adopt_data_disk", + "data_schema": [], + "errors": None, + "description_placeholders": {"reference": "/dev/sda1"}, + "last_step": True, + "preview": None, + } + + resp = await client.post(f"/api/repairs/issues/fix/{flow_id}") + + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data == { + "type": "create_entry", + "flow_id": flow_id, + "handler": "hassio", + "description": None, + "description_placeholders": None, + } + + assert not issue_registry.async_get_issue(domain="hassio", issue_id="1234") + + assert aioclient_mock.mock_calls[-1][0] == "post" + assert ( + str(aioclient_mock.mock_calls[-1][1]) + == "http://127.0.0.1/resolution/suggestion/1236" + ) From 922cc81a62e9ccd7ecf01127dfe70f71338aaddf Mon Sep 17 00:00:00 2001 From: On Freund Date: Tue, 9 Apr 2024 10:59:27 +0300 Subject: [PATCH 153/198] Configurable maximum concurrency in Risco local (#115226) * Configurable maximum concurrency in Risco local * Show advanced Risco options in advanced mode --- homeassistant/components/risco/__init__.py | 7 ++- homeassistant/components/risco/config_flow.py | 20 +++++-- homeassistant/components/risco/const.py | 8 ++- homeassistant/components/risco/strings.json | 3 +- tests/components/risco/test_config_flow.py | 53 ++++++++++++++++++- 5 files changed, 83 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 531cd982a1e..7ca18ea77c5 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -38,7 +38,9 @@ from homeassistant.helpers.storage import Store from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( + CONF_CONCURRENCY, DATA_COORDINATOR, + DEFAULT_CONCURRENCY, DEFAULT_SCAN_INTERVAL, DOMAIN, EVENTS_COORDINATOR, @@ -85,7 +87,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def _async_setup_local_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: data = entry.data - risco = RiscoLocal(data[CONF_HOST], data[CONF_PORT], data[CONF_PIN]) + concurrency = entry.options.get(CONF_CONCURRENCY, DEFAULT_CONCURRENCY) + risco = RiscoLocal( + data[CONF_HOST], data[CONF_PORT], data[CONF_PIN], concurrency=concurrency + ) try: await risco.connect() diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index 0f13721856c..5822177a243 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -35,8 +35,10 @@ from .const import ( CONF_CODE_ARM_REQUIRED, CONF_CODE_DISARM_REQUIRED, CONF_COMMUNICATION_DELAY, + CONF_CONCURRENCY, CONF_HA_STATES_TO_RISCO, CONF_RISCO_STATES_TO_HA, + DEFAULT_ADVANCED_OPTIONS, DEFAULT_OPTIONS, DOMAIN, MAX_COMMUNICATION_DELAY, @@ -225,11 +227,8 @@ class RiscoOptionsFlowHandler(OptionsFlow): self._data = {**DEFAULT_OPTIONS, **config_entry.options} def _options_schema(self) -> vol.Schema: - return vol.Schema( + schema = vol.Schema( { - vol.Required( - CONF_SCAN_INTERVAL, default=self._data[CONF_SCAN_INTERVAL] - ): int, vol.Required( CONF_CODE_ARM_REQUIRED, default=self._data[CONF_CODE_ARM_REQUIRED] ): bool, @@ -239,6 +238,19 @@ class RiscoOptionsFlowHandler(OptionsFlow): ): bool, } ) + if self.show_advanced_options: + self._data = {**DEFAULT_ADVANCED_OPTIONS, **self._data} + schema = schema.extend( + { + vol.Required( + CONF_SCAN_INTERVAL, default=self._data[CONF_SCAN_INTERVAL] + ): int, + vol.Required( + CONF_CONCURRENCY, default=self._data[CONF_CONCURRENCY] + ): int, + } + ) + return schema async def async_step_init( self, user_input: dict[str, Any] | None = None diff --git a/homeassistant/components/risco/const.py b/homeassistant/components/risco/const.py index a27aeae4bf0..f1240a704de 100644 --- a/homeassistant/components/risco/const.py +++ b/homeassistant/components/risco/const.py @@ -14,6 +14,7 @@ DATA_COORDINATOR = "risco" EVENTS_COORDINATOR = "risco_events" DEFAULT_SCAN_INTERVAL = 30 +DEFAULT_CONCURRENCY = 4 TYPE_LOCAL = "local" @@ -25,6 +26,7 @@ CONF_CODE_DISARM_REQUIRED = "code_disarm_required" CONF_RISCO_STATES_TO_HA = "risco_states_to_ha" CONF_HA_STATES_TO_RISCO = "ha_states_to_risco" CONF_COMMUNICATION_DELAY = "communication_delay" +CONF_CONCURRENCY = "concurrency" RISCO_GROUPS = ["A", "B", "C", "D"] RISCO_ARM = "arm" @@ -44,9 +46,13 @@ DEFAULT_HA_STATES_TO_RISCO = { } DEFAULT_OPTIONS = { - CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, CONF_CODE_ARM_REQUIRED: False, CONF_CODE_DISARM_REQUIRED: False, CONF_RISCO_STATES_TO_HA: DEFAULT_RISCO_STATES_TO_HA, CONF_HA_STATES_TO_RISCO: DEFAULT_HA_STATES_TO_RISCO, } + +DEFAULT_ADVANCED_OPTIONS = { + CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, + CONF_CONCURRENCY: DEFAULT_CONCURRENCY, +} diff --git a/homeassistant/components/risco/strings.json b/homeassistant/components/risco/strings.json index 69d7e571f43..e35b13394cb 100644 --- a/homeassistant/components/risco/strings.json +++ b/homeassistant/components/risco/strings.json @@ -36,7 +36,8 @@ "init": { "title": "Configure options", "data": { - "scan_interval": "How often to poll Risco (in seconds)", + "scan_interval": "How often to poll Risco Cloud (in seconds)", + "concurrency": "Maximum concurrent requests in Risco local", "code_arm_required": "Require PIN to arm", "code_disarm_required": "Require PIN to disarm" } diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index d031f4e8542..db39447c69a 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -46,11 +46,15 @@ TEST_HA_TO_RISCO = { } TEST_OPTIONS = { - "scan_interval": 10, "code_arm_required": True, "code_disarm_required": True, } +TEST_ADVANCED_OPTIONS = { + "scan_interval": 10, + "concurrency": 3, +} + async def test_cloud_form(hass: HomeAssistant) -> None: """Test we get the cloud form.""" @@ -387,6 +391,53 @@ async def test_options_flow(hass: HomeAssistant) -> None: } +async def test_advanced_options_flow(hass: HomeAssistant) -> None: + """Test options flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_CLOUD_DATA["username"], + data=TEST_CLOUD_DATA, + ) + + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init( + entry.entry_id, context={"show_advanced_options": True} + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "init" + assert "concurrency" in result["data_schema"].schema + assert "scan_interval" in result["data_schema"].schema + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={**TEST_OPTIONS, **TEST_ADVANCED_OPTIONS} + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "risco_to_ha" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=TEST_RISCO_TO_HA, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "ha_to_risco" + + with patch("homeassistant.components.risco.async_setup_entry", return_value=True): + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=TEST_HA_TO_RISCO, + ) + + assert result["type"] is FlowResultType.CREATE_ENTRY + assert entry.options == { + **TEST_OPTIONS, + **TEST_ADVANCED_OPTIONS, + "risco_states_to_ha": TEST_RISCO_TO_HA, + "ha_states_to_risco": TEST_HA_TO_RISCO, + } + + async def test_ha_to_risco_schema(hass: HomeAssistant) -> None: """Test that the schema for the ha-to-risco mapping step is generated properly.""" entry = MockConfigEntry( From fc60426213406142d7824bf64b1f5f75b6201c0d Mon Sep 17 00:00:00 2001 From: On Freund Date: Thu, 11 Apr 2024 00:26:15 +0300 Subject: [PATCH 154/198] Improve Risco exception logging (#115232) --- homeassistant/components/risco/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 7ca18ea77c5..d25579343c8 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -101,7 +101,7 @@ async def _async_setup_local_entry(hass: HomeAssistant, entry: ConfigEntry) -> b return False async def _error(error: Exception) -> None: - _LOGGER.error("Error in Risco library: %s", error) + _LOGGER.error("Error in Risco library", exc_info=error) entry.async_on_unload(risco.add_error_handler(_error)) From f284273ef6ef8ae5e91ba9826de5da09ea464437 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 8 Apr 2024 11:09:18 -1000 Subject: [PATCH 155/198] Fix misssing timeout in caldav (#115247) --- homeassistant/components/caldav/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/caldav/__init__.py b/homeassistant/components/caldav/__init__.py index eed06a3a005..3111460e968 100644 --- a/homeassistant/components/caldav/__init__.py +++ b/homeassistant/components/caldav/__init__.py @@ -34,6 +34,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: username=entry.data[CONF_USERNAME], password=entry.data[CONF_PASSWORD], ssl_verify_cert=entry.data[CONF_VERIFY_SSL], + timeout=10, ) try: await hass.async_add_executor_job(client.principal) From 14da34cd4def30c32107e98e48cf5f82ac195e28 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Mon, 8 Apr 2024 22:39:31 -0700 Subject: [PATCH 156/198] Fix Google Tasks parsing of remove responses (#115258) --- homeassistant/components/google_tasks/api.py | 5 +++-- tests/components/google_tasks/test_todo.py | 10 +++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/google_tasks/api.py b/homeassistant/components/google_tasks/api.py index 2658fdedc59..ed70f2f6f44 100644 --- a/homeassistant/components/google_tasks/api.py +++ b/homeassistant/components/google_tasks/api.py @@ -112,8 +112,9 @@ class AsyncConfigEntryAuth: raise GoogleTasksApiError( f"Google Tasks API responded with error ({exception.status_code})" ) from exception - data = json.loads(response) - _raise_if_error(data) + if response: + data = json.loads(response) + _raise_if_error(data) for task_id in task_ids: batch.add( diff --git a/tests/components/google_tasks/test_todo.py b/tests/components/google_tasks/test_todo.py index 83d419439d7..afbaabe5cd0 100644 --- a/tests/components/google_tasks/test_todo.py +++ b/tests/components/google_tasks/test_todo.py @@ -156,7 +156,7 @@ def create_response_object(api_response: dict | list) -> tuple[Response, bytes]: def create_batch_response_object( - content_ids: list[str], api_responses: list[dict | list | Response] + content_ids: list[str], api_responses: list[dict | list | Response | None] ) -> tuple[Response, bytes]: """Create a batch response in the multipart/mixed format.""" assert len(api_responses) == len(content_ids) @@ -166,7 +166,7 @@ def create_batch_response_object( body = "" if isinstance(api_response, Response): status = api_response.status - else: + elif api_response is not None: body = json.dumps(api_response) content.extend( [ @@ -194,7 +194,7 @@ def create_batch_response_object( def create_batch_response_handler( - api_responses: list[dict | list | Response], + api_responses: list[dict | list | Response | None], ) -> Callable[[Any], tuple[Response, bytes]]: """Create a fake http2lib response handler that supports generating batch responses. @@ -598,11 +598,11 @@ async def test_partial_update_status( [ LIST_TASK_LIST_RESPONSE, LIST_TASKS_RESPONSE_MULTIPLE, - [EMPTY_RESPONSE, EMPTY_RESPONSE, EMPTY_RESPONSE], # Delete batch + [None, None, None], # Delete batch empty responses LIST_TASKS_RESPONSE, # refresh after delete ] ) - ) + ), ], ) async def test_delete_todo_list_item( From 5723ed28d3fbc739b4295bbcf205220f2e394ff8 Mon Sep 17 00:00:00 2001 From: Klaas Schoute Date: Tue, 9 Apr 2024 18:34:04 +0200 Subject: [PATCH 157/198] Bump forecast-solar lib to v3.1.0 (#115272) --- homeassistant/components/forecast_solar/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/forecast_solar/manifest.json b/homeassistant/components/forecast_solar/manifest.json index 94b603e108c..f5dd79281e6 100644 --- a/homeassistant/components/forecast_solar/manifest.json +++ b/homeassistant/components/forecast_solar/manifest.json @@ -7,5 +7,5 @@ "integration_type": "service", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["forecast-solar==3.0.0"] + "requirements": ["forecast-solar==3.1.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index cbcae805bd0..88ce477e718 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -883,7 +883,7 @@ fnv-hash-fast==0.5.0 foobot_async==1.0.0 # homeassistant.components.forecast_solar -forecast-solar==3.0.0 +forecast-solar==3.1.0 # homeassistant.components.fortios fortiosapi==1.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 77dbd53a73e..5dc0aced2c3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -721,7 +721,7 @@ fnv-hash-fast==0.5.0 foobot_async==1.0.0 # homeassistant.components.forecast_solar -forecast-solar==3.0.0 +forecast-solar==3.1.0 # homeassistant.components.freebox freebox-api==1.1.0 From 08bd2696960319e349162fb8ae6ad1b708af8938 Mon Sep 17 00:00:00 2001 From: Stefan Agner Date: Fri, 12 Apr 2024 09:02:22 +0200 Subject: [PATCH 158/198] Support backup of add-ons with hyphens (#115274) Co-authored-by: J. Nick Koston --- homeassistant/components/hassio/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 90b155aff15..46ba00185f5 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -196,7 +196,7 @@ SCHEMA_BACKUP_PARTIAL = SCHEMA_BACKUP_FULL.extend( { vol.Optional(ATTR_HOMEASSISTANT): cv.boolean, vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.slug]), + vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [VALID_ADDON_SLUG]), } ) @@ -211,7 +211,7 @@ SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend( { vol.Optional(ATTR_HOMEASSISTANT): cv.boolean, vol.Optional(ATTR_FOLDERS): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [cv.slug]), + vol.Optional(ATTR_ADDONS): vol.All(cv.ensure_list, [VALID_ADDON_SLUG]), } ) From db2005d4ecbf31b65988b59291c9c5c9c0847f15 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Tue, 9 Apr 2024 17:09:55 +0200 Subject: [PATCH 159/198] Bump pymodbus v3.6.7 (#115279) Bump pymodbus v3.6.7. --- 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 956961c7e67..0fe8c7bc42d 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": "platinum", - "requirements": ["pymodbus==3.6.6"] + "requirements": ["pymodbus==3.6.7"] } diff --git a/requirements_all.txt b/requirements_all.txt index 88ce477e718..ad8dad10170 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1973,7 +1973,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.6.6 +pymodbus==3.6.7 # homeassistant.components.monoprice pymonoprice==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5dc0aced2c3..6e9b5fd9d0d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1533,7 +1533,7 @@ pymeteoclimatic==0.1.0 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.6.6 +pymodbus==3.6.7 # homeassistant.components.monoprice pymonoprice==0.4 From 150145c9b1cefe0146b649122267ccc65c7022dc Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Tue, 9 Apr 2024 21:10:22 +0200 Subject: [PATCH 160/198] Bump yt-dlp to 2024.04.09 (#115295) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index c86099a9ea4..940d1d7bb18 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -7,5 +7,5 @@ "iot_class": "calculated", "loggers": ["yt_dlp"], "quality_scale": "internal", - "requirements": ["yt-dlp==2024.03.10"] + "requirements": ["yt-dlp==2024.04.09"] } diff --git a/requirements_all.txt b/requirements_all.txt index ad8dad10170..ca93ab1f7f1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2919,7 +2919,7 @@ youless-api==1.0.1 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.03.10 +yt-dlp==2024.04.09 # homeassistant.components.zamg zamg==0.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6e9b5fd9d0d..162822d8cee 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2257,7 +2257,7 @@ youless-api==1.0.1 youtubeaio==1.1.5 # homeassistant.components.media_extractor -yt-dlp==2024.03.10 +yt-dlp==2024.04.09 # homeassistant.components.zamg zamg==0.3.6 From f941e5d5bbd82bac747c5ce945b328b9349b16e6 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Wed, 10 Apr 2024 14:58:35 +0300 Subject: [PATCH 161/198] Fix Aranet failure when the Bluetooth proxy is not providing a device name (#115298) Co-authored-by: J. Nick Koston --- .../components/aranet/config_flow.py | 20 +++++++++---------- homeassistant/components/aranet/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/aranet/__init__.py | 8 ++++++++ tests/components/aranet/test_config_flow.py | 20 +++++++++++++++++++ 6 files changed, 40 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/aranet/config_flow.py b/homeassistant/components/aranet/config_flow.py index cf5f24263dd..db89124c54d 100644 --- a/homeassistant/components/aranet/config_flow.py +++ b/homeassistant/components/aranet/config_flow.py @@ -2,10 +2,10 @@ from __future__ import annotations -import logging from typing import Any from aranet4.client import Aranet4Advertisement, Version as AranetVersion +from bluetooth_data_tools import human_readable_name import voluptuous as vol from homeassistant.components.bluetooth import ( @@ -18,11 +18,15 @@ from homeassistant.data_entry_flow import AbortFlow from .const import DOMAIN -_LOGGER = logging.getLogger(__name__) - MIN_VERSION = AranetVersion(1, 2, 0) +def _title(discovery_info: BluetoothServiceInfoBleak) -> str: + return discovery_info.device.name or human_readable_name( + None, "Aranet", discovery_info.address + ) + + class AranetConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Aranet.""" @@ -61,11 +65,8 @@ class AranetConfigFlow(ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: """Confirm discovery.""" - assert self._discovered_device is not None - adv = self._discovered_device assert self._discovery_info is not None - discovery_info = self._discovery_info - title = adv.readings.name if adv.readings else discovery_info.name + title = _title(self._discovery_info) if user_input is not None: return self.async_create_entry(title=title, data={}) @@ -101,10 +102,7 @@ class AranetConfigFlow(ConfigFlow, domain=DOMAIN): discovery_info.device, discovery_info.advertisement ) if adv.manufacturer_data: - self._discovered_devices[address] = ( - adv.readings.name if adv.readings else discovery_info.name, - adv, - ) + self._discovered_devices[address] = (_title(discovery_info), adv) if not self._discovered_devices: return self.async_abort(reason="no_devices_found") diff --git a/homeassistant/components/aranet/manifest.json b/homeassistant/components/aranet/manifest.json index 0d22a0d1859..152c56e80f3 100644 --- a/homeassistant/components/aranet/manifest.json +++ b/homeassistant/components/aranet/manifest.json @@ -19,5 +19,5 @@ "documentation": "https://www.home-assistant.io/integrations/aranet", "integration_type": "device", "iot_class": "local_push", - "requirements": ["aranet4==2.2.2"] + "requirements": ["aranet4==2.3.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index ca93ab1f7f1..0989932242a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -467,7 +467,7 @@ aprslib==0.7.0 aqualogic==2.6 # homeassistant.components.aranet -aranet4==2.2.2 +aranet4==2.3.3 # homeassistant.components.arcam_fmj arcam-fmj==1.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 162822d8cee..13d853941bf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -428,7 +428,7 @@ apprise==1.7.4 aprslib==0.7.0 # homeassistant.components.aranet -aranet4==2.2.2 +aranet4==2.3.3 # homeassistant.components.arcam_fmj arcam-fmj==1.4.0 diff --git a/tests/components/aranet/__init__.py b/tests/components/aranet/__init__.py index b559743067d..4dc9434bd65 100644 --- a/tests/components/aranet/__init__.py +++ b/tests/components/aranet/__init__.py @@ -58,6 +58,14 @@ VALID_DATA_SERVICE_INFO = fake_service_info( }, ) +VALID_DATA_SERVICE_INFO_WITH_NO_NAME = fake_service_info( + None, + "0000fce0-0000-1000-8000-00805f9b34fb", + { + 1794: b'\x21\x00\x02\x01\x00\x00\x00\x01\x8a\x02\xa5\x01\xb1&"Y\x01,\x01\xe8\x00\x88' + }, +) + VALID_ARANET2_DATA_SERVICE_INFO = fake_service_info( "Aranet2 12345", "0000fce0-0000-1000-8000-00805f9b34fb", diff --git a/tests/components/aranet/test_config_flow.py b/tests/components/aranet/test_config_flow.py index f3558c66daf..a779a93cd8f 100644 --- a/tests/components/aranet/test_config_flow.py +++ b/tests/components/aranet/test_config_flow.py @@ -12,6 +12,7 @@ from . import ( NOT_ARANET4_SERVICE_INFO, OLD_FIRMWARE_SERVICE_INFO, VALID_DATA_SERVICE_INFO, + VALID_DATA_SERVICE_INFO_WITH_NO_NAME, ) from tests.common import MockConfigEntry @@ -36,6 +37,25 @@ async def test_async_step_bluetooth_valid_device(hass: HomeAssistant) -> None: assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" +async def test_async_step_bluetooth_device_without_name(hass: HomeAssistant) -> None: + """Test discovery via bluetooth with a valid device that has no name.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_BLUETOOTH}, + data=VALID_DATA_SERVICE_INFO_WITH_NO_NAME, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "bluetooth_confirm" + with patch("homeassistant.components.aranet.async_setup_entry", return_value=True): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "Aranet (EEFF)" + assert result2["data"] == {} + assert result2["result"].unique_id == "aa:bb:cc:dd:ee:ff" + + async def test_async_step_bluetooth_not_aranet4(hass: HomeAssistant) -> None: """Test that we reject discovery via Bluetooth for an unrelated device.""" result = await hass.config_entries.flow.async_init( From 5c2e9142fa0b41c3d841316a4e624263eee1f178 Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 9 Apr 2024 21:22:46 +0200 Subject: [PATCH 162/198] Bump zha-quirks to 0.0.114 (#115299) --- homeassistant/components/zha/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index e9d75584064..7741673557d 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -24,7 +24,7 @@ "bellows==0.38.1", "pyserial==3.5", "pyserial-asyncio==0.6", - "zha-quirks==0.0.113", + "zha-quirks==0.0.114", "zigpy-deconz==0.23.1", "zigpy==0.63.5", "zigpy-xbee==0.20.1", diff --git a/requirements_all.txt b/requirements_all.txt index 0989932242a..ab009204b9d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2934,7 +2934,7 @@ zeroconf==0.132.0 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.113 +zha-quirks==0.0.114 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 13d853941bf..cfb38551ce3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2269,7 +2269,7 @@ zeroconf==0.132.0 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.113 +zha-quirks==0.0.114 # homeassistant.components.zha zigpy-deconz==0.23.1 From db5343164fd70e333ff2ebc2780195ce05287989 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Apr 2024 02:42:18 -1000 Subject: [PATCH 163/198] Ensure automations do not execute from a trigger if they are disabled (#115305) * Ensure automations are stopped as soon as the stop future is set * revert script changes and move them to #115325 --- .../components/automation/__init__.py | 18 ++++- tests/components/automation/test_init.py | 80 +++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index 0bd2ed87d20..fbebc82225f 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -812,6 +812,22 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): """Log helper callback.""" self._logger.log(level, "%s %s", msg, self.name, **kwargs) + async def _async_trigger_if_enabled( + self, + run_variables: dict[str, Any], + context: Context | None = None, + skip_condition: bool = False, + ) -> ScriptRunResult | None: + """Trigger automation if enabled. + + If the trigger starts but has a delay, the automation will be triggered + when the delay has passed so we need to make sure its still enabled before + executing the action. + """ + if not self._is_enabled: + return None + return await self.async_trigger(run_variables, context, skip_condition) + async def _async_attach_triggers( self, home_assistant_start: bool ) -> Callable[[], None] | None: @@ -835,7 +851,7 @@ class AutomationEntity(BaseAutomationEntity, RestoreEntity): return await async_initialize_triggers( self.hass, self._trigger_config, - self.async_trigger, + self._async_trigger_if_enabled, DOMAIN, str(self.name), self._log_callback, diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 00a7e6980d7..f6567285ab0 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -2650,3 +2650,83 @@ def test_deprecated_constants( import_and_test_deprecated_constant( caplog, automation, constant_name, replacement.__name__, replacement, "2025.1" ) + + +async def test_automation_turns_off_other_automation( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture +) -> None: + """Test an automation that turns off another automation.""" + hass.set_state(CoreState.not_running) + calls = async_mock_service(hass, "persistent_notification", "create") + hass.states.async_set("binary_sensor.presence", "on") + await hass.async_block_till_done() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "state", + "entity_id": "binary_sensor.presence", + "from": "on", + }, + "action": { + "service": "automation.turn_off", + "target": { + "entity_id": "automation.automation_1", + }, + "data": { + "stop_actions": True, + }, + }, + "id": "automation_0", + "mode": "single", + }, + { + "trigger": { + "platform": "state", + "entity_id": "binary_sensor.presence", + "from": "on", + "for": { + "hours": 0, + "minutes": 0, + "seconds": 5, + }, + }, + "action": { + "service": "persistent_notification.create", + "metadata": {}, + "data": { + "message": "Test race", + }, + }, + "id": "automation_1", + "mode": "single", + }, + ] + }, + ) + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("binary_sensor.presence", "off") + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + assert len(calls) == 0 + + await hass.services.async_call( + "automation", + "turn_on", + {"entity_id": "automation.automation_1"}, + blocking=True, + ) + hass.states.async_set("binary_sensor.presence", "off") + await hass.async_block_till_done() + assert len(calls) == 0 + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + assert len(calls) == 0 From 0d62e2e92a0972fbcbd3e0fb1be03670cd2bd08e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 9 Apr 2024 18:04:52 -1000 Subject: [PATCH 164/198] Bump bleak-retry-connector 3.5.0 (#115328) --- homeassistant/components/bluetooth/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 62296ddd8b8..58009216464 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -15,7 +15,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.21.1", - "bleak-retry-connector==3.4.0", + "bleak-retry-connector==3.5.0", "bluetooth-adapters==0.18.0", "bluetooth-auto-recovery==1.4.0", "bluetooth-data-tools==1.19.0", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b8c8b0fcb64..b3195eb8291 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -13,7 +13,7 @@ atomicwrites-homeassistant==1.4.1 attrs==23.2.0 awesomeversion==24.2.0 bcrypt==4.1.2 -bleak-retry-connector==3.4.0 +bleak-retry-connector==3.5.0 bleak==0.21.1 bluetooth-adapters==0.18.0 bluetooth-auto-recovery==1.4.0 diff --git a/requirements_all.txt b/requirements_all.txt index ab009204b9d..1c1df97b779 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -556,7 +556,7 @@ bizkaibus==0.1.1 bleak-esphome==1.0.0 # homeassistant.components.bluetooth -bleak-retry-connector==3.4.0 +bleak-retry-connector==3.5.0 # homeassistant.components.bluetooth bleak==0.21.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cfb38551ce3..5467f57c0cd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -478,7 +478,7 @@ bimmer-connected[china]==0.14.6 bleak-esphome==1.0.0 # homeassistant.components.bluetooth -bleak-retry-connector==3.4.0 +bleak-retry-connector==3.5.0 # homeassistant.components.bluetooth bleak==0.21.1 From 98bc7c0ed2602d578211b44b27a3765abf9f5b3f Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 10 Apr 2024 22:09:10 +0200 Subject: [PATCH 165/198] Secure against resetting a non active modbus (#115364) --- homeassistant/components/modbus/__init__.py | 3 +++ tests/components/modbus/test_init.py | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 94a84d3440d..23ad6ac1be6 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -440,6 +440,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_reset_platform(hass: HomeAssistant, integration_name: str) -> None: """Release modbus resources.""" + if DOMAIN not in hass.data: + _LOGGER.error("Modbus cannot reload, because it was never loaded") + return _LOGGER.info("Modbus reloading") hubs = hass.data[DOMAIN] for name in hubs: diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 0ca4703aa5f..dfbc066fb8a 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -25,6 +25,7 @@ import voluptuous as vol from homeassistant import config as hass_config from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.modbus import async_reset_platform from homeassistant.components.modbus.const import ( ATTR_ADDRESS, ATTR_HUB, @@ -1694,3 +1695,9 @@ async def test_no_entities(hass: HomeAssistant) -> None: ] } assert await async_setup_component(hass, DOMAIN, config) is False + + +async def test_reset_platform(hass: HomeAssistant) -> None: + """Run test for async_reset_platform.""" + await async_reset_platform(hass, "modbus") + assert DOMAIN not in hass.data From d055f987366b5c662fe9cbce09aaa2e8c12f286f Mon Sep 17 00:00:00 2001 From: jan iversen Date: Wed, 10 Apr 2024 21:39:53 +0200 Subject: [PATCH 166/198] Solve modbus test problem (#115376) Fix test. --- tests/components/modbus/conftest.py | 12 +++++++- .../modbus/fixtures/configuration.yaml | 4 +++ tests/components/modbus/test_init.py | 28 ++++++++++--------- 3 files changed, 30 insertions(+), 14 deletions(-) diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index f6eff0fd64b..62cf12958d3 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -52,6 +52,15 @@ def mock_pymodbus_fixture(): """Mock pymodbus.""" mock_pb = mock.AsyncMock() mock_pb.close = mock.MagicMock() + read_result = ReadResult([]) + mock_pb.read_coils.return_value = read_result + mock_pb.read_discrete_inputs.return_value = read_result + mock_pb.read_input_registers.return_value = read_result + mock_pb.read_holding_registers.return_value = read_result + mock_pb.write_register.return_value = read_result + mock_pb.write_registers.return_value = read_result + mock_pb.write_coil.return_value = read_result + mock_pb.write_coils.return_value = read_result with ( mock.patch( "homeassistant.components.modbus.modbus.AsyncModbusTcpClient", @@ -156,7 +165,7 @@ async def mock_pymodbus_exception_fixture(hass, do_exception, mock_modbus): @pytest.fixture(name="mock_pymodbus_return") async def mock_pymodbus_return_fixture(hass, register_words, mock_modbus): """Trigger update call with time_changed event.""" - read_result = ReadResult(register_words) if register_words else None + read_result = ReadResult(register_words if register_words else []) mock_modbus.read_coils.return_value = read_result mock_modbus.read_discrete_inputs.return_value = read_result mock_modbus.read_input_registers.return_value = read_result @@ -165,6 +174,7 @@ async def mock_pymodbus_return_fixture(hass, register_words, mock_modbus): mock_modbus.write_registers.return_value = read_result mock_modbus.write_coil.return_value = read_result mock_modbus.write_coils.return_value = read_result + return mock_modbus @pytest.fixture(name="mock_do_cycle") diff --git a/tests/components/modbus/fixtures/configuration.yaml b/tests/components/modbus/fixtures/configuration.yaml index 0f12ac88686..0a16d85e39d 100644 --- a/tests/components/modbus/fixtures/configuration.yaml +++ b/tests/components/modbus/fixtures/configuration.yaml @@ -3,3 +3,7 @@ modbus: host: "testHost" port: 5001 name: "testModbus" + sensors: + - name: "dummy" + address: 117 + slave: 0 diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index dfbc066fb8a..2c5810a7757 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -1561,7 +1561,7 @@ async def test_shutdown( ], ) async def test_stop_restart( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_pymodbus_return ) -> None: """Run test for service stop.""" @@ -1572,7 +1572,7 @@ async def test_stop_restart( await hass.async_block_till_done() assert hass.states.get(entity_id).state == "17" - mock_modbus.reset_mock() + mock_pymodbus_return.reset_mock() caplog.clear() data = { ATTR_HUB: TEST_MODBUS_NAME, @@ -1580,23 +1580,23 @@ async def test_stop_restart( await hass.services.async_call(DOMAIN, SERVICE_STOP, data, blocking=True) await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - assert mock_modbus.close.called + assert mock_pymodbus_return.close.called assert f"modbus {TEST_MODBUS_NAME} communication closed" in caplog.text - mock_modbus.reset_mock() + mock_pymodbus_return.reset_mock() caplog.clear() await hass.services.async_call(DOMAIN, SERVICE_RESTART, data, blocking=True) await hass.async_block_till_done() - assert not mock_modbus.close.called - assert mock_modbus.connect.called + assert not mock_pymodbus_return.close.called + assert mock_pymodbus_return.connect.called assert f"modbus {TEST_MODBUS_NAME} communication open" in caplog.text - mock_modbus.reset_mock() + mock_pymodbus_return.reset_mock() caplog.clear() await hass.services.async_call(DOMAIN, SERVICE_RESTART, data, blocking=True) await hass.async_block_till_done() - assert mock_modbus.close.called - assert mock_modbus.connect.called + assert mock_pymodbus_return.close.called + assert mock_pymodbus_return.connect.called assert f"modbus {TEST_MODBUS_NAME} communication closed" in caplog.text assert f"modbus {TEST_MODBUS_NAME} communication open" in caplog.text @@ -1626,7 +1626,7 @@ async def test_write_no_client(hass: HomeAssistant, mock_modbus) -> None: async def test_integration_reload( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mock_modbus, + mock_pymodbus_return, freezer: FrozenDateTimeFactory, ) -> None: """Run test for integration reload.""" @@ -1647,7 +1647,7 @@ async def test_integration_reload( @pytest.mark.parametrize("do_config", [{}]) async def test_integration_reload_failed( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_pymodbus_return ) -> None: """Run test for integration connect failure on reload.""" caplog.set_level(logging.INFO) @@ -1656,7 +1656,9 @@ async def test_integration_reload_failed( yaml_path = get_fixture_path("configuration.yaml", "modbus") with ( mock.patch.object(hass_config, "YAML_CONFIG_FILE", yaml_path), - mock.patch.object(mock_modbus, "connect", side_effect=ModbusException("error")), + mock.patch.object( + mock_pymodbus_return, "connect", side_effect=ModbusException("error") + ), ): await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True) await hass.async_block_till_done() @@ -1667,7 +1669,7 @@ async def test_integration_reload_failed( @pytest.mark.parametrize("do_config", [{}]) async def test_integration_setup_failed( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_modbus + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, mock_pymodbus_return ) -> None: """Run test for integration setup on reload.""" with mock.patch.object( From 4aca39b49e8d62d47d247a8a19a2d4957da49091 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 10 Apr 2024 11:38:34 -1000 Subject: [PATCH 167/198] Fix deadlock in holidays dynamic loading (#115385) --- homeassistant/components/holiday/__init__.py | 23 ++++++++++++++++- homeassistant/components/workday/__init__.py | 27 +++++++++++++++----- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/holiday/__init__.py b/homeassistant/components/holiday/__init__.py index 4f2c593d38e..c9a58f29215 100644 --- a/homeassistant/components/holiday/__init__.py +++ b/homeassistant/components/holiday/__init__.py @@ -2,15 +2,36 @@ from __future__ import annotations +from functools import partial + +from holidays import country_holidays + from homeassistant.config_entries import ConfigEntry -from homeassistant.const import Platform +from homeassistant.const import CONF_COUNTRY, Platform from homeassistant.core import HomeAssistant +from homeassistant.setup import SetupPhases, async_pause_setup + +from .const import CONF_PROVINCE PLATFORMS: list[Platform] = [Platform.CALENDAR] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Holiday from a config entry.""" + country: str = entry.data[CONF_COUNTRY] + province: str | None = entry.data.get(CONF_PROVINCE) + + # We only import here to ensure that that its not imported later + # in the event loop since the platforms will call country_holidays + # which loads python code from disk. + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # import executor job is used here because multiple integrations use + # the holidays library and it is not thread safe to import it in parallel + # https://github.com/python/cpython/issues/83065 + await hass.async_add_import_executor_job( + partial(country_holidays, country, subdiv=province) + ) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/workday/__init__.py b/homeassistant/components/workday/__init__.py index 077a6710b8d..f25cf41b992 100644 --- a/homeassistant/components/workday/__init__.py +++ b/homeassistant/components/workday/__init__.py @@ -11,6 +11,7 @@ from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue +from homeassistant.setup import SetupPhases, async_pause_setup from .const import CONF_PROVINCE, DOMAIN, PLATFORMS @@ -23,7 +24,11 @@ async def _async_validate_country_and_province( if not country: return try: - await hass.async_add_executor_job(country_holidays, country) + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # import executor job is used here because multiple integrations use + # the holidays library and it is not thread safe to import it in parallel + # https://github.com/python/cpython/issues/83065 + await hass.async_add_import_executor_job(country_holidays, country) except NotImplementedError as ex: async_create_issue( hass, @@ -41,9 +46,13 @@ async def _async_validate_country_and_province( if not province: return try: - await hass.async_add_executor_job( - partial(country_holidays, country, subdiv=province) - ) + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # import executor job is used here because multiple integrations use + # the holidays library and it is not thread safe to import it in parallel + # https://github.com/python/cpython/issues/83065 + await hass.async_add_import_executor_job( + partial(country_holidays, country, subdiv=province) + ) except NotImplementedError as ex: async_create_issue( hass, @@ -73,9 +82,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await _async_validate_country_and_province(hass, entry, country, province) if country and CONF_LANGUAGE not in entry.options: - cls: HolidayBase = await hass.async_add_executor_job( - partial(country_holidays, country, subdiv=province) - ) + with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES): + # import executor job is used here because multiple integrations use + # the holidays library and it is not thread safe to import it in parallel + # https://github.com/python/cpython/issues/83065 + cls: HolidayBase = await hass.async_add_import_executor_job( + partial(country_holidays, country, subdiv=province) + ) default_language = cls.default_language new_options = entry.options.copy() new_options[CONF_LANGUAGE] = default_language From 5fa06e5a9c1c364e5e879bc371bfda57a5ad4da7 Mon Sep 17 00:00:00 2001 From: Jessica Smith <8505845+NodeJSmith@users.noreply.github.com> Date: Thu, 11 Apr 2024 09:26:05 -0500 Subject: [PATCH 168/198] Bump whirlpool-sixth-sense to 0.18.8 (#115393) bump whirlpool to 0.18.8 --- homeassistant/components/whirlpool/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/whirlpool/manifest.json b/homeassistant/components/whirlpool/manifest.json index ee7861588ed..5618a3f61cb 100644 --- a/homeassistant/components/whirlpool/manifest.json +++ b/homeassistant/components/whirlpool/manifest.json @@ -7,5 +7,5 @@ "integration_type": "hub", "iot_class": "cloud_push", "loggers": ["whirlpool"], - "requirements": ["whirlpool-sixth-sense==0.18.7"] + "requirements": ["whirlpool-sixth-sense==0.18.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1c1df97b779..2e109a1ef13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2850,7 +2850,7 @@ webmin-xmlrpc==0.0.2 webrtc-noise-gain==1.2.3 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.18.7 +whirlpool-sixth-sense==0.18.8 # homeassistant.components.whois whois==0.9.27 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5467f57c0cd..cd370eb2487 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2197,7 +2197,7 @@ webmin-xmlrpc==0.0.2 webrtc-noise-gain==1.2.3 # homeassistant.components.whirlpool -whirlpool-sixth-sense==0.18.7 +whirlpool-sixth-sense==0.18.8 # homeassistant.components.whois whois==0.9.27 From a455e142ac688b43a21343d619f8d0a854ded675 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Thu, 11 Apr 2024 19:14:52 -0700 Subject: [PATCH 169/198] Fix bug in rainbird switch when turning off a switch that is already off (#115421) Fix big in rainbird switch when turning off a switch that is already off Co-authored-by: J. Nick Koston --- homeassistant/components/rainbird/switch.py | 3 ++- tests/components/rainbird/test_switch.py | 10 +++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/rainbird/switch.py b/homeassistant/components/rainbird/switch.py index a929f5b875b..7f43553aa41 100644 --- a/homeassistant/components/rainbird/switch.py +++ b/homeassistant/components/rainbird/switch.py @@ -123,7 +123,8 @@ class RainBirdSwitch(CoordinatorEntity[RainbirdUpdateCoordinator], SwitchEntity) # The device reflects the old state for a few moments. Update the # state manually and trigger a refresh after a short debounced delay. - self.coordinator.data.active_zones.remove(self._zone) + if self.is_on: + self.coordinator.data.active_zones.remove(self._zone) self.async_write_ha_state() await self.coordinator.async_request_refresh() diff --git a/tests/components/rainbird/test_switch.py b/tests/components/rainbird/test_switch.py index 0f9a139a69d..068fe03ac33 100644 --- a/tests/components/rainbird/test_switch.py +++ b/tests/components/rainbird/test_switch.py @@ -146,20 +146,24 @@ async def test_switch_on( @pytest.mark.parametrize( - "zone_state_response", - [ZONE_3_ON_RESPONSE], + ("zone_state_response", "start_state"), + [ + (ZONE_3_ON_RESPONSE, "on"), + (ZONE_OFF_RESPONSE, "off"), # Already off + ], ) async def test_switch_off( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, responses: list[AiohttpClientMockResponse], + start_state: str, ) -> None: """Test turning off irrigation switch.""" # Initially the test zone is on zone = hass.states.get("switch.rain_bird_sprinkler_3") assert zone is not None - assert zone.state == "on" + assert zone.state == start_state aioclient_mock.mock_calls.clear() responses.extend( From 2ed1cfd68d74f59188a5dd02bb01d077694a81ae Mon Sep 17 00:00:00 2001 From: Santobert Date: Thu, 11 Apr 2024 21:57:18 +0200 Subject: [PATCH 170/198] Bump pybotvac to 0.0.25 (#115435) Bump pybotvac --- homeassistant/components/neato/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/neato/manifest.json b/homeassistant/components/neato/manifest.json index 1d5edb7ca44..d6eff486b05 100644 --- a/homeassistant/components/neato/manifest.json +++ b/homeassistant/components/neato/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/neato", "iot_class": "cloud_polling", "loggers": ["pybotvac"], - "requirements": ["pybotvac==0.0.24"] + "requirements": ["pybotvac==0.0.25"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2e109a1ef13..4b7909958cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1718,7 +1718,7 @@ pybbox==0.0.5-alpha pyblackbird==0.6 # homeassistant.components.neato -pybotvac==0.0.24 +pybotvac==0.0.25 # homeassistant.components.braviatv pybravia==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cd370eb2487..624da305f89 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1350,7 +1350,7 @@ pybalboa==1.0.1 pyblackbird==0.6 # homeassistant.components.neato -pybotvac==0.0.24 +pybotvac==0.0.25 # homeassistant.components.braviatv pybravia==0.3.3 From 7f6514b03c6001914b76e2e7dcadc3036d96b659 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 12 Apr 2024 11:50:22 +0200 Subject: [PATCH 171/198] Update frontend to 20240404.2 (#115460) --- 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 028fb28f01b..d711314cabb 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==20240404.1"] + "requirements": ["home-assistant-frontend==20240404.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index b3195eb8291..366f72cd2bc 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==20240404.1 +home-assistant-frontend==20240404.2 home-assistant-intents==2024.4.3 httpx==0.27.0 ifaddr==0.2.0 diff --git a/requirements_all.txt b/requirements_all.txt index 4b7909958cb..194dda7caac 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1077,7 +1077,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240404.1 +home-assistant-frontend==20240404.2 # homeassistant.components.conversation home-assistant-intents==2024.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 624da305f89..dfa71c7ac3e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -876,7 +876,7 @@ hole==0.8.0 holidays==0.46 # homeassistant.components.frontend -home-assistant-frontend==20240404.1 +home-assistant-frontend==20240404.2 # homeassistant.components.conversation home-assistant-intents==2024.4.3 From 62eee52aedf8f1fd28257b7aa9c6212f52ba8548 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 12 Apr 2024 12:00:16 +0200 Subject: [PATCH 172/198] Bump version to 2024.4.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 e4359f5bbfb..ecfc1c6259c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -18,7 +18,7 @@ from .util.signal_type import SignalType APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 4 -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, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 9993c8e9cb8..74b6f6fa54e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.4.2" +version = "2024.4.3" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst" From 5826f9a4f605e2769a63a43f70a6dc49dcc6d87b Mon Sep 17 00:00:00 2001 From: Jonny Rimkus Date: Mon, 1 Apr 2024 10:50:21 +0200 Subject: [PATCH 173/198] Bump slixmpp version to 1.8.5 (#114448) * Update slixmpp to 1.8.5, hopefully fixes #113990 * Bump slixmpp version to 1.8.5 #114448 --- homeassistant/components/xmpp/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index 30dee6c842b..308c3d70978 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/xmpp", "iot_class": "cloud_push", "loggers": ["pyasn1", "slixmpp"], - "requirements": ["slixmpp==1.8.4", "emoji==2.8.0"] + "requirements": ["slixmpp==1.8.5", "emoji==2.8.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 194dda7caac..c93b665ea8c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2553,7 +2553,7 @@ sisyphus-control==3.1.3 slackclient==2.5.0 # homeassistant.components.xmpp -slixmpp==1.8.4 +slixmpp==1.8.5 # homeassistant.components.smart_meter_texas smart-meter-texas==0.4.7 From 5194faa8fd959832086fb0064beebd627f7f5a9f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 19 Apr 2024 18:18:32 +0200 Subject: [PATCH 174/198] Make Withings recoverable after internet outage (#115124) --- homeassistant/components/withings/__init__.py | 7 +- tests/components/withings/test_init.py | 105 ++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index c14fb465731..665a90ec5a7 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -12,6 +12,7 @@ from dataclasses import dataclass, field from datetime import timedelta from typing import TYPE_CHECKING, Any, cast +from aiohttp import ClientError from aiohttp.hdrs import METH_POST from aiohttp.web import Request, Response from aiowithings import NotificationCategory, WithingsClient @@ -340,7 +341,11 @@ class WithingsWebhookManager: async def async_unsubscribe_webhooks(client: WithingsClient) -> None: """Unsubscribe to all Withings webhooks.""" - current_webhooks = await client.list_notification_configurations() + try: + current_webhooks = await client.list_notification_configurations() + except ClientError: + LOGGER.exception("Error when unsubscribing webhooks") + return for webhook_configuration in current_webhooks: LOGGER.debug( diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index eb089f44216..42b2b8da965 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -5,6 +5,7 @@ from typing import Any from unittest.mock import AsyncMock, MagicMock, patch from urllib.parse import urlparse +from aiohttp import ClientConnectionError from aiohttp.hdrs import METH_HEAD from aiowithings import ( NotificationCategory, @@ -508,6 +509,110 @@ async def test_cloud_disconnect( assert withings.subscribe_notification.call_count == 12 +async def test_internet_disconnect( + hass: HomeAssistant, + withings: AsyncMock, + webhook_config_entry: MockConfigEntry, + hass_client_no_auth: ClientSessionGenerator, + freezer: FrozenDateTimeFactory, +) -> None: + """Test we can recover from internet disconnects.""" + await mock_cloud(hass) + await hass.async_block_till_done() + + with ( + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch.object(cloud, "async_is_connected", return_value=True), + patch.object(cloud, "async_active_subscription", return_value=True), + patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value="https://hooks.nabu.casa/ABCD", + ), + patch( + "homeassistant.components.withings.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.cloud.async_delete_cloudhook", + ), + patch( + "homeassistant.components.withings.webhook_generate_url", + ), + ): + await setup_integration(hass, webhook_config_entry) + await prepare_webhook_setup(hass, freezer) + + assert cloud.async_active_subscription(hass) is True + assert cloud.async_is_connected(hass) is True + assert withings.revoke_notification_configurations.call_count == 3 + assert withings.subscribe_notification.call_count == 6 + + await hass.async_block_till_done() + + withings.list_notification_configurations.side_effect = ClientConnectionError + + async_mock_cloud_connection_status(hass, False) + await hass.async_block_till_done() + + assert withings.revoke_notification_configurations.call_count == 3 + + async_mock_cloud_connection_status(hass, True) + await hass.async_block_till_done() + + assert withings.subscribe_notification.call_count == 12 + + +async def test_cloud_disconnect_retry( + hass: HomeAssistant, + withings: AsyncMock, + webhook_config_entry: MockConfigEntry, + hass_client_no_auth: ClientSessionGenerator, + freezer: FrozenDateTimeFactory, +) -> None: + """Test we retry to create webhook connection again after cloud disconnects.""" + await mock_cloud(hass) + await hass.async_block_till_done() + + with ( + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch.object(cloud, "async_is_connected", return_value=True), + patch.object( + cloud, "async_active_subscription", return_value=True + ) as mock_async_active_subscription, + patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value="https://hooks.nabu.casa/ABCD", + ), + patch( + "homeassistant.components.withings.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.cloud.async_delete_cloudhook", + ), + patch( + "homeassistant.components.withings.webhook_generate_url", + ), + ): + await setup_integration(hass, webhook_config_entry) + await prepare_webhook_setup(hass, freezer) + + assert cloud.async_active_subscription(hass) is True + assert cloud.async_is_connected(hass) is True + assert mock_async_active_subscription.call_count == 3 + + await hass.async_block_till_done() + + async_mock_cloud_connection_status(hass, False) + await hass.async_block_till_done() + + assert mock_async_active_subscription.call_count == 3 + + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert mock_async_active_subscription.call_count == 4 + + @pytest.mark.parametrize( ("body", "expected_code"), [ From 7eb6b2ca3362bd848be23563a3699d8206fb4a94 Mon Sep 17 00:00:00 2001 From: avee87 <6134677+avee87@users.noreply.github.com> Date: Fri, 19 Apr 2024 17:21:21 +0100 Subject: [PATCH 175/198] Fix Hyperion light not updating state (#115389) --- homeassistant/components/hyperion/sensor.py | 4 ++-- tests/components/hyperion/test_sensor.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hyperion/sensor.py b/homeassistant/components/hyperion/sensor.py index f537c282686..ad972806ae5 100644 --- a/homeassistant/components/hyperion/sensor.py +++ b/homeassistant/components/hyperion/sensor.py @@ -191,13 +191,13 @@ class HyperionVisiblePrioritySensor(HyperionSensor): if priority[KEY_COMPONENTID] == "COLOR": state_value = priority[KEY_VALUE][KEY_RGB] else: - state_value = priority[KEY_OWNER] + state_value = priority.get(KEY_OWNER) attrs = { "component_id": priority[KEY_COMPONENTID], "origin": priority[KEY_ORIGIN], "priority": priority[KEY_PRIORITY], - "owner": priority[KEY_OWNER], + "owner": priority.get(KEY_OWNER), } if priority[KEY_COMPONENTID] == "COLOR": diff --git a/tests/components/hyperion/test_sensor.py b/tests/components/hyperion/test_sensor.py index 65991b4b7e1..8900db177fc 100644 --- a/tests/components/hyperion/test_sensor.py +++ b/tests/components/hyperion/test_sensor.py @@ -159,7 +159,6 @@ async def test_visible_effect_state_changes(hass: HomeAssistant) -> None: KEY_ACTIVE: True, KEY_COMPONENTID: "COLOR", KEY_ORIGIN: "System", - KEY_OWNER: "System", KEY_PRIORITY: 250, KEY_VALUE: {KEY_RGB: [0, 0, 0]}, KEY_VISIBLE: True, From 42c13eb57feffa6db9825fc0ecb61feb9ed129bb Mon Sep 17 00:00:00 2001 From: slyoldfox Date: Fri, 19 Apr 2024 18:22:12 +0200 Subject: [PATCH 176/198] Add scheduled mode to renault charge mode (#115427) Co-authored-by: epenet <6771947+epenet@users.noreply.github.com> --- homeassistant/components/renault/select.py | 2 +- tests/components/renault/const.py | 21 ++++++++++++++++--- .../renault/snapshots/test_select.ambr | 12 +++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/renault/select.py b/homeassistant/components/renault/select.py index f6c8f73d24b..eb79e197937 100644 --- a/homeassistant/components/renault/select.py +++ b/homeassistant/components/renault/select.py @@ -71,6 +71,6 @@ SENSOR_TYPES: tuple[RenaultSelectEntityDescription, ...] = ( coordinator="charge_mode", data_key="chargeMode", translation_key="charge_mode", - options=["always", "always_charging", "schedule_mode"], + options=["always", "always_charging", "schedule_mode", "scheduled"], ), ) diff --git a/tests/components/renault/const.py b/tests/components/renault/const.py index d849c658149..19c40f6ec20 100644 --- a/tests/components/renault/const.py +++ b/tests/components/renault/const.py @@ -127,7 +127,12 @@ MOCK_VEHICLES = { { ATTR_ENTITY_ID: "select.reg_number_charge_mode", ATTR_ICON: "mdi:calendar-remove", - ATTR_OPTIONS: ["always", "always_charging", "schedule_mode"], + ATTR_OPTIONS: [ + "always", + "always_charging", + "schedule_mode", + "scheduled", + ], ATTR_STATE: "always", ATTR_UNIQUE_ID: "vf1aaaaa555777999_charge_mode", }, @@ -363,7 +368,12 @@ MOCK_VEHICLES = { { ATTR_ENTITY_ID: "select.reg_number_charge_mode", ATTR_ICON: "mdi:calendar-clock", - ATTR_OPTIONS: ["always", "always_charging", "schedule_mode"], + ATTR_OPTIONS: [ + "always", + "always_charging", + "schedule_mode", + "scheduled", + ], ATTR_STATE: "schedule_mode", ATTR_UNIQUE_ID: "vf1aaaaa555777999_charge_mode", }, @@ -599,7 +609,12 @@ MOCK_VEHICLES = { { ATTR_ENTITY_ID: "select.reg_number_charge_mode", ATTR_ICON: "mdi:calendar-remove", - ATTR_OPTIONS: ["always", "always_charging", "schedule_mode"], + ATTR_OPTIONS: [ + "always", + "always_charging", + "schedule_mode", + "scheduled", + ], ATTR_STATE: "always", ATTR_UNIQUE_ID: "vf1aaaaa555777123_charge_mode", }, diff --git a/tests/components/renault/snapshots/test_select.ambr b/tests/components/renault/snapshots/test_select.ambr index 7e8356ee070..0722cb5cab3 100644 --- a/tests/components/renault/snapshots/test_select.ambr +++ b/tests/components/renault/snapshots/test_select.ambr @@ -82,6 +82,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'config_entry_id': , @@ -121,6 +122,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'context': , @@ -175,6 +177,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'config_entry_id': , @@ -214,6 +217,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'context': , @@ -268,6 +272,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'config_entry_id': , @@ -307,6 +312,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'context': , @@ -401,6 +407,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'config_entry_id': , @@ -440,6 +447,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'context': , @@ -494,6 +502,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'config_entry_id': , @@ -533,6 +542,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'context': , @@ -587,6 +597,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'config_entry_id': , @@ -626,6 +637,7 @@ 'always', 'always_charging', 'schedule_mode', + 'scheduled', ]), }), 'context': , From 624762451410ff0205f99d7861bf4eb9560ab867 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 12 Apr 2024 14:13:06 -1000 Subject: [PATCH 177/198] Bump zeroconf to 0.132.1 (#115501) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 7c489517dd7..3bddbfea576 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.132.0"] + "requirements": ["zeroconf==0.132.1"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 366f72cd2bc..a63e2853ad2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -60,7 +60,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtc-noise-gain==1.2.3 yarl==1.9.4 -zeroconf==0.132.0 +zeroconf==0.132.1 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index c93b665ea8c..b0eae5b399a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2928,7 +2928,7 @@ zamg==0.3.6 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.132.0 +zeroconf==0.132.1 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dfa71c7ac3e..169b72e5acf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2263,7 +2263,7 @@ yt-dlp==2024.04.09 zamg==0.3.6 # homeassistant.components.zeroconf -zeroconf==0.132.0 +zeroconf==0.132.1 # homeassistant.components.zeversolar zeversolar==0.3.1 From e1a241607619212787f3e3a6ab49ee956b243a2a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 12 Apr 2024 19:05:08 -1000 Subject: [PATCH 178/198] Bump zeroconf to 0.132.2 (#115505) --- homeassistant/components/zeroconf/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 3bddbfea576..0a76af3b9c2 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -8,5 +8,5 @@ "iot_class": "local_push", "loggers": ["zeroconf"], "quality_scale": "internal", - "requirements": ["zeroconf==0.132.1"] + "requirements": ["zeroconf==0.132.2"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index a63e2853ad2..9b481092ed4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -60,7 +60,7 @@ voluptuous-serialize==2.6.0 voluptuous==0.13.1 webrtc-noise-gain==1.2.3 yarl==1.9.4 -zeroconf==0.132.1 +zeroconf==0.132.2 # Constrain pycryptodome to avoid vulnerability # see https://github.com/home-assistant/core/pull/16238 diff --git a/requirements_all.txt b/requirements_all.txt index b0eae5b399a..6717c1db138 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2928,7 +2928,7 @@ zamg==0.3.6 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.132.1 +zeroconf==0.132.2 # homeassistant.components.zeversolar zeversolar==0.3.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 169b72e5acf..2fba371b287 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2263,7 +2263,7 @@ yt-dlp==2024.04.09 zamg==0.3.6 # homeassistant.components.zeroconf -zeroconf==0.132.1 +zeroconf==0.132.2 # homeassistant.components.zeversolar zeversolar==0.3.1 From b770edc16ef020d62b404989178db7bca560f6d5 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Sat, 13 Apr 2024 18:26:33 +0200 Subject: [PATCH 179/198] Update pillow to 10.3.0 (#115524) --- homeassistant/components/doods/manifest.json | 2 +- homeassistant/components/generic/manifest.json | 2 +- homeassistant/components/image_upload/manifest.json | 2 +- homeassistant/components/matrix/manifest.json | 2 +- homeassistant/components/proxy/manifest.json | 2 +- homeassistant/components/qrcode/manifest.json | 2 +- homeassistant/components/seven_segments/manifest.json | 2 +- homeassistant/components/sighthound/manifest.json | 2 +- homeassistant/components/tensorflow/manifest.json | 2 +- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 73d7d3754ce..6a198ab34e7 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/doods", "iot_class": "local_polling", "loggers": ["pydoods"], - "requirements": ["pydoods==1.0.2", "Pillow==10.2.0"] + "requirements": ["pydoods==1.0.2", "Pillow==10.3.0"] } diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index 861e2cf26c2..65f6aa751ca 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["http"], "documentation": "https://www.home-assistant.io/integrations/generic", "iot_class": "local_push", - "requirements": ["ha-av==10.1.1", "Pillow==10.2.0"] + "requirements": ["ha-av==10.1.1", "Pillow==10.3.0"] } diff --git a/homeassistant/components/image_upload/manifest.json b/homeassistant/components/image_upload/manifest.json index ba9140b4ed8..7cbc484b830 100644 --- a/homeassistant/components/image_upload/manifest.json +++ b/homeassistant/components/image_upload/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/image_upload", "integration_type": "system", "quality_scale": "internal", - "requirements": ["Pillow==10.2.0"] + "requirements": ["Pillow==10.3.0"] } diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index 0838bcc3764..2ea310aa5a6 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/matrix", "iot_class": "cloud_push", "loggers": ["matrix_client"], - "requirements": ["matrix-nio==0.24.0", "Pillow==10.2.0"] + "requirements": ["matrix-nio==0.24.0", "Pillow==10.3.0"] } diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 1b05a768b64..42770d71792 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -3,5 +3,5 @@ "name": "Camera Proxy", "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["Pillow==10.2.0"] + "requirements": ["Pillow==10.3.0"] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index e3b202a9950..476f4e8c3c9 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/qrcode", "iot_class": "calculated", "loggers": ["pyzbar"], - "requirements": ["Pillow==10.2.0", "pyzbar==0.1.7"] + "requirements": ["Pillow==10.3.0", "pyzbar==0.1.7"] } diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 6c511e3f44e..5e05f496d1d 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -4,5 +4,5 @@ "codeowners": ["@fabaff"], "documentation": "https://www.home-assistant.io/integrations/seven_segments", "iot_class": "local_polling", - "requirements": ["Pillow==10.2.0"] + "requirements": ["Pillow==10.3.0"] } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index e63864af707..b97ccc5f9cf 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/sighthound", "iot_class": "cloud_polling", "loggers": ["simplehound"], - "requirements": ["Pillow==10.2.0", "simplehound==0.3"] + "requirements": ["Pillow==10.3.0", "simplehound==0.3"] } diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index b98c4c6e428..40dbadca64d 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -10,6 +10,6 @@ "tf-models-official==2.5.0", "pycocotools==2.0.6", "numpy==1.26.0", - "Pillow==10.2.0" + "Pillow==10.3.0" ] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 9b481092ed4..13a28e34d28 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -40,7 +40,7 @@ mutagen==1.47.0 orjson==3.9.15 packaging>=23.1 paho-mqtt==1.6.1 -Pillow==10.2.0 +Pillow==10.3.0 pip>=21.3.1 psutil-home-assistant==0.0.1 PyJWT==2.8.0 diff --git a/pyproject.toml b/pyproject.toml index 74b6f6fa54e..3a206a3f7fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ dependencies = [ "PyJWT==2.8.0", # PyJWT has loose dependency. We want the latest one. "cryptography==42.0.5", - "Pillow==10.2.0", + "Pillow==10.3.0", "pyOpenSSL==24.1.0", "orjson==3.9.15", "packaging>=23.1", diff --git a/requirements.txt b/requirements.txt index 519a8287d18..5635c4912e9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,7 +24,7 @@ Jinja2==3.1.3 lru-dict==1.3.0 PyJWT==2.8.0 cryptography==42.0.5 -Pillow==10.2.0 +Pillow==10.3.0 pyOpenSSL==24.1.0 orjson==3.9.15 packaging>=23.1 diff --git a/requirements_all.txt b/requirements_all.txt index 6717c1db138..640ad0e2306 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -42,7 +42,7 @@ Mastodon.py==1.8.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -Pillow==10.2.0 +Pillow==10.3.0 # homeassistant.components.plex PlexAPI==4.15.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2fba371b287..a65320152be 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ HATasmota==0.8.0 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -Pillow==10.2.0 +Pillow==10.3.0 # homeassistant.components.plex PlexAPI==4.15.11 From 038040db5e06c128b29703690fd4ca75ddace7ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 13 Apr 2024 10:35:07 -1000 Subject: [PATCH 180/198] Fix race in TimestampDataUpdateCoordinator (#115542) * Fix race in TimestampDataUpdateCoordinator The last_update_success_time value was being set after the listeners were fired which could lead to a loop because the listener may re-trigger an update because it thinks the data is stale * coverage * docstring --- homeassistant/helpers/update_coordinator.py | 28 ++++++++-------- tests/helpers/test_update_coordinator.py | 36 +++++++++++++++++++-- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 287e69f7085..98e635e5ac7 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -403,6 +403,8 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): if not auth_failed and self._listeners and not self.hass.is_stopping: self._schedule_refresh() + self._async_refresh_finished() + if not self.last_update_success and not previous_update_success: return @@ -413,6 +415,15 @@ class DataUpdateCoordinator(BaseDataUpdateCoordinatorProtocol, Generic[_DataT]): ): self.async_update_listeners() + @callback + def _async_refresh_finished(self) -> None: + """Handle when a refresh has finished. + + Called when refresh is finished before listeners are updated. + + To be overridden by subclasses. + """ + @callback def async_set_update_error(self, err: Exception) -> None: """Manually set an error, log the message and notify listeners.""" @@ -446,20 +457,9 @@ class TimestampDataUpdateCoordinator(DataUpdateCoordinator[_DataT]): last_update_success_time: datetime | None = None - async def _async_refresh( - self, - log_failures: bool = True, - raise_on_auth_failed: bool = False, - scheduled: bool = False, - raise_on_entry_error: bool = False, - ) -> None: - """Refresh data.""" - await super()._async_refresh( - log_failures, - raise_on_auth_failed, - scheduled, - raise_on_entry_error, - ) + @callback + def _async_refresh_finished(self) -> None: + """Handle when a refresh has finished.""" if self.last_update_success: self.last_update_success_time = utcnow() diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 25f72d76e3c..775dc08f1d4 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -1,6 +1,6 @@ """Tests for the update coordinator.""" -from datetime import timedelta +from datetime import datetime, timedelta import logging from unittest.mock import AsyncMock, Mock, patch import urllib.error @@ -12,7 +12,7 @@ import requests from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import CoreState, HomeAssistant +from homeassistant.core import CoreState, HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import update_coordinator from homeassistant.util.dt import utcnow @@ -716,3 +716,35 @@ async def test_always_callback_when_always_update_is_true( update_callback.reset_mock() remove_callbacks() + + +async def test_timestamp_date_update_coordinator(hass: HomeAssistant) -> None: + """Test last_update_success_time is set before calling listeners.""" + last_update_success_times: list[datetime | None] = [] + + async def refresh() -> int: + return 1 + + crd = update_coordinator.TimestampDataUpdateCoordinator[int]( + hass, + _LOGGER, + name="test", + update_method=refresh, + update_interval=timedelta(seconds=10), + ) + + @callback + def listener(): + last_update_success_times.append(crd.last_update_success_time) + + unsub = crd.async_add_listener(listener) + + await crd.async_refresh() + + assert len(last_update_success_times) == 1 + # Ensure the time is set before the listener is called + assert last_update_success_times != [None] + + unsub() + await crd.async_refresh() + assert len(last_update_success_times) == 1 From 37a82c878514c4377c51cbeee7396d436271c91e Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Mon, 15 Apr 2024 09:48:22 +1000 Subject: [PATCH 181/198] Fix Teslemetry sensor values (#115571) --- homeassistant/components/teslemetry/sensor.py | 5 + .../teslemetry/snapshots/test_sensor.ambr | 100 +++++++++--------- 2 files changed, 55 insertions(+), 50 deletions(-) diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py index 6284a0e5368..cced1090e2a 100644 --- a/homeassistant/components/teslemetry/sensor.py +++ b/homeassistant/components/teslemetry/sensor.py @@ -449,6 +449,11 @@ class TeslemetryVehicleSensorEntity(TeslemetryVehicleEntity, SensorEntity): """Initialize the sensor.""" super().__init__(vehicle, description.key) + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self._value + class TeslemetryVehicleTimeSensorEntity(TeslemetryVehicleEntity, SensorEntity): """Base class for Teslemetry vehicle metric sensors.""" diff --git a/tests/components/teslemetry/snapshots/test_sensor.ambr b/tests/components/teslemetry/snapshots/test_sensor.ambr index fad04d341c9..81142e40901 100644 --- a/tests/components/teslemetry/snapshots/test_sensor.ambr +++ b/tests/components/teslemetry/snapshots/test_sensor.ambr @@ -757,7 +757,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '77', }) # --- # name: test_sensors[sensor.test_battery_level-statealt] @@ -770,7 +770,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '77', }) # --- # name: test_sensors[sensor.test_battery_range-entry] @@ -816,7 +816,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '266.87', }) # --- # name: test_sensors[sensor.test_battery_range-statealt] @@ -829,7 +829,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '266.87', }) # --- # name: test_sensors[sensor.test_charge_cable-entry] @@ -875,7 +875,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'IEC', }) # --- # name: test_sensors[sensor.test_charge_cable-statealt] @@ -888,7 +888,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'IEC', }) # --- # name: test_sensors[sensor.test_charge_energy_added-entry] @@ -934,7 +934,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_charge_energy_added-statealt] @@ -947,7 +947,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_charge_rate-entry] @@ -993,7 +993,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_charge_rate-statealt] @@ -1006,7 +1006,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_charger_current-entry] @@ -1052,7 +1052,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_charger_current-statealt] @@ -1065,7 +1065,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_charger_power-entry] @@ -1111,7 +1111,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_charger_power-statealt] @@ -1124,7 +1124,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_charger_voltage-entry] @@ -1170,7 +1170,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2', }) # --- # name: test_sensors[sensor.test_charger_voltage-statealt] @@ -1183,7 +1183,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2', }) # --- # name: test_sensors[sensor.test_charging-entry] @@ -1229,7 +1229,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'Stopped', }) # --- # name: test_sensors[sensor.test_charging-statealt] @@ -1242,7 +1242,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'Stopped', }) # --- # name: test_sensors[sensor.test_distance_to_arrival-entry] @@ -1288,7 +1288,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0.039491', }) # --- # name: test_sensors[sensor.test_distance_to_arrival-statealt] @@ -1301,7 +1301,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_driver_temperature_setting-entry] @@ -1347,7 +1347,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '22', }) # --- # name: test_sensors[sensor.test_driver_temperature_setting-statealt] @@ -1360,7 +1360,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '22', }) # --- # name: test_sensors[sensor.test_estimate_battery_range-entry] @@ -1406,7 +1406,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '275.04', }) # --- # name: test_sensors[sensor.test_estimate_battery_range-statealt] @@ -1419,7 +1419,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '275.04', }) # --- # name: test_sensors[sensor.test_fast_charger_type-entry] @@ -1465,7 +1465,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'ACSingleWireCAN', }) # --- # name: test_sensors[sensor.test_fast_charger_type-statealt] @@ -1478,7 +1478,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'ACSingleWireCAN', }) # --- # name: test_sensors[sensor.test_ideal_battery_range-entry] @@ -1524,7 +1524,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '266.87', }) # --- # name: test_sensors[sensor.test_ideal_battery_range-statealt] @@ -1537,7 +1537,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '266.87', }) # --- # name: test_sensors[sensor.test_inside_temperature-entry] @@ -1583,7 +1583,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '29.8', }) # --- # name: test_sensors[sensor.test_inside_temperature-statealt] @@ -1596,7 +1596,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '29.8', }) # --- # name: test_sensors[sensor.test_odometer-entry] @@ -1642,7 +1642,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '6481.019282', }) # --- # name: test_sensors[sensor.test_odometer-statealt] @@ -1655,7 +1655,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '6481.019282', }) # --- # name: test_sensors[sensor.test_outside_temperature-entry] @@ -1701,7 +1701,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '30', }) # --- # name: test_sensors[sensor.test_outside_temperature-statealt] @@ -1714,7 +1714,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '30', }) # --- # name: test_sensors[sensor.test_passenger_temperature_setting-entry] @@ -1760,7 +1760,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '22', }) # --- # name: test_sensors[sensor.test_passenger_temperature_setting-statealt] @@ -1773,7 +1773,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '22', }) # --- # name: test_sensors[sensor.test_power-entry] @@ -1819,7 +1819,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '-7', }) # --- # name: test_sensors[sensor.test_power-statealt] @@ -1832,7 +1832,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '-7', }) # --- # name: test_sensors[sensor.test_shift_state-entry] @@ -2177,7 +2177,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2.775', }) # --- # name: test_sensors[sensor.test_tire_pressure_front_left-statealt] @@ -2190,7 +2190,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2.775', }) # --- # name: test_sensors[sensor.test_tire_pressure_front_right-entry] @@ -2236,7 +2236,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2.8', }) # --- # name: test_sensors[sensor.test_tire_pressure_front_right-statealt] @@ -2249,7 +2249,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2.8', }) # --- # name: test_sensors[sensor.test_tire_pressure_rear_left-entry] @@ -2295,7 +2295,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2.775', }) # --- # name: test_sensors[sensor.test_tire_pressure_rear_left-statealt] @@ -2308,7 +2308,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2.775', }) # --- # name: test_sensors[sensor.test_tire_pressure_rear_right-entry] @@ -2354,7 +2354,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2.775', }) # --- # name: test_sensors[sensor.test_tire_pressure_rear_right-statealt] @@ -2367,7 +2367,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '2.775', }) # --- # name: test_sensors[sensor.test_traffic_delay-entry] @@ -2413,7 +2413,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_traffic_delay-statealt] @@ -2426,7 +2426,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_usable_battery_level-entry] @@ -2472,7 +2472,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '77', }) # --- # name: test_sensors[sensor.test_usable_battery_level-statealt] @@ -2485,7 +2485,7 @@ 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '77', }) # --- # name: test_sensors[sensor.wall_connector_fault_state_code-entry] From 3d68ee99a4e653b6740934cc429e0256a5ceaf0a Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 14 Apr 2024 19:22:42 +0200 Subject: [PATCH 182/198] Modbus: Bump pymodbus v3.6.8 (#115574) --- 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 0fe8c7bc42d..5635adc9392 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": "platinum", - "requirements": ["pymodbus==3.6.7"] + "requirements": ["pymodbus==3.6.8"] } diff --git a/requirements_all.txt b/requirements_all.txt index 640ad0e2306..0c1725ca089 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1973,7 +1973,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.6.7 +pymodbus==3.6.8 # homeassistant.components.monoprice pymonoprice==0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a65320152be..ced79b46ae8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1533,7 +1533,7 @@ pymeteoclimatic==0.1.0 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==3.6.7 +pymodbus==3.6.8 # homeassistant.components.monoprice pymonoprice==0.4 From 66918d1686415d5abc51d2440c8b1211b28fbe7b Mon Sep 17 00:00:00 2001 From: Brett Adams Date: Sun, 21 Apr 2024 07:54:24 +1000 Subject: [PATCH 183/198] Fix sensor entity description in Teslemetry (#115614) Add description back to sensor entity --- homeassistant/components/teslemetry/sensor.py | 5 +- .../teslemetry/snapshots/test_sensor.ambr | 538 ++++++++++++++---- 2 files changed, 428 insertions(+), 115 deletions(-) diff --git a/homeassistant/components/teslemetry/sensor.py b/homeassistant/components/teslemetry/sensor.py index cced1090e2a..6380a4d0c71 100644 --- a/homeassistant/components/teslemetry/sensor.py +++ b/homeassistant/components/teslemetry/sensor.py @@ -58,7 +58,7 @@ SHIFT_STATES = {"P": "p", "D": "d", "R": "r", "N": "n"} class TeslemetrySensorEntityDescription(SensorEntityDescription): """Describes Teslemetry Sensor entity.""" - value_fn: Callable[[StateType], StateType | datetime] = lambda x: x + value_fn: Callable[[StateType], StateType] = lambda x: x VEHICLE_DESCRIPTIONS: tuple[TeslemetrySensorEntityDescription, ...] = ( @@ -447,12 +447,13 @@ class TeslemetryVehicleSensorEntity(TeslemetryVehicleEntity, SensorEntity): description: TeslemetrySensorEntityDescription, ) -> None: """Initialize the sensor.""" + self.entity_description = description super().__init__(vehicle, description.key) @property def native_value(self) -> StateType: """Return the state of the sensor.""" - return self._value + return self.entity_description.value_fn(self._value) class TeslemetryVehicleTimeSensorEntity(TeslemetryVehicleEntity, SensorEntity): diff --git a/tests/components/teslemetry/snapshots/test_sensor.ambr b/tests/components/teslemetry/snapshots/test_sensor.ambr index 81142e40901..0d817ad1f7e 100644 --- a/tests/components/teslemetry/snapshots/test_sensor.ambr +++ b/tests/components/teslemetry/snapshots/test_sensor.ambr @@ -719,7 +719,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -736,7 +738,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Battery level', 'platform': 'teslemetry', @@ -744,13 +746,16 @@ 'supported_features': 0, 'translation_key': 'charge_state_battery_level', 'unique_id': 'VINVINVIN-charge_state_battery_level', - 'unit_of_measurement': None, + 'unit_of_measurement': '%', }) # --- # name: test_sensors[sensor.test_battery_level-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'battery', 'friendly_name': 'Test Battery level', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , 'entity_id': 'sensor.test_battery_level', @@ -763,7 +768,10 @@ # name: test_sensors[sensor.test_battery_level-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'battery', 'friendly_name': 'Test Battery level', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , 'entity_id': 'sensor.test_battery_level', @@ -778,7 +786,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -794,8 +804,14 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Battery range', 'platform': 'teslemetry', @@ -803,33 +819,39 @@ 'supported_features': 0, 'translation_key': 'charge_state_battery_range', 'unique_id': 'VINVINVIN-charge_state_battery_range', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_battery_range-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'distance', 'friendly_name': 'Test Battery range', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_battery_range', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '266.87', + 'state': '429.48563328', }) # --- # name: test_sensors[sensor.test_battery_range-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'distance', 'friendly_name': 'Test Battery range', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_battery_range', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '266.87', + 'state': '429.48563328', }) # --- # name: test_sensors[sensor.test_charge_cable-entry] @@ -843,7 +865,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_charge_cable', 'has_entity_name': True, 'hidden_by': None, @@ -896,7 +918,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -912,8 +936,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charge energy added', 'platform': 'teslemetry', @@ -921,13 +948,16 @@ 'supported_features': 0, 'translation_key': 'charge_state_charge_energy_added', 'unique_id': 'VINVINVIN-charge_state_charge_energy_added', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_charge_energy_added-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Test Charge energy added', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_charge_energy_added', @@ -940,7 +970,10 @@ # name: test_sensors[sensor.test_charge_energy_added-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'energy', 'friendly_name': 'Test Charge energy added', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_charge_energy_added', @@ -955,13 +988,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_charge_rate', 'has_entity_name': True, 'hidden_by': None, @@ -971,8 +1006,11 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charge rate', 'platform': 'teslemetry', @@ -980,13 +1018,16 @@ 'supported_features': 0, 'translation_key': 'charge_state_charge_rate', 'unique_id': 'VINVINVIN-charge_state_charge_rate', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_charge_rate-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'speed', 'friendly_name': 'Test Charge rate', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_charge_rate', @@ -999,7 +1040,10 @@ # name: test_sensors[sensor.test_charge_rate-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'speed', 'friendly_name': 'Test Charge rate', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_charge_rate', @@ -1014,13 +1058,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_charger_current', 'has_entity_name': True, 'hidden_by': None, @@ -1031,7 +1077,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charger current', 'platform': 'teslemetry', @@ -1039,13 +1085,16 @@ 'supported_features': 0, 'translation_key': 'charge_state_charger_actual_current', 'unique_id': 'VINVINVIN-charge_state_charger_actual_current', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_charger_current-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'current', 'friendly_name': 'Test Charger current', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_charger_current', @@ -1058,7 +1107,10 @@ # name: test_sensors[sensor.test_charger_current-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'current', 'friendly_name': 'Test Charger current', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_charger_current', @@ -1073,7 +1125,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -1090,7 +1144,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charger power', 'platform': 'teslemetry', @@ -1098,13 +1152,16 @@ 'supported_features': 0, 'translation_key': 'charge_state_charger_power', 'unique_id': 'VINVINVIN-charge_state_charger_power', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_charger_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'power', 'friendly_name': 'Test Charger power', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_charger_power', @@ -1117,7 +1174,10 @@ # name: test_sensors[sensor.test_charger_power-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'power', 'friendly_name': 'Test Charger power', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_charger_power', @@ -1132,13 +1192,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_charger_voltage', 'has_entity_name': True, 'hidden_by': None, @@ -1149,7 +1211,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charger voltage', 'platform': 'teslemetry', @@ -1157,13 +1219,16 @@ 'supported_features': 0, 'translation_key': 'charge_state_charger_voltage', 'unique_id': 'VINVINVIN-charge_state_charger_voltage', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_charger_voltage-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', 'friendly_name': 'Test Charger voltage', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_charger_voltage', @@ -1176,7 +1241,10 @@ # name: test_sensors[sensor.test_charger_voltage-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'voltage', 'friendly_name': 'Test Charger voltage', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_charger_voltage', @@ -1191,7 +1259,16 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'starting', + 'charging', + 'stopped', + 'complete', + 'disconnected', + 'no_power', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -1208,7 +1285,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Charging', 'platform': 'teslemetry', @@ -1222,27 +1299,45 @@ # name: test_sensors[sensor.test_charging-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'enum', 'friendly_name': 'Test Charging', + 'options': list([ + 'starting', + 'charging', + 'stopped', + 'complete', + 'disconnected', + 'no_power', + ]), }), 'context': , 'entity_id': 'sensor.test_charging', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'Stopped', + 'state': 'stopped', }) # --- # name: test_sensors[sensor.test_charging-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'enum', 'friendly_name': 'Test Charging', + 'options': list([ + 'starting', + 'charging', + 'stopped', + 'complete', + 'disconnected', + 'no_power', + ]), }), 'context': , 'entity_id': 'sensor.test_charging', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'Stopped', + 'state': 'stopped', }) # --- # name: test_sensors[sensor.test_distance_to_arrival-entry] @@ -1250,7 +1345,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -1266,8 +1363,11 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Distance to arrival', 'platform': 'teslemetry', @@ -1275,26 +1375,32 @@ 'supported_features': 0, 'translation_key': 'drive_state_active_route_miles_to_arrival', 'unique_id': 'VINVINVIN-drive_state_active_route_miles_to_arrival', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_distance_to_arrival-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'distance', 'friendly_name': 'Test Distance to arrival', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_distance_to_arrival', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '0.039491', + 'state': '0.063555', }) # --- # name: test_sensors[sensor.test_distance_to_arrival-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'distance', 'friendly_name': 'Test Distance to arrival', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_distance_to_arrival', @@ -1309,13 +1415,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_driver_temperature_setting', 'has_entity_name': True, 'hidden_by': None, @@ -1325,8 +1433,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Driver temperature setting', 'platform': 'teslemetry', @@ -1334,13 +1445,16 @@ 'supported_features': 0, 'translation_key': 'climate_state_driver_temp_setting', 'unique_id': 'VINVINVIN-climate_state_driver_temp_setting', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_driver_temperature_setting-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', 'friendly_name': 'Test Driver temperature setting', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_driver_temperature_setting', @@ -1353,7 +1467,10 @@ # name: test_sensors[sensor.test_driver_temperature_setting-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', 'friendly_name': 'Test Driver temperature setting', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_driver_temperature_setting', @@ -1368,7 +1485,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -1384,8 +1503,14 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Estimate battery range', 'platform': 'teslemetry', @@ -1393,33 +1518,39 @@ 'supported_features': 0, 'translation_key': 'charge_state_est_battery_range', 'unique_id': 'VINVINVIN-charge_state_est_battery_range', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_estimate_battery_range-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'distance', 'friendly_name': 'Test Estimate battery range', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_estimate_battery_range', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '275.04', + 'state': '442.63397376', }) # --- # name: test_sensors[sensor.test_estimate_battery_range-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'distance', 'friendly_name': 'Test Estimate battery range', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_estimate_battery_range', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '275.04', + 'state': '442.63397376', }) # --- # name: test_sensors[sensor.test_fast_charger_type-entry] @@ -1433,7 +1564,7 @@ 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_fast_charger_type', 'has_entity_name': True, 'hidden_by': None, @@ -1486,7 +1617,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -1502,8 +1635,14 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Ideal battery range', 'platform': 'teslemetry', @@ -1511,33 +1650,39 @@ 'supported_features': 0, 'translation_key': 'charge_state_ideal_battery_range', 'unique_id': 'VINVINVIN-charge_state_ideal_battery_range', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_ideal_battery_range-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'distance', 'friendly_name': 'Test Ideal battery range', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_ideal_battery_range', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '266.87', + 'state': '429.48563328', }) # --- # name: test_sensors[sensor.test_ideal_battery_range-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'distance', 'friendly_name': 'Test Ideal battery range', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_ideal_battery_range', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '266.87', + 'state': '429.48563328', }) # --- # name: test_sensors[sensor.test_inside_temperature-entry] @@ -1545,7 +1690,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -1561,8 +1708,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Inside temperature', 'platform': 'teslemetry', @@ -1570,13 +1720,16 @@ 'supported_features': 0, 'translation_key': 'climate_state_inside_temp', 'unique_id': 'VINVINVIN-climate_state_inside_temp', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_inside_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', 'friendly_name': 'Test Inside temperature', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_inside_temperature', @@ -1589,7 +1742,10 @@ # name: test_sensors[sensor.test_inside_temperature-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', 'friendly_name': 'Test Inside temperature', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_inside_temperature', @@ -1604,13 +1760,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_odometer', 'has_entity_name': True, 'hidden_by': None, @@ -1620,8 +1778,14 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 0, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Odometer', 'platform': 'teslemetry', @@ -1629,33 +1793,39 @@ 'supported_features': 0, 'translation_key': 'vehicle_state_odometer', 'unique_id': 'VINVINVIN-vehicle_state_odometer', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_odometer-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'distance', 'friendly_name': 'Test Odometer', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_odometer', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '6481.019282', + 'state': '10430.189495371', }) # --- # name: test_sensors[sensor.test_odometer-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'distance', 'friendly_name': 'Test Odometer', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_odometer', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '6481.019282', + 'state': '10430.189495371', }) # --- # name: test_sensors[sensor.test_outside_temperature-entry] @@ -1663,7 +1833,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -1679,8 +1851,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Outside temperature', 'platform': 'teslemetry', @@ -1688,13 +1863,16 @@ 'supported_features': 0, 'translation_key': 'climate_state_outside_temp', 'unique_id': 'VINVINVIN-climate_state_outside_temp', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_outside_temperature-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', 'friendly_name': 'Test Outside temperature', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_outside_temperature', @@ -1707,7 +1885,10 @@ # name: test_sensors[sensor.test_outside_temperature-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', 'friendly_name': 'Test Outside temperature', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_outside_temperature', @@ -1722,13 +1903,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_passenger_temperature_setting', 'has_entity_name': True, 'hidden_by': None, @@ -1738,8 +1921,11 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Passenger temperature setting', 'platform': 'teslemetry', @@ -1747,13 +1933,16 @@ 'supported_features': 0, 'translation_key': 'climate_state_passenger_temp_setting', 'unique_id': 'VINVINVIN-climate_state_passenger_temp_setting', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_passenger_temperature_setting-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', 'friendly_name': 'Test Passenger temperature setting', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_passenger_temperature_setting', @@ -1766,7 +1955,10 @@ # name: test_sensors[sensor.test_passenger_temperature_setting-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', 'friendly_name': 'Test Passenger temperature setting', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_passenger_temperature_setting', @@ -1781,13 +1973,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_power', 'has_entity_name': True, 'hidden_by': None, @@ -1798,7 +1992,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Power', 'platform': 'teslemetry', @@ -1806,13 +2000,16 @@ 'supported_features': 0, 'translation_key': 'drive_state_power', 'unique_id': 'VINVINVIN-drive_state_power', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_power-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'power', 'friendly_name': 'Test Power', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_power', @@ -1825,7 +2022,10 @@ # name: test_sensors[sensor.test_power-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'power', 'friendly_name': 'Test Power', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_power', @@ -1840,7 +2040,14 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'options': list([ + 'p', + 'd', + 'r', + 'n', + ]), + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -1857,7 +2064,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Shift state', 'platform': 'teslemetry', @@ -1871,27 +2078,41 @@ # name: test_sensors[sensor.test_shift_state-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'enum', 'friendly_name': 'Test Shift state', + 'options': list([ + 'p', + 'd', + 'r', + 'n', + ]), }), 'context': , 'entity_id': 'sensor.test_shift_state', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'p', }) # --- # name: test_sensors[sensor.test_shift_state-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'enum', 'friendly_name': 'Test Shift state', + 'options': list([ + 'p', + 'd', + 'r', + 'n', + ]), }), 'context': , 'entity_id': 'sensor.test_shift_state', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': 'p', }) # --- # name: test_sensors[sensor.test_speed-entry] @@ -1899,7 +2120,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -1915,8 +2138,11 @@ }), 'name': None, 'options': dict({ + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Speed', 'platform': 'teslemetry', @@ -1924,33 +2150,39 @@ 'supported_features': 0, 'translation_key': 'drive_state_speed', 'unique_id': 'VINVINVIN-drive_state_speed', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_speed-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'speed', 'friendly_name': 'Test Speed', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_speed', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_speed-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'speed', 'friendly_name': 'Test Speed', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_speed', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': 'unknown', + 'state': '0', }) # --- # name: test_sensors[sensor.test_state_of_charge_at_arrival-entry] @@ -1958,13 +2190,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_state_of_charge_at_arrival', 'has_entity_name': True, 'hidden_by': None, @@ -1975,7 +2209,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'State of charge at arrival', 'platform': 'teslemetry', @@ -1983,13 +2217,16 @@ 'supported_features': 0, 'translation_key': 'drive_state_active_route_energy_at_arrival', 'unique_id': 'VINVINVIN-drive_state_active_route_energy_at_arrival', - 'unit_of_measurement': None, + 'unit_of_measurement': '%', }) # --- # name: test_sensors[sensor.test_state_of_charge_at_arrival-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'battery', 'friendly_name': 'Test State of charge at arrival', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , 'entity_id': 'sensor.test_state_of_charge_at_arrival', @@ -2002,7 +2239,10 @@ # name: test_sensors[sensor.test_state_of_charge_at_arrival-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'battery', 'friendly_name': 'Test State of charge at arrival', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , 'entity_id': 'sensor.test_state_of_charge_at_arrival', @@ -2139,13 +2379,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_tire_pressure_front_left', 'has_entity_name': True, 'hidden_by': None, @@ -2155,8 +2397,14 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Tire pressure front left', 'platform': 'teslemetry', @@ -2164,33 +2412,39 @@ 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_pressure_fl', 'unique_id': 'VINVINVIN-vehicle_state_tpms_pressure_fl', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_tire_pressure_front_left-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', 'friendly_name': 'Test Tire pressure front left', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_tire_pressure_front_left', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.775', + 'state': '40.2479739314961', }) # --- # name: test_sensors[sensor.test_tire_pressure_front_left-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', 'friendly_name': 'Test Tire pressure front left', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_tire_pressure_front_left', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.775', + 'state': '40.2479739314961', }) # --- # name: test_sensors[sensor.test_tire_pressure_front_right-entry] @@ -2198,13 +2452,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_tire_pressure_front_right', 'has_entity_name': True, 'hidden_by': None, @@ -2214,8 +2470,14 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Tire pressure front right', 'platform': 'teslemetry', @@ -2223,33 +2485,39 @@ 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_pressure_fr', 'unique_id': 'VINVINVIN-vehicle_state_tpms_pressure_fr', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_tire_pressure_front_right-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', 'friendly_name': 'Test Tire pressure front right', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_tire_pressure_front_right', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.8', + 'state': '40.6105682912393', }) # --- # name: test_sensors[sensor.test_tire_pressure_front_right-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', 'friendly_name': 'Test Tire pressure front right', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_tire_pressure_front_right', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.8', + 'state': '40.6105682912393', }) # --- # name: test_sensors[sensor.test_tire_pressure_rear_left-entry] @@ -2257,13 +2525,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_tire_pressure_rear_left', 'has_entity_name': True, 'hidden_by': None, @@ -2273,8 +2543,14 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Tire pressure rear left', 'platform': 'teslemetry', @@ -2282,33 +2558,39 @@ 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_pressure_rl', 'unique_id': 'VINVINVIN-vehicle_state_tpms_pressure_rl', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_tire_pressure_rear_left-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', 'friendly_name': 'Test Tire pressure rear left', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_tire_pressure_rear_left', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.775', + 'state': '40.2479739314961', }) # --- # name: test_sensors[sensor.test_tire_pressure_rear_left-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', 'friendly_name': 'Test Tire pressure rear left', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_tire_pressure_rear_left', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.775', + 'state': '40.2479739314961', }) # --- # name: test_sensors[sensor.test_tire_pressure_rear_right-entry] @@ -2316,13 +2598,15 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , 'disabled_by': None, 'domain': 'sensor', - 'entity_category': None, + 'entity_category': , 'entity_id': 'sensor.test_tire_pressure_rear_right', 'has_entity_name': True, 'hidden_by': None, @@ -2332,8 +2616,14 @@ }), 'name': None, 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + 'sensor.private': dict({ + 'suggested_unit_of_measurement': , + }), }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Tire pressure rear right', 'platform': 'teslemetry', @@ -2341,33 +2631,39 @@ 'supported_features': 0, 'translation_key': 'vehicle_state_tpms_pressure_rr', 'unique_id': 'VINVINVIN-vehicle_state_tpms_pressure_rr', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_tire_pressure_rear_right-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', 'friendly_name': 'Test Tire pressure rear right', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_tire_pressure_rear_right', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.775', + 'state': '40.2479739314961', }) # --- # name: test_sensors[sensor.test_tire_pressure_rear_right-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'pressure', 'friendly_name': 'Test Tire pressure rear right', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_tire_pressure_rear_right', 'last_changed': , 'last_reported': , 'last_updated': , - 'state': '2.775', + 'state': '40.2479739314961', }) # --- # name: test_sensors[sensor.test_traffic_delay-entry] @@ -2375,7 +2671,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -2392,7 +2690,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Traffic delay', 'platform': 'teslemetry', @@ -2400,13 +2698,16 @@ 'supported_features': 0, 'translation_key': 'drive_state_active_route_traffic_minutes_delay', 'unique_id': 'VINVINVIN-drive_state_active_route_traffic_minutes_delay', - 'unit_of_measurement': None, + 'unit_of_measurement': , }) # --- # name: test_sensors[sensor.test_traffic_delay-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'duration', 'friendly_name': 'Test Traffic delay', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_traffic_delay', @@ -2419,7 +2720,10 @@ # name: test_sensors[sensor.test_traffic_delay-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'duration', 'friendly_name': 'Test Traffic delay', + 'state_class': , + 'unit_of_measurement': , }), 'context': , 'entity_id': 'sensor.test_traffic_delay', @@ -2434,7 +2738,9 @@ 'aliases': set({ }), 'area_id': None, - 'capabilities': None, + 'capabilities': dict({ + 'state_class': , + }), 'config_entry_id': , 'device_class': None, 'device_id': , @@ -2451,7 +2757,7 @@ 'name': None, 'options': dict({ }), - 'original_device_class': None, + 'original_device_class': , 'original_icon': None, 'original_name': 'Usable battery level', 'platform': 'teslemetry', @@ -2459,13 +2765,16 @@ 'supported_features': 0, 'translation_key': 'charge_state_usable_battery_level', 'unique_id': 'VINVINVIN-charge_state_usable_battery_level', - 'unit_of_measurement': None, + 'unit_of_measurement': '%', }) # --- # name: test_sensors[sensor.test_usable_battery_level-state] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'battery', 'friendly_name': 'Test Usable battery level', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , 'entity_id': 'sensor.test_usable_battery_level', @@ -2478,7 +2787,10 @@ # name: test_sensors[sensor.test_usable_battery_level-statealt] StateSnapshot({ 'attributes': ReadOnlyDict({ + 'device_class': 'battery', 'friendly_name': 'Test Usable battery level', + 'state_class': , + 'unit_of_measurement': '%', }), 'context': , 'entity_id': 'sensor.test_usable_battery_level', From 09ed0aa399443656f064bdb94c7042b6994705df Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 16 Apr 2024 10:13:47 -0500 Subject: [PATCH 184/198] Bump httpcore to 1.0.5 (#115672) Fixes missing handling of EndOfStream errors --- homeassistant/package_constraints.txt | 2 +- script/gen_requirements_all.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 13a28e34d28..49c7cf4a992 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -107,7 +107,7 @@ regex==2021.8.28 # requirements so we can directly link HA versions to these library versions. anyio==4.3.0 h11==0.14.0 -httpcore==1.0.4 +httpcore==1.0.5 # Ensure we have a hyperframe version that works in Python 3.10 # 5.2.0 fixed a collections abc deprecation diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 9a9ff6821c7..1423ce92b89 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -100,7 +100,7 @@ regex==2021.8.28 # requirements so we can directly link HA versions to these library versions. anyio==4.3.0 h11==0.14.0 -httpcore==1.0.4 +httpcore==1.0.5 # Ensure we have a hyperframe version that works in Python 3.10 # 5.2.0 fixed a collections abc deprecation From 630763ad9ee3e443d41eee1155a9c7a69980c002 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 16 Apr 2024 01:30:51 -0500 Subject: [PATCH 185/198] Bump sqlparse to 0.5.0 (#115681) fixes https://github.com/home-assistant/core/security/dependabot/54 fixes https://github.com/home-assistant/core/security/dependabot/55 --- homeassistant/components/sql/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index dd44af89237..30d071f25af 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -5,5 +5,5 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sql", "iot_class": "local_polling", - "requirements": ["SQLAlchemy==2.0.29", "sqlparse==0.4.4"] + "requirements": ["SQLAlchemy==2.0.29", "sqlparse==0.5.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 0c1725ca089..cbe6ea8d74a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2595,7 +2595,7 @@ spiderpy==1.6.1 spotipy==2.23.0 # homeassistant.components.sql -sqlparse==0.4.4 +sqlparse==0.5.0 # homeassistant.components.srp_energy srpenergy==1.3.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ced79b46ae8..930aa27bcfb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1999,7 +1999,7 @@ spiderpy==1.6.1 spotipy==2.23.0 # homeassistant.components.sql -sqlparse==0.4.4 +sqlparse==0.5.0 # homeassistant.components.srp_energy srpenergy==1.3.6 From 8207fc29d28b7ef4f9865774f87ac3cb3ac7df79 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 17 Apr 2024 02:10:06 -0500 Subject: [PATCH 186/198] Bump aiohttp to 3.9.5 (#115727) changelog: https://github.com/aio-libs/aiohttp/compare/v3.9.4...v3.9.5 --- homeassistant/package_constraints.txt | 2 +- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 49c7cf4a992..b2f55381f4d 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -4,7 +4,7 @@ aiodhcpwatcher==1.0.0 aiodiscover==2.0.0 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.3.1 -aiohttp==3.9.4 +aiohttp==3.9.5 aiohttp_cors==0.7.0 astral==2.2 async-interrupt==1.1.1 diff --git a/pyproject.toml b/pyproject.toml index 3a206a3f7fa..3cb894e2342 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ ] requires-python = ">=3.12.0" dependencies = [ - "aiohttp==3.9.4", + "aiohttp==3.9.5", "aiohttp_cors==0.7.0", "aiohttp-fast-url-dispatcher==0.3.0", "aiohttp-zlib-ng==0.3.1", diff --git a/requirements.txt b/requirements.txt index 5635c4912e9..38bea26a8b6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ -c homeassistant/package_constraints.txt # Home Assistant Core -aiohttp==3.9.4 +aiohttp==3.9.5 aiohttp_cors==0.7.0 aiohttp-fast-url-dispatcher==0.3.0 aiohttp-zlib-ng==0.3.1 From db31a526e5dcb1a90afc4fa6c605bb3c3c070e82 Mon Sep 17 00:00:00 2001 From: epenet <6771947+epenet@users.noreply.github.com> Date: Wed, 17 Apr 2024 14:23:47 +0200 Subject: [PATCH 187/198] Bump renault-api to 0.2.2 (#115738) --- homeassistant/components/renault/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/renault/manifest.json b/homeassistant/components/renault/manifest.json index 98e1c8b1e7c..9891c838950 100644 --- a/homeassistant/components/renault/manifest.json +++ b/homeassistant/components/renault/manifest.json @@ -8,5 +8,5 @@ "iot_class": "cloud_polling", "loggers": ["renault_api"], "quality_scale": "platinum", - "requirements": ["renault-api==0.2.1"] + "requirements": ["renault-api==0.2.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index cbe6ea8d74a..5a728ac8429 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2429,7 +2429,7 @@ refoss-ha==1.2.0 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.2.1 +renault-api==0.2.2 # homeassistant.components.renson renson-endura-delta==1.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 930aa27bcfb..5faf622a9c4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1875,7 +1875,7 @@ refoss-ha==1.2.0 regenmaschine==2024.03.0 # homeassistant.components.renault -renault-api==0.2.1 +renault-api==0.2.2 # homeassistant.components.renson renson-endura-delta==1.7.1 From c4b504ce395bd694ded715a19986be210ed2607e Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Apr 2024 15:00:10 +0200 Subject: [PATCH 188/198] Fix homeworks import flow (#115761) --- .../components/homeworks/config_flow.py | 10 +----- .../components/homeworks/test_config_flow.py | 32 +------------------ 2 files changed, 2 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/homeworks/config_flow.py b/homeassistant/components/homeworks/config_flow.py index b2fe4e0e022..e54bbc61141 100644 --- a/homeassistant/components/homeworks/config_flow.py +++ b/homeassistant/components/homeworks/config_flow.py @@ -565,15 +565,7 @@ class HomeworksConfigFlowHandler(ConfigFlow, domain=DOMAIN): CONF_KEYPADS: [ { CONF_ADDR: keypad[CONF_ADDR], - CONF_BUTTONS: [ - { - CONF_LED: button[CONF_LED], - CONF_NAME: button[CONF_NAME], - CONF_NUMBER: button[CONF_NUMBER], - CONF_RELEASE_DELAY: button[CONF_RELEASE_DELAY], - } - for button in keypad[CONF_BUTTONS] - ], + CONF_BUTTONS: [], CONF_NAME: keypad[CONF_NAME], } for keypad in config[CONF_KEYPADS] diff --git a/tests/components/homeworks/test_config_flow.py b/tests/components/homeworks/test_config_flow.py index 4bdb5938f1c..a4159c9b693 100644 --- a/tests/components/homeworks/test_config_flow.py +++ b/tests/components/homeworks/test_config_flow.py @@ -9,7 +9,6 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAI from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN from homeassistant.components.homeworks.const import ( CONF_ADDR, - CONF_BUTTONS, CONF_DIMMERS, CONF_INDEX, CONF_KEYPADS, @@ -161,26 +160,6 @@ async def test_import_flow( { CONF_ADDR: "[02:08:02:01]", CONF_NAME: "Foyer Keypad", - CONF_BUTTONS: [ - { - CONF_NAME: "Morning", - CONF_NUMBER: 1, - CONF_LED: True, - CONF_RELEASE_DELAY: None, - }, - { - CONF_NAME: "Relax", - CONF_NUMBER: 2, - CONF_LED: True, - CONF_RELEASE_DELAY: None, - }, - { - CONF_NAME: "Dim up", - CONF_NUMBER: 3, - CONF_LED: False, - CONF_RELEASE_DELAY: 0.2, - }, - ], } ], }, @@ -207,16 +186,7 @@ async def test_import_flow( "keypads": [ { "addr": "[02:08:02:01]", - "buttons": [ - { - "led": True, - "name": "Morning", - "number": 1, - "release_delay": None, - }, - {"led": True, "name": "Relax", "number": 2, "release_delay": None}, - {"led": False, "name": "Dim up", "number": 3, "release_delay": 0.2}, - ], + "buttons": [], "name": "Foyer Keypad", } ], From 851a5497b4533a02081e9fc3eb5e7279ea024d3f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 17 Apr 2024 15:04:37 +0200 Subject: [PATCH 189/198] Allow [##:##:##] type keypad address in homeworks (#115762) Allow [##:##:##] type keypad address --- homeassistant/components/homeworks/config_flow.py | 2 +- tests/components/homeworks/test_config_flow.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/homeworks/config_flow.py b/homeassistant/components/homeworks/config_flow.py index e54bbc61141..b9515c306d6 100644 --- a/homeassistant/components/homeworks/config_flow.py +++ b/homeassistant/components/homeworks/config_flow.py @@ -93,7 +93,7 @@ BUTTON_EDIT = { } -validate_addr = cv.matches_regex(r"\[\d\d:\d\d:\d\d:\d\d\]") +validate_addr = cv.matches_regex(r"\[(?:\d\d:)?\d\d:\d\d:\d\d\]") async def validate_add_controller( diff --git a/tests/components/homeworks/test_config_flow.py b/tests/components/homeworks/test_config_flow.py index a4159c9b693..a66e743fcd6 100644 --- a/tests/components/homeworks/test_config_flow.py +++ b/tests/components/homeworks/test_config_flow.py @@ -544,8 +544,12 @@ async def test_options_add_remove_light_flow( ) +@pytest.mark.parametrize("keypad_address", ["[02:08:03:01]", "[02:08:03]"]) async def test_options_add_remove_keypad_flow( - hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_homeworks: MagicMock + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_homeworks: MagicMock, + keypad_address: str, ) -> None: """Test options flow to add and remove a keypad.""" mock_config_entry.add_to_hass(hass) @@ -566,7 +570,7 @@ async def test_options_add_remove_keypad_flow( result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={ - CONF_ADDR: "[02:08:03:01]", + CONF_ADDR: keypad_address, CONF_NAME: "Hall Keypad", }, ) @@ -592,7 +596,7 @@ async def test_options_add_remove_keypad_flow( ], "name": "Foyer Keypad", }, - {"addr": "[02:08:03:01]", "buttons": [], "name": "Hall Keypad"}, + {"addr": keypad_address, "buttons": [], "name": "Hall Keypad"}, ], "port": 1234, } @@ -612,7 +616,7 @@ async def test_options_add_remove_keypad_flow( assert result["step_id"] == "remove_keypad" assert result["data_schema"].schema["index"].options == { "0": "Foyer Keypad ([02:08:02:01])", - "1": "Hall Keypad ([02:08:03:01])", + "1": f"Hall Keypad ({keypad_address})", } result = await hass.config_entries.options.async_configure( @@ -625,7 +629,7 @@ async def test_options_add_remove_keypad_flow( {"addr": "[02:08:01:01]", "name": "Foyer Sconces", "rate": 1.0}, ], "host": "192.168.0.1", - "keypads": [{"addr": "[02:08:03:01]", "buttons": [], "name": "Hall Keypad"}], + "keypads": [{"addr": keypad_address, "buttons": [], "name": "Hall Keypad"}], "port": 1234, } await hass.async_block_till_done() From 40884473030cd82267dcb4678a4fca500a8274cf Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Fri, 19 Apr 2024 16:45:19 +0200 Subject: [PATCH 190/198] Add missing media_player features to Samsung TV (#115788) * add missing features * fix snapshot --- .../components/samsungtv/media_player.py | 16 +++++++++------- .../samsungtv/snapshots/test_init.ambr | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 36715c44a9b..ff347431a4a 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -46,15 +46,17 @@ from .triggers.turn_on import async_get_turn_on_trigger SOURCES = {"TV": "KEY_TV", "HDMI": "KEY_HDMI"} SUPPORT_SAMSUNGTV = ( - MediaPlayerEntityFeature.PAUSE - | MediaPlayerEntityFeature.VOLUME_STEP - | MediaPlayerEntityFeature.VOLUME_MUTE - | MediaPlayerEntityFeature.PREVIOUS_TRACK - | MediaPlayerEntityFeature.SELECT_SOURCE - | MediaPlayerEntityFeature.NEXT_TRACK - | MediaPlayerEntityFeature.TURN_OFF + MediaPlayerEntityFeature.NEXT_TRACK + | MediaPlayerEntityFeature.PAUSE | MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PLAY_MEDIA + | MediaPlayerEntityFeature.PREVIOUS_TRACK + | MediaPlayerEntityFeature.SELECT_SOURCE + | MediaPlayerEntityFeature.STOP + | MediaPlayerEntityFeature.TURN_OFF + | MediaPlayerEntityFeature.VOLUME_MUTE + | MediaPlayerEntityFeature.VOLUME_SET + | MediaPlayerEntityFeature.VOLUME_STEP ) diff --git a/tests/components/samsungtv/snapshots/test_init.ambr b/tests/components/samsungtv/snapshots/test_init.ambr index 404b9a6b3af..1b8cf4c999d 100644 --- a/tests/components/samsungtv/snapshots/test_init.ambr +++ b/tests/components/samsungtv/snapshots/test_init.ambr @@ -9,7 +9,7 @@ 'TV', 'HDMI', ]), - 'supported_features': , + 'supported_features': , }), 'context': , 'entity_id': 'media_player.any', @@ -51,7 +51,7 @@ 'original_name': None, 'platform': 'samsungtv', 'previous_unique_id': None, - 'supported_features': , + 'supported_features': , 'translation_key': None, 'unique_id': 'sample-entry-id', 'unit_of_measurement': None, From 6464218e5974f6c9ad276e1caae7b7a8bdad4176 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 19 Apr 2024 06:36:43 +0200 Subject: [PATCH 191/198] Bump aiounifi to v75 (#115819) --- homeassistant/components/unifi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 05dc2189908..305400a4b9d 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["aiounifi"], "quality_scale": "platinum", - "requirements": ["aiounifi==74"], + "requirements": ["aiounifi==75"], "ssdp": [ { "manufacturer": "Ubiquiti Networks", diff --git a/requirements_all.txt b/requirements_all.txt index 5a728ac8429..ea29da2a855 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -392,7 +392,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==74 +aiounifi==75 # homeassistant.components.vlc_telnet aiovlc==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5faf622a9c4..8411f66e540 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -365,7 +365,7 @@ aiotankerkoenig==0.4.1 aiotractive==0.5.6 # homeassistant.components.unifi -aiounifi==74 +aiounifi==75 # homeassistant.components.vlc_telnet aiovlc==0.1.0 From 32f82d480f03f89355489ddc2feee38b7b0e3153 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 19 Apr 2024 11:24:54 -0500 Subject: [PATCH 192/198] Ensure scripts with timeouts of zero timeout immediately (#115830) --- homeassistant/helpers/script.py | 25 ++++- tests/helpers/test_script.py | 178 ++++++++++++++++++++++++++++++++ 2 files changed, 198 insertions(+), 5 deletions(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index b4e02e0e4ad..2b0eb90827e 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -656,6 +656,12 @@ class _ScriptRun: # check if condition already okay if condition.async_template(self._hass, wait_template, self._variables, False): self._variables["wait"]["completed"] = True + self._changed() + return + + if timeout == 0: + self._changed() + self._async_handle_timeout() return futures, timeout_handle, timeout_future = self._async_futures_with_timeout( @@ -1085,6 +1091,11 @@ class _ScriptRun: self._variables["wait"] = {"remaining": timeout, "trigger": None} trace_set_result(wait=self._variables["wait"]) + if timeout == 0: + self._changed() + self._async_handle_timeout() + return + futures, timeout_handle, timeout_future = self._async_futures_with_timeout( timeout ) @@ -1115,6 +1126,14 @@ class _ScriptRun: futures, timeout_handle, timeout_future, remove_triggers ) + def _async_handle_timeout(self) -> None: + """Handle timeout.""" + self._variables["wait"]["remaining"] = 0.0 + if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True): + self._log(_TIMEOUT_MSG) + trace_set_result(wait=self._variables["wait"], timeout=True) + raise _AbortScript from TimeoutError() + async def _async_wait_with_optional_timeout( self, futures: list[asyncio.Future[None]], @@ -1125,11 +1144,7 @@ class _ScriptRun: try: await asyncio.wait(futures, return_when=asyncio.FIRST_COMPLETED) if timeout_future and timeout_future.done(): - self._variables["wait"]["remaining"] = 0.0 - if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True): - self._log(_TIMEOUT_MSG) - trace_set_result(wait=self._variables["wait"], timeout=True) - raise _AbortScript from TimeoutError() + self._async_handle_timeout() finally: if timeout_future and not timeout_future.done() and timeout_handle: timeout_handle.cancel() diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 409b3639d43..16db9fb7b05 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1311,6 +1311,184 @@ async def test_wait_timeout( assert_action_trace(expected_trace) +@pytest.mark.parametrize( + "timeout_param", [0, "{{ 0 }}", {"minutes": 0}, {"minutes": "{{ 0 }}"}] +) +async def test_wait_trigger_with_zero_timeout( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, timeout_param: int | str +) -> None: + """Test the wait trigger with zero timeout option.""" + event = "test_event" + events = async_capture_events(hass, event) + action = { + "wait_for_trigger": { + "platform": "state", + "entity_id": "switch.test", + "to": "off", + } + } + action["timeout"] = timeout_param + action["continue_on_timeout"] = True + sequence = cv.SCRIPT_SCHEMA([action, {"event": event}]) + sequence = await script.async_validate_actions_config(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + wait_started_flag = async_watch_for_action(script_obj, "wait") + hass.states.async_set("switch.test", "on") + hass.async_create_task(script_obj.async_run(context=Context())) + + try: + await asyncio.wait_for(wait_started_flag.wait(), 1) + except (AssertionError, TimeoutError): + await script_obj.async_stop() + raise + + assert not script_obj.is_running + assert len(events) == 1 + assert "(timeout: 0:00:00)" in caplog.text + + variable_wait = {"wait": {"trigger": None, "remaining": 0.0}} + expected_trace = { + "0": [ + { + "result": variable_wait, + "variables": variable_wait, + } + ], + "1": [{"result": {"event": "test_event", "event_data": {}}}], + } + assert_action_trace(expected_trace) + + +@pytest.mark.parametrize( + "timeout_param", [0, "{{ 0 }}", {"minutes": 0}, {"minutes": "{{ 0 }}"}] +) +async def test_wait_trigger_matches_with_zero_timeout( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, timeout_param: int | str +) -> None: + """Test the wait trigger that matches with zero timeout option.""" + event = "test_event" + events = async_capture_events(hass, event) + action = { + "wait_for_trigger": { + "platform": "state", + "entity_id": "switch.test", + "to": "off", + } + } + action["timeout"] = timeout_param + action["continue_on_timeout"] = True + sequence = cv.SCRIPT_SCHEMA([action, {"event": event}]) + sequence = await script.async_validate_actions_config(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + wait_started_flag = async_watch_for_action(script_obj, "wait") + hass.states.async_set("switch.test", "off") + hass.async_create_task(script_obj.async_run(context=Context())) + + try: + await asyncio.wait_for(wait_started_flag.wait(), 1) + except (AssertionError, TimeoutError): + await script_obj.async_stop() + raise + + assert not script_obj.is_running + assert len(events) == 1 + assert "(timeout: 0:00:00)" in caplog.text + + variable_wait = {"wait": {"trigger": None, "remaining": 0.0}} + expected_trace = { + "0": [ + { + "result": variable_wait, + "variables": variable_wait, + } + ], + "1": [{"result": {"event": "test_event", "event_data": {}}}], + } + assert_action_trace(expected_trace) + + +@pytest.mark.parametrize( + "timeout_param", [0, "{{ 0 }}", {"minutes": 0}, {"minutes": "{{ 0 }}"}] +) +async def test_wait_template_with_zero_timeout( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, timeout_param: int | str +) -> None: + """Test the wait template with zero timeout option.""" + event = "test_event" + events = async_capture_events(hass, event) + action = {"wait_template": "{{ states.switch.test.state == 'off' }}"} + action["timeout"] = timeout_param + action["continue_on_timeout"] = True + sequence = cv.SCRIPT_SCHEMA([action, {"event": event}]) + sequence = await script.async_validate_actions_config(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + wait_started_flag = async_watch_for_action(script_obj, "wait") + hass.states.async_set("switch.test", "on") + hass.async_create_task(script_obj.async_run(context=Context())) + + try: + await asyncio.wait_for(wait_started_flag.wait(), 1) + except (AssertionError, TimeoutError): + await script_obj.async_stop() + raise + + assert not script_obj.is_running + assert len(events) == 1 + assert "(timeout: 0:00:00)" in caplog.text + variable_wait = {"wait": {"completed": False, "remaining": 0.0}} + expected_trace = { + "0": [ + { + "result": variable_wait, + "variables": variable_wait, + } + ], + "1": [{"result": {"event": "test_event", "event_data": {}}}], + } + assert_action_trace(expected_trace) + + +@pytest.mark.parametrize( + "timeout_param", [0, "{{ 0 }}", {"minutes": 0}, {"minutes": "{{ 0 }}"}] +) +async def test_wait_template_matches_with_zero_timeout( + hass: HomeAssistant, caplog: pytest.LogCaptureFixture, timeout_param: int | str +) -> None: + """Test the wait template that matches with zero timeout option.""" + event = "test_event" + events = async_capture_events(hass, event) + action = {"wait_template": "{{ states.switch.test.state == 'off' }}"} + action["timeout"] = timeout_param + action["continue_on_timeout"] = True + sequence = cv.SCRIPT_SCHEMA([action, {"event": event}]) + sequence = await script.async_validate_actions_config(hass, sequence) + script_obj = script.Script(hass, sequence, "Test Name", "test_domain") + wait_started_flag = async_watch_for_action(script_obj, "wait") + hass.states.async_set("switch.test", "off") + hass.async_create_task(script_obj.async_run(context=Context())) + + try: + await asyncio.wait_for(wait_started_flag.wait(), 1) + except (AssertionError, TimeoutError): + await script_obj.async_stop() + raise + + assert not script_obj.is_running + assert len(events) == 1 + assert "(timeout: 0:00:00)" in caplog.text + variable_wait = {"wait": {"completed": True, "remaining": 0.0}} + expected_trace = { + "0": [ + { + "result": variable_wait, + "variables": variable_wait, + } + ], + "1": [{"result": {"event": "test_event", "event_data": {}}}], + } + assert_action_trace(expected_trace) + + @pytest.mark.parametrize( ("continue_on_timeout", "n_events"), [(False, 0), (True, 1), (None, 1)] ) From 13ed2d291931c44bfa5116e13377a32464a237de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A5le=20Stor=C3=B8=20Hauknes?= Date: Tue, 23 Apr 2024 15:53:31 +0200 Subject: [PATCH 193/198] Fix KeyError error when fetching sensors (Airthings) (#115844) --- homeassistant/components/airthings/sensor.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/airthings/sensor.py b/homeassistant/components/airthings/sensor.py index fc91d816aca..f0a3dc5be8f 100644 --- a/homeassistant/components/airthings/sensor.py +++ b/homeassistant/components/airthings/sensor.py @@ -157,3 +157,11 @@ class AirthingsHeaterEnergySensor( def native_value(self) -> StateType: """Return the value reported by the sensor.""" return self.coordinator.data[self._id].sensors[self.entity_description.key] # type: ignore[no-any-return] + + @property + def available(self) -> bool: + """Check if device and sensor is available in data.""" + return ( + super().available + and self.entity_description.key in self.coordinator.data[self._id].sensors + ) From b8b2f6427a0c841c17b320f7b79b591350ecf785 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 20 Apr 2024 05:36:03 -0500 Subject: [PATCH 194/198] Bump plexapi to 4.15.12 (#115872) --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 85362371715..ff0ab39b150 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_push", "loggers": ["plexapi", "plexwebsocket"], "requirements": [ - "PlexAPI==4.15.11", + "PlexAPI==4.15.12", "plexauth==0.0.6", "plexwebsocket==0.0.14" ], diff --git a/requirements_all.txt b/requirements_all.txt index ea29da2a855..4a4e9c94c69 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -45,7 +45,7 @@ Mastodon.py==1.8.1 Pillow==10.3.0 # homeassistant.components.plex -PlexAPI==4.15.11 +PlexAPI==4.15.12 # homeassistant.components.progettihwsw ProgettiHWSW==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8411f66e540..4acfa310c94 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -39,7 +39,7 @@ HATasmota==0.8.0 Pillow==10.3.0 # homeassistant.components.plex -PlexAPI==4.15.11 +PlexAPI==4.15.12 # homeassistant.components.progettihwsw ProgettiHWSW==0.1.3 From c9c7c7803e6097c0d555032d73e36ef34b1556e8 Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Sun, 21 Apr 2024 15:52:47 -0700 Subject: [PATCH 195/198] Bump ical to 8.0.0 (#115907) Co-authored-by: J. Nick Koston --- homeassistant/components/google/manifest.json | 2 +- homeassistant/components/local_calendar/manifest.json | 2 +- homeassistant/components/local_todo/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 00561cb5fd6..ac43dc58953 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.4", "oauth2client==4.1.3", "ical==7.0.3"] + "requirements": ["gcal-sync==6.0.4", "oauth2client==4.1.3", "ical==8.0.0"] } diff --git a/homeassistant/components/local_calendar/manifest.json b/homeassistant/components/local_calendar/manifest.json index 1c13970503d..b1c7d6a3a34 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.3"] + "requirements": ["ical==8.0.0"] } diff --git a/homeassistant/components/local_todo/manifest.json b/homeassistant/components/local_todo/manifest.json index 3bcb8af9f43..44c76a56a8f 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.3"] + "requirements": ["ical==8.0.0"] } diff --git a/requirements_all.txt b/requirements_all.txt index 4a4e9c94c69..2228c9d1bd6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1118,7 +1118,7 @@ ibmiotf==0.3.4 # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==7.0.3 +ical==8.0.0 # homeassistant.components.ping icmplib==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4acfa310c94..be285822e63 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -908,7 +908,7 @@ ibeacon-ble==1.2.0 # homeassistant.components.google # homeassistant.components.local_calendar # homeassistant.components.local_todo -ical==7.0.3 +ical==8.0.0 # homeassistant.components.ping icmplib==3.0 From 036b6fca25d7339ae17f2285a810dea7889f0ef9 Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 21 Apr 2024 11:44:58 +0200 Subject: [PATCH 196/198] Fix geo location attributes of Tankerkoenig sensors (#115914) * geo location attributes needs to be float * make mypy happy --- homeassistant/components/tankerkoenig/sensor.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tankerkoenig/sensor.py b/homeassistant/components/tankerkoenig/sensor.py index f2fdc2c45b7..33476e75262 100644 --- a/homeassistant/components/tankerkoenig/sensor.py +++ b/homeassistant/components/tankerkoenig/sensor.py @@ -91,7 +91,7 @@ class FuelPriceSensor(TankerkoenigCoordinatorEntity, SensorEntity): self._fuel_type = fuel_type self._attr_translation_key = fuel_type self._attr_unique_id = f"{station.id}_{fuel_type}" - attrs = { + attrs: dict[str, int | str | float | None] = { ATTR_BRAND: station.brand, ATTR_FUEL_TYPE: fuel_type, ATTR_STATION_NAME: station.name, @@ -102,8 +102,8 @@ class FuelPriceSensor(TankerkoenigCoordinatorEntity, SensorEntity): } if coordinator.show_on_map: - attrs[ATTR_LATITUDE] = str(station.lat) - attrs[ATTR_LONGITUDE] = str(station.lng) + attrs[ATTR_LATITUDE] = station.lat + attrs[ATTR_LONGITUDE] = station.lng self._attr_extra_state_attributes = attrs @property From b521acb72404bb57b3422978baf4d216f0ecb011 Mon Sep 17 00:00:00 2001 From: Raj Laud <50647620+rajlaud@users.noreply.github.com> Date: Mon, 22 Apr 2024 11:46:12 -0400 Subject: [PATCH 197/198] Use start helper in squeezebox for server discovery (#115978) --- homeassistant/components/squeezebox/media_player.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 007d880a263..d9478b6747d 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -28,7 +28,6 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, CONF_USERNAME, - EVENT_HOMEASSISTANT_START, ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import ( @@ -44,6 +43,7 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.start import async_at_start from homeassistant.util.dt import utcnow from .browse_media import ( @@ -207,12 +207,7 @@ async def async_setup_entry( platform.async_register_entity_service(SERVICE_UNSYNC, None, "async_unsync") # Start server discovery task if not already running - if hass.is_running: - hass.async_create_task(start_server_discovery(hass)) - else: - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_START, start_server_discovery(hass) - ) + config_entry.async_on_unload(async_at_start(hass, start_server_discovery)) class SqueezeBoxEntity(MediaPlayerEntity): From 4d551d68c6f8e86380b91c30671d4a7d13ab9ae1 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 23 Apr 2024 20:12:21 +0200 Subject: [PATCH 198/198] Bump version to 2024.4.4 --- homeassistant/const.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index ecfc1c6259c..892d16ba008 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -18,7 +18,7 @@ from .util.signal_type import SignalType APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 4 -PATCH_VERSION: Final = "3" +PATCH_VERSION: Final = "4" __short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__: Final = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 12, 0) diff --git a/pyproject.toml b/pyproject.toml index 3cb894e2342..b6206f107f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.4.3" +version = "2024.4.4" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"