diff --git a/build.yaml b/build.yaml index 60b6fa5ef3a10..bb1d8898837e6 100644 --- a/build.yaml +++ b/build.yaml @@ -1,10 +1,10 @@ image: ghcr.io/home-assistant/{arch}-homeassistant build_from: - aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.0 - armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.0 - armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.0 - amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.0 - i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.0 + aarch64: ghcr.io/home-assistant/aarch64-homeassistant-base:2025.10.1 + armhf: ghcr.io/home-assistant/armhf-homeassistant-base:2025.10.1 + armv7: ghcr.io/home-assistant/armv7-homeassistant-base:2025.10.1 + amd64: ghcr.io/home-assistant/amd64-homeassistant-base:2025.10.1 + i386: ghcr.io/home-assistant/i386-homeassistant-base:2025.10.1 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index fe7510c3bf578..1cbebea0c63c5 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -74,7 +74,10 @@ StreamType, ) from .helper import get_camera_from_entity_id -from .img_util import scale_jpeg_camera_image +from .img_util import ( + TurboJPEGSingleton, # noqa: F401 + scale_jpeg_camera_image, +) from .prefs import ( CameraPreferences, DynamicStreamSettings, # noqa: F401 diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index e15ea92dece19..a6f9c7a1a79ea 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -19,7 +19,7 @@ errors as alexa_errors, smart_home as alexa_smart_home, ) -from homeassistant.components.camera.webrtc import async_register_ice_servers +from homeassistant.components.camera import async_register_ice_servers from homeassistant.components.google_assistant import smart_home as ga from homeassistant.const import __version__ as HA_VERSION from homeassistant.core import Context, HassJob, HomeAssistant, callback diff --git a/homeassistant/components/conversation/chat_log.py b/homeassistant/components/conversation/chat_log.py index 6ac5a082bee81..736bf128e6088 100644 --- a/homeassistant/components/conversation/chat_log.py +++ b/homeassistant/components/conversation/chat_log.py @@ -569,14 +569,17 @@ async def async_provide_llm_data( if llm_api: prompt_parts.append(llm_api.api_prompt) - prompt_parts.append( - await self._async_expand_prompt_template( - llm_context, - llm.BASE_PROMPT, - llm_context.language, - user_name, + # Append current date and time to the prompt if the corresponding tool is not provided + llm_tools: list[llm.Tool] = llm_api.tools if llm_api else [] + if not any(tool.name.endswith("GetDateTime") for tool in llm_tools): + prompt_parts.append( + await self._async_expand_prompt_template( + llm_context, + llm.DATE_TIME_PROMPT, + llm_context.language, + user_name, + ) ) - ) if extra_system_prompt := ( # Take new system prompt if one was given diff --git a/homeassistant/components/demo/alarm_control_panel.py b/homeassistant/components/demo/alarm_control_panel.py index 64474b4beb639..9716eccc2c165 100644 --- a/homeassistant/components/demo/alarm_control_panel.py +++ b/homeassistant/components/demo/alarm_control_panel.py @@ -5,7 +5,9 @@ import datetime from homeassistant.components.alarm_control_panel import AlarmControlPanelState -from homeassistant.components.manual.alarm_control_panel import ManualAlarm +from homeassistant.components.manual.alarm_control_panel import ( # pylint: disable=hass-component-root-import + ManualAlarm, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ARMING_TIME, CONF_DELAY_TIME, CONF_TRIGGER_TIME from homeassistant.core import HomeAssistant diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index ed13f24cfd7b4..af7b493497564 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -139,6 +139,7 @@ async def async_close_cover(self, **kwargs: Any) -> None: self.async_write_ha_state() return + self._is_opening = False self._is_closing = True self._listen_cover() self._requested_closing = True @@ -162,6 +163,7 @@ async def async_open_cover(self, **kwargs: Any) -> None: return self._is_opening = True + self._is_closing = False self._listen_cover() self._requested_closing = False self.async_write_ha_state() @@ -181,10 +183,14 @@ async def async_set_cover_position(self, **kwargs: Any) -> None: if self._position == position: return + self._is_closing = position < (self._position or 0) + self._is_opening = not self._is_closing + self._listen_cover() self._requested_closing = ( self._position is not None and position < self._position ) + self.async_write_ha_state() async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover til to a specific position.""" diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 3522ed00ddadc..f4bfc61560822 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -6,5 +6,5 @@ "iot_class": "local_polling", "loggers": ["pydoods"], "quality_scale": "legacy", - "requirements": ["pydoods==1.0.2", "Pillow==11.3.0"] + "requirements": ["pydoods==1.0.2", "Pillow==12.0.0"] } diff --git a/homeassistant/components/generic/manifest.json b/homeassistant/components/generic/manifest.json index bef0d81d77bf2..642dad1006091 100644 --- a/homeassistant/components/generic/manifest.json +++ b/homeassistant/components/generic/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/generic", "integration_type": "device", "iot_class": "local_push", - "requirements": ["av==13.1.0", "Pillow==11.3.0"] + "requirements": ["av==13.1.0", "Pillow==12.0.0"] } diff --git a/homeassistant/components/go2rtc/__init__.py b/homeassistant/components/go2rtc/__init__.py index 5ee449f383349..6abb16d36ea01 100644 --- a/homeassistant/components/go2rtc/__init__.py +++ b/homeassistant/components/go2rtc/__init__.py @@ -30,8 +30,8 @@ WebRTCMessage, WebRTCSendMessage, async_register_webrtc_provider, + get_dynamic_camera_stream_settings, ) -from homeassistant.components.camera.prefs import get_dynamic_camera_stream_settings from homeassistant.components.default_config import DOMAIN as DEFAULT_CONFIG_DOMAIN from homeassistant.components.stream import Orientation from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry diff --git a/homeassistant/components/habitica/manifest.json b/homeassistant/components/habitica/manifest.json index 30443f1d1da82..93cb01f1304f4 100644 --- a/homeassistant/components/habitica/manifest.json +++ b/homeassistant/components/habitica/manifest.json @@ -7,5 +7,5 @@ "iot_class": "cloud_polling", "loggers": ["habiticalib"], "quality_scale": "platinum", - "requirements": ["habiticalib==0.4.5"] + "requirements": ["habiticalib==0.4.6"] } diff --git a/homeassistant/components/image_upload/manifest.json b/homeassistant/components/image_upload/manifest.json index 34013c28a186c..a37ab4c010a03 100644 --- a/homeassistant/components/image_upload/manifest.json +++ b/homeassistant/components/image_upload/manifest.json @@ -7,5 +7,5 @@ "documentation": "https://www.home-assistant.io/integrations/image_upload", "integration_type": "system", "quality_scale": "internal", - "requirements": ["Pillow==11.3.0"] + "requirements": ["Pillow==12.0.0"] } diff --git a/homeassistant/components/lovelace/cast.py b/homeassistant/components/lovelace/cast.py index 635425ba3dca9..a22e94b9b1f0f 100644 --- a/homeassistant/components/lovelace/cast.py +++ b/homeassistant/components/lovelace/cast.py @@ -8,7 +8,7 @@ from pychromecast.const import CAST_TYPE_CHROMECAST from homeassistant.components.cast import DOMAIN as CAST_DOMAIN -from homeassistant.components.cast.home_assistant_cast import ( +from homeassistant.components.cast.home_assistant_cast import ( # pylint: disable=hass-component-root-import ATTR_URL_PATH, ATTR_VIEW_PATH, NO_URL_AVAILABLE_ERROR, diff --git a/homeassistant/components/matrix/manifest.json b/homeassistant/components/matrix/manifest.json index 103c410855c24..81cd2a3f490c8 100644 --- a/homeassistant/components/matrix/manifest.json +++ b/homeassistant/components/matrix/manifest.json @@ -6,5 +6,5 @@ "iot_class": "cloud_push", "loggers": ["matrix_client"], "quality_scale": "legacy", - "requirements": ["matrix-nio==0.25.2", "Pillow==11.3.0"] + "requirements": ["matrix-nio==0.25.2", "Pillow==12.0.0"] } diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index af68aa446f5d5..c586df030c13d 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -4,5 +4,5 @@ "codeowners": [], "documentation": "https://www.home-assistant.io/integrations/proxy", "quality_scale": "legacy", - "requirements": ["Pillow==11.3.0"] + "requirements": ["Pillow==12.0.0"] } diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 70926adb29b7e..6cc68e531514e 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -6,5 +6,5 @@ "iot_class": "calculated", "loggers": ["pyzbar"], "quality_scale": "legacy", - "requirements": ["Pillow==11.3.0", "pyzbar==0.1.7"] + "requirements": ["Pillow==12.0.0", "pyzbar==0.1.7"] } diff --git a/homeassistant/components/rapt_ble/config_flow.py b/homeassistant/components/rapt_ble/config_flow.py index 805a2cf8cbd2f..3bbd18f387c1b 100644 --- a/homeassistant/components/rapt_ble/config_flow.py +++ b/homeassistant/components/rapt_ble/config_flow.py @@ -72,7 +72,7 @@ async def async_step_user( title=self._discovered_devices[address], data={} ) - current_addresses = self._async_current_ids() + current_addresses = self._async_current_ids(include_ignore=False) for discovery_info in async_discovered_service_info(self.hass, False): address = discovery_info.address if address in current_addresses or address in self._discovered_devices: diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 413e9424b1574..1aa2b4fea69e8 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/seven_segments", "iot_class": "local_polling", "quality_scale": "legacy", - "requirements": ["Pillow==11.3.0"] + "requirements": ["Pillow==12.0.0"] } diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index 3e3ee6ef2fa15..596e9c1751a84 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -6,5 +6,5 @@ "iot_class": "cloud_polling", "loggers": ["simplehound"], "quality_scale": "legacy", - "requirements": ["Pillow==11.3.0", "simplehound==0.3"] + "requirements": ["Pillow==12.0.0", "simplehound==0.3"] } diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index a2719ec6ba93b..f7d72587c5b03 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -39,7 +39,9 @@ async_process_play_media_url, ) from homeassistant.components.plex import PLEX_URI_SCHEME -from homeassistant.components.plex.services import process_plex_payload +from homeassistant.components.plex.services import ( # pylint: disable=hass-component-root-import + process_plex_payload, +) from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.helpers import entity_registry as er diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 7dc6bab16b991..2ee49edb23ec0 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -441,9 +441,7 @@ def __init__( # Keep import here so that we can import stream integration # without installing reqs - from homeassistant.components.camera.img_util import ( # noqa: PLC0415 - TurboJPEGSingleton, - ) + from homeassistant.components.camera import TurboJPEGSingleton # noqa: PLC0415 self._packet: Packet | None = None self._event: asyncio.Event = asyncio.Event() diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 1144fd7a4afc4..64e9587711014 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -11,6 +11,6 @@ "tf-models-official==2.5.0", "pycocotools==2.0.6", "numpy==2.3.2", - "Pillow==11.3.0" + "Pillow==12.0.0" ] } diff --git a/homeassistant/components/volvo/icons.json b/homeassistant/components/volvo/icons.json index 94c9375e47ae9..72f766553c606 100644 --- a/homeassistant/components/volvo/icons.json +++ b/homeassistant/components/volvo/icons.json @@ -348,6 +348,12 @@ "odometer": { "default": "mdi:counter" }, + "service_warning": { + "default": "mdi:wrench-clock", + "state": { + "no_warning": "mdi:car-wrench" + } + }, "target_battery_charge_level": { "default": "mdi:battery-medium" }, diff --git a/homeassistant/components/volvo/sensor.py b/homeassistant/components/volvo/sensor.py index d9d455f1cde95..77e3fdfa29d79 100644 --- a/homeassistant/components/volvo/sensor.py +++ b/homeassistant/components/volvo/sensor.py @@ -332,6 +332,25 @@ def _direction_value(field: VolvoCarsApiBaseModel) -> str | None: state_class=SensorStateClass.TOTAL_INCREASING, suggested_display_precision=1, ), + # diagnostics endpoint + VolvoSensorDescription( + key="service_warning", + api_field="serviceWarning", + device_class=SensorDeviceClass.ENUM, + options=[ + "distance_driven_almost_time_for_service", + "distance_driven_overdue_for_service", + "distance_driven_time_for_service", + "engine_hours_almost_time_for_service", + "engine_hours_overdue_for_service", + "engine_hours_time_for_service", + "no_warning", + "regular_maintenance_almost_time_for_service", + "regular_maintenance_overdue_for_service", + "regular_maintenance_time_for_service", + "unknown_warning", + ], + ), # energy state endpoint VolvoSensorDescription( key="target_battery_charge_level", diff --git a/homeassistant/components/volvo/strings.json b/homeassistant/components/volvo/strings.json index 8cce41a839f59..a518fc92514c6 100644 --- a/homeassistant/components/volvo/strings.json +++ b/homeassistant/components/volvo/strings.json @@ -309,6 +309,22 @@ "odometer": { "name": "Odometer" }, + "service_warning": { + "name": "Service", + "state": { + "distance_driven_almost_time_for_service": "Almost time for distance service", + "distance_driven_overdue_for_service": "Distance service overdue", + "distance_driven_time_for_service": "Time for distance service", + "engine_hours_almost_time_for_service": "Almost time for engine service", + "engine_hours_overdue_for_service": "Engine service overdue", + "engine_hours_time_for_service": "Time for engine service", + "no_warning": "No warning", + "regular_maintenance_almost_time_for_service": "Almost time for service", + "regular_maintenance_overdue_for_service": "Service overdue", + "regular_maintenance_time_for_service": "Time for service", + "unknown_warning": "Unknown warning" + } + }, "target_battery_charge_level": { "name": "Target battery charge level" }, diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 1eb30fe751211..900abb0b9c7f9 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -58,7 +58,7 @@ LLM_API_ASSIST = "assist" -BASE_PROMPT = ( +DATE_TIME_PROMPT = ( 'Current time is {{ now().strftime("%H:%M:%S") }}. ' 'Today\'s date is {{ now().strftime("%Y-%m-%d") }}.\n' ) @@ -592,6 +592,8 @@ def _async_get_tools( for intent_handler in intent_handlers ] + tools.append(GetDateTimeTool()) + if exposed_entities: if exposed_entities[CALENDAR_DOMAIN]: names = [] @@ -1181,3 +1183,29 @@ async def async_call( "success": True, "result": "\n".join(prompt), } + + +class GetDateTimeTool(Tool): + """Tool for getting the current date and time.""" + + name = "GetDateTime" + description = "Provides the current date and time." + + async def async_call( + self, + hass: HomeAssistant, + tool_input: ToolInput, + llm_context: LLMContext, + ) -> JsonObjectType: + """Get the current date and time.""" + now = dt_util.now() + + return { + "success": True, + "result": { + "date": now.strftime("%Y-%m-%d"), + "time": now.strftime("%H:%M:%S"), + "timezone": now.strftime("%Z"), + "weekday": now.strftime("%A"), + }, + } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ae799828bf90f..c51e6a589c64b 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -49,7 +49,7 @@ mutagen==1.47.0 orjson==3.11.3 packaging>=23.1 paho-mqtt==2.1.0 -Pillow==11.3.0 +Pillow==12.0.0 propcache==0.4.1 psutil-home-assistant==0.0.1 PyJWT==2.10.1 diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py index 95f7b0a0feb64..ae7644419dc69 100644 --- a/pylint/plugins/hass_imports.py +++ b/pylint/plugins/hass_imports.py @@ -128,8 +128,6 @@ class ObsoleteImportMatch: _IGNORE_ROOT_IMPORT = ( "automation", "bluetooth", - "camera", - "cast", "device_automation", "device_tracker", "ffmpeg", @@ -138,8 +136,6 @@ class ObsoleteImportMatch: "homeassistant", "homeassistant_hardware", "http", - "manual", - "plex", "recorder", "rest", "script", diff --git a/pyproject.toml b/pyproject.toml index 561e99a985b75..68eff5063f6ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ dependencies = [ "PyJWT==2.10.1", # PyJWT has loose dependency. We want the latest one. "cryptography==46.0.2", - "Pillow==11.3.0", + "Pillow==12.0.0", "propcache==0.4.1", "pyOpenSSL==25.3.0", "orjson==3.11.3", diff --git a/requirements.txt b/requirements.txt index 2d64703e83fca..cea224937bfc1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -30,7 +30,7 @@ Jinja2==3.1.6 lru-dict==1.3.0 PyJWT==2.10.1 cryptography==46.0.2 -Pillow==11.3.0 +Pillow==12.0.0 propcache==0.4.1 pyOpenSSL==25.3.0 orjson==3.11.3 diff --git a/requirements_all.txt b/requirements_all.txt index adc8d35ed8feb..c63f14fa5eeba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -36,7 +36,7 @@ PSNAWP==3.0.0 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -Pillow==11.3.0 +Pillow==12.0.0 # homeassistant.components.plex PlexAPI==4.15.16 @@ -1148,7 +1148,7 @@ ha-philipsjs==3.2.4 ha-silabs-firmware-client==0.2.0 # homeassistant.components.habitica -habiticalib==0.4.5 +habiticalib==0.4.6 # homeassistant.components.bluetooth habluetooth==5.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a87200c786acd..f3a9260bed216 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -36,7 +36,7 @@ PSNAWP==3.0.0 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -Pillow==11.3.0 +Pillow==12.0.0 # homeassistant.components.plex PlexAPI==4.15.16 @@ -1009,7 +1009,7 @@ ha-philipsjs==3.2.4 ha-silabs-firmware-client==0.2.0 # homeassistant.components.habitica -habiticalib==0.4.5 +habiticalib==0.4.6 # homeassistant.components.bluetooth habluetooth==5.7.0 diff --git a/tests/components/anthropic/test_conversation.py b/tests/components/anthropic/test_conversation.py index 7d1a49e9f0018..06d31d415e5c3 100644 --- a/tests/components/anthropic/test_conversation.py +++ b/tests/components/anthropic/test_conversation.py @@ -454,7 +454,10 @@ def completion_result(*args, messages, **kwargs): agent_id=agent_id, ) - assert "Today's date is 2024-06-03." in mock_create.mock_calls[1][2]["system"] + assert ( + "You are a voice assistant for Home Assistant." + in mock_create.mock_calls[1][2]["system"] + ) assert result.response.response_type == intent.IntentResponseType.ACTION_DONE assert ( diff --git a/tests/components/comfoconnect/test_sensor.py b/tests/components/comfoconnect/test_sensor.py index 5cae566379a4f..454cb54ff1754 100644 --- a/tests/components/comfoconnect/test_sensor.py +++ b/tests/components/comfoconnect/test_sensor.py @@ -13,7 +13,7 @@ COMPONENT = "comfoconnect" VALID_CONFIG = { - COMPONENT: {"host": "1.2.3.4"}, + COMPONENT: {"host": "192.0.2.1"}, SENSOR_DOMAIN: { "platform": COMPONENT, "resources": [ @@ -31,7 +31,10 @@ def mock_bridge_discover() -> Generator[MagicMock]: """Mock the bridge discover method.""" with patch("pycomfoconnect.bridge.Bridge.discover") as mock_bridge_discover: - mock_bridge_discover.return_value[0].uuid.hex.return_value = "00" + bridge = MagicMock() + bridge.uuid.hex.return_value = "00" + bridge.host = "192.0.2.1" + mock_bridge_discover.return_value = [bridge] yield mock_bridge_discover @@ -44,11 +47,28 @@ def mock_comfoconnect_command() -> Generator[MagicMock]: yield mock_comfoconnect_command +@pytest.fixture +def mock_comfoconnect_connect() -> Generator[MagicMock]: + """Mock the ComfoConnect connect method.""" + with patch("pycomfoconnect.comfoconnect.ComfoConnect.connect") as mock_connect: + yield mock_connect + + +@pytest.fixture(autouse=True) +def mock_comfoconnect_disconnect() -> Generator[MagicMock]: + """Mock the ComfoConnect disconnect method, autouse=True to mock in teardown.""" + with patch( + "pycomfoconnect.comfoconnect.ComfoConnect.disconnect" + ) as mock_disconnect: + yield mock_disconnect + + @pytest.fixture async def setup_sensor( hass: HomeAssistant, mock_bridge_discover: MagicMock, mock_comfoconnect_command: MagicMock, + mock_comfoconnect_connect: MagicMock, ) -> None: """Set up demo sensor component.""" with assert_setup_component(1, SENSOR_DOMAIN): diff --git a/tests/components/conversation/test_chat_log.py b/tests/components/conversation/test_chat_log.py index e851512b36e5a..3fc13e93508ee 100644 --- a/tests/components/conversation/test_chat_log.py +++ b/tests/components/conversation/test_chat_log.py @@ -156,6 +156,106 @@ async def async_get_api_instance( assert chat_log.llm_api.api.id == "assist|my-api" +async def test_dynamic_time_injection( + hass: HomeAssistant, mock_conversation_input: ConversationInput +) -> None: + """Test that dynamic time injection works correctly.""" + + class MyAPI(llm.API): + """Test API.""" + + async def async_get_api_instance( + self, llm_context: llm.LLMContext + ) -> llm.APIInstance: + """Return a list of tools.""" + return llm.APIInstance(self, "My API Prompt", llm_context, []) + + not_assist_1_api = MyAPI(hass=hass, id="not-assist-1", name="Not Assist 1") + llm.async_register_api(hass, not_assist_1_api) + + not_assist_2_api = MyAPI(hass=hass, id="not-assist-2", name="Not Assist 2") + llm.async_register_api(hass, not_assist_2_api) + + # Helper to track which prompts are rendered + rendered_prompts = [] + + async def fake_expand_prompt_template( + llm_context, prompt, language, user_name=None + ): + rendered_prompts.append(prompt) + return prompt + + # Case 1: No API used -> prompt should contain the time + with ( + chat_session.async_get_chat_session(hass) as session, + async_get_chat_log(hass, session, mock_conversation_input) as chat_log, + ): + chat_log._async_expand_prompt_template = fake_expand_prompt_template + rendered_prompts.clear() + await chat_log.async_provide_llm_data( + mock_conversation_input.as_llm_context("test"), + user_llm_hass_api=None, + user_llm_prompt=None, + ) + assert llm.DATE_TIME_PROMPT in rendered_prompts + + # Case 2: Single API (not assist) -> prompt should contain the time + with ( + chat_session.async_get_chat_session(hass) as session, + async_get_chat_log(hass, session, mock_conversation_input) as chat_log, + ): + chat_log._async_expand_prompt_template = fake_expand_prompt_template + rendered_prompts.clear() + await chat_log.async_provide_llm_data( + mock_conversation_input.as_llm_context("test"), + user_llm_hass_api=["not-assist-1"], + user_llm_prompt=None, + ) + assert llm.DATE_TIME_PROMPT in rendered_prompts + + # Case 3: Single API (assist) -> prompt should NOT contain the time + with ( + chat_session.async_get_chat_session(hass) as session, + async_get_chat_log(hass, session, mock_conversation_input) as chat_log, + ): + chat_log._async_expand_prompt_template = fake_expand_prompt_template + rendered_prompts.clear() + await chat_log.async_provide_llm_data( + mock_conversation_input.as_llm_context("test"), + user_llm_hass_api=[llm.LLM_API_ASSIST], + user_llm_prompt=None, + ) + assert llm.DATE_TIME_PROMPT not in rendered_prompts + + # Case 4: Merged API (without assist) -> prompt should contain the time + with ( + chat_session.async_get_chat_session(hass) as session, + async_get_chat_log(hass, session, mock_conversation_input) as chat_log, + ): + chat_log._async_expand_prompt_template = fake_expand_prompt_template + rendered_prompts.clear() + await chat_log.async_provide_llm_data( + mock_conversation_input.as_llm_context("test"), + user_llm_hass_api=["not-assist-1", "not-assist-2"], + user_llm_prompt=None, + ) + assert llm.DATE_TIME_PROMPT in rendered_prompts + + # Case 5: Merged API (with assist) -> prompt should NOT contain the time + with ( + chat_session.async_get_chat_session(hass) as session, + async_get_chat_log(hass, session, mock_conversation_input) as chat_log, + ): + chat_log._async_expand_prompt_template = fake_expand_prompt_template + rendered_prompts.clear() + await chat_log.async_provide_llm_data( + mock_conversation_input.as_llm_context("test"), + user_llm_hass_api=[llm.LLM_API_ASSIST, "not-assist-1"], + user_llm_prompt=None, + ) + assert llm.DATE_TIME_PROMPT not in rendered_prompts + + async def test_template_error( hass: HomeAssistant, mock_conversation_input: ConversationInput, diff --git a/tests/components/demo/test_cover.py b/tests/components/demo/test_cover.py index dcec921c01dfe..3a2e51ca6b16d 100644 --- a/tests/components/demo/test_cover.py +++ b/tests/components/demo/test_cover.py @@ -154,12 +154,17 @@ async def test_set_cover_position(hass: HomeAssistant) -> None: """Test moving the cover to a specific position.""" state = hass.states.get(ENTITY_COVER) assert state.attributes[ATTR_CURRENT_POSITION] == 70 + + # close to 10% await hass.services.async_call( COVER_DOMAIN, SERVICE_SET_COVER_POSITION, {ATTR_ENTITY_ID: ENTITY_COVER, ATTR_POSITION: 10}, blocking=True, ) + state = hass.states.get(ENTITY_COVER) + assert state.state == CoverState.CLOSING + for _ in range(6): future = dt_util.utcnow() + timedelta(seconds=1) async_fire_time_changed(hass, future) @@ -167,6 +172,26 @@ async def test_set_cover_position(hass: HomeAssistant) -> None: state = hass.states.get(ENTITY_COVER) assert state.attributes[ATTR_CURRENT_POSITION] == 10 + assert state.state == CoverState.OPEN + + # open to 80% + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: ENTITY_COVER, ATTR_POSITION: 80}, + blocking=True, + ) + state = hass.states.get(ENTITY_COVER) + assert state.state == CoverState.OPENING + + for _ in range(7): + future = dt_util.utcnow() + timedelta(seconds=1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_COVER) + assert state.attributes[ATTR_CURRENT_POSITION] == 80 + assert state.state == CoverState.OPEN async def test_stop_cover(hass: HomeAssistant) -> None: diff --git a/tests/components/go2rtc/test_init.py b/tests/components/go2rtc/test_init.py index 7b748096ca535..5b85bfee3dbac 100644 --- a/tests/components/go2rtc/test_init.py +++ b/tests/components/go2rtc/test_init.py @@ -21,6 +21,9 @@ from webrtc_models import RTCIceCandidateInit from homeassistant.components.camera import ( + DATA_CAMERA_PREFS, + CameraPreferences, + DynamicStreamSettings, StreamType, WebRTCAnswer as HAWebRTCAnswer, WebRTCCandidate as HAWebRTCCandidate, @@ -29,11 +32,6 @@ WebRTCSendMessage, async_get_image, ) -from homeassistant.components.camera.const import DATA_CAMERA_PREFS -from homeassistant.components.camera.prefs import ( - CameraPreferences, - DynamicStreamSettings, -) from homeassistant.components.default_config import DOMAIN as DEFAULT_CONFIG_DOMAIN from homeassistant.components.go2rtc import HomeAssistant, WebRTCProvider from homeassistant.components.go2rtc.const import ( diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index a42980ec2af0f..6dad8a83461ef 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -9,7 +9,7 @@ from homeassistant.components import camera, ffmpeg from homeassistant.components.binary_sensor import BinarySensorDeviceClass -from homeassistant.components.camera.img_util import TurboJPEGSingleton +from homeassistant.components.camera import TurboJPEGSingleton from homeassistant.components.event import EventDeviceClass from homeassistant.components.homekit.accessories import HomeBridge from homeassistant.components.homekit.const import ( diff --git a/tests/components/matter/fixtures/nodes/door_lock_with_unbolt.json b/tests/components/matter/fixtures/nodes/door_lock_with_unbolt.json index dfa7794f28bea..f1fb518452e8b 100644 --- a/tests/components/matter/fixtures/nodes/door_lock_with_unbolt.json +++ b/tests/components/matter/fixtures/nodes/door_lock_with_unbolt.json @@ -43,9 +43,9 @@ "0/40/0": 1, "0/40/1": "TEST_VENDOR", "0/40/2": 65521, - "0/40/3": "Mock Door Lock", + "0/40/3": "Mock Door Lock with unbolt", "0/40/4": 32769, - "0/40/5": "Mock Door Lock", + "0/40/5": "Mock Door Lock with unbolt", "0/40/6": "**REDACTED**", "0/40/7": 0, "0/40/8": "TEST_VERSION", diff --git a/tests/components/matter/snapshots/test_binary_sensor.ambr b/tests/components/matter/snapshots/test_binary_sensor.ambr index 5d1c9b029f952..f78ad24bd0180 100644 --- a/tests/components/matter/snapshots/test_binary_sensor.ambr +++ b/tests/components/matter/snapshots/test_binary_sensor.ambr @@ -146,7 +146,7 @@ 'state': 'off', }) # --- -# name: test_binary_sensors[door_lock_with_unbolt][binary_sensor.mock_door_lock_battery-entry] +# name: test_binary_sensors[door_lock_with_unbolt][binary_sensor.mock_door_lock_with_unbolt_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -159,7 +159,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': , - 'entity_id': 'binary_sensor.mock_door_lock_battery', + 'entity_id': 'binary_sensor.mock_door_lock_with_unbolt_battery', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -181,21 +181,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensors[door_lock_with_unbolt][binary_sensor.mock_door_lock_battery-state] +# name: test_binary_sensors[door_lock_with_unbolt][binary_sensor.mock_door_lock_with_unbolt_battery-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'battery', - 'friendly_name': 'Mock Door Lock Battery', + 'friendly_name': 'Mock Door Lock with unbolt Battery', }), 'context': , - 'entity_id': 'binary_sensor.mock_door_lock_battery', + 'entity_id': 'binary_sensor.mock_door_lock_with_unbolt_battery', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_binary_sensors[door_lock_with_unbolt][binary_sensor.mock_door_lock_door-entry] +# name: test_binary_sensors[door_lock_with_unbolt][binary_sensor.mock_door_lock_with_unbolt_door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -208,7 +208,7 @@ 'disabled_by': None, 'domain': 'binary_sensor', 'entity_category': None, - 'entity_id': 'binary_sensor.mock_door_lock_door', + 'entity_id': 'binary_sensor.mock_door_lock_with_unbolt_door', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -230,14 +230,14 @@ 'unit_of_measurement': None, }) # --- -# name: test_binary_sensors[door_lock_with_unbolt][binary_sensor.mock_door_lock_door-state] +# name: test_binary_sensors[door_lock_with_unbolt][binary_sensor.mock_door_lock_with_unbolt_door-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'door', - 'friendly_name': 'Mock Door Lock Door', + 'friendly_name': 'Mock Door Lock with unbolt Door', }), 'context': , - 'entity_id': 'binary_sensor.mock_door_lock_door', + 'entity_id': 'binary_sensor.mock_door_lock_with_unbolt_door', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/matter/snapshots/test_lock.ambr b/tests/components/matter/snapshots/test_lock.ambr index 15cc2354c809a..240bf3e7e3b92 100644 --- a/tests/components/matter/snapshots/test_lock.ambr +++ b/tests/components/matter/snapshots/test_lock.ambr @@ -99,7 +99,7 @@ 'state': 'unlocked', }) # --- -# name: test_locks[door_lock_with_unbolt][lock.mock_door_lock-entry] +# name: test_locks[door_lock_with_unbolt][lock.mock_door_lock_with_unbolt-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -112,7 +112,7 @@ 'disabled_by': None, 'domain': 'lock', 'entity_category': None, - 'entity_id': 'lock.mock_door_lock', + 'entity_id': 'lock.mock_door_lock_with_unbolt', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -134,15 +134,15 @@ 'unit_of_measurement': None, }) # --- -# name: test_locks[door_lock_with_unbolt][lock.mock_door_lock-state] +# name: test_locks[door_lock_with_unbolt][lock.mock_door_lock_with_unbolt-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'changed_by': 'Unknown', - 'friendly_name': 'Mock Door Lock', + 'friendly_name': 'Mock Door Lock with unbolt', 'supported_features': , }), 'context': , - 'entity_id': 'lock.mock_door_lock', + 'entity_id': 'lock.mock_door_lock_with_unbolt', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/matter/snapshots/test_number.ambr b/tests/components/matter/snapshots/test_number.ambr index cd0f0b3fff1b7..bd22389652a91 100644 --- a/tests/components/matter/snapshots/test_number.ambr +++ b/tests/components/matter/snapshots/test_number.ambr @@ -748,7 +748,7 @@ 'state': '3', }) # --- -# name: test_numbers[door_lock_with_unbolt][number.mock_door_lock_autorelock_time-entry] +# name: test_numbers[door_lock_with_unbolt][number.mock_door_lock_with_unbolt_autorelock_time-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -766,7 +766,7 @@ 'disabled_by': None, 'domain': 'number', 'entity_category': , - 'entity_id': 'number.mock_door_lock_autorelock_time', + 'entity_id': 'number.mock_door_lock_with_unbolt_autorelock_time', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -788,10 +788,10 @@ 'unit_of_measurement': , }) # --- -# name: test_numbers[door_lock_with_unbolt][number.mock_door_lock_autorelock_time-state] +# name: test_numbers[door_lock_with_unbolt][number.mock_door_lock_with_unbolt_autorelock_time-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Door Lock Autorelock time', + 'friendly_name': 'Mock Door Lock with unbolt Autorelock time', 'max': 65534, 'min': 0, 'mode': , @@ -799,14 +799,14 @@ 'unit_of_measurement': , }), 'context': , - 'entity_id': 'number.mock_door_lock_autorelock_time', + 'entity_id': 'number.mock_door_lock_with_unbolt_autorelock_time', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '60', }) # --- -# name: test_numbers[door_lock_with_unbolt][number.mock_door_lock_user_code_temporary_disable_time-entry] +# name: test_numbers[door_lock_with_unbolt][number.mock_door_lock_with_unbolt_user_code_temporary_disable_time-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -824,7 +824,7 @@ 'disabled_by': None, 'domain': 'number', 'entity_category': , - 'entity_id': 'number.mock_door_lock_user_code_temporary_disable_time', + 'entity_id': 'number.mock_door_lock_with_unbolt_user_code_temporary_disable_time', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -846,10 +846,10 @@ 'unit_of_measurement': , }) # --- -# name: test_numbers[door_lock_with_unbolt][number.mock_door_lock_user_code_temporary_disable_time-state] +# name: test_numbers[door_lock_with_unbolt][number.mock_door_lock_with_unbolt_user_code_temporary_disable_time-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Door Lock User code temporary disable time', + 'friendly_name': 'Mock Door Lock with unbolt User code temporary disable time', 'max': 255, 'min': 1, 'mode': , @@ -857,14 +857,14 @@ 'unit_of_measurement': , }), 'context': , - 'entity_id': 'number.mock_door_lock_user_code_temporary_disable_time', + 'entity_id': 'number.mock_door_lock_with_unbolt_user_code_temporary_disable_time', 'last_changed': , 'last_reported': , 'last_updated': , 'state': '10', }) # --- -# name: test_numbers[door_lock_with_unbolt][number.mock_door_lock_wrong_code_limit-entry] +# name: test_numbers[door_lock_with_unbolt][number.mock_door_lock_with_unbolt_wrong_code_limit-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -882,7 +882,7 @@ 'disabled_by': None, 'domain': 'number', 'entity_category': , - 'entity_id': 'number.mock_door_lock_wrong_code_limit', + 'entity_id': 'number.mock_door_lock_with_unbolt_wrong_code_limit', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -904,17 +904,17 @@ 'unit_of_measurement': None, }) # --- -# name: test_numbers[door_lock_with_unbolt][number.mock_door_lock_wrong_code_limit-state] +# name: test_numbers[door_lock_with_unbolt][number.mock_door_lock_with_unbolt_wrong_code_limit-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Door Lock Wrong code limit', + 'friendly_name': 'Mock Door Lock with unbolt Wrong code limit', 'max': 255, 'min': 1, 'mode': , 'step': 1, }), 'context': , - 'entity_id': 'number.mock_door_lock_wrong_code_limit', + 'entity_id': 'number.mock_door_lock_with_unbolt_wrong_code_limit', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/matter/snapshots/test_select.ambr b/tests/components/matter/snapshots/test_select.ambr index 5dfc7ba90c009..9f5e5415419fc 100644 --- a/tests/components/matter/snapshots/test_select.ambr +++ b/tests/components/matter/snapshots/test_select.ambr @@ -560,7 +560,7 @@ 'state': 'silent', }) # --- -# name: test_selects[door_lock_with_unbolt][select.mock_door_lock_power_on_behavior_on_startup-entry] +# name: test_selects[door_lock_with_unbolt][select.mock_door_lock_with_unbolt_power_on_behavior_on_startup-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -580,7 +580,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.mock_door_lock_power_on_behavior_on_startup', + 'entity_id': 'select.mock_door_lock_with_unbolt_power_on_behavior_on_startup', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -602,10 +602,10 @@ 'unit_of_measurement': None, }) # --- -# name: test_selects[door_lock_with_unbolt][select.mock_door_lock_power_on_behavior_on_startup-state] +# name: test_selects[door_lock_with_unbolt][select.mock_door_lock_with_unbolt_power_on_behavior_on_startup-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Door Lock Power-on behavior on startup', + 'friendly_name': 'Mock Door Lock with unbolt Power-on behavior on startup', 'options': list([ 'on', 'off', @@ -614,14 +614,14 @@ ]), }), 'context': , - 'entity_id': 'select.mock_door_lock_power_on_behavior_on_startup', + 'entity_id': 'select.mock_door_lock_with_unbolt_power_on_behavior_on_startup', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_selects[door_lock_with_unbolt][select.mock_door_lock_sound_volume-entry] +# name: test_selects[door_lock_with_unbolt][select.mock_door_lock_with_unbolt_sound_volume-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -641,7 +641,7 @@ 'disabled_by': None, 'domain': 'select', 'entity_category': , - 'entity_id': 'select.mock_door_lock_sound_volume', + 'entity_id': 'select.mock_door_lock_with_unbolt_sound_volume', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -663,10 +663,10 @@ 'unit_of_measurement': None, }) # --- -# name: test_selects[door_lock_with_unbolt][select.mock_door_lock_sound_volume-state] +# name: test_selects[door_lock_with_unbolt][select.mock_door_lock_with_unbolt_sound_volume-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Door Lock Sound volume', + 'friendly_name': 'Mock Door Lock with unbolt Sound volume', 'options': list([ 'silent', 'low', @@ -675,7 +675,7 @@ ]), }), 'context': , - 'entity_id': 'select.mock_door_lock_sound_volume', + 'entity_id': 'select.mock_door_lock_with_unbolt_sound_volume', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/matter/snapshots/test_switch.ambr b/tests/components/matter/snapshots/test_switch.ambr index ad13a448030ad..d11680fa73a03 100644 --- a/tests/components/matter/snapshots/test_switch.ambr +++ b/tests/components/matter/snapshots/test_switch.ambr @@ -194,7 +194,7 @@ 'state': 'off', }) # --- -# name: test_switches[door_lock_with_unbolt][switch.mock_door_lock-entry] +# name: test_switches[door_lock_with_unbolt][switch.mock_door_lock_with_unbolt-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -207,7 +207,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': None, - 'entity_id': 'switch.mock_door_lock', + 'entity_id': 'switch.mock_door_lock_with_unbolt', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -229,21 +229,21 @@ 'unit_of_measurement': None, }) # --- -# name: test_switches[door_lock_with_unbolt][switch.mock_door_lock-state] +# name: test_switches[door_lock_with_unbolt][switch.mock_door_lock_with_unbolt-state] StateSnapshot({ 'attributes': ReadOnlyDict({ 'device_class': 'outlet', - 'friendly_name': 'Mock Door Lock', + 'friendly_name': 'Mock Door Lock with unbolt', }), 'context': , - 'entity_id': 'switch.mock_door_lock', + 'entity_id': 'switch.mock_door_lock_with_unbolt', 'last_changed': , 'last_reported': , 'last_updated': , 'state': 'off', }) # --- -# name: test_switches[door_lock_with_unbolt][switch.mock_door_lock_privacy_mode_button-entry] +# name: test_switches[door_lock_with_unbolt][switch.mock_door_lock_with_unbolt_privacy_mode_button-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ }), @@ -256,7 +256,7 @@ 'disabled_by': None, 'domain': 'switch', 'entity_category': , - 'entity_id': 'switch.mock_door_lock_privacy_mode_button', + 'entity_id': 'switch.mock_door_lock_with_unbolt_privacy_mode_button', 'has_entity_name': True, 'hidden_by': None, 'icon': None, @@ -278,13 +278,13 @@ 'unit_of_measurement': None, }) # --- -# name: test_switches[door_lock_with_unbolt][switch.mock_door_lock_privacy_mode_button-state] +# name: test_switches[door_lock_with_unbolt][switch.mock_door_lock_with_unbolt_privacy_mode_button-state] StateSnapshot({ 'attributes': ReadOnlyDict({ - 'friendly_name': 'Mock Door Lock Privacy mode button', + 'friendly_name': 'Mock Door Lock with unbolt Privacy mode button', }), 'context': , - 'entity_id': 'switch.mock_door_lock_privacy_mode_button', + 'entity_id': 'switch.mock_door_lock_with_unbolt_privacy_mode_button', 'last_changed': , 'last_reported': , 'last_updated': , diff --git a/tests/components/matter/test_lock.py b/tests/components/matter/test_lock.py index e6566202c595b..5c652389888e6 100644 --- a/tests/components/matter/test_lock.py +++ b/tests/components/matter/test_lock.py @@ -204,7 +204,7 @@ async def test_lock_with_unbolt( matter_node: MatterNode, ) -> None: """Test door lock.""" - state = hass.states.get("lock.mock_door_lock") + state = hass.states.get("lock.mock_door_lock_with_unbolt") assert state assert state.state == LockState.LOCKED assert state.attributes["supported_features"] & LockEntityFeature.OPEN @@ -213,7 +213,7 @@ async def test_lock_with_unbolt( "lock", "unlock", { - "entity_id": "lock.mock_door_lock", + "entity_id": "lock.mock_door_lock_with_unbolt", }, blocking=True, ) @@ -231,7 +231,7 @@ async def test_lock_with_unbolt( "lock", "open", { - "entity_id": "lock.mock_door_lock", + "entity_id": "lock.mock_door_lock_with_unbolt", }, blocking=True, ) @@ -244,20 +244,20 @@ async def test_lock_with_unbolt( ) await hass.async_block_till_done() - state = hass.states.get("lock.mock_door_lock") + state = hass.states.get("lock.mock_door_lock_with_unbolt") assert state assert state.state == LockState.OPENING set_node_attribute(matter_node, 1, 257, 0, 0) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("lock.mock_door_lock") + state = hass.states.get("lock.mock_door_lock_with_unbolt") assert state assert state.state == LockState.UNLOCKED set_node_attribute(matter_node, 1, 257, 0, 3) await trigger_subscription_callback(hass, matter_client) - state = hass.states.get("lock.mock_door_lock") + state = hass.states.get("lock.mock_door_lock_with_unbolt") assert state assert state.state == LockState.OPEN diff --git a/tests/components/ollama/test_conversation.py b/tests/components/ollama/test_conversation.py index 4e5ddf286ba1d..5a8a2f74bc00a 100644 --- a/tests/components/ollama/test_conversation.py +++ b/tests/components/ollama/test_conversation.py @@ -95,7 +95,10 @@ async def test_chat( ] # AGENT_DETAIL event contains the raw prompt passed to the model detail_event = trace_events[1] - assert "Current time is" in detail_event["data"]["messages"][0]["content"] + assert ( + "You are a voice assistant for Home Assistant." + in detail_event["data"]["messages"][0]["content"] + ) async def test_chat_stream( diff --git a/tests/components/rapt_ble/test_config_flow.py b/tests/components/rapt_ble/test_config_flow.py index 2189b8a610cc9..ef93622bd600f 100644 --- a/tests/components/rapt_ble/test_config_flow.py +++ b/tests/components/rapt_ble/test_config_flow.py @@ -54,6 +54,38 @@ async def test_async_step_user_no_devices_found(hass: HomeAssistant) -> None: assert result["reason"] == "no_devices_found" +async def test_async_step_user_replaces_ignored(hass: HomeAssistant) -> None: + """Test setup from service info cache replaces an ignored entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id=RAPT_MAC, + data={}, + source=config_entries.SOURCE_IGNORE, + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.rapt_ble.config_flow.async_discovered_service_info", + return_value=[COMPLETE_SERVICE_INFO], + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + with patch( + "homeassistant.components.rapt_ble.async_setup_entry", return_value=True + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"address": RAPT_MAC}, + ) + assert result2["type"] is FlowResultType.CREATE_ENTRY + assert result2["title"] == "RAPT Pill 0666" + assert result2["data"] == {} + assert result2["result"].unique_id == RAPT_MAC + + async def test_async_step_user_with_found_devices(hass: HomeAssistant) -> None: """Test setup from service info cache with devices found.""" with patch( diff --git a/tests/components/stream/test_worker.py b/tests/components/stream/test_worker.py index 276b410965248..d6baf53a732bd 100644 --- a/tests/components/stream/test_worker.py +++ b/tests/components/stream/test_worker.py @@ -1005,7 +1005,7 @@ async def test_get_image(hass: HomeAssistant, h264_video, filename) -> None: # Since libjpeg-turbo is not installed on the CI runner, we use a mock with patch( - "homeassistant.components.camera.img_util.TurboJPEGSingleton" + "homeassistant.components.camera.TurboJPEGSingleton" ) as mock_turbo_jpeg_singleton: mock_turbo_jpeg_singleton.instance.return_value = mock_turbo_jpeg() stream = create_stream(hass, h264_video, {}, dynamic_stream_settings()) @@ -1068,7 +1068,7 @@ async def test_get_image_rotated(hass: HomeAssistant, h264_video, filename) -> N # Since libjpeg-turbo is not installed on the CI runner, we use a mock with patch( - "homeassistant.components.camera.img_util.TurboJPEGSingleton" + "homeassistant.components.camera.TurboJPEGSingleton" ) as mock_turbo_jpeg_singleton: mock_turbo_jpeg_singleton.instance.return_value = mock_turbo_jpeg() for orientation in (Orientation.NO_TRANSFORM, Orientation.ROTATE_RIGHT): diff --git a/tests/components/unifiprotect/test_camera.py b/tests/components/unifiprotect/test_camera.py index 9c78e09d26435..8541895e9b3c0 100644 --- a/tests/components/unifiprotect/test_camera.py +++ b/tests/components/unifiprotect/test_camera.py @@ -21,8 +21,8 @@ async_get_image, async_get_stream_source, async_register_webrtc_provider, + get_camera_from_entity_id, ) -from homeassistant.components.camera.helper import get_camera_from_entity_id from homeassistant.components.unifiprotect.const import ( ATTR_BITRATE, ATTR_CHANNEL_ID, diff --git a/tests/components/volvo/snapshots/test_sensor.ambr b/tests/components/volvo/snapshots/test_sensor.ambr index 6075f7400dc01..1e2441f7c070a 100644 --- a/tests/components/volvo/snapshots/test_sensor.ambr +++ b/tests/components/volvo/snapshots/test_sensor.ambr @@ -752,6 +752,82 @@ 'state': '30000', }) # --- +# name: test_sensor[ex30_2024][sensor.volvo_ex30_service-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'distance_driven_almost_time_for_service', + 'distance_driven_overdue_for_service', + 'distance_driven_time_for_service', + 'engine_hours_almost_time_for_service', + 'engine_hours_overdue_for_service', + 'engine_hours_time_for_service', + 'no_warning', + 'regular_maintenance_almost_time_for_service', + 'regular_maintenance_overdue_for_service', + 'regular_maintenance_time_for_service', + 'unknown_warning', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_ex30_service', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Service', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'service_warning', + 'unique_id': 'yv1abcdefg1234567_service_warning', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[ex30_2024][sensor.volvo_ex30_service-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Volvo EX30 Service', + 'options': list([ + 'distance_driven_almost_time_for_service', + 'distance_driven_overdue_for_service', + 'distance_driven_time_for_service', + 'engine_hours_almost_time_for_service', + 'engine_hours_overdue_for_service', + 'engine_hours_time_for_service', + 'no_warning', + 'regular_maintenance_almost_time_for_service', + 'regular_maintenance_overdue_for_service', + 'regular_maintenance_time_for_service', + 'unknown_warning', + ]), + }), + 'context': , + 'entity_id': 'sensor.volvo_ex30_service', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_warning', + }) +# --- # name: test_sensor[ex30_2024][sensor.volvo_ex30_target_battery_charge_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -1591,6 +1667,82 @@ 'state': '30000', }) # --- +# name: test_sensor[s90_diesel_2018][sensor.volvo_s90_service-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'distance_driven_almost_time_for_service', + 'distance_driven_overdue_for_service', + 'distance_driven_time_for_service', + 'engine_hours_almost_time_for_service', + 'engine_hours_overdue_for_service', + 'engine_hours_time_for_service', + 'no_warning', + 'regular_maintenance_almost_time_for_service', + 'regular_maintenance_overdue_for_service', + 'regular_maintenance_time_for_service', + 'unknown_warning', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_s90_service', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Service', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'service_warning', + 'unique_id': 'yv1abcdefg1234567_service_warning', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[s90_diesel_2018][sensor.volvo_s90_service-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Volvo S90 Service', + 'options': list([ + 'distance_driven_almost_time_for_service', + 'distance_driven_overdue_for_service', + 'distance_driven_time_for_service', + 'engine_hours_almost_time_for_service', + 'engine_hours_overdue_for_service', + 'engine_hours_time_for_service', + 'no_warning', + 'regular_maintenance_almost_time_for_service', + 'regular_maintenance_overdue_for_service', + 'regular_maintenance_time_for_service', + 'unknown_warning', + ]), + }), + 'context': , + 'entity_id': 'sensor.volvo_s90_service', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_warning', + }) +# --- # name: test_sensor[s90_diesel_2018][sensor.volvo_s90_time_to_engine_service-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -2791,6 +2943,82 @@ 'state': '30000', }) # --- +# name: test_sensor[xc40_electric_2024][sensor.volvo_xc40_service-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'distance_driven_almost_time_for_service', + 'distance_driven_overdue_for_service', + 'distance_driven_time_for_service', + 'engine_hours_almost_time_for_service', + 'engine_hours_overdue_for_service', + 'engine_hours_time_for_service', + 'no_warning', + 'regular_maintenance_almost_time_for_service', + 'regular_maintenance_overdue_for_service', + 'regular_maintenance_time_for_service', + 'unknown_warning', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc40_service', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Service', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'service_warning', + 'unique_id': 'yv1abcdefg1234567_service_warning', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[xc40_electric_2024][sensor.volvo_xc40_service-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Volvo XC40 Service', + 'options': list([ + 'distance_driven_almost_time_for_service', + 'distance_driven_overdue_for_service', + 'distance_driven_time_for_service', + 'engine_hours_almost_time_for_service', + 'engine_hours_overdue_for_service', + 'engine_hours_time_for_service', + 'no_warning', + 'regular_maintenance_almost_time_for_service', + 'regular_maintenance_overdue_for_service', + 'regular_maintenance_time_for_service', + 'unknown_warning', + ]), + }), + 'context': , + 'entity_id': 'sensor.volvo_xc40_service', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_warning', + }) +# --- # name: test_sensor[xc40_electric_2024][sensor.volvo_xc40_target_battery_charge_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -3865,6 +4093,82 @@ 'state': '30000', }) # --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_service-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'distance_driven_almost_time_for_service', + 'distance_driven_overdue_for_service', + 'distance_driven_time_for_service', + 'engine_hours_almost_time_for_service', + 'engine_hours_overdue_for_service', + 'engine_hours_time_for_service', + 'no_warning', + 'regular_maintenance_almost_time_for_service', + 'regular_maintenance_overdue_for_service', + 'regular_maintenance_time_for_service', + 'unknown_warning', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc60_service', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Service', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'service_warning', + 'unique_id': 'yv1abcdefg1234567_service_warning', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_service-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Volvo XC60 Service', + 'options': list([ + 'distance_driven_almost_time_for_service', + 'distance_driven_overdue_for_service', + 'distance_driven_time_for_service', + 'engine_hours_almost_time_for_service', + 'engine_hours_overdue_for_service', + 'engine_hours_time_for_service', + 'no_warning', + 'regular_maintenance_almost_time_for_service', + 'regular_maintenance_overdue_for_service', + 'regular_maintenance_time_for_service', + 'unknown_warning', + ]), + }), + 'context': , + 'entity_id': 'sensor.volvo_xc60_service', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_warning', + }) +# --- # name: test_sensor[xc60_phev_2020][sensor.volvo_xc60_target_battery_charge_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -4648,6 +4952,82 @@ 'state': '30000', }) # --- +# name: test_sensor[xc90_petrol_2019][sensor.volvo_xc90_service-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'distance_driven_almost_time_for_service', + 'distance_driven_overdue_for_service', + 'distance_driven_time_for_service', + 'engine_hours_almost_time_for_service', + 'engine_hours_overdue_for_service', + 'engine_hours_time_for_service', + 'no_warning', + 'regular_maintenance_almost_time_for_service', + 'regular_maintenance_overdue_for_service', + 'regular_maintenance_time_for_service', + 'unknown_warning', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc90_service', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Service', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'service_warning', + 'unique_id': 'yv1abcdefg1234567_service_warning', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[xc90_petrol_2019][sensor.volvo_xc90_service-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Volvo XC90 Service', + 'options': list([ + 'distance_driven_almost_time_for_service', + 'distance_driven_overdue_for_service', + 'distance_driven_time_for_service', + 'engine_hours_almost_time_for_service', + 'engine_hours_overdue_for_service', + 'engine_hours_time_for_service', + 'no_warning', + 'regular_maintenance_almost_time_for_service', + 'regular_maintenance_overdue_for_service', + 'regular_maintenance_time_for_service', + 'unknown_warning', + ]), + }), + 'context': , + 'entity_id': 'sensor.volvo_xc90_service', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_warning', + }) +# --- # name: test_sensor[xc90_petrol_2019][sensor.volvo_xc90_time_to_engine_service-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5848,6 +6228,82 @@ 'state': '30000', }) # --- +# name: test_sensor[xc90_phev_2024][sensor.volvo_xc90_service-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'distance_driven_almost_time_for_service', + 'distance_driven_overdue_for_service', + 'distance_driven_time_for_service', + 'engine_hours_almost_time_for_service', + 'engine_hours_overdue_for_service', + 'engine_hours_time_for_service', + 'no_warning', + 'regular_maintenance_almost_time_for_service', + 'regular_maintenance_overdue_for_service', + 'regular_maintenance_time_for_service', + 'unknown_warning', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.volvo_xc90_service', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Service', + 'platform': 'volvo', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'service_warning', + 'unique_id': 'yv1abcdefg1234567_service_warning', + 'unit_of_measurement': None, + }) +# --- +# name: test_sensor[xc90_phev_2024][sensor.volvo_xc90_service-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'enum', + 'friendly_name': 'Volvo XC90 Service', + 'options': list([ + 'distance_driven_almost_time_for_service', + 'distance_driven_overdue_for_service', + 'distance_driven_time_for_service', + 'engine_hours_almost_time_for_service', + 'engine_hours_overdue_for_service', + 'engine_hours_time_for_service', + 'no_warning', + 'regular_maintenance_almost_time_for_service', + 'regular_maintenance_overdue_for_service', + 'regular_maintenance_time_for_service', + 'unknown_warning', + ]), + }), + 'context': , + 'entity_id': 'sensor.volvo_xc90_service', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'no_warning', + }) +# --- # name: test_sensor[xc90_phev_2024][sensor.volvo_xc90_target_battery_charge_level-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/helpers/test_llm.py b/tests/helpers/test_llm.py index accc681ca9d14..4fbb38e2aa0ad 100644 --- a/tests/helpers/test_llm.py +++ b/tests/helpers/test_llm.py @@ -183,19 +183,23 @@ class MyIntentHandler(intent.IntentHandler): assert len(llm.async_get_apis(hass)) == 1 api = await llm.async_get_api(hass, "assist", llm_context) - assert [tool.name for tool in api.tools] == ["GetLiveContext"] + assert [tool.name for tool in api.tools] == ["GetDateTime", "GetLiveContext"] # Match all intent_handler.platforms = None api = await llm.async_get_api(hass, "assist", llm_context) - assert [tool.name for tool in api.tools] == ["test_intent", "GetLiveContext"] + assert [tool.name for tool in api.tools] == [ + "test_intent", + "GetDateTime", + "GetLiveContext", + ] # Match specific domain intent_handler.platforms = {"light"} api = await llm.async_get_api(hass, "assist", llm_context) - assert len(api.tools) == 2 + assert len(api.tools) == 3 tool = api.tools[0] assert tool.name == "test_intent" assert tool.description == "Execute Home Assistant test_intent intent" @@ -374,6 +378,7 @@ class MyIntentHandler(intent.IntentHandler): "HassUnpauseTimer", "HassTimerStatus", "Super_crazy_intent_with_unique_name", + "GetDateTime", ] @@ -390,7 +395,7 @@ class MyIntentHandler(intent.IntentHandler): assert len(llm.async_get_apis(hass)) == 1 api = await llm.async_get_api(hass, "assist", llm_context) - assert len(api.tools) == 1 + assert len(api.tools) == 2 tool = api.tools[0] assert tool.name == "test_intent" assert tool.description == "my intent handler" @@ -1463,6 +1468,41 @@ async def test_todo_get_items_tool(hass: HomeAssistant) -> None: } +async def test_get_date_time_tool(hass: HomeAssistant) -> None: + """Test the GetDateTime tool.""" + + assert await async_setup_component(hass, "homeassistant", {}) + context = Context() + llm_context = llm.LLMContext( + platform="test_platform", + context=context, + language="*", + assistant="conversation", + device_id=None, + ) + api = await llm.async_get_api(hass, "assist", llm_context) + tool = next((tool for tool in api.tools if tool.name == "GetDateTime"), None) + assert tool is not None + + now = dt_util.parse_datetime("2025-09-22 12:30:45Z") + + with patch("homeassistant.util.dt.now", return_value=now): + result = await tool.async_call( + hass, + llm.ToolInput("GetDateTime", {}), + llm_context, + ) + assert result == { + "success": True, + "result": { + "date": "2025-09-22", + "time": "12:30:45", + "timezone": "UTC", + "weekday": "Monday", + }, + } + + async def test_no_tools_exposed(hass: HomeAssistant) -> None: """Test that tools are not exposed when no entities are exposed.""" assert await async_setup_component(hass, "homeassistant", {}) @@ -1475,7 +1515,7 @@ async def test_no_tools_exposed(hass: HomeAssistant) -> None: device_id=None, ) api = await llm.async_get_api(hass, "assist", llm_context) - assert api.tools == [] + assert [tool.name for tool in api.tools] == ["GetDateTime"] async def test_merged_api(hass: HomeAssistant, llm_context: llm.LLMContext) -> None: