This commit is contained in:
Jonh Sady 2025-02-03 18:56:32 -03:00
commit c0db1c8f81
21 changed files with 183 additions and 30 deletions

View File

@ -274,6 +274,11 @@ class AssistSatelliteEntity(entity.Entity):
try: try:
await self.async_start_conversation(announcement) await self.async_start_conversation(announcement)
except Exception:
# Clear prompt on error
self._conversation_id = None
self._extra_system_prompt = None
raise
finally: finally:
self._is_announcing = False self._is_announcing = False

View File

@ -354,6 +354,7 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
"""Wait for a backup to complete.""" """Wait for a backup to complete."""
backup_complete = asyncio.Event() backup_complete = asyncio.Event()
backup_id: str | None = None backup_id: str | None = None
create_errors: list[dict[str, str]] = []
@callback @callback
def on_job_progress(data: Mapping[str, Any]) -> None: def on_job_progress(data: Mapping[str, Any]) -> None:
@ -361,6 +362,7 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
nonlocal backup_id nonlocal backup_id
if data.get("done") is True: if data.get("done") is True:
backup_id = data.get("reference") backup_id = data.get("reference")
create_errors.extend(data.get("errors", []))
backup_complete.set() backup_complete.set()
unsub = self._async_listen_job_events(backup.job_id, on_job_progress) unsub = self._async_listen_job_events(backup.job_id, on_job_progress)
@ -369,8 +371,11 @@ class SupervisorBackupReaderWriter(BackupReaderWriter):
await backup_complete.wait() await backup_complete.wait()
finally: finally:
unsub() unsub()
if not backup_id: if not backup_id or create_errors:
raise BackupReaderWriterError("Backup failed") # 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]: async def open_backup() -> AsyncIterator[bytes]:
try: try:

View File

@ -0,0 +1 @@
"""Virtual integration: Heicko."""

View File

@ -0,0 +1,6 @@
{
"domain": "heicko",
"name": "Heicko",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -9,5 +9,5 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["onedrive_personal_sdk"], "loggers": ["onedrive_personal_sdk"],
"quality_scale": "bronze", "quality_scale": "bronze",
"requirements": ["onedrive-personal-sdk==0.0.1"] "requirements": ["onedrive-personal-sdk==0.0.2"]
} }

View File

@ -7,7 +7,7 @@
"iot_class": "local_polling", "iot_class": "local_polling",
"loggers": ["roborock"], "loggers": ["roborock"],
"requirements": [ "requirements": [
"python-roborock==2.9.7", "python-roborock==2.11.1",
"vacuum-map-parser-roborock==0.1.2" "vacuum-map-parser-roborock==0.1.2"
] ]
} }

View File

@ -0,0 +1 @@
"""Virtual integration: Smart Rollos."""

View File

@ -0,0 +1,6 @@
{
"domain": "smart_rollos",
"name": "Smart Rollos",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -541,9 +541,8 @@ class TodoistProjectData:
return None return None
# All task Labels (optional parameter). # All task Labels (optional parameter).
task[LABELS] = [ labels = data.labels or []
label.name for label in self._labels if label.name in data.labels task[LABELS] = [label.name for label in self._labels if label.name in labels]
]
if self._label_whitelist and ( if self._label_whitelist and (
not any(label in task[LABELS] for label in self._label_whitelist) not any(label in task[LABELS] for label in self._label_whitelist)
): ):

View File

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/todoist", "documentation": "https://www.home-assistant.io/integrations/todoist",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"loggers": ["todoist"], "loggers": ["todoist"],
"requirements": ["todoist-api-python==2.1.2"] "requirements": ["todoist-api-python==2.1.7"]
} }

View File

@ -0,0 +1 @@
"""Virtual integration: Ublockout."""

View File

@ -0,0 +1,6 @@
{
"domain": "ublockout",
"name": "Ublockout",
"integration_type": "virtual",
"supported_by": "motion_blinds"
}

View File

@ -2521,6 +2521,11 @@
"config_flow": false, "config_flow": false,
"iot_class": "local_polling" "iot_class": "local_polling"
}, },
"heicko": {
"name": "Heicko",
"integration_type": "virtual",
"supported_by": "motion_blinds"
},
"heiwa": { "heiwa": {
"name": "Heiwa", "name": "Heiwa",
"integration_type": "virtual", "integration_type": "virtual",
@ -5812,6 +5817,11 @@
"config_flow": true, "config_flow": true,
"iot_class": "cloud_polling" "iot_class": "cloud_polling"
}, },
"smart_rollos": {
"name": "Smart Rollos",
"integration_type": "virtual",
"supported_by": "motion_blinds"
},
"smarther": { "smarther": {
"name": "Smarther", "name": "Smarther",
"integration_type": "virtual", "integration_type": "virtual",
@ -6747,6 +6757,11 @@
"integration_type": "virtual", "integration_type": "virtual",
"supported_by": "overkiz" "supported_by": "overkiz"
}, },
"ublockout": {
"name": "Ublockout",
"integration_type": "virtual",
"supported_by": "motion_blinds"
},
"uk_transport": { "uk_transport": {
"name": "UK Transport", "name": "UK Transport",
"integration_type": "hub", "integration_type": "hub",

6
requirements_all.txt generated
View File

@ -1559,7 +1559,7 @@ omnilogic==0.4.5
ondilo==0.5.0 ondilo==0.5.0
# homeassistant.components.onedrive # homeassistant.components.onedrive
onedrive-personal-sdk==0.0.1 onedrive-personal-sdk==0.0.2
# homeassistant.components.onvif # homeassistant.components.onvif
onvif-zeep-async==3.2.5 onvif-zeep-async==3.2.5
@ -2452,7 +2452,7 @@ python-rabbitair==0.0.8
python-ripple-api==0.0.3 python-ripple-api==0.0.3
# homeassistant.components.roborock # homeassistant.components.roborock
python-roborock==2.9.7 python-roborock==2.11.1
# homeassistant.components.smarttub # homeassistant.components.smarttub
python-smarttub==0.0.38 python-smarttub==0.0.38
@ -2899,7 +2899,7 @@ tilt-ble==0.2.3
tmb==0.0.4 tmb==0.0.4
# homeassistant.components.todoist # homeassistant.components.todoist
todoist-api-python==2.1.2 todoist-api-python==2.1.7
# homeassistant.components.tolo # homeassistant.components.tolo
tololib==1.1.0 tololib==1.1.0

View File

@ -1304,7 +1304,7 @@ omnilogic==0.4.5
ondilo==0.5.0 ondilo==0.5.0
# homeassistant.components.onedrive # homeassistant.components.onedrive
onedrive-personal-sdk==0.0.1 onedrive-personal-sdk==0.0.2
# homeassistant.components.onvif # homeassistant.components.onvif
onvif-zeep-async==3.2.5 onvif-zeep-async==3.2.5
@ -1982,7 +1982,7 @@ python-picnic-api==1.1.0
python-rabbitair==0.0.8 python-rabbitair==0.0.8
# homeassistant.components.roborock # homeassistant.components.roborock
python-roborock==2.9.7 python-roborock==2.11.1
# homeassistant.components.smarttub # homeassistant.components.smarttub
python-smarttub==0.0.38 python-smarttub==0.0.38
@ -2321,7 +2321,7 @@ thinqconnect==1.0.2
tilt-ble==0.2.3 tilt-ble==0.2.3
# homeassistant.components.todoist # homeassistant.components.todoist
todoist-api-python==2.1.2 todoist-api-python==2.1.7
# homeassistant.components.tolo # homeassistant.components.tolo
tololib==1.1.0 tololib==1.1.0

View File

@ -34,6 +34,7 @@ def classic_led_ctrl_mock():
) )
classic_led_ctrl_mock.name = "Mock classicLEDcontrol+e" classic_led_ctrl_mock.name = "Mock classicLEDcontrol+e"
classic_led_ctrl_mock.aquarium_name = "Mock Aquarium" 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_mode = LightMode.DAYCL_MODE
classic_led_ctrl_mock.light_level = (10, 39) classic_led_ctrl_mock.light_level = (10, 39)
return classic_led_ctrl_mock return classic_led_ctrl_mock
@ -47,6 +48,7 @@ def heater_mock():
heater_mock.device_type = EheimDeviceType.VERSION_EHEIM_EXT_HEATER heater_mock.device_type = EheimDeviceType.VERSION_EHEIM_EXT_HEATER
heater_mock.name = "Mock Heater" heater_mock.name = "Mock Heater"
heater_mock.aquarium_name = "Mock Aquarium" heater_mock.aquarium_name = "Mock Aquarium"
heater_mock.sw_version = "1.0.0_1.0.0"
heater_mock.temperature_unit = HeaterUnit.CELSIUS heater_mock.temperature_unit = HeaterUnit.CELSIUS
heater_mock.current_temperature = 24.2 heater_mock.current_temperature = 24.2
heater_mock.target_temperature = 25.5 heater_mock.target_temperature = 25.5

View File

@ -1360,11 +1360,40 @@ async def test_reader_writer_create_partial_backup_error(
assert supervisor_client.backups.partial_backup.call_count == 1 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") @pytest.mark.usefixtures("hassio_client", "setup_integration")
async def test_reader_writer_create_missing_reference_error( async def test_reader_writer_create_missing_reference_error(
hass: HomeAssistant, hass: HomeAssistant,
hass_ws_client: WebSocketGenerator, hass_ws_client: WebSocketGenerator,
supervisor_client: AsyncMock, supervisor_client: AsyncMock,
supervisor_event: dict[str, Any],
) -> None: ) -> None:
"""Test missing reference error when generating a backup.""" """Test missing reference error when generating a backup."""
client = await hass_ws_client(hass) 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 assert supervisor_client.backups.partial_backup.call_count == 1
await client.send_json_auto_id( await client.send_json_auto_id(
{ {"type": "supervisor/event", "data": supervisor_event}
"type": "supervisor/event",
"data": {
"event": "job",
"data": {"done": True, "uuid": TEST_JOB_ID},
},
}
) )
response = await client.receive_json() response = await client.receive_json()
assert response["success"] assert response["success"]

View File

@ -38,7 +38,7 @@ def temp_dir_prefix() -> str:
return "test" return "test"
@pytest.fixture @pytest.fixture(autouse=True)
def mock_temp_dir(temp_dir_prefix: str) -> Generator[str]: def mock_temp_dir(temp_dir_prefix: str) -> Generator[str]:
"""Mock the certificate temp directory.""" """Mock the certificate temp directory."""
with patch( with patch(

View File

@ -4,7 +4,6 @@ import asyncio
from collections.abc import Callable from collections.abc import Callable
from datetime import timedelta from datetime import timedelta
from pathlib import Path from pathlib import Path
from random import getrandbits
import shutil import shutil
import tempfile import tempfile
from unittest.mock import MagicMock, patch 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( async def test_return_default_get_file_path(
hass: HomeAssistant, mock_temp_dir: str hass: HomeAssistant, mock_temp_dir: str
) -> None: ) -> None:
@ -211,12 +209,8 @@ async def test_return_default_get_file_path(
and mqtt.util.get_file_path("some_option", "mydefault") == "mydefault" and mqtt.util.get_file_path("some_option", "mydefault") == "mydefault"
) )
with patch( temp_dir = Path(tempfile.gettempdir()) / mock_temp_dir
"homeassistant.components.mqtt.util.TEMP_DIR_NAME", assert await hass.async_add_executor_job(_get_file_path, temp_dir)
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)
async def test_waiting_for_client_not_loaded( async def test_waiting_for_client_not_loaded(

View File

@ -70,6 +70,7 @@ def make_api_task(
section_id=None, section_id=None,
url="https://todoist.com", url="https://todoist.com",
sync_id=None, sync_id=None,
duration=None,
) )
@ -94,6 +95,7 @@ def mock_api(tasks: list[Task]) -> AsyncMock:
url="", url="",
is_inbox_project=False, is_inbox_project=False,
is_team_inbox=False, is_team_inbox=False,
can_assign_tasks=False,
order=1, order=1,
parent_id=None, parent_id=None,
view_style="list", view_style="list",

View File

@ -1084,3 +1084,90 @@ async def test_start_conversation(
# Wait for TTS # Wait for TTS
await tts_sent.wait() await tts_sent.wait()
await conversation_task 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()