311 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			311 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
"""Tests for HKDevice."""
 | 
						|
 | 
						|
import dataclasses
 | 
						|
 | 
						|
from aiohomekit.controller import TransportType
 | 
						|
import pytest
 | 
						|
 | 
						|
from homeassistant.components.homekit_controller.const import (
 | 
						|
    DOMAIN,
 | 
						|
    IDENTIFIER_ACCESSORY_ID,
 | 
						|
    IDENTIFIER_LEGACY_ACCESSORY_ID,
 | 
						|
    IDENTIFIER_LEGACY_SERIAL_NUMBER,
 | 
						|
)
 | 
						|
from homeassistant.components.thread import async_add_dataset
 | 
						|
from homeassistant.core import HomeAssistant
 | 
						|
from homeassistant.exceptions import HomeAssistantError
 | 
						|
from homeassistant.helpers import device_registry as dr
 | 
						|
 | 
						|
from .common import setup_accessories_from_file, setup_platform, setup_test_accessories
 | 
						|
 | 
						|
from tests.common import MockConfigEntry
 | 
						|
 | 
						|
 | 
						|
@dataclasses.dataclass
 | 
						|
class DeviceMigrationTest:
 | 
						|
    """Holds the expected state before and after testing a device identifier migration."""
 | 
						|
 | 
						|
    fixture: str
 | 
						|
    manufacturer: str
 | 
						|
    before: set[tuple[str, str, str]]
 | 
						|
    after: set[tuple[str, str]]
 | 
						|
 | 
						|
 | 
						|
DEVICE_MIGRATION_TESTS = [
 | 
						|
    # 0401.3521.0679 was incorrectly treated as a serial number, it should be stripped out during migration
 | 
						|
    DeviceMigrationTest(
 | 
						|
        fixture="ryse_smart_bridge_four_shades.json",
 | 
						|
        manufacturer="RYSE Inc.",
 | 
						|
        before={
 | 
						|
            (DOMAIN, IDENTIFIER_LEGACY_ACCESSORY_ID, "00:00:00:00:00:00"),
 | 
						|
        },
 | 
						|
        after={(IDENTIFIER_ACCESSORY_ID, "00:00:00:00:00:00:aid:1")},
 | 
						|
    ),
 | 
						|
    # This shade has a serial of 1.0.0, which we should already ignore. Make sure it gets migrated to a 2-tuple
 | 
						|
    DeviceMigrationTest(
 | 
						|
        fixture="ryse_smart_bridge_four_shades.json",
 | 
						|
        manufacturer="RYSE Inc.",
 | 
						|
        before={
 | 
						|
            (DOMAIN, IDENTIFIER_LEGACY_ACCESSORY_ID, "00:00:00:00:00:00_3"),
 | 
						|
        },
 | 
						|
        after={(IDENTIFIER_ACCESSORY_ID, "00:00:00:00:00:00:aid:3")},
 | 
						|
    ),
 | 
						|
    # Test migrating a Hue bridge - it has a valid serial number and has an accessory id
 | 
						|
    DeviceMigrationTest(
 | 
						|
        fixture="hue_bridge.json",
 | 
						|
        manufacturer="Philips Lighting",
 | 
						|
        before={
 | 
						|
            (DOMAIN, IDENTIFIER_LEGACY_ACCESSORY_ID, "00:00:00:00:00:00"),
 | 
						|
        },
 | 
						|
        after={
 | 
						|
            (IDENTIFIER_ACCESSORY_ID, "00:00:00:00:00:00:aid:1"),
 | 
						|
        },
 | 
						|
    ),
 | 
						|
    # Test migrating a Hue remote - it has a valid serial number
 | 
						|
    # Originally as a non-hub non-broken device it wouldn't have had an accessory id
 | 
						|
    DeviceMigrationTest(
 | 
						|
        fixture="hue_bridge.json",
 | 
						|
        manufacturer="Philips",
 | 
						|
        before={
 | 
						|
            (DOMAIN, IDENTIFIER_LEGACY_SERIAL_NUMBER, "6623462389072572"),
 | 
						|
        },
 | 
						|
        after={
 | 
						|
            (IDENTIFIER_ACCESSORY_ID, "00:00:00:00:00:00:aid:6623462389072572"),
 | 
						|
        },
 | 
						|
    ),
 | 
						|
    # Test migrating a Koogeek LS1. This is just for completeness (testing hub and hub-less devices)
 | 
						|
    DeviceMigrationTest(
 | 
						|
        fixture="koogeek_ls1.json",
 | 
						|
        manufacturer="Koogeek",
 | 
						|
        before={
 | 
						|
            (DOMAIN, IDENTIFIER_LEGACY_ACCESSORY_ID, "00:00:00:00:00:00"),
 | 
						|
            (DOMAIN, IDENTIFIER_LEGACY_SERIAL_NUMBER, "AAAA011111111111"),
 | 
						|
        },
 | 
						|
        after={
 | 
						|
            (IDENTIFIER_ACCESSORY_ID, "00:00:00:00:00:00:aid:1"),
 | 
						|
        },
 | 
						|
    ),
 | 
						|
]
 | 
						|
 | 
						|
 | 
						|
@pytest.mark.parametrize("variant", DEVICE_MIGRATION_TESTS)
 | 
						|
async def test_migrate_device_id_no_serial_skip_if_other_owner(
 | 
						|
    hass: HomeAssistant, variant: DeviceMigrationTest
 | 
						|
) -> None:
 | 
						|
    """Don't migrate unrelated devices.
 | 
						|
 | 
						|
    Create a device registry entry that needs migrate, but belongs to a different
 | 
						|
    config entry. It should be ignored.
 | 
						|
    """
 | 
						|
    device_registry = dr.async_get(hass)
 | 
						|
 | 
						|
    bridge = device_registry.async_get_or_create(
 | 
						|
        config_entry_id="XX",
 | 
						|
        identifiers=variant.before,
 | 
						|
        manufacturer="RYSE Inc.",
 | 
						|
        model="RYSE SmartBridge",
 | 
						|
        name="Wiring Closet",
 | 
						|
        sw_version="1.3.0",
 | 
						|
        hw_version="0101.2136.0344",
 | 
						|
    )
 | 
						|
 | 
						|
    accessories = await setup_accessories_from_file(hass, variant.fixture)
 | 
						|
    await setup_test_accessories(hass, accessories)
 | 
						|
 | 
						|
    bridge = device_registry.async_get(bridge.id)
 | 
						|
 | 
						|
    assert bridge.identifiers == variant.before
 | 
						|
    assert bridge.config_entries == {"XX"}
 | 
						|
 | 
						|
 | 
						|
@pytest.mark.parametrize("variant", DEVICE_MIGRATION_TESTS)
 | 
						|
async def test_migrate_device_id_no_serial(
 | 
						|
    hass: HomeAssistant, variant: DeviceMigrationTest
 | 
						|
) -> None:
 | 
						|
    """Test that a Ryse smart bridge with four shades can be migrated correctly in HA."""
 | 
						|
    device_registry = dr.async_get(hass)
 | 
						|
 | 
						|
    accessories = await setup_accessories_from_file(hass, variant.fixture)
 | 
						|
 | 
						|
    fake_controller = await setup_platform(hass)
 | 
						|
    await fake_controller.add_paired_device(accessories, "00:00:00:00:00:00")
 | 
						|
    config_entry = MockConfigEntry(
 | 
						|
        version=1,
 | 
						|
        domain="homekit_controller",
 | 
						|
        entry_id="TestData",
 | 
						|
        data={"AccessoryPairingID": "00:00:00:00:00:00"},
 | 
						|
        title="test",
 | 
						|
    )
 | 
						|
    config_entry.add_to_hass(hass)
 | 
						|
 | 
						|
    device = device_registry.async_get_or_create(
 | 
						|
        config_entry_id=config_entry.entry_id,
 | 
						|
        identifiers=variant.before,
 | 
						|
        manufacturer="Dummy Manufacturer",
 | 
						|
        model="Dummy Model",
 | 
						|
        name="Dummy Name",
 | 
						|
        sw_version="99999999991",
 | 
						|
        hw_version="99999999999",
 | 
						|
    )
 | 
						|
 | 
						|
    await hass.config_entries.async_setup(config_entry.entry_id)
 | 
						|
    await hass.async_block_till_done()
 | 
						|
 | 
						|
    device = device_registry.async_get(device.id)
 | 
						|
 | 
						|
    assert device.identifiers == variant.after
 | 
						|
    assert device.manufacturer == variant.manufacturer
 | 
						|
 | 
						|
 | 
						|
async def test_migrate_ble_unique_id(hass: HomeAssistant) -> None:
 | 
						|
    """Test that a config entry with incorrect unique_id is repaired."""
 | 
						|
    accessories = await setup_accessories_from_file(hass, "anker_eufycam.json")
 | 
						|
 | 
						|
    fake_controller = await setup_platform(hass)
 | 
						|
    await fake_controller.add_paired_device(accessories, "02:03:EF:02:03:EF")
 | 
						|
    config_entry = MockConfigEntry(
 | 
						|
        version=1,
 | 
						|
        domain="homekit_controller",
 | 
						|
        entry_id="TestData",
 | 
						|
        data={"AccessoryPairingID": "02:03:EF:02:03:EF"},
 | 
						|
        title="test",
 | 
						|
        unique_id="01:02:AB:01:02:AB",
 | 
						|
    )
 | 
						|
    config_entry.add_to_hass(hass)
 | 
						|
 | 
						|
    assert config_entry.unique_id == "01:02:AB:01:02:AB"
 | 
						|
 | 
						|
    await hass.config_entries.async_setup(config_entry.entry_id)
 | 
						|
    await hass.async_block_till_done()
 | 
						|
 | 
						|
    assert config_entry.unique_id == "02:03:ef:02:03:ef"
 | 
						|
 | 
						|
 | 
						|
async def test_thread_provision_no_creds(hass: HomeAssistant) -> None:
 | 
						|
    """Test that we don't migrate to thread when there are no creds available."""
 | 
						|
    accessories = await setup_accessories_from_file(hass, "nanoleaf_strip_nl55.json")
 | 
						|
 | 
						|
    fake_controller = await setup_platform(hass)
 | 
						|
    await fake_controller.add_paired_device(accessories, "02:03:EF:02:03:EF")
 | 
						|
    config_entry = MockConfigEntry(
 | 
						|
        version=1,
 | 
						|
        domain="homekit_controller",
 | 
						|
        entry_id="TestData",
 | 
						|
        data={"AccessoryPairingID": "02:03:EF:02:03:EF"},
 | 
						|
        title="test",
 | 
						|
        unique_id="02:03:ef:02:03:ef",
 | 
						|
    )
 | 
						|
    config_entry.add_to_hass(hass)
 | 
						|
 | 
						|
    fake_controller.transport_type = TransportType.BLE
 | 
						|
 | 
						|
    await hass.config_entries.async_setup(config_entry.entry_id)
 | 
						|
    await hass.async_block_till_done()
 | 
						|
 | 
						|
    with pytest.raises(HomeAssistantError):
 | 
						|
        await hass.services.async_call(
 | 
						|
            "button",
 | 
						|
            "press",
 | 
						|
            {
 | 
						|
                "entity_id": "button.nanoleaf_strip_3b32_provision_preferred_thread_credentials"
 | 
						|
            },
 | 
						|
            blocking=True,
 | 
						|
        )
 | 
						|
 | 
						|
 | 
						|
async def test_thread_provision(hass: HomeAssistant) -> None:
 | 
						|
    """Test that a when a thread provision works the config entry is updated."""
 | 
						|
    await async_add_dataset(
 | 
						|
        hass,
 | 
						|
        "Tests",
 | 
						|
        "0E080000000000010000000300000F35060004001FFFE0020811111111222222220708FDAD70BF"
 | 
						|
        "E5AA15DD051000112233445566778899AABBCCDDEEFF030E4F70656E54687265616444656D6F01"
 | 
						|
        "0212340410445F2B5CA6F2A93A55CE570A70EFEECB0C0402A0F7F8",
 | 
						|
    )
 | 
						|
 | 
						|
    accessories = await setup_accessories_from_file(hass, "nanoleaf_strip_nl55.json")
 | 
						|
 | 
						|
    fake_controller = await setup_platform(hass)
 | 
						|
    await fake_controller.add_paired_device(accessories, "00:00:00:00:00:00")
 | 
						|
    config_entry = MockConfigEntry(
 | 
						|
        version=1,
 | 
						|
        domain="homekit_controller",
 | 
						|
        entry_id="TestData",
 | 
						|
        data={"AccessoryPairingID": "00:00:00:00:00:00"},
 | 
						|
        title="test",
 | 
						|
        unique_id="00:00:00:00:00:00",
 | 
						|
    )
 | 
						|
    config_entry.add_to_hass(hass)
 | 
						|
 | 
						|
    fake_controller.transport_type = TransportType.BLE
 | 
						|
 | 
						|
    # Needs a COAP transport to do migration
 | 
						|
    fake_controller.transports = {TransportType.COAP: fake_controller}
 | 
						|
 | 
						|
    # Fake discovery won't have an address/port - set one so the migration works
 | 
						|
    discovery = fake_controller.discoveries["00:00:00:00:00:00"]
 | 
						|
    discovery.description.address = "127.0.0.1"
 | 
						|
    discovery.description.port = 53
 | 
						|
 | 
						|
    await hass.config_entries.async_setup(config_entry.entry_id)
 | 
						|
    await hass.async_block_till_done()
 | 
						|
 | 
						|
    await hass.services.async_call(
 | 
						|
        "button",
 | 
						|
        "press",
 | 
						|
        {
 | 
						|
            "entity_id": "button.nanoleaf_strip_3b32_provision_preferred_thread_credentials"
 | 
						|
        },
 | 
						|
        blocking=True,
 | 
						|
    )
 | 
						|
 | 
						|
    assert config_entry.data["Connection"] == "CoAP"
 | 
						|
 | 
						|
 | 
						|
async def test_thread_provision_migration_failed(hass: HomeAssistant) -> None:
 | 
						|
    """Test that when a device 'migrates' but doesn't show up in CoAP, we remain in BLE mode."""
 | 
						|
    await async_add_dataset(
 | 
						|
        hass,
 | 
						|
        "Tests",
 | 
						|
        "0E080000000000010000000300000F35060004001FFFE0020811111111222222220708FDAD70BF"
 | 
						|
        "E5AA15DD051000112233445566778899AABBCCDDEEFF030E4F70656E54687265616444656D6F01"
 | 
						|
        "0212340410445F2B5CA6F2A93A55CE570A70EFEECB0C0402A0F7F8",
 | 
						|
    )
 | 
						|
 | 
						|
    accessories = await setup_accessories_from_file(hass, "nanoleaf_strip_nl55.json")
 | 
						|
 | 
						|
    fake_controller = await setup_platform(hass)
 | 
						|
    await fake_controller.add_paired_device(accessories, "00:00:00:00:00:00")
 | 
						|
    config_entry = MockConfigEntry(
 | 
						|
        version=1,
 | 
						|
        domain="homekit_controller",
 | 
						|
        entry_id="TestData",
 | 
						|
        data={"AccessoryPairingID": "00:00:00:00:00:00", "Connection": "BLE"},
 | 
						|
        title="test",
 | 
						|
        unique_id="00:00:00:00:00:00",
 | 
						|
    )
 | 
						|
    config_entry.add_to_hass(hass)
 | 
						|
 | 
						|
    fake_controller.transport_type = TransportType.BLE
 | 
						|
 | 
						|
    # Needs a COAP transport to do migration
 | 
						|
    fake_controller.transports = {TransportType.COAP: fake_controller}
 | 
						|
 | 
						|
    await hass.config_entries.async_setup(config_entry.entry_id)
 | 
						|
    await hass.async_block_till_done()
 | 
						|
 | 
						|
    # Make sure not disoverable via CoAP
 | 
						|
    del fake_controller.discoveries["00:00:00:00:00:00"]
 | 
						|
 | 
						|
    with pytest.raises(HomeAssistantError):
 | 
						|
        await hass.services.async_call(
 | 
						|
            "button",
 | 
						|
            "press",
 | 
						|
            {
 | 
						|
                "entity_id": "button.nanoleaf_strip_3b32_provision_preferred_thread_credentials"
 | 
						|
            },
 | 
						|
            blocking=True,
 | 
						|
        )
 | 
						|
 | 
						|
    assert config_entry.data["Connection"] == "BLE"
 |