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:
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

View File

@ -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:

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",
"loggers": ["onedrive_personal_sdk"],
"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",
"loggers": ["roborock"],
"requirements": [
"python-roborock==2.9.7",
"python-roborock==2.11.1",
"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
# 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)
):

View File

@ -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"]
}

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,
"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",

6
requirements_all.txt generated
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"]

View File

@ -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(

View File

@ -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(

View File

@ -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",

View File

@ -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()