"""Tests for ZHA config flow.""" from collections.abc import Generator from unittest.mock import AsyncMock, MagicMock, create_autospec, patch import pytest import serial.tools.list_ports from zigpy.backups import BackupManager import zigpy.config from zigpy.config import CONF_DEVICE_PATH import zigpy.types from homeassistant import config_entries from homeassistant.components.usb import UsbServiceInfo from homeassistant.components.zha import radio_manager from homeassistant.components.zha.core.const import DOMAIN, RadioType from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry PROBE_FUNCTION_PATH = "zigbee.application.ControllerApplication.probe" @pytest.fixture(autouse=True) def disable_platform_only(): """Disable platforms to speed up tests.""" with patch("homeassistant.components.zha.PLATFORMS", []): yield @pytest.fixture(autouse=True) def reduce_reconnect_timeout(): """Reduces reconnect timeout to speed up tests.""" with patch("homeassistant.components.zha.radio_manager.CONNECT_DELAY_S", 0.0001): yield @pytest.fixture(autouse=True) def mock_app(): """Mock zigpy app interface.""" mock_app = AsyncMock() mock_app.backups = create_autospec(BackupManager, instance=True) mock_app.backups.backups = [] with patch( "zigpy.application.ControllerApplication.new", AsyncMock(return_value=mock_app) ): yield mock_app @pytest.fixture def backup(): """Zigpy network backup with non-default settings.""" backup = zigpy.backups.NetworkBackup() backup.node_info.ieee = zigpy.types.EUI64.convert("AA:BB:CC:DD:11:22:33:44") return backup def mock_detect_radio_type(radio_type=RadioType.ezsp, ret=True): """Mock `detect_radio_type` that just sets the appropriate attributes.""" async def detect(self): self.radio_type = radio_type self.device_settings = radio_type.controller.SCHEMA_DEVICE( {CONF_DEVICE_PATH: self.device_path} ) return ret return detect def com_port(device="/dev/ttyUSB1234"): """Mock of a serial port.""" port = serial.tools.list_ports_common.ListPortInfo("/dev/ttyUSB1234") port.serial_number = "1234" port.manufacturer = "Virtual serial port" port.device = device port.description = "Some serial port" return port @pytest.fixture def mock_connect_zigpy_app() -> Generator[None, None, None]: """Mock the radio connection.""" mock_connect_app = MagicMock() mock_connect_app.__aenter__.return_value.backups.backups = [MagicMock()] mock_connect_app.__aenter__.return_value.backups.create_backup.return_value = ( MagicMock() ) with patch( "homeassistant.components.zha.radio_manager.ZhaRadioManager._connect_zigpy_app", return_value=mock_connect_app, ): yield @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) async def test_migrate_matching_port( hass: HomeAssistant, mock_connect_zigpy_app, ) -> None: """Test automatic migration.""" # Set up the config entry config_entry = MockConfigEntry( data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, domain=DOMAIN, options={}, title="Test", version=3, ) config_entry.add_to_hass(hass) migration_data = { "new_discovery_info": { "name": "Test Updated", "port": { "path": "socket://some/virtual_port", "baudrate": 115200, "flow_control": "hardware", }, "radio_type": "efr32", }, "old_discovery_info": { "hw": { "name": "Test", "port": { "path": "/dev/ttyTEST123", "baudrate": 115200, "flow_control": "hardware", }, "radio_type": "efr32", } }, } migration_helper = radio_manager.ZhaMultiPANMigrationHelper(hass, config_entry) assert await migration_helper.async_initiate_migration(migration_data) # Check the ZHA config entry data is updated assert config_entry.data == { "device": { "path": "socket://some/virtual_port", "baudrate": 115200, "flow_control": "hardware", }, "radio_type": "ezsp", } assert config_entry.title == "Test Updated" await migration_helper.async_finish_migration() @patch( "homeassistant.components.zha.radio_manager.ZhaRadioManager.detect_radio_type", mock_detect_radio_type(), ) @patch("homeassistant.components.zha.async_setup_entry", AsyncMock(return_value=True)) async def test_migrate_matching_port_usb( hass: HomeAssistant, mock_connect_zigpy_app, ) -> None: """Test automatic migration.""" # Set up the config entry config_entry = MockConfigEntry( data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, domain=DOMAIN, options={}, title="Test", version=3, ) config_entry.add_to_hass(hass) migration_data = { "new_discovery_info": { "name": "Test Updated", "port": { "path": "socket://some/virtual_port", "baudrate": 115200, "flow_control": "hardware", }, "radio_type": "efr32", }, "old_discovery_info": { "usb": UsbServiceInfo("/dev/ttyTEST123", "blah", "blah", None, None, None) }, } migration_helper = radio_manager.ZhaMultiPANMigrationHelper(hass, config_entry) assert await migration_helper.async_initiate_migration(migration_data) # Check the ZHA config entry data is updated assert config_entry.data == { "device": { "path": "socket://some/virtual_port", "baudrate": 115200, "flow_control": "hardware", }, "radio_type": "ezsp", } assert config_entry.title == "Test Updated" await migration_helper.async_finish_migration() async def test_migrate_matching_port_config_entry_not_loaded( hass: HomeAssistant, mock_connect_zigpy_app, ) -> None: """Test automatic migration.""" # Set up the config entry config_entry = MockConfigEntry( data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, domain=DOMAIN, options={}, title="Test", ) config_entry.add_to_hass(hass) config_entry.state = config_entries.ConfigEntryState.SETUP_IN_PROGRESS migration_data = { "new_discovery_info": { "name": "Test Updated", "port": { "path": "socket://some/virtual_port", "baudrate": 115200, "flow_control": "hardware", }, "radio_type": "efr32", }, "old_discovery_info": { "hw": { "name": "Test", "port": { "path": "/dev/ttyTEST123", "baudrate": 115200, "flow_control": "hardware", }, "radio_type": "efr32", } }, } migration_helper = radio_manager.ZhaMultiPANMigrationHelper(hass, config_entry) assert await migration_helper.async_initiate_migration(migration_data) # Check the ZHA config entry data is updated assert config_entry.data == { "device": { "path": "socket://some/virtual_port", "baudrate": 115200, "flow_control": "hardware", }, "radio_type": "ezsp", } assert config_entry.title == "Test Updated" await migration_helper.async_finish_migration() @patch( "homeassistant.components.zha.radio_manager.ZhaRadioManager.async_restore_backup_step_1", side_effect=OSError, ) async def test_migrate_matching_port_retry( mock_restore_backup_step_1, hass: HomeAssistant, mock_connect_zigpy_app, ) -> None: """Test automatic migration.""" # Set up the config entry config_entry = MockConfigEntry( data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, domain=DOMAIN, options={}, title="Test", ) config_entry.add_to_hass(hass) config_entry.state = config_entries.ConfigEntryState.SETUP_IN_PROGRESS migration_data = { "new_discovery_info": { "name": "Test Updated", "port": { "path": "socket://some/virtual_port", "baudrate": 115200, "flow_control": "hardware", }, "radio_type": "efr32", }, "old_discovery_info": { "hw": { "name": "Test", "port": { "path": "/dev/ttyTEST123", "baudrate": 115200, "flow_control": "hardware", }, "radio_type": "efr32", } }, } migration_helper = radio_manager.ZhaMultiPANMigrationHelper(hass, config_entry) assert await migration_helper.async_initiate_migration(migration_data) # Check the ZHA config entry data is updated assert config_entry.data == { "device": { "path": "socket://some/virtual_port", "baudrate": 115200, "flow_control": "hardware", }, "radio_type": "ezsp", } assert config_entry.title == "Test Updated" with pytest.raises(OSError): await migration_helper.async_finish_migration() assert mock_restore_backup_step_1.call_count == 100 async def test_migrate_non_matching_port( hass: HomeAssistant, mock_connect_zigpy_app, ) -> None: """Test automatic migration.""" # Set up the config entry config_entry = MockConfigEntry( data={"device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp"}, domain=DOMAIN, options={}, title="Test", ) config_entry.add_to_hass(hass) migration_data = { "new_discovery_info": { "name": "Test Updated", "port": { "path": "socket://some/virtual_port", "baudrate": 115200, "flow_control": "hardware", }, "radio_type": "efr32", }, "old_discovery_info": { "hw": { "name": "Test", "port": { "path": "/dev/ttyTEST456", "baudrate": 115200, "flow_control": "hardware", }, "radio_type": "efr32", } }, } migration_helper = radio_manager.ZhaMultiPANMigrationHelper(hass, config_entry) assert not await migration_helper.async_initiate_migration(migration_data) # Check the ZHA config entry data is not updated assert config_entry.data == { "device": {"path": "/dev/ttyTEST123"}, "radio_type": "ezsp", } assert config_entry.title == "Test"