"""Test repairs handling for Shelly.""" from typing import Any from unittest.mock import Mock, patch from aioshelly.const import MODEL_CAMERA, MODEL_PLUG, MODEL_WALL_DISPLAY from aioshelly.exceptions import DeviceConnectionError, NotInitialized, RpcCallError import pytest from homeassistant.components.shelly.const import ( BLE_SCANNER_FIRMWARE_UNSUPPORTED_ISSUE_ID, COIOT_UNCONFIGURED_ISSUE_ID, CONF_BLE_SCANNER_MODE, DEPRECATED_FIRMWARE_ISSUE_ID, DOMAIN, OPEN_WIFI_AP_ISSUE_ID, OUTBOUND_WEBSOCKET_INCORRECTLY_ENABLED_ISSUE_ID, PUSH_UPDATE_ISSUE_ID, RTSP_DISABLED_ISSUE_ID, BLEScannerMode, DeprecatedFirmwareInfo, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import issue_registry as ir from homeassistant.helpers.network import NoURLAvailableError from homeassistant.setup import async_setup_component from . import MOCK_MAC, init_integration, mock_block_device_push_update_failure from tests.components.repairs import process_repair_fix_flow, start_repair_fix_flow from tests.typing import ClientSessionGenerator async def test_ble_scanner_unsupported_firmware_issue( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_rpc_device: Mock, issue_registry: ir.IssueRegistry, ) -> None: """Test repair issues handling for BLE scanner with unsupported firmware.""" issue_id = BLE_SCANNER_FIRMWARE_UNSUPPORTED_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() await init_integration( hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} ) assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 client = await hass_client() result = await start_repair_fix_flow(client, DOMAIN, issue_id) flow_id = result["flow_id"] assert result["step_id"] == "confirm" result = await process_repair_fix_flow(client, flow_id) assert result["type"] == "create_entry" assert mock_rpc_device.trigger_ota_update.call_count == 1 # Assert the issue is no longer present assert not issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 0 async def test_unsupported_firmware_issue_update_not_available( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_rpc_device: Mock, monkeypatch: pytest.MonkeyPatch, issue_registry: ir.IssueRegistry, ) -> None: """Test repair issues handling when firmware update is not available.""" issue_id = BLE_SCANNER_FIRMWARE_UNSUPPORTED_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() await init_integration( hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} ) assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 client = await hass_client() result = await start_repair_fix_flow(client, DOMAIN, issue_id) flow_id = result["flow_id"] assert result["step_id"] == "confirm" monkeypatch.setitem(mock_rpc_device.status, "sys", {"available_updates": {}}) result = await process_repair_fix_flow(client, flow_id) assert result["type"] == "abort" assert result["reason"] == "update_not_available" assert mock_rpc_device.trigger_ota_update.call_count == 0 assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 @pytest.mark.parametrize( "exception", [DeviceConnectionError, RpcCallError(999, "Unknown error")] ) async def test_unsupported_firmware_issue_exc( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_rpc_device: Mock, issue_registry: ir.IssueRegistry, exception: Exception, ) -> None: """Test repair issues handling when OTA update ends with an exception.""" issue_id = BLE_SCANNER_FIRMWARE_UNSUPPORTED_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() await init_integration( hass, 2, options={CONF_BLE_SCANNER_MODE: BLEScannerMode.ACTIVE} ) assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 client = await hass_client() result = await start_repair_fix_flow(client, DOMAIN, issue_id) flow_id = result["flow_id"] assert result["step_id"] == "confirm" mock_rpc_device.trigger_ota_update.side_effect = exception result = await process_repair_fix_flow(client, flow_id) assert result["type"] == "abort" assert result["reason"] == "cannot_connect" assert mock_rpc_device.trigger_ota_update.call_count == 1 assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 async def test_outbound_websocket_incorrectly_enabled_issue( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_rpc_device: Mock, issue_registry: ir.IssueRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test repair issues handling for the outbound WebSocket incorrectly enabled.""" ws_url = "ws://10.10.10.10:8123/api/shelly/ws" monkeypatch.setitem( mock_rpc_device.config, "ws", {"enable": True, "server": ws_url} ) issue_id = OUTBOUND_WEBSOCKET_INCORRECTLY_ENABLED_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() await init_integration(hass, 2) assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 client = await hass_client() result = await start_repair_fix_flow(client, DOMAIN, issue_id) flow_id = result["flow_id"] assert result["step_id"] == "confirm" result = await process_repair_fix_flow(client, flow_id) assert result["type"] == "create_entry" assert mock_rpc_device.ws_setconfig.call_count == 1 assert mock_rpc_device.ws_setconfig.call_args[0] == (False, ws_url) assert mock_rpc_device.trigger_reboot.call_count == 1 # Assert the issue is no longer present assert not issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 0 async def test_repairs_skipped_when_device_not_initialized( hass: HomeAssistant, mock_rpc_device: Mock, issue_registry: ir.IssueRegistry, ) -> None: """Test repair checks are skipped when the RPC device is not initialized.""" mock_rpc_device.initialized = False type(mock_rpc_device).config = property( lambda self: (_ for _ in ()).throw(NotInitialized) ) await init_integration(hass, 2) assert len(issue_registry.issues) == 0 @pytest.mark.parametrize( "exception", [DeviceConnectionError, RpcCallError(999, "Unknown error")] ) async def test_outbound_websocket_incorrectly_enabled_issue_exc( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_rpc_device: Mock, issue_registry: ir.IssueRegistry, monkeypatch: pytest.MonkeyPatch, exception: Exception, ) -> None: """Test repair issues handling when ws_setconfig ends with an exception.""" ws_url = "ws://10.10.10.10:8123/api/shelly/ws" monkeypatch.setitem( mock_rpc_device.config, "ws", {"enable": True, "server": ws_url} ) issue_id = OUTBOUND_WEBSOCKET_INCORRECTLY_ENABLED_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() await init_integration(hass, 2) assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 client = await hass_client() result = await start_repair_fix_flow(client, DOMAIN, issue_id) flow_id = result["flow_id"] assert result["step_id"] == "confirm" mock_rpc_device.ws_setconfig.side_effect = exception result = await process_repair_fix_flow(client, flow_id) assert result["type"] == "abort" assert result["reason"] == "cannot_connect" assert mock_rpc_device.ws_setconfig.call_count == 1 assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 async def test_deprecated_firmware_issue( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_rpc_device: Mock, issue_registry: ir.IssueRegistry, ) -> None: """Test repair issues handling deprecated firmware.""" issue_id = DEPRECATED_FIRMWARE_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() with patch( "homeassistant.components.shelly.repairs.DEPRECATED_FIRMWARES", { MODEL_WALL_DISPLAY: DeprecatedFirmwareInfo( {"min_firmware": "2.3.0", "ha_version": "2025.10.0"} ) }, ): await init_integration(hass, 2, model=MODEL_WALL_DISPLAY) # The default fw version in tests is 1.0.0, the repair issue should be created. assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 client = await hass_client() result = await start_repair_fix_flow(client, DOMAIN, issue_id) flow_id = result["flow_id"] assert result["step_id"] == "confirm" result = await process_repair_fix_flow(client, flow_id) assert result["type"] == "create_entry" assert mock_rpc_device.trigger_ota_update.call_count == 1 # Assert the issue is no longer present assert not issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 0 async def test_open_wifi_ap_issue( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_rpc_device: Mock, issue_registry: ir.IssueRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test repair issues handling for open WiFi AP.""" monkeypatch.setitem( mock_rpc_device.config, "wifi", {"ap": {"enable": True, "is_open": True}}, ) issue_id = OPEN_WIFI_AP_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() await init_integration(hass, 2) assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 client = await hass_client() result = await start_repair_fix_flow(client, DOMAIN, issue_id) flow_id = result["flow_id"] assert result["step_id"] == "init" assert result["type"] == "menu" result = await process_repair_fix_flow(client, flow_id, {"next_step_id": "confirm"}) assert result["type"] == "create_entry" assert mock_rpc_device.wifi_setconfig.call_count == 1 assert mock_rpc_device.wifi_setconfig.call_args[1] == {"ap_enable": False} assert mock_rpc_device.trigger_reboot.call_count == 1 assert not issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 0 async def test_open_wifi_ap_issue_no_restart( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_rpc_device: Mock, issue_registry: ir.IssueRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test repair issues handling for open WiFi AP when restart not required.""" monkeypatch.setitem( mock_rpc_device.config, "wifi", {"ap": {"enable": True, "is_open": True}}, ) issue_id = OPEN_WIFI_AP_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() await init_integration(hass, 2) assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 client = await hass_client() result = await start_repair_fix_flow(client, DOMAIN, issue_id) flow_id = result["flow_id"] assert result["step_id"] == "init" assert result["type"] == "menu" mock_rpc_device.wifi_setconfig.return_value = {"restart_required": False} result = await process_repair_fix_flow(client, flow_id, {"next_step_id": "confirm"}) assert result["type"] == "create_entry" assert mock_rpc_device.wifi_setconfig.call_count == 1 assert mock_rpc_device.wifi_setconfig.call_args[1] == {"ap_enable": False} assert mock_rpc_device.trigger_reboot.call_count == 0 assert not issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 0 @pytest.mark.parametrize( "exception", [DeviceConnectionError, RpcCallError(999, "Unknown error")] ) async def test_open_wifi_ap_issue_exc( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_rpc_device: Mock, issue_registry: ir.IssueRegistry, monkeypatch: pytest.MonkeyPatch, exception: Exception, ) -> None: """Test repair issues handling when wifi_setconfig ends with an exception.""" monkeypatch.setitem( mock_rpc_device.config, "wifi", {"ap": {"enable": True, "is_open": True}}, ) issue_id = OPEN_WIFI_AP_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() await init_integration(hass, 2) assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 client = await hass_client() result = await start_repair_fix_flow(client, DOMAIN, issue_id) flow_id = result["flow_id"] assert result["step_id"] == "init" assert result["type"] == "menu" mock_rpc_device.wifi_setconfig.side_effect = exception result = await process_repair_fix_flow(client, flow_id, {"next_step_id": "confirm"}) assert result["type"] == "abort" assert result["reason"] == "cannot_connect" assert mock_rpc_device.wifi_setconfig.call_count == 1 assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 async def test_no_open_wifi_ap_issue_with_password( hass: HomeAssistant, mock_rpc_device: Mock, issue_registry: ir.IssueRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test no repair issue is created when WiFi AP has a password.""" monkeypatch.setitem( mock_rpc_device.config, "wifi", {"ap": {"enable": True, "is_open": False}}, ) issue_id = OPEN_WIFI_AP_ISSUE_ID.format(unique=MOCK_MAC) await init_integration(hass, 2) assert not issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 0 async def test_no_open_wifi_ap_issue_when_disabled( hass: HomeAssistant, mock_rpc_device: Mock, issue_registry: ir.IssueRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test no repair issue is created when WiFi AP is disabled.""" monkeypatch.setitem( mock_rpc_device.config, "wifi", {"ap": {"enable": False, "is_open": True}}, ) issue_id = OPEN_WIFI_AP_ISSUE_ID.format(unique=MOCK_MAC) await init_integration(hass, 2) assert not issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 0 async def test_open_wifi_ap_issue_ignore( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_rpc_device: Mock, issue_registry: ir.IssueRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test ignoring the open WiFi AP issue.""" monkeypatch.setitem( mock_rpc_device.config, "wifi", {"ap": {"enable": True, "is_open": True}}, ) issue_id = OPEN_WIFI_AP_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() await init_integration(hass, 2) assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 client = await hass_client() result = await start_repair_fix_flow(client, DOMAIN, issue_id) flow_id = result["flow_id"] assert result["step_id"] == "init" assert result["type"] == "menu" result = await process_repair_fix_flow(client, flow_id, {"next_step_id": "ignore"}) assert result["type"] == "abort" assert result["reason"] == "issue_ignored" assert mock_rpc_device.wifi_setconfig.call_count == 0 assert (issue := issue_registry.async_get_issue(DOMAIN, issue_id)) assert issue.dismissed_version @pytest.mark.parametrize( "ignore_missing_translations", ["component.shelly.issues.other_issue.title"] ) async def test_other_fixable_issues( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_rpc_device: Mock, issue_registry: ir.IssueRegistry, ) -> None: """Test fixing another issue.""" issue_id = "other_issue" assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() entry = await init_integration(hass, 2) assert mock_rpc_device.initialized is True ir.async_create_issue( hass, DOMAIN, issue_id, data={"entry_id": entry.entry_id}, is_fixable=True, severity=ir.IssueSeverity.ERROR, translation_key="other_issue", ) assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 client = await hass_client() result = await start_repair_fix_flow(client, DOMAIN, issue_id) flow_id = result["flow_id"] assert result["step_id"] == "confirm" assert result["type"] == "form" result = await process_repair_fix_flow(client, flow_id) assert result["type"] == "create_entry" @pytest.mark.parametrize( "coiot", [ {"enabled": False, "update_period": 15, "peer": "10.10.10.10:5683"}, {"enabled": True, "update_period": 15, "peer": "7.7.7.7:5683"}, ], ) async def test_coiot_disabled_or_wrong_peer_issue( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_block_device: Mock, issue_registry: ir.IssueRegistry, monkeypatch: pytest.MonkeyPatch, coiot: dict[str, Any], ) -> None: """Test repair issues handling wrong or disabled CoIoT configuration.""" monkeypatch.setitem(mock_block_device.settings, "coiot", coiot) issue_id = COIOT_UNCONFIGURED_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() await init_integration(hass, 1) await mock_block_device_push_update_failure(hass, mock_block_device) assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 client = await hass_client() result = await start_repair_fix_flow(client, DOMAIN, issue_id) flow_id = result["flow_id"] assert result["step_id"] == "init" assert result["type"] == "menu" result = await process_repair_fix_flow(client, flow_id, {"next_step_id": "confirm"}) assert result["type"] == "create_entry" assert mock_block_device.configure_coiot_protocol.call_count == 1 # Assert the issue is no longer present assert not issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 0 async def test_coiot_exception( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_block_device: Mock, issue_registry: ir.IssueRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test CoIoT exception handling in fix flow.""" monkeypatch.setitem( mock_block_device.settings, "coiot", {"enabled": False, "update_period": 15, "peer": "7.7.7.7:5683"}, ) issue_id = COIOT_UNCONFIGURED_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() await init_integration(hass, 1) await mock_block_device_push_update_failure(hass, mock_block_device) assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 client = await hass_client() result = await start_repair_fix_flow(client, DOMAIN, issue_id) flow_id = result["flow_id"] assert result["step_id"] == "init" assert result["type"] == "menu" mock_block_device.configure_coiot_protocol.side_effect = DeviceConnectionError result = await process_repair_fix_flow(client, flow_id, {"next_step_id": "confirm"}) assert result["type"] == "abort" assert result["reason"] == "cannot_connect" assert mock_block_device.configure_coiot_protocol.call_count == 1 assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 @pytest.mark.parametrize( "raw_url", [ "http://10.10.10.10:8123", "https://homeassistant.local:443", ], ) async def test_coiot_configured_no_issue_created( hass: HomeAssistant, mock_block_device: Mock, issue_registry: ir.IssueRegistry, monkeypatch: pytest.MonkeyPatch, raw_url: str, ) -> None: """Test no repair issues when CoIoT configuration is valid.""" issue_id = COIOT_UNCONFIGURED_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) with patch( "homeassistant.components.shelly.utils.get_url", return_value=raw_url, ): await hass.async_block_till_done() await init_integration(hass, 1) await mock_block_device_push_update_failure(hass, mock_block_device) assert issue_registry.async_get_issue(DOMAIN, issue_id) is None async def test_coiot_key_missing_no_issue_created( hass: HomeAssistant, mock_block_device: Mock, issue_registry: ir.IssueRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test no repair issues when CoIoT configuration is missing.""" monkeypatch.delitem( mock_block_device.settings, "coiot", ) issue_id = COIOT_UNCONFIGURED_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() await init_integration(hass, 1) assert issue_registry.async_get_issue(DOMAIN, issue_id) is None async def test_coiot_push_issue_when_missing_hass_url( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_block_device: Mock, issue_registry: ir.IssueRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test CoIoT push update issue created when HA URL is not available.""" issue_id = PUSH_UPDATE_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() await init_integration(hass, 1) with patch( "homeassistant.components.shelly.utils.get_url", side_effect=NoURLAvailableError(), ): await mock_block_device_push_update_failure(hass, mock_block_device) assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 async def test_coiot_fix_flow_no_hass_url( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_block_device: Mock, issue_registry: ir.IssueRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test CoIoT repair issue when HA URL is not available.""" monkeypatch.setitem( mock_block_device.settings, "coiot", {"enabled": False, "update_period": 15, "peer": "7.7.7.7:5683"}, ) issue_id = COIOT_UNCONFIGURED_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() await init_integration(hass, 1) await mock_block_device_push_update_failure(hass, mock_block_device) assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 client = await hass_client() result = await start_repair_fix_flow(client, DOMAIN, issue_id) flow_id = result["flow_id"] assert result["step_id"] == "init" assert result["type"] == "menu" with patch( "homeassistant.components.shelly.utils.get_url", side_effect=NoURLAvailableError(), ): result = await process_repair_fix_flow( client, flow_id, {"next_step_id": "confirm"} ) assert result["type"] == "abort" assert result["reason"] == "cannot_configure" assert mock_block_device.configure_coiot_protocol.call_count == 0 assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 async def test_coiot_issue_ignore( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_block_device: Mock, issue_registry: ir.IssueRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test ignoring the CoIoT unconfigured issue.""" monkeypatch.setitem( mock_block_device.settings, "coiot", {"enabled": False, "update_period": 15, "peer": "7.7.7.7:5683"}, ) issue_id = COIOT_UNCONFIGURED_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() await init_integration(hass, 1) await mock_block_device_push_update_failure(hass, mock_block_device) assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 client = await hass_client() result = await start_repair_fix_flow(client, DOMAIN, issue_id) flow_id = result["flow_id"] assert result["step_id"] == "init" assert result["type"] == "menu" result = await process_repair_fix_flow(client, flow_id, {"next_step_id": "ignore"}) assert result["type"] == "abort" assert result["reason"] == "issue_ignored" assert mock_block_device.configure_coiot_protocol.call_count == 0 assert (issue := issue_registry.async_get_issue(DOMAIN, issue_id)) assert issue.dismissed_version async def test_plug_1_push_update_issue_created( hass: HomeAssistant, mock_block_device: Mock, issue_registry: ir.IssueRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test push update repair issue when device is Shelly Plug 1.""" monkeypatch.setattr(mock_block_device, "model", MODEL_PLUG) issue_id = PUSH_UPDATE_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() await init_integration(hass, 1, model=MODEL_PLUG) await mock_block_device_push_update_failure(hass, mock_block_device) assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 async def test_rtsp_disabled_issue( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_camera_rpc_device: Mock, issue_registry: ir.IssueRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test repair issue when camera RTSP is disabled.""" monkeypatch.setitem( mock_camera_rpc_device.config["camera:0"]["rtsp"], "enable", False ) issue_id = RTSP_DISABLED_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() await init_integration(hass, 3, MODEL_CAMERA) assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 client = await hass_client() result = await start_repair_fix_flow(client, DOMAIN, issue_id) assert result["step_id"] == "init" assert result["type"] == "menu" result = await process_repair_fix_flow( client, result["flow_id"], {"next_step_id": "confirm"} ) assert result["type"] == "create_entry" assert mock_camera_rpc_device.set_camera_rtsp.call_count == 1 assert mock_camera_rpc_device.set_camera_rtsp.call_args[0] == (0, True) assert not issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 0 async def test_no_rtsp_disabled_issue_when_enabled( hass: HomeAssistant, mock_camera_rpc_device: Mock, issue_registry: ir.IssueRegistry, ) -> None: """Test no repair issue when camera RTSP is enabled.""" issue_id = RTSP_DISABLED_ISSUE_ID.format(unique=MOCK_MAC) await init_integration(hass, 3, MODEL_CAMERA) assert not issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 0 async def test_rtsp_disabled_issue_ignore( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_camera_rpc_device: Mock, issue_registry: ir.IssueRegistry, monkeypatch: pytest.MonkeyPatch, ) -> None: """Test ignoring the RTSP disabled issue.""" monkeypatch.setitem( mock_camera_rpc_device.config["camera:0"]["rtsp"], "enable", False ) issue_id = RTSP_DISABLED_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() await init_integration(hass, 3, MODEL_CAMERA) assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 client = await hass_client() result = await start_repair_fix_flow(client, DOMAIN, issue_id) assert result["step_id"] == "init" assert result["type"] == "menu" result = await process_repair_fix_flow( client, result["flow_id"], {"next_step_id": "ignore"} ) assert result["type"] == "abort" assert result["reason"] == "issue_ignored" assert mock_camera_rpc_device.set_camera_rtsp.call_count == 0 assert (issue := issue_registry.async_get_issue(DOMAIN, issue_id)) assert issue.dismissed_version @pytest.mark.parametrize( "exception", [DeviceConnectionError, RpcCallError(999, "Unknown error")] ) async def test_rtsp_disabled_issue_exc( hass: HomeAssistant, hass_client: ClientSessionGenerator, mock_camera_rpc_device: Mock, issue_registry: ir.IssueRegistry, monkeypatch: pytest.MonkeyPatch, exception: Exception, ) -> None: """Test repair issue handling when set_camera_rtsp ends with an exception.""" mock_camera_rpc_device.set_camera_rtsp.side_effect = exception monkeypatch.setitem( mock_camera_rpc_device.config["camera:0"]["rtsp"], "enable", False ) issue_id = RTSP_DISABLED_ISSUE_ID.format(unique=MOCK_MAC) assert await async_setup_component(hass, "repairs", {}) await hass.async_block_till_done() await init_integration(hass, 3, MODEL_CAMERA) assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1 client = await hass_client() result = await start_repair_fix_flow(client, DOMAIN, issue_id) assert result["step_id"] == "init" assert result["type"] == "menu" result = await process_repair_fix_flow( client, result["flow_id"], {"next_step_id": "confirm"} ) assert result["type"] == "abort" assert result["reason"] == "cannot_connect" assert mock_camera_rpc_device.set_camera_rtsp.call_count == 1 assert issue_registry.async_get_issue(DOMAIN, issue_id) assert len(issue_registry.issues) == 1