diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 610fed902adcf..d87187b55be9e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,7 +18,7 @@ repos: exclude_types: [csv, json, html] exclude: ^tests/fixtures/|homeassistant/generated/|tests/components/.*/snapshots/ - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-executables-have-shebangs stages: [manual] diff --git a/homeassistant/components/assist_satellite/entity.py b/homeassistant/components/assist_satellite/entity.py index e7a10ef63f67a..3d562544c6870 100644 --- a/homeassistant/components/assist_satellite/entity.py +++ b/homeassistant/components/assist_satellite/entity.py @@ -11,7 +11,7 @@ from typing import Any, Literal, final from hassil import Intents, recognize -from hassil.expression import Expression, ListReference, Sequence +from hassil.expression import Expression, Group, ListReference from hassil.intents import WildcardSlotList from homeassistant.components import conversation, media_source, stt, tts @@ -413,7 +413,7 @@ def _question_response_to_answer( for intent in intents.intents.values(): for intent_data in intent.data: for sentence in intent_data.sentences: - _collect_list_references(sentence, wildcard_names) + _collect_list_references(sentence.expression, wildcard_names) for wildcard_name in wildcard_names: intents.slot_lists[wildcard_name] = WildcardSlotList(wildcard_name) @@ -727,9 +727,9 @@ async def _resolve_announcement_media_id( def _collect_list_references(expression: Expression, list_names: set[str]) -> None: """Collect list reference names recursively.""" - if isinstance(expression, Sequence): - seq: Sequence = expression - for item in seq.items: + if isinstance(expression, Group): + grp: Group = expression + for item in grp.items: _collect_list_references(item, list_names) elif isinstance(expression, ListReference): # {list} diff --git a/homeassistant/components/assist_satellite/manifest.json b/homeassistant/components/assist_satellite/manifest.json index 97362f157e448..184de57605055 100644 --- a/homeassistant/components/assist_satellite/manifest.json +++ b/homeassistant/components/assist_satellite/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/assist_satellite", "integration_type": "entity", "quality_scale": "internal", - "requirements": ["hassil==2.2.3"] + "requirements": ["hassil==3.1.0"] } diff --git a/homeassistant/components/conversation/default_agent.py b/homeassistant/components/conversation/default_agent.py index bed4b4c0dd629..3fb305098e774 100644 --- a/homeassistant/components/conversation/default_agent.py +++ b/homeassistant/components/conversation/default_agent.py @@ -14,7 +14,7 @@ import time from typing import IO, Any, cast -from hassil.expression import Expression, ListReference, Sequence, TextChunk +from hassil.expression import Expression, Group, ListReference, TextChunk from hassil.intents import ( Intents, SlotList, @@ -1183,7 +1183,7 @@ def _rebuild_trigger_intents(self) -> None: for trigger_intent in trigger_intents.intents.values(): for intent_data in trigger_intent.data: for sentence in intent_data.sentences: - _collect_list_references(sentence, wildcard_names) + _collect_list_references(sentence.expression, wildcard_names) for wildcard_name in wildcard_names: trigger_intents.slot_lists[wildcard_name] = WildcardSlotList(wildcard_name) @@ -1520,9 +1520,9 @@ def _get_match_error_response( def _collect_list_references(expression: Expression, list_names: set[str]) -> None: """Collect list reference names recursively.""" - if isinstance(expression, Sequence): - seq: Sequence = expression - for item in seq.items: + if isinstance(expression, Group): + grp: Group = expression + for item in grp.items: _collect_list_references(item, list_names) elif isinstance(expression, ListReference): # {list} diff --git a/homeassistant/components/conversation/manifest.json b/homeassistant/components/conversation/manifest.json index 31adffad0645e..e7d096212ba7a 100644 --- a/homeassistant/components/conversation/manifest.json +++ b/homeassistant/components/conversation/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/conversation", "integration_type": "system", "quality_scale": "internal", - "requirements": ["hassil==2.2.3", "home-assistant-intents==2025.7.30"] + "requirements": ["hassil==3.1.0", "home-assistant-intents==2025.7.30"] } diff --git a/homeassistant/components/fan/intent.py b/homeassistant/components/fan/intent.py new file mode 100644 index 0000000000000..ef088a4bba98b --- /dev/null +++ b/homeassistant/components/fan/intent.py @@ -0,0 +1,31 @@ +"""Intents for the fan integration.""" + +import voluptuous as vol + +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent + +from . import ATTR_PERCENTAGE, DOMAIN, SERVICE_TURN_ON + +INTENT_FAN_SET_SPEED = "HassFanSetSpeed" + + +async def async_setup_intents(hass: HomeAssistant) -> None: + """Set up the fan intents.""" + intent.async_register( + hass, + intent.ServiceIntentHandler( + INTENT_FAN_SET_SPEED, + DOMAIN, + SERVICE_TURN_ON, + description="Sets a fan's speed by percentage", + required_domains={DOMAIN}, + platforms={DOMAIN}, + required_slots={ + ATTR_PERCENTAGE: intent.IntentSlotInfo( + description="The speed percentage of the fan", + value_schema=vol.All(vol.Coerce(int), vol.Range(min=0, max=100)), + ) + }, + ), + ) diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index f73b7156d3ea1..ae51fe166c4f6 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -217,6 +217,13 @@ async def determine_api_version( _LOGGER.debug( "Connection to %s failed: %s, trying API version 5", holeV6.base_url, ex_v6 ) + else: + # It seems that occasionally the auth can succeed unexpectedly when there is a valid session + _LOGGER.warning( + "Authenticated with %s through v6 API, but succeeded with an incorrect password. This is a known bug", + holeV6.base_url, + ) + return 6 holeV5 = api_by_version(hass, entry, 5, password="wrong_token") try: await holeV5.get_data() diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 21f481bcb5183..5fa00656e5ad2 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -36,7 +36,7 @@ go2rtc-client==0.2.1 ha-ffmpeg==3.2.2 habluetooth==5.0.1 hass-nabucasa==0.111.2 -hassil==2.2.3 +hassil==3.1.0 home-assistant-bluetooth==1.13.1 home-assistant-frontend==20250811.0 home-assistant-intents==2025.7.30 @@ -45,7 +45,7 @@ ifaddr==0.2.0 Jinja2==3.1.6 lru-dict==1.3.0 mutagen==1.47.0 -orjson==3.11.1 +orjson==3.11.2 packaging>=23.1 paho-mqtt==2.1.0 Pillow==11.3.0 diff --git a/pyproject.toml b/pyproject.toml index c4b4bfcdd7d3c..1f74056ac91de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,7 +61,7 @@ dependencies = [ "Pillow==11.3.0", "propcache==0.3.2", "pyOpenSSL==25.1.0", - "orjson==3.11.1", + "orjson==3.11.2", "packaging>=23.1", "psutil-home-assistant==0.0.1", "python-slugify==8.0.4", diff --git a/requirements.txt b/requirements.txt index f0f49ac519bd9..502e1e225cfe0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -33,7 +33,7 @@ cryptography==45.0.3 Pillow==11.3.0 propcache==0.3.2 pyOpenSSL==25.1.0 -orjson==3.11.1 +orjson==3.11.2 packaging>=23.1 psutil-home-assistant==0.0.1 python-slugify==8.0.4 diff --git a/requirements_all.txt b/requirements_all.txt index 7bfc45ae5227a..220b23cb47e25 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1140,7 +1140,7 @@ hass-splunk==0.1.1 # homeassistant.components.assist_satellite # homeassistant.components.conversation -hassil==2.2.3 +hassil==3.1.0 # homeassistant.components.jewish_calendar hdate[astral]==1.1.2 diff --git a/requirements_test.txt b/requirements_test.txt index 2f680240f6e38..9df62168b1960 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -35,19 +35,19 @@ requests-mock==1.12.1 respx==0.22.0 syrupy==4.9.1 tqdm==4.67.1 -types-aiofiles==24.1.0.20250708 +types-aiofiles==24.1.0.20250809 types-atomicwrites==1.4.5.1 -types-croniter==6.0.0.20250626 +types-croniter==6.0.0.20250809 types-caldav==1.3.0.20250516 types-chardet==0.1.5 types-decorator==5.2.0.20250324 -types-pexpect==4.9.0.20250516 -types-protobuf==6.30.2.20250703 -types-psutil==7.0.0.20250601 -types-pyserial==3.5.0.20250326 -types-python-dateutil==2.9.0.20250708 +types-pexpect==4.9.0.20250809 +types-protobuf==6.30.2.20250809 +types-psutil==7.0.0.20250801 +types-pyserial==3.5.0.20250809 +types-python-dateutil==2.9.0.20250809 types-python-slugify==8.0.2.20240310 -types-pytz==2025.2.0.20250516 -types-PyYAML==6.0.12.20250516 -types-requests==2.32.4.20250611 +types-pytz==2025.2.0.20250809 +types-PyYAML==6.0.12.20250809 +types-requests==2.32.4.20250809 types-xmltodict==0.13.0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3cc54a40ab890..e3f832b464b38 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -998,7 +998,7 @@ hass-nabucasa==0.111.2 # homeassistant.components.assist_satellite # homeassistant.components.conversation -hassil==2.2.3 +hassil==3.1.0 # homeassistant.components.jewish_calendar hdate[astral]==1.1.2 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index 4b8aafce70ff4..6dbb086f27387 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -31,7 +31,7 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.8.9,source=/uv,target=/bin/uv \ PyTurboJPEG==1.8.0 \ go2rtc-client==0.2.1 \ ha-ffmpeg==3.2.2 \ - hassil==2.2.3 \ + hassil==3.1.0 \ home-assistant-intents==2025.7.30 \ mutagen==1.47.0 \ pymicro-vad==1.0.1 \ diff --git a/tests/components/asuswrt/conftest.py b/tests/components/asuswrt/conftest.py index e6dd42a23fddc..95c8f3dbf747e 100644 --- a/tests/components/asuswrt/conftest.py +++ b/tests/components/asuswrt/conftest.py @@ -222,7 +222,7 @@ def _set_fail_types(fail_types): def mock_controller_connect_http_sens_detect(): """Mock a successful sensor detection using http library.""" - def _get_sensors_side_effect(datatype): + async def _get_sensors_side_effect(datatype): if datatype == AsusData.TEMPERATURE: return list(MOCK_TEMPERATURES_HTTP) if datatype == AsusData.CPU: diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index 3ce3246c1d66e..c782605aab33f 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -377,7 +377,6 @@ async def test_cpu_sensors_http( connect_http_sens_detect, ) -> None: """Test creating AsusWRT cpu sensors.""" - connect_http_sens_detect(AsusData.CPU) config_entry, sensor_prefix = _setup_entry(hass, CONFIG_DATA_HTTP, SENSORS_CPU) config_entry.add_to_hass(hass) diff --git a/tests/components/fan/test_intent.py b/tests/components/fan/test_intent.py new file mode 100644 index 0000000000000..450d81e9dff52 --- /dev/null +++ b/tests/components/fan/test_intent.py @@ -0,0 +1,37 @@ +"""Intent tests for the fan platform.""" + +from homeassistant.components.fan import ( + ATTR_PERCENTAGE, + DOMAIN, + SERVICE_TURN_ON, + intent as fan_intent, +) +from homeassistant.const import STATE_OFF +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent + +from tests.common import async_mock_service + + +async def test_set_speed_intent(hass: HomeAssistant) -> None: + """Test set speed intent for fans.""" + await fan_intent.async_setup_intents(hass) + + entity_id = f"{DOMAIN}.test_fan" + hass.states.async_set(entity_id, STATE_OFF) + calls = async_mock_service(hass, DOMAIN, SERVICE_TURN_ON) + + response = await intent.async_handle( + hass, + "test", + fan_intent.INTENT_FAN_SET_SPEED, + {"name": {"value": "test fan"}, ATTR_PERCENTAGE: {"value": 50}}, + ) + await hass.async_block_till_done() + + assert response.response_type == intent.IntentResponseType.ACTION_DONE + assert len(calls) == 1 + call = calls[0] + assert call.domain == DOMAIN + assert call.service == SERVICE_TURN_ON + assert call.data == {"entity_id": entity_id, "percentage": 50} diff --git a/tests/components/pi_hole/__init__.py b/tests/components/pi_hole/__init__.py index c20f22ac58d2e..c2edb51e06641 100644 --- a/tests/components/pi_hole/__init__.py +++ b/tests/components/pi_hole/__init__.py @@ -221,12 +221,16 @@ async def authenticate_side_effect(*_args, **_kwargs): if wrong_host: raise HoleConnectionError("Cannot authenticate with Pi-hole: err") password = getattr(mocked_hole, "password", None) + if ( raise_exception or incorrect_app_password + or api_version == 5 or (api_version == 6 and password not in ["newkey", "apikey"]) ): - if api_version == 6: + if api_version == 6 and ( + incorrect_app_password or password not in ["newkey", "apikey"] + ): raise HoleError("Authentication failed: Invalid password") raise HoleConnectionError diff --git a/tests/components/squeezebox/conftest.py b/tests/components/squeezebox/conftest.py index 97aca31fa0587..2dd9403d53f32 100644 --- a/tests/components/squeezebox/conftest.py +++ b/tests/components/squeezebox/conftest.py @@ -271,6 +271,7 @@ def mock_pysqueezebox_player(uuid: str) -> MagicMock: "homeassistant.components.squeezebox.Player", autospec=True ) as mock_player: mock_player.async_browse = AsyncMock(side_effect=mock_async_browse) + mock_player.async_query = AsyncMock(return_value=MagicMock()) mock_player.generate_image_url_from_track_id = MagicMock( return_value="http://lms.internal:9000/html/images/favorites.png" )