From 00dd86fb4b933cc8bbefd520dee7a5d412d507e6 Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:11:49 +0200 Subject: [PATCH 01/23] Update requests to 2.32.3 (#118868) Co-authored-by: Robert Resch --- 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 690b0f2615d02..e24ccc4fac9f5 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -53,7 +53,7 @@ python-slugify==8.0.4 PyTurboJPEG==1.7.1 pyudev==0.24.1 PyYAML==6.0.1 -requests==2.31.0 +requests==2.32.3 SQLAlchemy==2.0.30 typing-extensions>=4.12.0,<5.0 ulid-transform==0.9.0 diff --git a/pyproject.toml b/pyproject.toml index 516a2e5bf7259..bfae6c15cd6f4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ dependencies = [ "psutil-home-assistant==0.0.1", "python-slugify==8.0.4", "PyYAML==6.0.1", - "requests==2.31.0", + "requests==2.32.3", "SQLAlchemy==2.0.30", "typing-extensions>=4.12.0,<5.0", "ulid-transform==0.9.0", diff --git a/requirements.txt b/requirements.txt index 7e2107a44900d..2701c7b6099ff 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,7 +34,7 @@ pip>=21.3.1 psutil-home-assistant==0.0.1 python-slugify==8.0.4 PyYAML==6.0.1 -requests==2.31.0 +requests==2.32.3 SQLAlchemy==2.0.30 typing-extensions>=4.12.0,<5.0 ulid-transform==0.9.0 From 0f9a91d36980263d9347b25f3239e04bdd90e324 Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 6 Jun 2024 13:20:34 -0500 Subject: [PATCH 02/23] Prioritize literal text with name slots in sentence matching (#118900) Prioritize literal text with name slots --- .../components/conversation/default_agent.py | 11 ++++- .../test_default_agent_intents.py | 48 +++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index d545488329298..7bb2c2182b397 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -429,8 +429,15 @@ def _recognize( intent_context=intent_context, language=language, ): - if ("name" in result.entities) and ( - not result.entities["name"].is_wildcard + # Prioritize results with a "name" slot, but still prefer ones with + # more literal text matched. + if ( + ("name" in result.entities) + and (not result.entities["name"].is_wildcard) + and ( + (name_result is None) + or (result.text_chunks_matched > name_result.text_chunks_matched) + ) ): name_result = result diff --git a/tests/components/conversation/test_default_agent_intents.py b/tests/components/conversation/test_default_agent_intents.py index f5050f4483e38..b1c4a6d51af0e 100644 --- a/tests/components/conversation/test_default_agent_intents.py +++ b/tests/components/conversation/test_default_agent_intents.py @@ -1,5 +1,7 @@ """Test intents for the default agent.""" +from unittest.mock import patch + import pytest from homeassistant.components import ( @@ -7,6 +9,7 @@ cover, light, media_player, + todo, vacuum, valve, ) @@ -35,6 +38,27 @@ from tests.common import async_mock_service +class MockTodoListEntity(todo.TodoListEntity): + """Test todo list entity.""" + + def __init__(self, items: list[todo.TodoItem] | None = None) -> None: + """Initialize entity.""" + self._attr_todo_items = items or [] + + @property + def items(self) -> list[todo.TodoItem]: + """Return the items in the To-do list.""" + return self._attr_todo_items + + async def async_create_todo_item(self, item: todo.TodoItem) -> None: + """Add an item to the To-do list.""" + self._attr_todo_items.append(item) + + async def async_delete_todo_items(self, uids: list[str]) -> None: + """Delete an item in the To-do list.""" + self._attr_todo_items = [item for item in self.items if item.uid not in uids] + + @pytest.fixture async def init_components(hass: HomeAssistant): """Initialize relevant components with empty configs.""" @@ -365,3 +389,27 @@ async def test_turn_floor_lights_on_off( assert {s.entity_id for s in result.response.matched_states} == { bedroom_light.entity_id } + + +async def test_todo_add_item_fr( + hass: HomeAssistant, + init_components, +) -> None: + """Test that wildcard matches prioritize results with more literal text matched.""" + assert await async_setup_component(hass, todo.DOMAIN, {}) + hass.states.async_set("todo.liste_des_courses", 0, {}) + + with ( + patch.object(hass.config, "language", "fr"), + patch( + "homeassistant.components.todo.intent.ListAddItemIntent.async_handle", + return_value=intent.IntentResponse(hass.config.language), + ) as mock_handle, + ): + await conversation.async_converse( + hass, "Ajoute de la farine a la liste des courses", None, Context(), None + ) + mock_handle.assert_called_once() + assert mock_handle.call_args.args + intent_obj = mock_handle.call_args.args[0] + assert intent_obj.slots.get("item", {}).get("value", "").strip() == "farine" From 5a7332a13507820300f4992c752ea278f666f971 Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Thu, 6 Jun 2024 16:10:03 +0400 Subject: [PATCH 03/23] Check if imap message text has a value instead of checking if its not None (#118901) * Check if message_text has a value instead of checking if its not None * Strip message_text to ensure that its actually empty or not * Add test with multipart payload having empty plain text --- homeassistant/components/imap/coordinator.py | 6 +-- tests/components/imap/const.py | 39 ++++++++++++++++++++ tests/components/imap/test_init.py | 3 ++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/imap/coordinator.py b/homeassistant/components/imap/coordinator.py index c0123b89ee416..a9d0fdfbd48a5 100644 --- a/homeassistant/components/imap/coordinator.py +++ b/homeassistant/components/imap/coordinator.py @@ -195,13 +195,13 @@ def text(self) -> str: ): message_untyped_text = str(part.get_payload()) - if message_text is not None: + if message_text is not None and message_text.strip(): return message_text - if message_html is not None: + if message_html: return message_html - if message_untyped_text is not None: + if message_untyped_text: return message_untyped_text return str(self.email_message.get_payload()) diff --git a/tests/components/imap/const.py b/tests/components/imap/const.py index 677eea7a473aa..037960c9e5daa 100644 --- a/tests/components/imap/const.py +++ b/tests/components/imap/const.py @@ -59,6 +59,11 @@ b"Content-Transfer-Encoding: 7bit\r\n\r\nTest body\r\n" ) +TEST_CONTENT_TEXT_PLAIN_EMPTY = ( + b'Content-Type: text/plain; charset="utf-8"\r\n' + b"Content-Transfer-Encoding: 7bit\r\n\r\n \r\n" +) + TEST_CONTENT_TEXT_BASE64 = ( b'Content-Type: text/plain; charset="utf-8"\r\n' b"Content-Transfer-Encoding: base64\r\n\r\nVGVzdCBib2R5\r\n" @@ -108,6 +113,15 @@ + b"\r\n--Mark=_100584970350292485166--\r\n" ) +TEST_CONTENT_MULTIPART_EMPTY_PLAIN = ( + b"\r\nThis is a multi-part message in MIME format.\r\n" + b"\r\n--Mark=_100584970350292485166\r\n" + + TEST_CONTENT_TEXT_PLAIN_EMPTY + + b"\r\n--Mark=_100584970350292485166\r\n" + + TEST_CONTENT_HTML + + b"\r\n--Mark=_100584970350292485166--\r\n" +) + TEST_CONTENT_MULTIPART_BASE64 = ( b"\r\nThis is a multi-part message in MIME format.\r\n" b"\r\n--Mark=_100584970350292485166\r\n" @@ -155,6 +169,18 @@ ], ) +TEST_FETCH_RESPONSE_TEXT_PLAIN_EMPTY = ( + "OK", + [ + b"1 FETCH (BODY[] {" + + str(len(TEST_MESSAGE + TEST_CONTENT_TEXT_PLAIN_EMPTY)).encode("utf-8") + + b"}", + bytearray(TEST_MESSAGE + TEST_CONTENT_TEXT_PLAIN_EMPTY), + b")", + b"Fetch completed (0.0001 + 0.000 secs).", + ], +) + TEST_FETCH_RESPONSE_TEXT_PLAIN_ALT = ( "OK", [ @@ -249,6 +275,19 @@ b"Fetch completed (0.0001 + 0.000 secs).", ], ) +TEST_FETCH_RESPONSE_MULTIPART_EMPTY_PLAIN = ( + "OK", + [ + b"1 FETCH (BODY[] {" + + str(len(TEST_MESSAGE_MULTIPART + TEST_CONTENT_MULTIPART_EMPTY_PLAIN)).encode( + "utf-8" + ) + + b"}", + bytearray(TEST_MESSAGE_MULTIPART + TEST_CONTENT_MULTIPART_EMPTY_PLAIN), + b")", + b"Fetch completed (0.0001 + 0.000 secs).", + ], +) TEST_FETCH_RESPONSE_MULTIPART_BASE64 = ( "OK", [ diff --git a/tests/components/imap/test_init.py b/tests/components/imap/test_init.py index e6e6ffe71143c..fe10770fc6434 100644 --- a/tests/components/imap/test_init.py +++ b/tests/components/imap/test_init.py @@ -29,6 +29,7 @@ TEST_FETCH_RESPONSE_MULTIPART, TEST_FETCH_RESPONSE_MULTIPART_BASE64, TEST_FETCH_RESPONSE_MULTIPART_BASE64_INVALID, + TEST_FETCH_RESPONSE_MULTIPART_EMPTY_PLAIN, TEST_FETCH_RESPONSE_NO_SUBJECT_TO_FROM, TEST_FETCH_RESPONSE_TEXT_BARE, TEST_FETCH_RESPONSE_TEXT_OTHER, @@ -116,6 +117,7 @@ async def test_entry_startup_fails( (TEST_FETCH_RESPONSE_TEXT_OTHER, True), (TEST_FETCH_RESPONSE_HTML, True), (TEST_FETCH_RESPONSE_MULTIPART, True), + (TEST_FETCH_RESPONSE_MULTIPART_EMPTY_PLAIN, True), (TEST_FETCH_RESPONSE_MULTIPART_BASE64, True), (TEST_FETCH_RESPONSE_BINARY, True), ], @@ -129,6 +131,7 @@ async def test_entry_startup_fails( "other", "html", "multipart", + "multipart_empty_plain", "multipart_base64", "binary", ], From 86b13e8ae35816793e0f4102600c3095d6a6d044 Mon Sep 17 00:00:00 2001 From: Jan-Philipp Benecke Date: Wed, 5 Jun 2024 23:37:14 +0200 Subject: [PATCH 04/23] Fix flaky Google Assistant test (#118914) * Fix flaky Google Assistant test * Trigger full ci --- tests/components/google_assistant/test_http.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index 1dac75875a64b..416d569b2867b 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -577,6 +577,8 @@ async def test_async_get_users_from_store(tmpdir: py.path.local) -> None: assert await async_get_users(hass) == ["agent_1"] + await hass.async_stop() + VALID_STORE_DATA = json.dumps( { From 394c13af1dd2e1093681830736d6a54cd42fd79c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 5 Jun 2024 22:37:35 -0500 Subject: [PATCH 05/23] Revert "Bump orjson to 3.10.3 (#116945)" (#118920) This reverts commit dc50095d0618f545a7ee80d2f10b9997c1bc40da. --- 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 e24ccc4fac9f5..b1d82e3c58b1a 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -39,7 +39,7 @@ ifaddr==0.2.0 Jinja2==3.1.4 lru-dict==1.3.0 mutagen==1.47.0 -orjson==3.10.3 +orjson==3.9.15 packaging>=23.1 paho-mqtt==1.6.1 Pillow==10.3.0 diff --git a/pyproject.toml b/pyproject.toml index bfae6c15cd6f4..c3e03374b555a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,7 +53,7 @@ dependencies = [ "cryptography==42.0.5", "Pillow==10.3.0", "pyOpenSSL==24.1.0", - "orjson==3.10.3", + "orjson==3.9.15", "packaging>=23.1", "pip>=21.3.1", "psutil-home-assistant==0.0.1", diff --git a/requirements.txt b/requirements.txt index 2701c7b6099ff..05b0eb35c1e69 100644 --- a/requirements.txt +++ b/requirements.txt @@ -28,7 +28,7 @@ PyJWT==2.8.0 cryptography==42.0.5 Pillow==10.3.0 pyOpenSSL==24.1.0 -orjson==3.10.3 +orjson==3.9.15 packaging>=23.1 pip>=21.3.1 psutil-home-assistant==0.0.1 From 6e9a53d02e434b769e3c646c1be37fc7db6eb363 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 6 Jun 2024 12:26:07 +0200 Subject: [PATCH 06/23] Bump `imgw-pib` backend library to version `1.0.2` (#118953) Bump imgw-pib to version 1.0.2 Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/imgw_pib/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/imgw_pib/manifest.json b/homeassistant/components/imgw_pib/manifest.json index c6a230244ec8b..9a9994a73e533 100644 --- a/homeassistant/components/imgw_pib/manifest.json +++ b/homeassistant/components/imgw_pib/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/imgw_pib", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["imgw_pib==1.0.1"] + "requirements": ["imgw_pib==1.0.2"] } diff --git a/requirements_all.txt b/requirements_all.txt index 286e447a0da7f..6ecb9660fc9ab 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1146,7 +1146,7 @@ iglo==1.2.7 ihcsdk==2.8.5 # homeassistant.components.imgw_pib -imgw_pib==1.0.1 +imgw_pib==1.0.2 # homeassistant.components.incomfort incomfort-client==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8888e9f632d82..af1126c3298bf 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -933,7 +933,7 @@ idasen-ha==2.5.3 ifaddr==0.2.0 # homeassistant.components.imgw_pib -imgw_pib==1.0.1 +imgw_pib==1.0.2 # homeassistant.components.influxdb influxdb-client==1.24.0 From 62f73cfccac0995f44d89de01a7efc47192b4a7d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 6 Jun 2024 14:21:30 +0300 Subject: [PATCH 07/23] Fix Alarm control panel not require code in several integrations (#118961) --- homeassistant/components/agent_dvr/alarm_control_panel.py | 1 + homeassistant/components/blink/alarm_control_panel.py | 1 + homeassistant/components/egardia/alarm_control_panel.py | 1 + homeassistant/components/hive/alarm_control_panel.py | 1 + homeassistant/components/ialarm/alarm_control_panel.py | 1 + homeassistant/components/lupusec/alarm_control_panel.py | 1 + homeassistant/components/nx584/alarm_control_panel.py | 1 + homeassistant/components/overkiz/alarm_control_panel.py | 1 + homeassistant/components/point/alarm_control_panel.py | 1 + homeassistant/components/spc/alarm_control_panel.py | 1 + homeassistant/components/tuya/alarm_control_panel.py | 1 + homeassistant/components/xiaomi_miio/alarm_control_panel.py | 1 + 12 files changed, 12 insertions(+) diff --git a/homeassistant/components/agent_dvr/alarm_control_panel.py b/homeassistant/components/agent_dvr/alarm_control_panel.py index e703bcad6ae0d..f098184321f78 100644 --- a/homeassistant/components/agent_dvr/alarm_control_panel.py +++ b/homeassistant/components/agent_dvr/alarm_control_panel.py @@ -43,6 +43,7 @@ class AgentBaseStation(AlarmControlPanelEntity): | AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_NIGHT ) + _attr_code_arm_required = False _attr_has_entity_name = True _attr_name = None diff --git a/homeassistant/components/blink/alarm_control_panel.py b/homeassistant/components/blink/alarm_control_panel.py index b7dc50a5c517b..0ad15cf0d31dc 100644 --- a/homeassistant/components/blink/alarm_control_panel.py +++ b/homeassistant/components/blink/alarm_control_panel.py @@ -46,6 +46,7 @@ class BlinkSyncModuleHA( """Representation of a Blink Alarm Control Panel.""" _attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY + _attr_code_arm_required = False _attr_has_entity_name = True _attr_name = None diff --git a/homeassistant/components/egardia/alarm_control_panel.py b/homeassistant/components/egardia/alarm_control_panel.py index ad08b8cbc4d4d..706ba0db71976 100644 --- a/homeassistant/components/egardia/alarm_control_panel.py +++ b/homeassistant/components/egardia/alarm_control_panel.py @@ -67,6 +67,7 @@ class EgardiaAlarm(AlarmControlPanelEntity): """Representation of a Egardia alarm.""" _attr_state: str | None + _attr_code_arm_required = False _attr_supported_features = ( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY diff --git a/homeassistant/components/hive/alarm_control_panel.py b/homeassistant/components/hive/alarm_control_panel.py index 78e8606a43ceb..06383784a3f22 100644 --- a/homeassistant/components/hive/alarm_control_panel.py +++ b/homeassistant/components/hive/alarm_control_panel.py @@ -51,6 +51,7 @@ class HiveAlarmControlPanelEntity(HiveEntity, AlarmControlPanelEntity): | AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.TRIGGER ) + _attr_code_arm_required = False async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" diff --git a/homeassistant/components/ialarm/alarm_control_panel.py b/homeassistant/components/ialarm/alarm_control_panel.py index a7118fb03cce9..912f04a1d1eb4 100644 --- a/homeassistant/components/ialarm/alarm_control_panel.py +++ b/homeassistant/components/ialarm/alarm_control_panel.py @@ -37,6 +37,7 @@ class IAlarmPanel( AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY ) + _attr_code_arm_required = False def __init__(self, coordinator: IAlarmDataUpdateCoordinator) -> None: """Create the entity with a DataUpdateCoordinator.""" diff --git a/homeassistant/components/lupusec/alarm_control_panel.py b/homeassistant/components/lupusec/alarm_control_panel.py index 090d9ab3cedb9..73aba775a2a19 100644 --- a/homeassistant/components/lupusec/alarm_control_panel.py +++ b/homeassistant/components/lupusec/alarm_control_panel.py @@ -48,6 +48,7 @@ class LupusecAlarm(LupusecDevice, AlarmControlPanelEntity): AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY ) + _attr_code_arm_required = False def __init__( self, data: lupupy.Lupusec, device: lupupy.devices.LupusecAlarm, entry_id: str diff --git a/homeassistant/components/nx584/alarm_control_panel.py b/homeassistant/components/nx584/alarm_control_panel.py index a86cda83dd74e..2e306de5908c4 100644 --- a/homeassistant/components/nx584/alarm_control_panel.py +++ b/homeassistant/components/nx584/alarm_control_panel.py @@ -100,6 +100,7 @@ class NX584Alarm(AlarmControlPanelEntity): AlarmControlPanelEntityFeature.ARM_HOME | AlarmControlPanelEntityFeature.ARM_AWAY ) + _attr_code_arm_required = False def __init__(self, name: str, alarm_client: client.Client, url: str) -> None: """Init the nx584 alarm panel.""" diff --git a/homeassistant/components/overkiz/alarm_control_panel.py b/homeassistant/components/overkiz/alarm_control_panel.py index 72c99982a1bbf..151f91790cfaa 100644 --- a/homeassistant/components/overkiz/alarm_control_panel.py +++ b/homeassistant/components/overkiz/alarm_control_panel.py @@ -240,6 +240,7 @@ class OverkizAlarmControlPanel(OverkizDescriptiveEntity, AlarmControlPanelEntity """Representation of an Overkiz Alarm Control Panel.""" entity_description: OverkizAlarmDescription + _attr_code_arm_required = False def __init__( self, diff --git a/homeassistant/components/point/alarm_control_panel.py b/homeassistant/components/point/alarm_control_panel.py index b04742af06a72..844d1eba553fc 100644 --- a/homeassistant/components/point/alarm_control_panel.py +++ b/homeassistant/components/point/alarm_control_panel.py @@ -55,6 +55,7 @@ class MinutPointAlarmControl(AlarmControlPanelEntity): """The platform class required by Home Assistant.""" _attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY + _attr_code_arm_required = False def __init__(self, point_client: MinutPointClient, home_id: str) -> None: """Initialize the entity.""" diff --git a/homeassistant/components/spc/alarm_control_panel.py b/homeassistant/components/spc/alarm_control_panel.py index ae349d2497e55..7e584ff5e6329 100644 --- a/homeassistant/components/spc/alarm_control_panel.py +++ b/homeassistant/components/spc/alarm_control_panel.py @@ -62,6 +62,7 @@ class SpcAlarm(AlarmControlPanelEntity): | AlarmControlPanelEntityFeature.ARM_AWAY | AlarmControlPanelEntityFeature.ARM_NIGHT ) + _attr_code_arm_required = False def __init__(self, area: Area, api: SpcWebGateway) -> None: """Initialize the SPC alarm panel.""" diff --git a/homeassistant/components/tuya/alarm_control_panel.py b/homeassistant/components/tuya/alarm_control_panel.py index 868f6634bc94a..29da625a99009 100644 --- a/homeassistant/components/tuya/alarm_control_panel.py +++ b/homeassistant/components/tuya/alarm_control_panel.py @@ -88,6 +88,7 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity): """Tuya Alarm Entity.""" _attr_name = None + _attr_code_arm_required = False def __init__( self, diff --git a/homeassistant/components/xiaomi_miio/alarm_control_panel.py b/homeassistant/components/xiaomi_miio/alarm_control_panel.py index 72530227e889d..58d5ed247ade7 100644 --- a/homeassistant/components/xiaomi_miio/alarm_control_panel.py +++ b/homeassistant/components/xiaomi_miio/alarm_control_panel.py @@ -54,6 +54,7 @@ class XiaomiGatewayAlarm(AlarmControlPanelEntity): _attr_icon = "mdi:shield-home" _attr_supported_features = AlarmControlPanelEntityFeature.ARM_AWAY + _attr_code_arm_required = False def __init__( self, gateway_device, gateway_name, model, mac_address, gateway_device_id From d6e1d05e87d62c35c92b58152805fb2ef7466b8f Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 6 Jun 2024 14:34:03 +0300 Subject: [PATCH 08/23] Bump python-holidays to 0.50 (#118965) --- 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 5ac6611592dae..bc7ce0e8dd142 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.49", "babel==2.13.1"] + "requirements": ["holidays==0.50", "babel==2.13.1"] } diff --git a/homeassistant/components/workday/manifest.json b/homeassistant/components/workday/manifest.json index 7faf82ad71a00..71c26a30e947b 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.49"] + "requirements": ["holidays==0.50"] } diff --git a/requirements_all.txt b/requirements_all.txt index 6ecb9660fc9ab..c88a0a67238cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1084,7 +1084,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.49 +holidays==0.50 # homeassistant.components.frontend home-assistant-frontend==20240605.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index af1126c3298bf..06a48f20f865b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -886,7 +886,7 @@ hole==0.8.0 # homeassistant.components.holiday # homeassistant.components.workday -holidays==0.49 +holidays==0.50 # homeassistant.components.frontend home-assistant-frontend==20240605.0 From 14da1e9b23bee2278281f8036837ac5a6fb6cac0 Mon Sep 17 00:00:00 2001 From: David Knowles Date: Thu, 6 Jun 2024 11:28:13 -0400 Subject: [PATCH 09/23] Bump pydrawise to 2024.6.3 (#118977) --- homeassistant/components/hydrawise/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/hydrawise/manifest.json b/homeassistant/components/hydrawise/manifest.json index 0426b8bf2cc7d..dc6408407e783 100644 --- a/homeassistant/components/hydrawise/manifest.json +++ b/homeassistant/components/hydrawise/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/hydrawise", "iot_class": "cloud_polling", "loggers": ["pydrawise"], - "requirements": ["pydrawise==2024.6.2"] + "requirements": ["pydrawise==2024.6.3"] } diff --git a/requirements_all.txt b/requirements_all.txt index c88a0a67238cb..3e39ff0ba8621 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1794,7 +1794,7 @@ pydiscovergy==3.0.1 pydoods==1.0.2 # homeassistant.components.hydrawise -pydrawise==2024.6.2 +pydrawise==2024.6.3 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 06a48f20f865b..2be2dfb702296 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1405,7 +1405,7 @@ pydexcom==0.2.3 pydiscovergy==3.0.1 # homeassistant.components.hydrawise -pydrawise==2024.6.2 +pydrawise==2024.6.3 # homeassistant.components.android_ip_webcam pydroid-ipcam==2.0.0 From 52d1432d8182d82311aaa72c098a3284211b0f96 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 6 Jun 2024 17:14:02 +0200 Subject: [PATCH 10/23] Bump `imgw-pib` library to version `1.0.4` (#118978) Co-authored-by: Maciej Bieniek <478555+bieniu@users.noreply.github.com> --- homeassistant/components/imgw_pib/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/imgw_pib/manifest.json b/homeassistant/components/imgw_pib/manifest.json index 9a9994a73e533..fe714691f133b 100644 --- a/homeassistant/components/imgw_pib/manifest.json +++ b/homeassistant/components/imgw_pib/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/imgw_pib", "iot_class": "cloud_polling", "quality_scale": "platinum", - "requirements": ["imgw_pib==1.0.2"] + "requirements": ["imgw_pib==1.0.4"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3e39ff0ba8621..5d0a195b8e8e4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1146,7 +1146,7 @@ iglo==1.2.7 ihcsdk==2.8.5 # homeassistant.components.imgw_pib -imgw_pib==1.0.2 +imgw_pib==1.0.4 # homeassistant.components.incomfort incomfort-client==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2be2dfb702296..ae1a1f3fd7211 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -933,7 +933,7 @@ idasen-ha==2.5.3 ifaddr==0.2.0 # homeassistant.components.imgw_pib -imgw_pib==1.0.2 +imgw_pib==1.0.4 # homeassistant.components.influxdb influxdb-client==1.24.0 From 1f6be7b4d1a1e3cc312e9d6d8da16709121595ba Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jun 2024 09:17:36 +0200 Subject: [PATCH 11/23] Fix unit of measurement for airgradient sensor (#118981) --- homeassistant/components/airgradient/sensor.py | 1 + homeassistant/components/airgradient/strings.json | 2 +- .../airgradient/snapshots/test_sensor.ambr | 15 ++++++++------- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/airgradient/sensor.py b/homeassistant/components/airgradient/sensor.py index e2fc580fce52b..f21f13b80ab5e 100644 --- a/homeassistant/components/airgradient/sensor.py +++ b/homeassistant/components/airgradient/sensor.py @@ -103,6 +103,7 @@ class AirGradientSensorEntityDescription(SensorEntityDescription): AirGradientSensorEntityDescription( key="pm003", translation_key="pm003_count", + native_unit_of_measurement="particles/dL", state_class=SensorStateClass.MEASUREMENT, value_fn=lambda status: status.pm003_count, ), diff --git a/homeassistant/components/airgradient/strings.json b/homeassistant/components/airgradient/strings.json index 3b1e9f9ee4148..20322eed33c59 100644 --- a/homeassistant/components/airgradient/strings.json +++ b/homeassistant/components/airgradient/strings.json @@ -48,7 +48,7 @@ "name": "Nitrogen index" }, "pm003_count": { - "name": "PM0.3 count" + "name": "PM0.3" }, "raw_total_volatile_organic_component": { "name": "Raw total VOC" diff --git a/tests/components/airgradient/snapshots/test_sensor.ambr b/tests/components/airgradient/snapshots/test_sensor.ambr index 27d8043a3950d..b9b6be41ff4f9 100644 --- a/tests/components/airgradient/snapshots/test_sensor.ambr +++ b/tests/components/airgradient/snapshots/test_sensor.ambr @@ -150,7 +150,7 @@ 'state': '1', }) # --- -# name: test_all_entities[sensor.airgradient_pm0_3_count-entry] +# name: test_all_entities[sensor.airgradient_pm0_3-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -164,7 +164,7 @@ 'disabled_by': None, 'domain': 'sensor', 'entity_category': None, - 'entity_id': 'sensor.airgradient_pm0_3_count', + 'entity_id': 'sensor.airgradient_pm0_3', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -176,23 +176,24 @@ }), 'original_device_class': None, 'original_icon': None, - 'original_name': 'PM0.3 count', + 'original_name': 'PM0.3', 'platform': 'airgradient', 'previous_unique_id': None, 'supported_features': 0, 'translation_key': 'pm003_count', 'unique_id': '84fce612f5b8-pm003', - 'unit_of_measurement': None, + 'unit_of_measurement': 'particles/dL', }) # --- -# name: test_all_entities[sensor.airgradient_pm0_3_count-state] +# name: test_all_entities[sensor.airgradient_pm0_3-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Airgradient PM0.3 count', + 'friendly_name': 'Airgradient PM0.3', 'state_class': , + 'unit_of_measurement': 'particles/dL', }), 'context': , - 'entity_id': 'sensor.airgradient_pm0_3_count', + 'entity_id': 'sensor.airgradient_pm0_3', 'last_changed': , 'last_reported': , 'last_updated': , From 56db7fc7dce30ebbdb9ff13e0cef1441a711360d Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Thu, 6 Jun 2024 20:41:25 -0500 Subject: [PATCH 12/23] Fix exposure checks on some intents (#118988) * Check exposure in climate intent * Check exposure in todo list * Check exposure for weather * Check exposure in humidity intents * Add extra checks to weather tests * Add more checks to todo intent test * Move climate intents to async_match_targets * Update test_intent.py * Update test_intent.py * Remove patch --- homeassistant/components/climate/intent.py | 90 ++------ homeassistant/components/humidifier/intent.py | 45 ++-- homeassistant/components/todo/intent.py | 20 +- homeassistant/components/weather/intent.py | 52 ++--- homeassistant/helpers/intent.py | 2 + tests/components/climate/test_intent.py | 196 +++++++++++++++--- tests/components/humidifier/test_intent.py | 128 +++++++++++- tests/components/todo/test_init.py | 42 +++- tests/components/weather/test_intent.py | 76 ++++--- 9 files changed, 450 insertions(+), 201 deletions(-) diff --git a/homeassistant/components/climate/intent.py b/homeassistant/components/climate/intent.py index 48b5c134bbdf0..53d0891fcda97 100644 --- a/homeassistant/components/climate/intent.py +++ b/homeassistant/components/climate/intent.py @@ -4,11 +4,10 @@ import voluptuous as vol -from homeassistant.core import HomeAssistant, State +from homeassistant.core import HomeAssistant from homeassistant.helpers import intent -from homeassistant.helpers.entity_component import EntityComponent -from . import DOMAIN, ClimateEntity +from . import DOMAIN INTENT_GET_TEMPERATURE = "HassClimateGetTemperature" @@ -23,7 +22,10 @@ class GetTemperatureIntent(intent.IntentHandler): intent_type = INTENT_GET_TEMPERATURE description = "Gets the current temperature of a climate device or entity" - slot_schema = {vol.Optional("area"): str, vol.Optional("name"): str} + slot_schema = { + vol.Optional("area"): intent.non_empty_string, + vol.Optional("name"): intent.non_empty_string, + } platforms = {DOMAIN} async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: @@ -31,74 +33,24 @@ async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) - component: EntityComponent[ClimateEntity] = hass.data[DOMAIN] - entities: list[ClimateEntity] = list(component.entities) - climate_entity: ClimateEntity | None = None - climate_state: State | None = None + name: str | None = None + if "name" in slots: + name = slots["name"]["value"] - if not entities: - raise intent.IntentHandleError("No climate entities") + area: str | None = None + if "area" in slots: + area = slots["area"]["value"] - name_slot = slots.get("name", {}) - entity_name: str | None = name_slot.get("value") - entity_text: str | None = name_slot.get("text") - - area_slot = slots.get("area", {}) - area_id = area_slot.get("value") - - if area_id: - # Filter by area and optionally name - area_name = area_slot.get("text") - - for maybe_climate in intent.async_match_states( - hass, name=entity_name, area_name=area_id, domains=[DOMAIN] - ): - climate_state = maybe_climate - break - - if climate_state is None: - raise intent.NoStatesMatchedError( - reason=intent.MatchFailedReason.AREA, - name=entity_text or entity_name, - area=area_name or area_id, - floor=None, - domains={DOMAIN}, - device_classes=None, - ) - - climate_entity = component.get_entity(climate_state.entity_id) - elif entity_name: - # Filter by name - for maybe_climate in intent.async_match_states( - hass, name=entity_name, domains=[DOMAIN] - ): - climate_state = maybe_climate - break - - if climate_state is None: - raise intent.NoStatesMatchedError( - reason=intent.MatchFailedReason.NAME, - name=entity_name, - area=None, - floor=None, - domains={DOMAIN}, - device_classes=None, - ) - - climate_entity = component.get_entity(climate_state.entity_id) - else: - # First entity - climate_entity = entities[0] - climate_state = hass.states.get(climate_entity.entity_id) - - assert climate_entity is not None - - if climate_state is None: - raise intent.IntentHandleError(f"No state for {climate_entity.name}") - - assert climate_state is not None + match_constraints = intent.MatchTargetsConstraints( + name=name, area_name=area, domains=[DOMAIN], assistant=intent_obj.assistant + ) + match_result = intent.async_match_targets(hass, match_constraints) + if not match_result.is_match: + raise intent.MatchFailedError( + result=match_result, constraints=match_constraints + ) response = intent_obj.create_response() response.response_type = intent.IntentResponseType.QUERY_ANSWER - response.async_set_states(matched_states=[climate_state]) + response.async_set_states(matched_states=match_result.states) return response diff --git a/homeassistant/components/humidifier/intent.py b/homeassistant/components/humidifier/intent.py index c713f08b857f8..425fdbcc679d4 100644 --- a/homeassistant/components/humidifier/intent.py +++ b/homeassistant/components/humidifier/intent.py @@ -35,7 +35,7 @@ class HumidityHandler(intent.IntentHandler): intent_type = INTENT_HUMIDITY description = "Set desired humidity level" slot_schema = { - vol.Required("name"): cv.string, + vol.Required("name"): intent.non_empty_string, vol.Required("humidity"): vol.All(vol.Coerce(int), vol.Range(0, 100)), } platforms = {DOMAIN} @@ -44,18 +44,19 @@ async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse """Handle the hass intent.""" hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) - states = list( - intent.async_match_states( - hass, - name=slots["name"]["value"], - states=hass.states.async_all(DOMAIN), - ) - ) - if not states: - raise intent.IntentHandleError("No entities matched") + match_constraints = intent.MatchTargetsConstraints( + name=slots["name"]["value"], + domains=[DOMAIN], + assistant=intent_obj.assistant, + ) + match_result = intent.async_match_targets(hass, match_constraints) + if not match_result.is_match: + raise intent.MatchFailedError( + result=match_result, constraints=match_constraints + ) - state = states[0] + state = match_result.states[0] service_data = {ATTR_ENTITY_ID: state.entity_id} humidity = slots["humidity"]["value"] @@ -89,7 +90,7 @@ class SetModeHandler(intent.IntentHandler): intent_type = INTENT_MODE description = "Set humidifier mode" slot_schema = { - vol.Required("name"): cv.string, + vol.Required("name"): intent.non_empty_string, vol.Required("mode"): cv.string, } platforms = {DOMAIN} @@ -98,18 +99,18 @@ async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse """Handle the hass intent.""" hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) - states = list( - intent.async_match_states( - hass, - name=slots["name"]["value"], - states=hass.states.async_all(DOMAIN), - ) + match_constraints = intent.MatchTargetsConstraints( + name=slots["name"]["value"], + domains=[DOMAIN], + assistant=intent_obj.assistant, ) + match_result = intent.async_match_targets(hass, match_constraints) + if not match_result.is_match: + raise intent.MatchFailedError( + result=match_result, constraints=match_constraints + ) - if not states: - raise intent.IntentHandleError("No entities matched") - - state = states[0] + state = match_result.states[0] service_data = {ATTR_ENTITY_ID: state.entity_id} intent.async_test_feature(state, HumidifierEntityFeature.MODES, "modes") diff --git a/homeassistant/components/todo/intent.py b/homeassistant/components/todo/intent.py index c3c18ea304f07..50afe916b271d 100644 --- a/homeassistant/components/todo/intent.py +++ b/homeassistant/components/todo/intent.py @@ -4,7 +4,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import intent -import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from . import DOMAIN, TodoItem, TodoItemStatus, TodoListEntity @@ -22,7 +21,7 @@ class ListAddItemIntent(intent.IntentHandler): intent_type = INTENT_LIST_ADD_ITEM description = "Add item to a todo list" - slot_schema = {"item": cv.string, "name": cv.string} + slot_schema = {"item": intent.non_empty_string, "name": intent.non_empty_string} platforms = {DOMAIN} async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: @@ -37,18 +36,19 @@ async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse target_list: TodoListEntity | None = None # Find matching list - for list_state in intent.async_match_states( - hass, name=list_name, domains=[DOMAIN] - ): - target_list = component.get_entity(list_state.entity_id) - if target_list is not None: - break + match_constraints = intent.MatchTargetsConstraints( + name=list_name, domains=[DOMAIN], assistant=intent_obj.assistant + ) + match_result = intent.async_match_targets(hass, match_constraints) + if not match_result.is_match: + raise intent.MatchFailedError( + result=match_result, constraints=match_constraints + ) + target_list = component.get_entity(match_result.states[0].entity_id) if target_list is None: raise intent.IntentHandleError(f"No to-do list: {list_name}") - assert target_list is not None - # Add to list await target_list.async_create_todo_item( TodoItem(summary=item, status=TodoItemStatus.NEEDS_ACTION) diff --git a/homeassistant/components/weather/intent.py b/homeassistant/components/weather/intent.py index cbb46b943e861..e00a386b619ab 100644 --- a/homeassistant/components/weather/intent.py +++ b/homeassistant/components/weather/intent.py @@ -6,10 +6,8 @@ from homeassistant.core import HomeAssistant, State from homeassistant.helpers import intent -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity_component import EntityComponent -from . import DOMAIN, WeatherEntity +from . import DOMAIN INTENT_GET_WEATHER = "HassGetWeather" @@ -24,7 +22,7 @@ class GetWeatherIntent(intent.IntentHandler): intent_type = INTENT_GET_WEATHER description = "Gets the current weather" - slot_schema = {vol.Optional("name"): cv.string} + slot_schema = {vol.Optional("name"): intent.non_empty_string} platforms = {DOMAIN} async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: @@ -32,43 +30,21 @@ async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse hass = intent_obj.hass slots = self.async_validate_slots(intent_obj.slots) - weather: WeatherEntity | None = None weather_state: State | None = None - component: EntityComponent[WeatherEntity] = hass.data[DOMAIN] - entities = list(component.entities) - + name: str | None = None if "name" in slots: - # Named weather entity - weather_name = slots["name"]["value"] + name = slots["name"]["value"] - # Find matching weather entity - matching_states = intent.async_match_states( - hass, name=weather_name, domains=[DOMAIN] + match_constraints = intent.MatchTargetsConstraints( + name=name, domains=[DOMAIN], assistant=intent_obj.assistant + ) + match_result = intent.async_match_targets(hass, match_constraints) + if not match_result.is_match: + raise intent.MatchFailedError( + result=match_result, constraints=match_constraints ) - for maybe_weather_state in matching_states: - weather = component.get_entity(maybe_weather_state.entity_id) - if weather is not None: - weather_state = maybe_weather_state - break - - if weather is None: - raise intent.IntentHandleError( - f"No weather entity named {weather_name}" - ) - elif entities: - # First weather entity - weather = entities[0] - weather_name = weather.name - weather_state = hass.states.get(weather.entity_id) - - if weather is None: - raise intent.IntentHandleError("No weather entity") - - if weather_state is None: - raise intent.IntentHandleError(f"No state for weather: {weather.name}") - assert weather is not None - assert weather_state is not None + weather_state = match_result.states[0] # Create response response = intent_obj.create_response() @@ -77,8 +53,8 @@ async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse success_results=[ intent.IntentResponseTarget( type=intent.IntentResponseTargetType.ENTITY, - name=weather_name, - id=weather.entity_id, + name=weather_state.name, + id=weather_state.entity_id, ) ] ) diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index ccef934d6ade8..d7c0f90e2f94b 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -712,6 +712,7 @@ def async_match_states( domains: Collection[str] | None = None, device_classes: Collection[str] | None = None, states: list[State] | None = None, + assistant: str | None = None, ) -> Iterable[State]: """Simplified interface to async_match_targets that returns states matching the constraints.""" result = async_match_targets( @@ -722,6 +723,7 @@ def async_match_states( floor_name=floor_name, domains=domains, device_classes=device_classes, + assistant=assistant, ), states=states, ) diff --git a/tests/components/climate/test_intent.py b/tests/components/climate/test_intent.py index 1aaea386320be..c9bc27fce5361 100644 --- a/tests/components/climate/test_intent.py +++ b/tests/components/climate/test_intent.py @@ -1,21 +1,23 @@ """Test climate intents.""" from collections.abc import Generator -from unittest.mock import patch import pytest +from homeassistant.components import conversation from homeassistant.components.climate import ( DOMAIN, ClimateEntity, HVACMode, intent as climate_intent, ) +from homeassistant.components.homeassistant.exposed_entities import async_expose_entity from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import Platform, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.helpers import area_registry as ar, entity_registry as er, intent from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, @@ -113,6 +115,7 @@ async def test_get_temperature( entity_registry: er.EntityRegistry, ) -> None: """Test HassClimateGetTemperature intent.""" + assert await async_setup_component(hass, "homeassistant", {}) await climate_intent.async_setup_intents(hass) climate_1 = MockClimateEntity() @@ -148,10 +151,14 @@ async def test_get_temperature( # First climate entity will be selected (no area) response = await intent.async_handle( - hass, "test", climate_intent.INTENT_GET_TEMPERATURE, {} + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {}, + assistant=conversation.DOMAIN, ) assert response.response_type == intent.IntentResponseType.QUERY_ANSWER - assert len(response.matched_states) == 1 + assert response.matched_states assert response.matched_states[0].entity_id == climate_1.entity_id state = response.matched_states[0] assert state.attributes["current_temperature"] == 10.0 @@ -162,6 +169,7 @@ async def test_get_temperature( "test", climate_intent.INTENT_GET_TEMPERATURE, {"area": {"value": bedroom_area.name}}, + assistant=conversation.DOMAIN, ) assert response.response_type == intent.IntentResponseType.QUERY_ANSWER assert len(response.matched_states) == 1 @@ -175,6 +183,7 @@ async def test_get_temperature( "test", climate_intent.INTENT_GET_TEMPERATURE, {"name": {"value": "Climate 2"}}, + assistant=conversation.DOMAIN, ) assert response.response_type == intent.IntentResponseType.QUERY_ANSWER assert len(response.matched_states) == 1 @@ -189,6 +198,7 @@ async def test_get_temperature( "test", climate_intent.INTENT_GET_TEMPERATURE, {"area": {"value": office_area.name}}, + assistant=conversation.DOMAIN, ) # Exception should contain details of what we tried to match @@ -197,7 +207,7 @@ async def test_get_temperature( constraints = error.value.constraints assert constraints.name is None assert constraints.area_name == office_area.name - assert constraints.domains == {DOMAIN} + assert constraints.domains and (set(constraints.domains) == {DOMAIN}) assert constraints.device_classes is None # Check wrong name @@ -214,7 +224,7 @@ async def test_get_temperature( constraints = error.value.constraints assert constraints.name == "Does not exist" assert constraints.area_name is None - assert constraints.domains == {DOMAIN} + assert constraints.domains and (set(constraints.domains) == {DOMAIN}) assert constraints.device_classes is None # Check wrong name with area @@ -231,7 +241,7 @@ async def test_get_temperature( constraints = error.value.constraints assert constraints.name == "Climate 1" assert constraints.area_name == bedroom_area.name - assert constraints.domains == {DOMAIN} + assert constraints.domains and (set(constraints.domains) == {DOMAIN}) assert constraints.device_classes is None @@ -239,62 +249,190 @@ async def test_get_temperature_no_entities( hass: HomeAssistant, ) -> None: """Test HassClimateGetTemperature intent with no climate entities.""" + assert await async_setup_component(hass, "homeassistant", {}) await climate_intent.async_setup_intents(hass) await create_mock_platform(hass, []) - with pytest.raises(intent.IntentHandleError): + with pytest.raises(intent.MatchFailedError) as err: await intent.async_handle( - hass, "test", climate_intent.INTENT_GET_TEMPERATURE, {} + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {}, + assistant=conversation.DOMAIN, ) + assert err.value.result.no_match_reason == intent.MatchFailedReason.DOMAIN -async def test_get_temperature_no_state( +async def test_not_exposed( hass: HomeAssistant, area_registry: ar.AreaRegistry, entity_registry: er.EntityRegistry, ) -> None: - """Test HassClimateGetTemperature intent when states are missing.""" + """Test HassClimateGetTemperature intent when entities aren't exposed.""" + assert await async_setup_component(hass, "homeassistant", {}) await climate_intent.async_setup_intents(hass) climate_1 = MockClimateEntity() climate_1._attr_name = "Climate 1" climate_1._attr_unique_id = "1234" + climate_1._attr_current_temperature = 10.0 entity_registry.async_get_or_create( DOMAIN, "test", "1234", suggested_object_id="climate_1" ) - await create_mock_platform(hass, [climate_1]) + climate_2 = MockClimateEntity() + climate_2._attr_name = "Climate 2" + climate_2._attr_unique_id = "5678" + climate_2._attr_current_temperature = 22.0 + entity_registry.async_get_or_create( + DOMAIN, "test", "5678", suggested_object_id="climate_2" + ) + await create_mock_platform(hass, [climate_1, climate_2]) + + # Add climate entities to same area living_room_area = area_registry.async_create(name="Living Room") + bedroom_area = area_registry.async_create(name="Bedroom") entity_registry.async_update_entity( climate_1.entity_id, area_id=living_room_area.id ) + entity_registry.async_update_entity( + climate_2.entity_id, area_id=living_room_area.id + ) - with ( - patch("homeassistant.core.StateMachine.get", return_value=None), - pytest.raises(intent.IntentHandleError), - ): + # Should fail with empty name + with pytest.raises(intent.InvalidSlotInfo): await intent.async_handle( - hass, "test", climate_intent.INTENT_GET_TEMPERATURE, {} + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {"name": {"value": ""}}, + assistant=conversation.DOMAIN, ) - with ( - patch("homeassistant.core.StateMachine.async_all", return_value=[]), - pytest.raises(intent.MatchFailedError) as error, - ): + # Should fail with empty area + with pytest.raises(intent.InvalidSlotInfo): await intent.async_handle( hass, "test", climate_intent.INTENT_GET_TEMPERATURE, - {"area": {"value": "Living Room"}}, + {"area": {"value": ""}}, + assistant=conversation.DOMAIN, ) - # Exception should contain details of what we tried to match - assert isinstance(error.value, intent.MatchFailedError) - assert error.value.result.no_match_reason == intent.MatchFailedReason.AREA - constraints = error.value.constraints - assert constraints.name is None - assert constraints.area_name == "Living Room" - assert constraints.domains == {DOMAIN} - assert constraints.device_classes is None + # Expose second, hide first + async_expose_entity(hass, conversation.DOMAIN, climate_1.entity_id, False) + async_expose_entity(hass, conversation.DOMAIN, climate_2.entity_id, True) + + # Second climate entity is exposed + response = await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {}, + assistant=conversation.DOMAIN, + ) + assert response.response_type == intent.IntentResponseType.QUERY_ANSWER + assert len(response.matched_states) == 1 + assert response.matched_states[0].entity_id == climate_2.entity_id + + # Using the area should work + response = await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {"area": {"value": living_room_area.name}}, + assistant=conversation.DOMAIN, + ) + assert response.response_type == intent.IntentResponseType.QUERY_ANSWER + assert len(response.matched_states) == 1 + assert response.matched_states[0].entity_id == climate_2.entity_id + + # Using the name of the exposed entity should work + response = await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {"name": {"value": climate_2.name}}, + assistant=conversation.DOMAIN, + ) + assert response.response_type == intent.IntentResponseType.QUERY_ANSWER + assert len(response.matched_states) == 1 + assert response.matched_states[0].entity_id == climate_2.entity_id + + # Using the name of the *unexposed* entity should fail + with pytest.raises(intent.MatchFailedError) as err: + await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {"name": {"value": climate_1.name}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == intent.MatchFailedReason.NAME + + # Expose first, hide second + async_expose_entity(hass, conversation.DOMAIN, climate_1.entity_id, True) + async_expose_entity(hass, conversation.DOMAIN, climate_2.entity_id, False) + + # Second climate entity is exposed + response = await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {}, + assistant=conversation.DOMAIN, + ) + assert response.response_type == intent.IntentResponseType.QUERY_ANSWER + assert len(response.matched_states) == 1 + assert response.matched_states[0].entity_id == climate_1.entity_id + + # Wrong area name + with pytest.raises(intent.MatchFailedError) as err: + await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {"area": {"value": bedroom_area.name}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == intent.MatchFailedReason.AREA + + # Neither are exposed + async_expose_entity(hass, conversation.DOMAIN, climate_1.entity_id, False) + async_expose_entity(hass, conversation.DOMAIN, climate_2.entity_id, False) + + with pytest.raises(intent.MatchFailedError) as err: + await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == intent.MatchFailedReason.ASSISTANT + + # Should fail with area + with pytest.raises(intent.MatchFailedError) as err: + await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {"area": {"value": living_room_area.name}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == intent.MatchFailedReason.ASSISTANT + + # Should fail with both names + for name in (climate_1.name, climate_2.name): + with pytest.raises(intent.MatchFailedError) as err: + await intent.async_handle( + hass, + "test", + climate_intent.INTENT_GET_TEMPERATURE, + {"name": {"value": name}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == intent.MatchFailedReason.ASSISTANT diff --git a/tests/components/humidifier/test_intent.py b/tests/components/humidifier/test_intent.py index 936369f8aa70d..6318c5f136de8 100644 --- a/tests/components/humidifier/test_intent.py +++ b/tests/components/humidifier/test_intent.py @@ -2,6 +2,8 @@ import pytest +from homeassistant.components import conversation +from homeassistant.components.homeassistant.exposed_entities import async_expose_entity from homeassistant.components.humidifier import ( ATTR_AVAILABLE_MODES, ATTR_HUMIDITY, @@ -19,13 +21,22 @@ STATE_ON, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.intent import IntentHandleError, async_handle +from homeassistant.helpers.intent import ( + IntentHandleError, + IntentResponseType, + InvalidSlotInfo, + MatchFailedError, + MatchFailedReason, + async_handle, +) +from homeassistant.setup import async_setup_component from tests.common import async_mock_service async def test_intent_set_humidity(hass: HomeAssistant) -> None: """Test the set humidity intent.""" + assert await async_setup_component(hass, "homeassistant", {}) hass.states.async_set( "humidifier.bedroom_humidifier", STATE_ON, {ATTR_HUMIDITY: 40} ) @@ -38,6 +49,7 @@ async def test_intent_set_humidity(hass: HomeAssistant) -> None: "test", intent.INTENT_HUMIDITY, {"name": {"value": "Bedroom humidifier"}, "humidity": {"value": "50"}}, + assistant=conversation.DOMAIN, ) await hass.async_block_till_done() @@ -54,6 +66,7 @@ async def test_intent_set_humidity(hass: HomeAssistant) -> None: async def test_intent_set_humidity_and_turn_on(hass: HomeAssistant) -> None: """Test the set humidity intent for turned off humidifier.""" + assert await async_setup_component(hass, "homeassistant", {}) hass.states.async_set( "humidifier.bedroom_humidifier", STATE_OFF, {ATTR_HUMIDITY: 40} ) @@ -66,6 +79,7 @@ async def test_intent_set_humidity_and_turn_on(hass: HomeAssistant) -> None: "test", intent.INTENT_HUMIDITY, {"name": {"value": "Bedroom humidifier"}, "humidity": {"value": "50"}}, + assistant=conversation.DOMAIN, ) await hass.async_block_till_done() @@ -89,6 +103,7 @@ async def test_intent_set_humidity_and_turn_on(hass: HomeAssistant) -> None: async def test_intent_set_mode(hass: HomeAssistant) -> None: """Test the set mode intent.""" + assert await async_setup_component(hass, "homeassistant", {}) hass.states.async_set( "humidifier.bedroom_humidifier", STATE_ON, @@ -108,6 +123,7 @@ async def test_intent_set_mode(hass: HomeAssistant) -> None: "test", intent.INTENT_MODE, {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}}, + assistant=conversation.DOMAIN, ) await hass.async_block_till_done() @@ -127,6 +143,7 @@ async def test_intent_set_mode(hass: HomeAssistant) -> None: async def test_intent_set_mode_and_turn_on(hass: HomeAssistant) -> None: """Test the set mode intent.""" + assert await async_setup_component(hass, "homeassistant", {}) hass.states.async_set( "humidifier.bedroom_humidifier", STATE_OFF, @@ -146,6 +163,7 @@ async def test_intent_set_mode_and_turn_on(hass: HomeAssistant) -> None: "test", intent.INTENT_MODE, {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}}, + assistant=conversation.DOMAIN, ) await hass.async_block_till_done() @@ -169,6 +187,7 @@ async def test_intent_set_mode_and_turn_on(hass: HomeAssistant) -> None: async def test_intent_set_mode_tests_feature(hass: HomeAssistant) -> None: """Test the set mode intent where modes are not supported.""" + assert await async_setup_component(hass, "homeassistant", {}) hass.states.async_set( "humidifier.bedroom_humidifier", STATE_ON, {ATTR_HUMIDITY: 40} ) @@ -181,6 +200,7 @@ async def test_intent_set_mode_tests_feature(hass: HomeAssistant) -> None: "test", intent.INTENT_MODE, {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}}, + assistant=conversation.DOMAIN, ) assert str(excinfo.value) == "Entity bedroom humidifier does not support modes" assert len(mode_calls) == 0 @@ -191,6 +211,7 @@ async def test_intent_set_unknown_mode( hass: HomeAssistant, available_modes: list[str] | None ) -> None: """Test the set mode intent for unsupported mode.""" + assert await async_setup_component(hass, "homeassistant", {}) hass.states.async_set( "humidifier.bedroom_humidifier", STATE_ON, @@ -210,6 +231,111 @@ async def test_intent_set_unknown_mode( "test", intent.INTENT_MODE, {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "eco"}}, + assistant=conversation.DOMAIN, ) assert str(excinfo.value) == "Entity bedroom humidifier does not support eco mode" assert len(mode_calls) == 0 + + +async def test_intent_errors(hass: HomeAssistant) -> None: + """Test the error conditions for set humidity and set mode intents.""" + assert await async_setup_component(hass, "homeassistant", {}) + entity_id = "humidifier.bedroom_humidifier" + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_HUMIDITY: 40, + ATTR_SUPPORTED_FEATURES: 1, + ATTR_AVAILABLE_MODES: ["home", "away"], + ATTR_MODE: None, + }, + ) + async_mock_service(hass, DOMAIN, SERVICE_SET_HUMIDITY) + async_mock_service(hass, DOMAIN, SERVICE_SET_MODE) + await intent.async_setup_intents(hass) + + # Humidifiers are exposed by default + result = await async_handle( + hass, + "test", + intent.INTENT_HUMIDITY, + {"name": {"value": "Bedroom humidifier"}, "humidity": {"value": "50"}}, + assistant=conversation.DOMAIN, + ) + assert result.response_type == IntentResponseType.ACTION_DONE + + result = await async_handle( + hass, + "test", + intent.INTENT_MODE, + {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}}, + assistant=conversation.DOMAIN, + ) + assert result.response_type == IntentResponseType.ACTION_DONE + + # Unexposing it should fail + async_expose_entity(hass, conversation.DOMAIN, entity_id, False) + + with pytest.raises(MatchFailedError) as err: + await async_handle( + hass, + "test", + intent.INTENT_HUMIDITY, + {"name": {"value": "Bedroom humidifier"}, "humidity": {"value": "50"}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == MatchFailedReason.ASSISTANT + + with pytest.raises(MatchFailedError) as err: + await async_handle( + hass, + "test", + intent.INTENT_MODE, + {"name": {"value": "Bedroom humidifier"}, "mode": {"value": "away"}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == MatchFailedReason.ASSISTANT + + # Expose again to test other errors + async_expose_entity(hass, conversation.DOMAIN, entity_id, True) + + # Empty name should fail + with pytest.raises(InvalidSlotInfo): + await async_handle( + hass, + "test", + intent.INTENT_HUMIDITY, + {"name": {"value": ""}, "humidity": {"value": "50"}}, + assistant=conversation.DOMAIN, + ) + + with pytest.raises(InvalidSlotInfo): + await async_handle( + hass, + "test", + intent.INTENT_MODE, + {"name": {"value": ""}, "mode": {"value": "away"}}, + assistant=conversation.DOMAIN, + ) + + # Wrong name should fail + with pytest.raises(MatchFailedError) as err: + await async_handle( + hass, + "test", + intent.INTENT_HUMIDITY, + {"name": {"value": "does not exist"}, "humidity": {"value": "50"}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == MatchFailedReason.NAME + + with pytest.raises(MatchFailedError) as err: + await async_handle( + hass, + "test", + intent.INTENT_MODE, + {"name": {"value": "does not exist"}, "mode": {"value": "away"}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == MatchFailedReason.NAME diff --git a/tests/components/todo/test_init.py b/tests/components/todo/test_init.py index 4b8e35c9061da..72cfaf7e54407 100644 --- a/tests/components/todo/test_init.py +++ b/tests/components/todo/test_init.py @@ -9,6 +9,8 @@ import pytest import voluptuous as vol +from homeassistant.components import conversation +from homeassistant.components.homeassistant.exposed_entities import async_expose_entity from homeassistant.components.todo import ( DOMAIN, TodoItem, @@ -23,6 +25,7 @@ from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import intent from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.setup import async_setup_component from tests.common import ( MockConfigEntry, @@ -1110,6 +1113,7 @@ async def test_add_item_intent( hass_ws_client: WebSocketGenerator, ) -> None: """Test adding items to lists using an intent.""" + assert await async_setup_component(hass, "homeassistant", {}) await todo_intent.async_setup_intents(hass) entity1 = MockTodoListEntity() @@ -1128,6 +1132,7 @@ async def test_add_item_intent( "test", todo_intent.INTENT_LIST_ADD_ITEM, {"item": {"value": "beer"}, "name": {"value": "list 1"}}, + assistant=conversation.DOMAIN, ) assert response.response_type == intent.IntentResponseType.ACTION_DONE @@ -1143,6 +1148,7 @@ async def test_add_item_intent( "test", todo_intent.INTENT_LIST_ADD_ITEM, {"item": {"value": "cheese"}, "name": {"value": "List 2"}}, + assistant=conversation.DOMAIN, ) assert response.response_type == intent.IntentResponseType.ACTION_DONE @@ -1157,6 +1163,7 @@ async def test_add_item_intent( "test", todo_intent.INTENT_LIST_ADD_ITEM, {"item": {"value": "wine"}, "name": {"value": "lIST 2"}}, + assistant=conversation.DOMAIN, ) assert response.response_type == intent.IntentResponseType.ACTION_DONE @@ -1165,13 +1172,46 @@ async def test_add_item_intent( assert entity2.items[1].summary == "wine" assert entity2.items[1].status == TodoItemStatus.NEEDS_ACTION + # Should fail if lists are not exposed + async_expose_entity(hass, conversation.DOMAIN, entity1.entity_id, False) + async_expose_entity(hass, conversation.DOMAIN, entity2.entity_id, False) + with pytest.raises(intent.MatchFailedError) as err: + await intent.async_handle( + hass, + "test", + todo_intent.INTENT_LIST_ADD_ITEM, + {"item": {"value": "cookies"}, "name": {"value": "list 1"}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == intent.MatchFailedReason.ASSISTANT + # Missing list - with pytest.raises(intent.IntentHandleError): + with pytest.raises(intent.MatchFailedError): await intent.async_handle( hass, "test", todo_intent.INTENT_LIST_ADD_ITEM, {"item": {"value": "wine"}, "name": {"value": "This list does not exist"}}, + assistant=conversation.DOMAIN, + ) + + # Fail with empty name/item + with pytest.raises(intent.InvalidSlotInfo): + await intent.async_handle( + hass, + "test", + todo_intent.INTENT_LIST_ADD_ITEM, + {"item": {"value": "wine"}, "name": {"value": ""}}, + assistant=conversation.DOMAIN, + ) + + with pytest.raises(intent.InvalidSlotInfo): + await intent.async_handle( + hass, + "test", + todo_intent.INTENT_LIST_ADD_ITEM, + {"item": {"value": ""}, "name": {"value": "list 1"}}, + assistant=conversation.DOMAIN, ) diff --git a/tests/components/weather/test_intent.py b/tests/components/weather/test_intent.py index 1fde5882d6e29..0f9884791a584 100644 --- a/tests/components/weather/test_intent.py +++ b/tests/components/weather/test_intent.py @@ -1,9 +1,9 @@ """Test weather intents.""" -from unittest.mock import patch - import pytest +from homeassistant.components import conversation +from homeassistant.components.homeassistant.exposed_entities import async_expose_entity from homeassistant.components.weather import ( DOMAIN, WeatherEntity, @@ -16,15 +16,18 @@ async def test_get_weather(hass: HomeAssistant) -> None: """Test get weather for first entity and by name.""" + assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "weather", {"weather": {}}) entity1 = WeatherEntity() entity1._attr_name = "Weather 1" entity1.entity_id = "weather.test_1" + async_expose_entity(hass, conversation.DOMAIN, entity1.entity_id, True) entity2 = WeatherEntity() entity2._attr_name = "Weather 2" entity2.entity_id = "weather.test_2" + async_expose_entity(hass, conversation.DOMAIN, entity2.entity_id, True) await hass.data[DOMAIN].async_add_entities([entity1, entity2]) @@ -45,15 +48,31 @@ async def test_get_weather(hass: HomeAssistant) -> None: "test", weather_intent.INTENT_GET_WEATHER, {"name": {"value": "Weather 2"}}, + assistant=conversation.DOMAIN, ) assert response.response_type == intent.IntentResponseType.QUERY_ANSWER assert len(response.matched_states) == 1 state = response.matched_states[0] assert state.entity_id == entity2.entity_id + # Should fail if not exposed + async_expose_entity(hass, conversation.DOMAIN, entity1.entity_id, False) + async_expose_entity(hass, conversation.DOMAIN, entity2.entity_id, False) + for name in (entity1.name, entity2.name): + with pytest.raises(intent.MatchFailedError) as err: + await intent.async_handle( + hass, + "test", + weather_intent.INTENT_GET_WEATHER, + {"name": {"value": name}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == intent.MatchFailedReason.ASSISTANT + async def test_get_weather_wrong_name(hass: HomeAssistant) -> None: """Test get weather with the wrong name.""" + assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "weather", {"weather": {}}) entity1 = WeatherEntity() @@ -63,48 +82,43 @@ async def test_get_weather_wrong_name(hass: HomeAssistant) -> None: await hass.data[DOMAIN].async_add_entities([entity1]) await weather_intent.async_setup_intents(hass) + async_expose_entity(hass, conversation.DOMAIN, entity1.entity_id, True) # Incorrect name - with pytest.raises(intent.IntentHandleError): + with pytest.raises(intent.MatchFailedError) as err: await intent.async_handle( hass, "test", weather_intent.INTENT_GET_WEATHER, {"name": {"value": "not the right name"}}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == intent.MatchFailedReason.NAME + + # Empty name + with pytest.raises(intent.InvalidSlotInfo): + await intent.async_handle( + hass, + "test", + weather_intent.INTENT_GET_WEATHER, + {"name": {"value": ""}}, + assistant=conversation.DOMAIN, ) async def test_get_weather_no_entities(hass: HomeAssistant) -> None: """Test get weather with no weather entities.""" + assert await async_setup_component(hass, "homeassistant", {}) assert await async_setup_component(hass, "weather", {"weather": {}}) await weather_intent.async_setup_intents(hass) # No weather entities - with pytest.raises(intent.IntentHandleError): - await intent.async_handle(hass, "test", weather_intent.INTENT_GET_WEATHER, {}) - - -async def test_get_weather_no_state(hass: HomeAssistant) -> None: - """Test get weather when state is not returned.""" - assert await async_setup_component(hass, "weather", {"weather": {}}) - - entity1 = WeatherEntity() - entity1._attr_name = "Weather 1" - entity1.entity_id = "weather.test_1" - - await hass.data[DOMAIN].async_add_entities([entity1]) - - await weather_intent.async_setup_intents(hass) - - # Success with state - response = await intent.async_handle( - hass, "test", weather_intent.INTENT_GET_WEATHER, {} - ) - assert response.response_type == intent.IntentResponseType.QUERY_ANSWER - - # Failure without state - with ( - patch("homeassistant.core.StateMachine.get", return_value=None), - pytest.raises(intent.IntentHandleError), - ): - await intent.async_handle(hass, "test", weather_intent.INTENT_GET_WEATHER, {}) + with pytest.raises(intent.MatchFailedError) as err: + await intent.async_handle( + hass, + "test", + weather_intent.INTENT_GET_WEATHER, + {}, + assistant=conversation.DOMAIN, + ) + assert err.value.result.no_match_reason == intent.MatchFailedReason.DOMAIN From cfa619b67e5be90d91e14a8abbed4685054d245c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Jun 2024 02:21:53 -0500 Subject: [PATCH 13/23] Remove isal from after_dependencies in http (#119000) --- homeassistant/bootstrap.py | 13 ++++++++++--- homeassistant/components/http/manifest.json | 1 - tests/test_circular_imports.py | 4 ++-- tests/test_requirements.py | 9 ++++----- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 391c6ebfa453f..74196cdc62569 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -134,8 +134,15 @@ DEBUGGER_INTEGRATIONS = {"debugpy"} + +# Core integrations are unconditionally loaded CORE_INTEGRATIONS = {"homeassistant", "persistent_notification"} -LOGGING_INTEGRATIONS = { + +# Integrations that are loaded right after the core is set up +LOGGING_AND_HTTP_DEPS_INTEGRATIONS = { + # isal is loaded right away before `http` to ensure if its + # enabled, that `isal` is up to date. + "isal", # Set log levels "logger", # Error logging @@ -214,8 +221,8 @@ } SETUP_ORDER = ( - # Load logging as soon as possible - ("logging", LOGGING_INTEGRATIONS), + # Load logging and http deps as soon as possible + ("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS), # Setup frontend and recorder ("frontend, recorder", {*FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS}), # Start up debuggers. Start these first in case they want to wait. diff --git a/homeassistant/components/http/manifest.json b/homeassistant/components/http/manifest.json index b48a188cf47bf..fb804251edc03 100644 --- a/homeassistant/components/http/manifest.json +++ b/homeassistant/components/http/manifest.json @@ -1,7 +1,6 @@ { "domain": "http", "name": "HTTP", - "after_dependencies": ["isal"], "codeowners": ["@home-assistant/core"], "documentation": "https://www.home-assistant.io/integrations/http", "integration_type": "system", diff --git a/tests/test_circular_imports.py b/tests/test_circular_imports.py index 79f0fd9caf71d..dfdee65b2b0a0 100644 --- a/tests/test_circular_imports.py +++ b/tests/test_circular_imports.py @@ -10,7 +10,7 @@ DEBUGGER_INTEGRATIONS, DEFAULT_INTEGRATIONS, FRONTEND_INTEGRATIONS, - LOGGING_INTEGRATIONS, + LOGGING_AND_HTTP_DEPS_INTEGRATIONS, RECORDER_INTEGRATIONS, STAGE_1_INTEGRATIONS, ) @@ -23,7 +23,7 @@ { *DEBUGGER_INTEGRATIONS, *CORE_INTEGRATIONS, - *LOGGING_INTEGRATIONS, + *LOGGING_AND_HTTP_DEPS_INTEGRATIONS, *FRONTEND_INTEGRATIONS, *RECORDER_INTEGRATIONS, *STAGE_1_INTEGRATIONS, diff --git a/tests/test_requirements.py b/tests/test_requirements.py index 2b2415e22a829..73f3f54c3c486 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -591,7 +591,7 @@ async def test_discovery_requirements_mqtt(hass: HomeAssistant) -> None: ) as mock_process: await async_get_integration_with_requirements(hass, "mqtt_comp") - assert len(mock_process.mock_calls) == 2 + assert len(mock_process.mock_calls) == 1 assert mock_process.mock_calls[0][1][1] == mqtt.requirements @@ -608,13 +608,12 @@ async def test_discovery_requirements_ssdp(hass: HomeAssistant) -> None: ) as mock_process: await async_get_integration_with_requirements(hass, "ssdp_comp") - assert len(mock_process.mock_calls) == 4 + assert len(mock_process.mock_calls) == 3 assert mock_process.mock_calls[0][1][1] == ssdp.requirements assert { mock_process.mock_calls[1][1][0], mock_process.mock_calls[2][1][0], - mock_process.mock_calls[3][1][0], - } == {"network", "recorder", "isal"} + } == {"network", "recorder"} @pytest.mark.parametrize( @@ -638,7 +637,7 @@ async def test_discovery_requirements_zeroconf( ) as mock_process: await async_get_integration_with_requirements(hass, "comp") - assert len(mock_process.mock_calls) == 4 + assert len(mock_process.mock_calls) == 3 assert mock_process.mock_calls[0][1][1] == zeroconf.requirements From 5bb4e4f5d9d31301863749d2b5dd4724a0b61886 Mon Sep 17 00:00:00 2001 From: Shay Levy Date: Fri, 7 Jun 2024 10:50:05 +0300 Subject: [PATCH 14/23] Hold connection lock in Shelly RPC reconnect (#119009) --- homeassistant/components/shelly/coordinator.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/shelly/coordinator.py b/homeassistant/components/shelly/coordinator.py index cf6e9cc897f5b..c12e1aea289aa 100644 --- a/homeassistant/components/shelly/coordinator.py +++ b/homeassistant/components/shelly/coordinator.py @@ -584,11 +584,13 @@ async def _async_update_data(self) -> None: raise UpdateFailed( f"Sleeping device did not update within {self.sleep_period} seconds interval" ) - if self.device.connected: - return - if not await self._async_device_connect_task(): - raise UpdateFailed("Device reconnect error") + async with self._connection_lock: + if self.device.connected: # Already connected + return + + if not await self._async_device_connect_task(): + raise UpdateFailed("Device reconnect error") async def _async_disconnected(self, reconnect: bool) -> None: """Handle device disconnected.""" From 581fb2f9f41e48339b5b067404853259e13a86d1 Mon Sep 17 00:00:00 2001 From: Mike Degatano Date: Fri, 7 Jun 2024 04:52:15 -0400 Subject: [PATCH 15/23] Always have addon url in detached_addon_missing (#119011) --- homeassistant/components/hassio/issues.py | 7 +++---- tests/components/hassio/test_issues.py | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/hassio/issues.py b/homeassistant/components/hassio/issues.py index 2de6f71d838d3..9c2152489d633 100644 --- a/homeassistant/components/hassio/issues.py +++ b/homeassistant/components/hassio/issues.py @@ -267,15 +267,14 @@ def add_issue(self, issue: Issue) -> None: placeholders = {PLACEHOLDER_KEY_REFERENCE: issue.reference} if issue.key == ISSUE_KEY_ADDON_DETACHED_ADDON_MISSING: + placeholders[PLACEHOLDER_KEY_ADDON_URL] = ( + f"/hassio/addon/{issue.reference}" + ) addons = get_addons_info(self._hass) if addons and issue.reference in addons: placeholders[PLACEHOLDER_KEY_ADDON] = addons[issue.reference][ "name" ] - if "url" in addons[issue.reference]: - placeholders[PLACEHOLDER_KEY_ADDON_URL] = addons[ - issue.reference - ]["url"] else: placeholders[PLACEHOLDER_KEY_ADDON] = issue.reference diff --git a/tests/components/hassio/test_issues.py b/tests/components/hassio/test_issues.py index c6db7d56261a5..ff0e4a8dd9211 100644 --- a/tests/components/hassio/test_issues.py +++ b/tests/components/hassio/test_issues.py @@ -878,6 +878,6 @@ async def test_supervisor_issues_detached_addon_missing( placeholders={ "reference": "test", "addon": "test", - "addon_url": "https://github.com/home-assistant/addons/test", + "addon_url": "/hassio/addon/test", }, ) From de3a0841d8cd8262f9c74d82320553a58f952243 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 30 May 2024 21:42:11 +0200 Subject: [PATCH 16/23] Increase test coverage for KNX Climate (#117903) * Increase test coverage fro KNX Climate * fix test type annotation --- tests/components/knx/test_climate.py | 80 ++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 21 deletions(-) diff --git a/tests/components/knx/test_climate.py b/tests/components/knx/test_climate.py index c81a6fccf1549..3b286a0cdb991 100644 --- a/tests/components/knx/test_climate.py +++ b/tests/components/knx/test_climate.py @@ -54,11 +54,12 @@ async def test_climate_basic_temperature_set( assert len(events) == 1 -@pytest.mark.parametrize("heat_cool", [False, True]) +@pytest.mark.parametrize("heat_cool_ga", [None, "4/4/4"]) async def test_climate_on_off( - hass: HomeAssistant, knx: KNXTestKit, heat_cool: bool + hass: HomeAssistant, knx: KNXTestKit, heat_cool_ga: str | None ) -> None: """Test KNX climate on/off.""" + on_off_ga = "3/3/3" await knx.setup_integration( { ClimateSchema.PLATFORM: { @@ -66,15 +67,15 @@ async def test_climate_on_off( ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3", ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4", ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5", - ClimateSchema.CONF_ON_OFF_ADDRESS: "1/2/8", + ClimateSchema.CONF_ON_OFF_ADDRESS: on_off_ga, ClimateSchema.CONF_ON_OFF_STATE_ADDRESS: "1/2/9", } | ( { - ClimateSchema.CONF_HEAT_COOL_ADDRESS: "1/2/10", + ClimateSchema.CONF_HEAT_COOL_ADDRESS: heat_cool_ga, ClimateSchema.CONF_HEAT_COOL_STATE_ADDRESS: "1/2/11", } - if heat_cool + if heat_cool_ga else {} ) } @@ -82,7 +83,7 @@ async def test_climate_on_off( await hass.async_block_till_done() # read heat/cool state - if heat_cool: + if heat_cool_ga: await knx.assert_read("1/2/11") await knx.receive_response("1/2/11", 0) # cool # read temperature state @@ -102,7 +103,7 @@ async def test_climate_on_off( {"entity_id": "climate.test"}, blocking=True, ) - await knx.assert_write("1/2/8", 0) + await knx.assert_write(on_off_ga, 0) assert hass.states.get("climate.test").state == "off" # turn on @@ -112,8 +113,8 @@ async def test_climate_on_off( {"entity_id": "climate.test"}, blocking=True, ) - await knx.assert_write("1/2/8", 1) - if heat_cool: + await knx.assert_write(on_off_ga, 1) + if heat_cool_ga: # does not fall back to default hvac mode after turn_on assert hass.states.get("climate.test").state == "cool" else: @@ -126,7 +127,7 @@ async def test_climate_on_off( {"entity_id": "climate.test", "hvac_mode": HVACMode.OFF}, blocking=True, ) - await knx.assert_write("1/2/8", 0) + await knx.assert_write(on_off_ga, 0) # set hvac mode to heat await hass.services.async_call( @@ -135,15 +136,19 @@ async def test_climate_on_off( {"entity_id": "climate.test", "hvac_mode": HVACMode.HEAT}, blocking=True, ) - if heat_cool: + if heat_cool_ga: # only set new hvac_mode without changing on/off - actuator shall handle that - await knx.assert_write("1/2/10", 1) + await knx.assert_write(heat_cool_ga, 1) else: - await knx.assert_write("1/2/8", 1) + await knx.assert_write(on_off_ga, 1) -async def test_climate_hvac_mode(hass: HomeAssistant, knx: KNXTestKit) -> None: +@pytest.mark.parametrize("on_off_ga", [None, "4/4/4"]) +async def test_climate_hvac_mode( + hass: HomeAssistant, knx: KNXTestKit, on_off_ga: str | None +) -> None: """Test KNX climate hvac mode.""" + controller_mode_ga = "3/3/3" await knx.setup_integration( { ClimateSchema.PLATFORM: { @@ -151,11 +156,17 @@ async def test_climate_hvac_mode(hass: HomeAssistant, knx: KNXTestKit) -> None: ClimateSchema.CONF_TEMPERATURE_ADDRESS: "1/2/3", ClimateSchema.CONF_TARGET_TEMPERATURE_ADDRESS: "1/2/4", ClimateSchema.CONF_TARGET_TEMPERATURE_STATE_ADDRESS: "1/2/5", - ClimateSchema.CONF_CONTROLLER_MODE_ADDRESS: "1/2/6", + ClimateSchema.CONF_CONTROLLER_MODE_ADDRESS: controller_mode_ga, ClimateSchema.CONF_CONTROLLER_MODE_STATE_ADDRESS: "1/2/7", - ClimateSchema.CONF_ON_OFF_ADDRESS: "1/2/8", ClimateSchema.CONF_OPERATION_MODES: ["Auto"], } + | ( + { + ClimateSchema.CONF_ON_OFF_ADDRESS: on_off_ga, + } + if on_off_ga + else {} + ) } ) @@ -171,23 +182,50 @@ async def test_climate_hvac_mode(hass: HomeAssistant, knx: KNXTestKit) -> None: await knx.assert_read("1/2/5") await knx.receive_response("1/2/5", RAW_FLOAT_22_0) - # turn hvac mode to off + # turn hvac mode to off - set_hvac_mode() doesn't send to on_off if dedicated hvac mode is available await hass.services.async_call( "climate", "set_hvac_mode", {"entity_id": "climate.test", "hvac_mode": HVACMode.OFF}, blocking=True, ) - await knx.assert_write("1/2/6", (0x06,)) + await knx.assert_write(controller_mode_ga, (0x06,)) - # turn hvac on + # set hvac to non default mode await hass.services.async_call( "climate", "set_hvac_mode", - {"entity_id": "climate.test", "hvac_mode": HVACMode.HEAT}, + {"entity_id": "climate.test", "hvac_mode": HVACMode.COOL}, + blocking=True, + ) + await knx.assert_write(controller_mode_ga, (0x03,)) + + # turn off + await hass.services.async_call( + "climate", + "turn_off", + {"entity_id": "climate.test"}, blocking=True, ) - await knx.assert_write("1/2/6", (0x01,)) + if on_off_ga: + await knx.assert_write(on_off_ga, 0) + else: + await knx.assert_write(controller_mode_ga, (0x06,)) + assert hass.states.get("climate.test").state == "off" + + # turn on + await hass.services.async_call( + "climate", + "turn_on", + {"entity_id": "climate.test"}, + blocking=True, + ) + if on_off_ga: + await knx.assert_write(on_off_ga, 1) + else: + # restore last hvac mode + await knx.assert_write(controller_mode_ga, (0x03,)) + assert hass.states.get("climate.test").state == "cool" async def test_climate_preset_mode( From 31b44b7846ffcd330317980d73a218a1162606c2 Mon Sep 17 00:00:00 2001 From: Matthias Alphart Date: Thu, 6 Jun 2024 22:40:04 +0200 Subject: [PATCH 17/23] Fix KNX `climate.set_hvac_mode` not turning `on` (#119012) --- homeassistant/components/knx/climate.py | 5 +---- tests/components/knx/test_climate.py | 10 +++++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 674e76d66e3d3..e1179641cdc5d 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -283,16 +283,13 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: ) if knx_controller_mode in self._device.mode.controller_modes: await self._device.mode.set_controller_mode(knx_controller_mode) - self.async_write_ha_state() - return if self._device.supports_on_off: if hvac_mode == HVACMode.OFF: await self._device.turn_off() elif not self._device.is_on: - # for default hvac mode, otherwise above would have triggered await self._device.turn_on() - self.async_write_ha_state() + self.async_write_ha_state() @property def preset_mode(self) -> str | None: diff --git a/tests/components/knx/test_climate.py b/tests/components/knx/test_climate.py index 3b286a0cdb991..9c431386b4345 100644 --- a/tests/components/knx/test_climate.py +++ b/tests/components/knx/test_climate.py @@ -128,6 +128,7 @@ async def test_climate_on_off( blocking=True, ) await knx.assert_write(on_off_ga, 0) + assert hass.states.get("climate.test").state == "off" # set hvac mode to heat await hass.services.async_call( @@ -137,10 +138,11 @@ async def test_climate_on_off( blocking=True, ) if heat_cool_ga: - # only set new hvac_mode without changing on/off - actuator shall handle that await knx.assert_write(heat_cool_ga, 1) + await knx.assert_write(on_off_ga, 1) else: await knx.assert_write(on_off_ga, 1) + assert hass.states.get("climate.test").state == "heat" @pytest.mark.parametrize("on_off_ga", [None, "4/4/4"]) @@ -190,6 +192,9 @@ async def test_climate_hvac_mode( blocking=True, ) await knx.assert_write(controller_mode_ga, (0x06,)) + if on_off_ga: + await knx.assert_write(on_off_ga, 0) + assert hass.states.get("climate.test").state == "off" # set hvac to non default mode await hass.services.async_call( @@ -199,6 +204,9 @@ async def test_climate_hvac_mode( blocking=True, ) await knx.assert_write(controller_mode_ga, (0x03,)) + if on_off_ga: + await knx.assert_write(on_off_ga, 1) + assert hass.states.get("climate.test").state == "cool" # turn off await hass.services.async_call( From 1cbd3ab9307fed9e75f898bbe2c4f66a8c8990f5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Jun 2024 13:09:48 -0500 Subject: [PATCH 18/23] Fix refactoring error in snmp switch (#119028) --- homeassistant/components/snmp/switch.py | 76 ++++++++++++++----------- homeassistant/components/snmp/util.py | 36 +++++++++--- 2 files changed, 72 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/snmp/switch.py b/homeassistant/components/snmp/switch.py index 40083ed4213f6..02a94aeb8c14c 100644 --- a/homeassistant/components/snmp/switch.py +++ b/homeassistant/components/snmp/switch.py @@ -8,6 +8,8 @@ import pysnmp.hlapi.asyncio as hlapi from pysnmp.hlapi.asyncio import ( CommunityData, + ObjectIdentity, + ObjectType, UdpTransportTarget, UsmUserData, getCmd, @@ -63,7 +65,12 @@ MAP_PRIV_PROTOCOLS, SNMP_VERSIONS, ) -from .util import RequestArgsType, async_create_request_cmd_args +from .util import ( + CommandArgsType, + RequestArgsType, + async_create_command_cmd_args, + async_create_request_cmd_args, +) _LOGGER = logging.getLogger(__name__) @@ -125,23 +132,23 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the SNMP switch.""" - name = config.get(CONF_NAME) - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) + name: str = config[CONF_NAME] + host: str = config[CONF_HOST] + port: int = config[CONF_PORT] community = config.get(CONF_COMMUNITY) baseoid: str = config[CONF_BASEOID] - command_oid = config.get(CONF_COMMAND_OID) - command_payload_on = config.get(CONF_COMMAND_PAYLOAD_ON) - command_payload_off = config.get(CONF_COMMAND_PAYLOAD_OFF) + command_oid: str | None = config.get(CONF_COMMAND_OID) + command_payload_on: str | None = config.get(CONF_COMMAND_PAYLOAD_ON) + command_payload_off: str | None = config.get(CONF_COMMAND_PAYLOAD_OFF) version: str = config[CONF_VERSION] username = config.get(CONF_USERNAME) authkey = config.get(CONF_AUTH_KEY) authproto: str = config[CONF_AUTH_PROTOCOL] privkey = config.get(CONF_PRIV_KEY) privproto: str = config[CONF_PRIV_PROTOCOL] - payload_on = config.get(CONF_PAYLOAD_ON) - payload_off = config.get(CONF_PAYLOAD_OFF) - vartype = config.get(CONF_VARTYPE) + payload_on: str = config[CONF_PAYLOAD_ON] + payload_off: str = config[CONF_PAYLOAD_OFF] + vartype: str = config[CONF_VARTYPE] if version == "3": if not authkey: @@ -159,9 +166,11 @@ async def async_setup_platform( else: auth_data = CommunityData(community, mpModel=SNMP_VERSIONS[version]) + transport = UdpTransportTarget((host, port)) request_args = await async_create_request_cmd_args( - hass, auth_data, UdpTransportTarget((host, port)), baseoid + hass, auth_data, transport, baseoid ) + command_args = await async_create_command_cmd_args(hass, auth_data, transport) async_add_entities( [ @@ -177,6 +186,7 @@ async def async_setup_platform( command_payload_off, vartype, request_args, + command_args, ) ], True, @@ -188,21 +198,22 @@ class SnmpSwitch(SwitchEntity): def __init__( self, - name, - host, - port, - baseoid, - commandoid, - payload_on, - payload_off, - command_payload_on, - command_payload_off, - vartype, - request_args, + name: str, + host: str, + port: int, + baseoid: str, + commandoid: str | None, + payload_on: str, + payload_off: str, + command_payload_on: str | None, + command_payload_off: str | None, + vartype: str, + request_args: RequestArgsType, + command_args: CommandArgsType, ) -> None: """Initialize the switch.""" - self._name = name + self._attr_name = name self._baseoid = baseoid self._vartype = vartype @@ -215,7 +226,8 @@ def __init__( self._payload_on = payload_on self._payload_off = payload_off self._target = UdpTransportTarget((host, port)) - self._request_args: RequestArgsType = request_args + self._request_args = request_args + self._command_args = command_args async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" @@ -226,7 +238,7 @@ async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" await self._execute_command(self._command_payload_off) - async def _execute_command(self, command): + async def _execute_command(self, command: str) -> None: # User did not set vartype and command is not a digit if self._vartype == "none" and not self._command_payload_on.isdigit(): await self._set(command) @@ -265,14 +277,12 @@ async def async_update(self) -> None: self._state = None @property - def name(self): - """Return the switch's name.""" - return self._name - - @property - def is_on(self): + def is_on(self) -> bool | None: """Return true if switch is on; False if off. None if unknown.""" return self._state - async def _set(self, value): - await setCmd(*self._request_args, value) + async def _set(self, value: Any) -> None: + """Set the state of the switch.""" + await setCmd( + *self._command_args, ObjectType(ObjectIdentity(self._commandoid), value) + ) diff --git a/homeassistant/components/snmp/util.py b/homeassistant/components/snmp/util.py index 23adbdf0b9098..dd3e9a6b6d20e 100644 --- a/homeassistant/components/snmp/util.py +++ b/homeassistant/components/snmp/util.py @@ -25,6 +25,14 @@ _LOGGER = logging.getLogger(__name__) +type CommandArgsType = tuple[ + SnmpEngine, + UsmUserData | CommunityData, + UdpTransportTarget | Udp6TransportTarget, + ContextData, +] + + type RequestArgsType = tuple[ SnmpEngine, UsmUserData | CommunityData, @@ -34,20 +42,34 @@ ] +async def async_create_command_cmd_args( + hass: HomeAssistant, + auth_data: UsmUserData | CommunityData, + target: UdpTransportTarget | Udp6TransportTarget, +) -> CommandArgsType: + """Create command arguments. + + The ObjectType needs to be created dynamically by the caller. + """ + engine = await async_get_snmp_engine(hass) + return (engine, auth_data, target, ContextData()) + + async def async_create_request_cmd_args( hass: HomeAssistant, auth_data: UsmUserData | CommunityData, target: UdpTransportTarget | Udp6TransportTarget, object_id: str, ) -> RequestArgsType: - """Create request arguments.""" - return ( - await async_get_snmp_engine(hass), - auth_data, - target, - ContextData(), - ObjectType(ObjectIdentity(object_id)), + """Create request arguments. + + The same ObjectType is used for all requests. + """ + engine, auth_data, target, context_data = await async_create_command_cmd_args( + hass, auth_data, target ) + object_type = ObjectType(ObjectIdentity(object_id)) + return (engine, auth_data, target, context_data, object_type) @singleton(DATA_SNMP_ENGINE) From 20b77aa15f37f1fed2f5e8d89181030b421a6421 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 7 Jun 2024 02:07:47 -0500 Subject: [PATCH 19/23] Fix remember_the_milk calling configurator async api from the wrong thread (#119029) --- homeassistant/components/remember_the_milk/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/remember_the_milk/__init__.py b/homeassistant/components/remember_the_milk/__init__.py index 3d1654960a7ba..425a12d5c4d16 100644 --- a/homeassistant/components/remember_the_milk/__init__.py +++ b/homeassistant/components/remember_the_milk/__init__.py @@ -137,7 +137,7 @@ def register_account_callback(fields: list[dict[str, str]]) -> None: configurator.request_done(hass, request_id) - request_id = configurator.async_request_config( + request_id = configurator.request_config( hass, f"{DOMAIN} - {account_name}", callback=register_account_callback, From b5693ca6047a14e0f703d044833bafd9ff525f1c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jun 2024 09:18:16 +0200 Subject: [PATCH 20/23] Fix AirGradient name (#119046) --- homeassistant/components/airgradient/manifest.json | 2 +- homeassistant/generated/integrations.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/airgradient/manifest.json b/homeassistant/components/airgradient/manifest.json index c30d7a4c42fc7..b9a1e2da54f82 100644 --- a/homeassistant/components/airgradient/manifest.json +++ b/homeassistant/components/airgradient/manifest.json @@ -1,6 +1,6 @@ { "domain": "airgradient", - "name": "Airgradient", + "name": "AirGradient", "codeowners": ["@airgradienthq", "@joostlek"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/airgradient", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 70995bb3d6375..27b7e0466e13b 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -94,7 +94,7 @@ "iot_class": "local_polling" }, "airgradient": { - "name": "Airgradient", + "name": "AirGradient", "integration_type": "device", "config_flow": true, "iot_class": "local_polling" From 093f07c04e88e546ef24a055430d4e2772ada71b Mon Sep 17 00:00:00 2001 From: Marc Mueller <30130371+cdce8p@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:13:33 +0200 Subject: [PATCH 21/23] Add type ignore comments (#119052) --- homeassistant/components/google_assistant_sdk/__init__.py | 2 +- homeassistant/components/google_assistant_sdk/helpers.py | 2 +- homeassistant/components/google_sheets/__init__.py | 4 +++- homeassistant/components/google_sheets/config_flow.py | 4 +++- homeassistant/components/nest/api.py | 4 ++-- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/google_assistant_sdk/__init__.py b/homeassistant/components/google_assistant_sdk/__init__.py index 52950a82b9360..b92b3c54579a2 100644 --- a/homeassistant/components/google_assistant_sdk/__init__.py +++ b/homeassistant/components/google_assistant_sdk/__init__.py @@ -165,7 +165,7 @@ async def async_process( await session.async_ensure_token_valid() self.assistant = None if not self.assistant or user_input.language != self.language: - credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) + credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call] self.language = user_input.language self.assistant = TextAssistant(credentials, self.language) diff --git a/homeassistant/components/google_assistant_sdk/helpers.py b/homeassistant/components/google_assistant_sdk/helpers.py index b6b13f92fcf1c..24da381e8e085 100644 --- a/homeassistant/components/google_assistant_sdk/helpers.py +++ b/homeassistant/components/google_assistant_sdk/helpers.py @@ -72,7 +72,7 @@ async def async_send_text_commands( entry.async_start_reauth(hass) raise - credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) + credentials = Credentials(session.token[CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call] language_code = entry.options.get(CONF_LANGUAGE_CODE, default_language_code(hass)) with TextAssistant( credentials, language_code, audio_out=bool(media_players) diff --git a/homeassistant/components/google_sheets/__init__.py b/homeassistant/components/google_sheets/__init__.py index f346f913e0c83..713a801257d87 100644 --- a/homeassistant/components/google_sheets/__init__.py +++ b/homeassistant/components/google_sheets/__init__.py @@ -93,7 +93,9 @@ async def async_setup_service(hass: HomeAssistant) -> None: def _append_to_sheet(call: ServiceCall, entry: ConfigEntry) -> None: """Run append in the executor.""" - service = Client(Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN])) + service = Client( + Credentials(entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call] + ) try: sheet = service.open_by_key(entry.unique_id) except RefreshError: diff --git a/homeassistant/components/google_sheets/config_flow.py b/homeassistant/components/google_sheets/config_flow.py index a0a99742249e4..ab0c084c317a4 100644 --- a/homeassistant/components/google_sheets/config_flow.py +++ b/homeassistant/components/google_sheets/config_flow.py @@ -61,7 +61,9 @@ async def async_step_reauth_confirm( async def async_oauth_create_entry(self, data: dict[str, Any]) -> ConfigFlowResult: """Create an entry for the flow, or update existing entry.""" - service = Client(Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN])) + service = Client( + Credentials(data[CONF_TOKEN][CONF_ACCESS_TOKEN]) # type: ignore[no-untyped-call] + ) if self.reauth_entry: _LOGGER.debug("service.open_by_key") diff --git a/homeassistant/components/nest/api.py b/homeassistant/components/nest/api.py index 8c9ca4bec96bb..3ef26747115a6 100644 --- a/homeassistant/components/nest/api.py +++ b/homeassistant/components/nest/api.py @@ -57,7 +57,7 @@ async def async_get_creds(self) -> Credentials: # even when it is expired to fully hand off this responsibility and # know it is working at startup (then if not, fail loudly). token = self._oauth_session.token - creds = Credentials( + creds = Credentials( # type: ignore[no-untyped-call] token=token["access_token"], refresh_token=token["refresh_token"], token_uri=OAUTH2_TOKEN, @@ -92,7 +92,7 @@ async def async_get_access_token(self) -> str: async def async_get_creds(self) -> Credentials: """Return an OAuth credential for Pub/Sub Subscriber.""" - return Credentials( + return Credentials( # type: ignore[no-untyped-call] token=self._access_token, token_uri=OAUTH2_TOKEN, scopes=SDM_SCOPES, From ed22e98861a5c98a66f1cffab7a44fd56951941c Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 7 Jun 2024 16:31:50 +0200 Subject: [PATCH 22/23] Fix Azure Data Explorer strings (#119067) --- homeassistant/components/azure_data_explorer/strings.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/azure_data_explorer/strings.json b/homeassistant/components/azure_data_explorer/strings.json index a3a82a6eb3c65..640058725794b 100644 --- a/homeassistant/components/azure_data_explorer/strings.json +++ b/homeassistant/components/azure_data_explorer/strings.json @@ -5,12 +5,13 @@ "title": "Setup your Azure Data Explorer integration", "description": "Enter connection details.", "data": { - "clusteringesturi": "Cluster Ingest URI", + "cluster_ingest_uri": "Cluster ingest URI", "database": "Database name", "table": "Table name", "client_id": "Client ID", "client_secret": "Client secret", - "authority_id": "Authority ID" + "authority_id": "Authority ID", + "use_queued_ingestion": "Use queued ingestion" } } }, From 3f70e2b6f043562fb0a5610d88550c1f99fa1bd4 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 7 Jun 2024 20:26:53 +0200 Subject: [PATCH 23/23] Bump version to 2024.6.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 e4ece15cd5786..86be19b95d88c 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -24,7 +24,7 @@ APPLICATION_NAME: Final = "HomeAssistant" MAJOR_VERSION: Final = 2024 MINOR_VERSION: Final = 6 -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 c3e03374b555a..867bc1d1513bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "homeassistant" -version = "2024.6.0" +version = "2024.6.1" license = {text = "Apache-2.0"} description = "Open-source home automation platform running on Python 3." readme = "README.rst"