"""Tests for the Velbus component initialisation.""" from unittest.mock import AsyncMock, MagicMock, patch import pytest from syrupy.assertion import SnapshotAssertion from velbusaio.exceptions import VelbusConnectionFailed from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.velbus import ( VelbusConfigEntry, async_remove_config_entry_device, ) from homeassistant.components.velbus.const import DOMAIN from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, CONF_NAME, CONF_PORT, SERVICE_TURN_ON from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr, entity_registry as er from . import init_integration from .const import PORT_TCP from tests.common import MockConfigEntry async def test_setup_connection_failed( hass: HomeAssistant, config_entry: VelbusConfigEntry, controller: MagicMock, ) -> None: """Test the setup that fails during velbus connect.""" controller.return_value.connect.side_effect = VelbusConnectionFailed() await hass.config_entries.async_setup(config_entry.entry_id) assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_start_failed( hass: HomeAssistant, config_entry: VelbusConfigEntry, controller: MagicMock, entity_registry: er.EntityRegistry, ) -> None: """Test setup fails during velbus start task, should result in no entries.""" controller.return_value.start.side_effect = ConnectionError() await init_integration(hass, config_entry) assert config_entry.state is ConfigEntryState.LOADED assert ( er.async_entries_for_config_entry(entity_registry, config_entry.entry_id) == [] ) async def test_unload_entry( hass: HomeAssistant, config_entry: ConfigEntry, ) -> None: """Test being able to unload an entry.""" await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() assert config_entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) async def test_device_identifier_migration( hass: HomeAssistant, config_entry: ConfigEntry, device_registry: dr.DeviceRegistry, ) -> None: """Test being able to unload an entry.""" original_identifiers = {(DOMAIN, "module_address", "module_serial")} target_identifiers = {(DOMAIN, "module_address")} device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers=original_identifiers, # type: ignore[arg-type] name="channel_name", manufacturer="Velleman", model="module_type_name", sw_version="module_sw_version", ) assert device_registry.async_get_device( identifiers=original_identifiers # type: ignore[arg-type] ) assert not device_registry.async_get_device(identifiers=target_identifiers) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() assert not device_registry.async_get_device( identifiers=original_identifiers # type: ignore[arg-type] ) device_entry = device_registry.async_get_device(identifiers=target_identifiers) assert device_entry assert device_entry.name == "channel_name" assert device_entry.manufacturer == "Velleman" assert device_entry.model == "module_type_name" assert device_entry.sw_version == "module_sw_version" async def test_migrate_config_entry( hass: HomeAssistant, controller: MagicMock, ) -> None: """Test successful migration of entry data.""" legacy_config = {CONF_NAME: "fake_name", CONF_PORT: "1.2.3.4:5678"} entry = MockConfigEntry(domain=DOMAIN, unique_id="my own id", data=legacy_config) assert entry.version == 1 assert entry.minor_version == 1 entry.add_to_hass(hass) # test in case we do not have a cache with ( patch("os.path.isdir", return_value=True), patch("shutil.rmtree") as mock_rmtree, ): await hass.config_entries.async_setup(entry.entry_id) assert dict(entry.data) == legacy_config assert entry.version == 3 assert entry.minor_version == 2 mock_rmtree.assert_called_once() async def test_migrate_config_entry_32( hass: HomeAssistant, controller: MagicMock, ) -> None: """Test successful migration of entry data.""" legacy_config = {CONF_NAME: "fake_name", CONF_PORT: "1.2.3.4:5678"} entry = MockConfigEntry( domain=DOMAIN, unique_id="my own id", data=legacy_config, version=2, minor_version=2, ) assert entry.version == 2 assert entry.minor_version == 2 entry.add_to_hass(hass) # test in case we do not have a cache with ( patch("os.path.isdir", return_value=True), patch("shutil.rmtree") as mock_rmtree, ): await hass.config_entries.async_setup(entry.entry_id) assert dict(entry.data) == legacy_config assert entry.version == 3 assert entry.minor_version == 2 mock_rmtree.assert_called_once() @pytest.mark.parametrize( ("unique_id", "expected"), [("vid:pid_serial_manufacturer_decription", "serial"), (None, None)], ) async def test_migrate_config_entry_unique_id( hass: HomeAssistant, controller: AsyncMock, unique_id: str, expected: str, ) -> None: """Test the migration of unique id.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_PORT: PORT_TCP, CONF_NAME: "velbus home"}, unique_id=unique_id, ) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) assert entry.unique_id == expected assert entry.version == 3 assert entry.minor_version == 2 async def test_api_call( hass: HomeAssistant, mock_relay: AsyncMock, config_entry: MockConfigEntry, ) -> None: """Test the api call decorator action.""" await init_integration(hass, config_entry) mock_relay.turn_on.side_effect = OSError() with pytest.raises(HomeAssistantError): await hass.services.async_call( SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "switch.living_room_relayname"}, blocking=True, ) async def test_device_registry( hass: HomeAssistant, config_entry: MockConfigEntry, device_registry: dr.DeviceRegistry, snapshot: SnapshotAssertion, ) -> None: """Test the velbus device registry.""" await init_integration(hass, config_entry) # Ensure devices are correctly registered device_entries = dr.async_entries_for_config_entry( device_registry, config_entry.entry_id ) # Sort by identifier to ensure consistent order in snapshot assert sorted(device_entries, key=lambda x: list(x.identifiers)[0][1]) == snapshot device_parent = device_registry.async_get_device(identifiers={(DOMAIN, "88")}) assert device_parent.via_device_id is None device = device_registry.async_get_device(identifiers={(DOMAIN, "88-9")}) assert device.via_device_id == device_parent.id device_no_sub = device_registry.async_get_device(identifiers={(DOMAIN, "2")}) assert device_no_sub.via_device_id is None async def test_remove_config_entry_device( hass: HomeAssistant, config_entry: MockConfigEntry, device_registry: dr.DeviceRegistry, ) -> None: """Test that any Velbus device can be removed.""" await init_integration(hass, config_entry) # Active device (found on bus) can be removed; scan will recreate it active_device = device_registry.async_get_device(identifiers={(DOMAIN, "1")}) assert active_device is not None result = await async_remove_config_entry_device(hass, config_entry, active_device) assert result is True # Stale device (not on bus) can also be removed stale_device = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "999")}, name="Missing Module", manufacturer="Velleman", model="VMBX", ) result = await async_remove_config_entry_device(hass, config_entry, stale_device) assert result is True device_registry.async_update_device( stale_device.id, remove_config_entry_id=config_entry.entry_id ) stale_device_after = device_registry.async_get(stale_device.id) assert ( stale_device_after is None or config_entry.entry_id not in stale_device_after.config_entries ) async def test_remove_config_entry_device_detaches_subdevices( hass: HomeAssistant, config_entry: MockConfigEntry, device_registry: dr.DeviceRegistry, ) -> None: """Test that removing a device also detaches its sub-devices.""" await init_integration(hass, config_entry) stale_device = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "999")}, name="Missing Module", manufacturer="Velleman", model="VMBX", ) sub_device = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "999-1")}, name="Missing Module Channel 1", manufacturer="Velleman", model="VMBX", via_device=(DOMAIN, "999"), ) result = await async_remove_config_entry_device(hass, config_entry, stale_device) assert result is True sub_device_after = device_registry.async_get(sub_device.id) assert sub_device_after is None or ( config_entry.entry_id not in sub_device_after.config_entries and sub_device_after.via_device_id is None )