diff --git a/homeassistant/components/assist_satellite/entity.py b/homeassistant/components/assist_satellite/entity.py index c901bc7d928..902cf731a5d 100644 --- a/homeassistant/components/assist_satellite/entity.py +++ b/homeassistant/components/assist_satellite/entity.py @@ -274,6 +274,11 @@ class AssistSatelliteEntity(entity.Entity): try: await self.async_start_conversation(announcement) + except Exception: + # Clear prompt on error + self._conversation_id = None + self._extra_system_prompt = None + raise finally: self._is_announcing = False diff --git a/homeassistant/components/hassio/backup.py b/homeassistant/components/hassio/backup.py index 34d1c62aed7..4aad984cc54 100644 --- a/homeassistant/components/hassio/backup.py +++ b/homeassistant/components/hassio/backup.py @@ -354,6 +354,7 @@ class SupervisorBackupReaderWriter(BackupReaderWriter): """Wait for a backup to complete.""" backup_complete = asyncio.Event() backup_id: str | None = None + create_errors: list[dict[str, str]] = [] @callback def on_job_progress(data: Mapping[str, Any]) -> None: @@ -361,6 +362,7 @@ class SupervisorBackupReaderWriter(BackupReaderWriter): nonlocal backup_id if data.get("done") is True: backup_id = data.get("reference") + create_errors.extend(data.get("errors", [])) backup_complete.set() unsub = self._async_listen_job_events(backup.job_id, on_job_progress) @@ -369,8 +371,11 @@ class SupervisorBackupReaderWriter(BackupReaderWriter): await backup_complete.wait() finally: unsub() - if not backup_id: - raise BackupReaderWriterError("Backup failed") + if not backup_id or create_errors: + # We should add more specific error handling here in the future + raise BackupReaderWriterError( + f"Backup failed: {create_errors or 'no backup_id'}" + ) async def open_backup() -> AsyncIterator[bytes]: try: diff --git a/homeassistant/components/heicko/__init__.py b/homeassistant/components/heicko/__init__.py new file mode 100644 index 00000000000..65c527f5252 --- /dev/null +++ b/homeassistant/components/heicko/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Heicko.""" diff --git a/homeassistant/components/heicko/manifest.json b/homeassistant/components/heicko/manifest.json new file mode 100644 index 00000000000..d8f939a5bed --- /dev/null +++ b/homeassistant/components/heicko/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "heicko", + "name": "Heicko", + "integration_type": "virtual", + "supported_by": "motion_blinds" +} diff --git a/homeassistant/components/onedrive/manifest.json b/homeassistant/components/onedrive/manifest.json index 767426058c1..263c73a9f69 100644 --- a/homeassistant/components/onedrive/manifest.json +++ b/homeassistant/components/onedrive/manifest.json @@ -9,5 +9,5 @@ "iot_class": "cloud_polling", "loggers": ["onedrive_personal_sdk"], "quality_scale": "bronze", - "requirements": ["onedrive-personal-sdk==0.0.1"] + "requirements": ["onedrive-personal-sdk==0.0.2"] } diff --git a/homeassistant/components/roborock/manifest.json b/homeassistant/components/roborock/manifest.json index 76d7ab98a34..db2654d4baa 100644 --- a/homeassistant/components/roborock/manifest.json +++ b/homeassistant/components/roborock/manifest.json @@ -7,7 +7,7 @@ "iot_class": "local_polling", "loggers": ["roborock"], "requirements": [ - "python-roborock==2.9.7", + "python-roborock==2.11.1", "vacuum-map-parser-roborock==0.1.2" ] } diff --git a/homeassistant/components/smart_rollos/__init__.py b/homeassistant/components/smart_rollos/__init__.py new file mode 100644 index 00000000000..d4bb8c7fb1b --- /dev/null +++ b/homeassistant/components/smart_rollos/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Smart Rollos.""" diff --git a/homeassistant/components/smart_rollos/manifest.json b/homeassistant/components/smart_rollos/manifest.json new file mode 100644 index 00000000000..f093f740bd6 --- /dev/null +++ b/homeassistant/components/smart_rollos/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "smart_rollos", + "name": "Smart Rollos", + "integration_type": "virtual", + "supported_by": "motion_blinds" +} diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 94581439ae9..8c61394d300 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -541,9 +541,8 @@ class TodoistProjectData: return None # All task Labels (optional parameter). - task[LABELS] = [ - label.name for label in self._labels if label.name in data.labels - ] + labels = data.labels or [] + task[LABELS] = [label.name for label in self._labels if label.name in labels] if self._label_whitelist and ( not any(label in task[LABELS] for label in self._label_whitelist) ): diff --git a/homeassistant/components/todoist/manifest.json b/homeassistant/components/todoist/manifest.json index 72d76108353..791f5642aad 100644 --- a/homeassistant/components/todoist/manifest.json +++ b/homeassistant/components/todoist/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/todoist", "iot_class": "cloud_polling", "loggers": ["todoist"], - "requirements": ["todoist-api-python==2.1.2"] + "requirements": ["todoist-api-python==2.1.7"] } diff --git a/homeassistant/components/ublockout/__init__.py b/homeassistant/components/ublockout/__init__.py new file mode 100644 index 00000000000..87127e331da --- /dev/null +++ b/homeassistant/components/ublockout/__init__.py @@ -0,0 +1 @@ +"""Virtual integration: Ublockout.""" diff --git a/homeassistant/components/ublockout/manifest.json b/homeassistant/components/ublockout/manifest.json new file mode 100644 index 00000000000..d5ef46b8ad2 --- /dev/null +++ b/homeassistant/components/ublockout/manifest.json @@ -0,0 +1,6 @@ +{ + "domain": "ublockout", + "name": "Ublockout", + "integration_type": "virtual", + "supported_by": "motion_blinds" +} diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 0fd2dbbb87a..d96f902d789 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -2521,6 +2521,11 @@ "config_flow": false, "iot_class": "local_polling" }, + "heicko": { + "name": "Heicko", + "integration_type": "virtual", + "supported_by": "motion_blinds" + }, "heiwa": { "name": "Heiwa", "integration_type": "virtual", @@ -5812,6 +5817,11 @@ "config_flow": true, "iot_class": "cloud_polling" }, + "smart_rollos": { + "name": "Smart Rollos", + "integration_type": "virtual", + "supported_by": "motion_blinds" + }, "smarther": { "name": "Smarther", "integration_type": "virtual", @@ -6747,6 +6757,11 @@ "integration_type": "virtual", "supported_by": "overkiz" }, + "ublockout": { + "name": "Ublockout", + "integration_type": "virtual", + "supported_by": "motion_blinds" + }, "uk_transport": { "name": "UK Transport", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index 06ba3bf5c88..537778b9dae 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1559,7 +1559,7 @@ omnilogic==0.4.5 ondilo==0.5.0 # homeassistant.components.onedrive -onedrive-personal-sdk==0.0.1 +onedrive-personal-sdk==0.0.2 # homeassistant.components.onvif onvif-zeep-async==3.2.5 @@ -2452,7 +2452,7 @@ python-rabbitair==0.0.8 python-ripple-api==0.0.3 # homeassistant.components.roborock -python-roborock==2.9.7 +python-roborock==2.11.1 # homeassistant.components.smarttub python-smarttub==0.0.38 @@ -2899,7 +2899,7 @@ tilt-ble==0.2.3 tmb==0.0.4 # homeassistant.components.todoist -todoist-api-python==2.1.2 +todoist-api-python==2.1.7 # homeassistant.components.tolo tololib==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 575e6f6b404..eb4ed113467 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1304,7 +1304,7 @@ omnilogic==0.4.5 ondilo==0.5.0 # homeassistant.components.onedrive -onedrive-personal-sdk==0.0.1 +onedrive-personal-sdk==0.0.2 # homeassistant.components.onvif onvif-zeep-async==3.2.5 @@ -1982,7 +1982,7 @@ python-picnic-api==1.1.0 python-rabbitair==0.0.8 # homeassistant.components.roborock -python-roborock==2.9.7 +python-roborock==2.11.1 # homeassistant.components.smarttub python-smarttub==0.0.38 @@ -2321,7 +2321,7 @@ thinqconnect==1.0.2 tilt-ble==0.2.3 # homeassistant.components.todoist -todoist-api-python==2.1.2 +todoist-api-python==2.1.7 # homeassistant.components.tolo tololib==1.1.0 diff --git a/tests/components/eheimdigital/conftest.py b/tests/components/eheimdigital/conftest.py index ef52eade9ae..afb97b97569 100644 --- a/tests/components/eheimdigital/conftest.py +++ b/tests/components/eheimdigital/conftest.py @@ -34,6 +34,7 @@ def classic_led_ctrl_mock(): ) classic_led_ctrl_mock.name = "Mock classicLEDcontrol+e" classic_led_ctrl_mock.aquarium_name = "Mock Aquarium" + classic_led_ctrl_mock.sw_version = "1.0.0_1.0.0" classic_led_ctrl_mock.light_mode = LightMode.DAYCL_MODE classic_led_ctrl_mock.light_level = (10, 39) return classic_led_ctrl_mock @@ -47,6 +48,7 @@ def heater_mock(): heater_mock.device_type = EheimDeviceType.VERSION_EHEIM_EXT_HEATER heater_mock.name = "Mock Heater" heater_mock.aquarium_name = "Mock Aquarium" + heater_mock.sw_version = "1.0.0_1.0.0" heater_mock.temperature_unit = HeaterUnit.CELSIUS heater_mock.current_temperature = 24.2 heater_mock.target_temperature = 25.5 diff --git a/tests/components/hassio/test_backup.py b/tests/components/hassio/test_backup.py index f35ddeaabbd..ab3335e00dc 100644 --- a/tests/components/hassio/test_backup.py +++ b/tests/components/hassio/test_backup.py @@ -1360,11 +1360,40 @@ async def test_reader_writer_create_partial_backup_error( assert supervisor_client.backups.partial_backup.call_count == 1 +@pytest.mark.parametrize( + "supervisor_event", + [ + # Missing backup reference + { + "event": "job", + "data": { + "done": True, + "uuid": TEST_JOB_ID, + }, + }, + # Errors + { + "event": "job", + "data": { + "done": True, + "errors": [ + { + "type": "BackupMountDownError", + "message": "test_mount is down, cannot back-up to it", + } + ], + "uuid": TEST_JOB_ID, + "reference": "test_slug", + }, + }, + ], +) @pytest.mark.usefixtures("hassio_client", "setup_integration") async def test_reader_writer_create_missing_reference_error( hass: HomeAssistant, hass_ws_client: WebSocketGenerator, supervisor_client: AsyncMock, + supervisor_event: dict[str, Any], ) -> None: """Test missing reference error when generating a backup.""" client = await hass_ws_client(hass) @@ -1395,13 +1424,7 @@ async def test_reader_writer_create_missing_reference_error( assert supervisor_client.backups.partial_backup.call_count == 1 await client.send_json_auto_id( - { - "type": "supervisor/event", - "data": { - "event": "job", - "data": {"done": True, "uuid": TEST_JOB_ID}, - }, - } + {"type": "supervisor/event", "data": supervisor_event} ) response = await client.receive_json() assert response["success"] diff --git a/tests/components/mqtt/conftest.py b/tests/components/mqtt/conftest.py index 2a1e4012f51..87bbcecebe5 100644 --- a/tests/components/mqtt/conftest.py +++ b/tests/components/mqtt/conftest.py @@ -38,7 +38,7 @@ def temp_dir_prefix() -> str: return "test" -@pytest.fixture +@pytest.fixture(autouse=True) def mock_temp_dir(temp_dir_prefix: str) -> Generator[str]: """Mock the certificate temp directory.""" with patch( diff --git a/tests/components/mqtt/test_util.py b/tests/components/mqtt/test_util.py index dd72902056d..df91764b0fb 100644 --- a/tests/components/mqtt/test_util.py +++ b/tests/components/mqtt/test_util.py @@ -4,7 +4,6 @@ import asyncio from collections.abc import Callable from datetime import timedelta from pathlib import Path -from random import getrandbits import shutil import tempfile from unittest.mock import MagicMock, patch @@ -199,7 +198,6 @@ async def test_reading_non_exitisting_certificate_file() -> None: ) -@pytest.mark.parametrize("temp_dir_prefix", "unknown") async def test_return_default_get_file_path( hass: HomeAssistant, mock_temp_dir: str ) -> None: @@ -211,12 +209,8 @@ async def test_return_default_get_file_path( and mqtt.util.get_file_path("some_option", "mydefault") == "mydefault" ) - with patch( - "homeassistant.components.mqtt.util.TEMP_DIR_NAME", - f"home-assistant-mqtt-other-{getrandbits(10):03x}", - ) as temp_dir_name: - tempdir = Path(tempfile.gettempdir()) / temp_dir_name - assert await hass.async_add_executor_job(_get_file_path, tempdir) + temp_dir = Path(tempfile.gettempdir()) / mock_temp_dir + assert await hass.async_add_executor_job(_get_file_path, temp_dir) async def test_waiting_for_client_not_loaded( diff --git a/tests/components/todoist/conftest.py b/tests/components/todoist/conftest.py index 4b2bfea2e30..84f0fa740e9 100644 --- a/tests/components/todoist/conftest.py +++ b/tests/components/todoist/conftest.py @@ -70,6 +70,7 @@ def make_api_task( section_id=None, url="https://todoist.com", sync_id=None, + duration=None, ) @@ -94,6 +95,7 @@ def mock_api(tasks: list[Task]) -> AsyncMock: url="", is_inbox_project=False, is_team_inbox=False, + can_assign_tasks=False, order=1, parent_id=None, view_style="list", diff --git a/tests/components/voip/test_voip.py b/tests/components/voip/test_voip.py index 442f4a62392..3e3e5337417 100644 --- a/tests/components/voip/test_voip.py +++ b/tests/components/voip/test_voip.py @@ -1084,3 +1084,90 @@ async def test_start_conversation( # Wait for TTS await tts_sent.wait() await conversation_task + + +@pytest.mark.usefixtures("socket_enabled") +async def test_start_conversation_user_doesnt_pick_up( + hass: HomeAssistant, + voip_devices: VoIPDevices, + voip_device: VoIPDevice, +) -> None: + """Test start conversation when the user doesn't pick up.""" + assert await async_setup_component(hass, "voip", {}) + + pipeline = assist_pipeline.Pipeline( + conversation_engine="test engine", + conversation_language="en", + language="en", + name="test pipeline", + stt_engine="test stt", + stt_language="en", + tts_engine="test tts", + tts_language="en", + tts_voice=None, + wake_word_entity=None, + wake_word_id=None, + ) + + satellite = async_get_satellite_entity(hass, voip.DOMAIN, voip_device.voip_id) + assert isinstance(satellite, VoipAssistSatellite) + assert ( + satellite.supported_features + & assist_satellite.AssistSatelliteEntityFeature.START_CONVERSATION + ) + + # Protocol has already been mocked, but "outgoing_call" is not async + mock_protocol: AsyncMock = hass.data[DOMAIN].protocol + mock_protocol.outgoing_call = Mock() + + pipeline_started = asyncio.Event() + + async def async_pipeline_from_audio_stream( + hass: HomeAssistant, + context: Context, + *args, + conversation_extra_system_prompt: str | None = None, + **kwargs, + ): + # System prompt should be not be set due to timeout (user not picking up) + assert conversation_extra_system_prompt is None + + pipeline_started.set() + + with ( + patch( + "homeassistant.components.assist_satellite.entity.async_get_pipeline", + return_value=pipeline, + ), + patch( + "homeassistant.components.voip.assist_satellite.VoipAssistSatellite.async_start_conversation", + side_effect=TimeoutError, + ), + patch( + "homeassistant.components.assist_satellite.entity.async_pipeline_from_audio_stream", + new=async_pipeline_from_audio_stream, + ), + patch( + "homeassistant.components.assist_satellite.entity.tts_generate_media_source_id", + return_value="test media id", + ), + ): + satellite.transport = Mock() + + # Error should clear system prompt + with pytest.raises(TimeoutError): + await hass.services.async_call( + assist_satellite.DOMAIN, + "start_conversation", + { + "entity_id": satellite.entity_id, + "start_message": "test announcement", + "extra_system_prompt": "test prompt", + }, + blocking=True, + ) + + # Trigger a pipeline so we can check if the system prompt was cleared + satellite.on_chunk(bytes(_ONE_SECOND)) + async with asyncio.timeout(1): + await pipeline_started.wait()