async def test_get_actions( hass: HomeAssistant, device_reg: device_registry.DeviceRegistry, entity_reg: entity_registry.EntityRegistry, ) -> None: """Test we get the expected actions from a button.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) expected_actions = [{ "domain": DOMAIN, "type": "press", "device_id": device_entry.id, "entity_id": "button.test_5678", }] actions = await async_get_device_automations(hass, DeviceAutomationType.ACTION, device_entry.id) assert_lists_same(actions, expected_actions)
async def test_get_conditions( hass: HomeAssistant, device_reg: device_registry.DeviceRegistry, entity_reg: entity_registry.EntityRegistry, ) -> None: """Test we get the expected conditions from a NEW_DOMAIN.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) expected_conditions = [ { "condition": "device", "domain": DOMAIN, "type": "is_off", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, { "condition": "device", "domain": DOMAIN, "type": "is_on", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, ] conditions = await async_get_device_automations(hass, "condition", device_entry.id) assert_lists_same(conditions, expected_conditions)
async def test_get_trigger_capabilities(hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry) -> None: """Test we get the expected capabilities from a update trigger.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) expected_capabilities = { "extra_fields": [{ "name": "for", "optional": True, "type": "positive_time_period_dict" }] } triggers = await async_get_device_automations(hass, DeviceAutomationType.TRIGGER, device_entry.id) for trigger in triggers: capabilities = await async_get_device_automation_capabilities( hass, DeviceAutomationType.TRIGGER, trigger) assert capabilities == expected_capabilities
async def test_get_triggers( hass: HomeAssistant, device_reg: device_registry.DeviceRegistry, entity_reg: EntityRegistry, ) -> None: """Test we get the expected triggers from a button.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) expected_triggers = [{ "platform": "device", "domain": DOMAIN, "type": "pressed", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", "metadata": { "secondary": False }, }] triggers = await async_get_device_automations(hass, DeviceAutomationType.TRIGGER, device_entry.id) assert_lists_same(triggers, expected_triggers)
async def test_dimmer_switch_unique_id_fix_original_entity_was_deleted( hass: HomeAssistant, entity_reg: EntityRegistry): """Test that roll out unique id entity id changed to the original unique id.""" config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=MAC_ADDRESS) config_entry.add_to_hass(hass) dimmer = _mocked_dimmer() rollout_unique_id = MAC_ADDRESS.replace(":", "").upper() original_unique_id = tplink.legacy_device_id(dimmer) rollout_dimmer_entity_reg = entity_reg.async_get_or_create( config_entry=config_entry, platform=DOMAIN, domain="light", unique_id=rollout_unique_id, original_name="Rollout dimmer", ) with _patch_discovery(device=dimmer), _patch_single_discovery( device=dimmer): await setup.async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() migrated_dimmer_entity_reg = entity_reg.async_get_or_create( config_entry=config_entry, platform=DOMAIN, domain="light", unique_id=original_unique_id, original_name="Migrated dimmer", ) assert migrated_dimmer_entity_reg.entity_id == rollout_dimmer_entity_reg.entity_id
def is_sensor_continuous(ent_reg: er.EntityRegistry, entity_id: str) -> bool: """Determine if a sensor is continuous by checking its state class. Sensors with a unit_of_measurement are also considered continuous, but are filtered already by the SQL query generated by _get_events """ if not (entry := ent_reg.async_get(entity_id)): # Entity not registered, so can't have a state class return False
def check_and_enable_disabled_entities( entity_registry: EntityRegistry, expected_entities: MappingProxyType ) -> None: """Ensure that the expected_entities are correctly disabled.""" for expected_entity in expected_entities: if expected_entity.get(ATTR_DEFAULT_DISABLED): entity_id = expected_entity[ATTR_ENTITY_ID] registry_entry = entity_registry.entities.get(entity_id) assert registry_entry.disabled assert registry_entry.disabled_by == "integration" entity_registry.async_update_entity(entity_id, **{"disabled_by": None})
async def test_migration_device_online_end_to_end_after_downgrade( hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry): """Test migration from single config entry can happen again after a downgrade.""" config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry.add_to_hass(hass) already_migrated_config_entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: IP_ADDRESS}, unique_id=MAC_ADDRESS) already_migrated_config_entry.add_to_hass(hass) device = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, name=ALIAS, ) light_entity_reg = entity_reg.async_get_or_create( config_entry=config_entry, platform=DOMAIN, domain="light", unique_id=MAC_ADDRESS, original_name=ALIAS, device_id=device.id, ) power_sensor_entity_reg = entity_reg.async_get_or_create( config_entry=config_entry, platform=DOMAIN, domain="sensor", unique_id=f"{MAC_ADDRESS}_sensor", original_name=ALIAS, device_id=device.id, ) with _patch_discovery(), _patch_single_discovery(): await setup.async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() assert device.config_entries == {config_entry.entry_id} assert light_entity_reg.config_entry_id == config_entry.entry_id assert power_sensor_entity_reg.config_entry_id == config_entry.entry_id assert er.async_entries_for_config_entry(entity_reg, config_entry) == [] hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() legacy_entry = None for entry in hass.config_entries.async_entries(DOMAIN): if entry.unique_id == DOMAIN: legacy_entry = entry break assert legacy_entry is None
async def _get_entity_and_device(self, ent_reg: EntityRegistry, dev_reg: DeviceRegistry) -> \ tuple[RegistryEntry, DeviceEntry] | tuple[None, None]: """Fetch the entity and device entries.""" entity_entry = ent_reg.async_get(self.entity_id) if not entity_entry: return None, None device_entry = dev_reg.devices.get(entity_entry.device_id) return entity_entry, device_entry
def _exclude_by_entity_registry( ent_reg: entity_registry.EntityRegistry, entity_id: str, include_entity_category: bool, include_hidden: bool, ) -> bool: """Filter out hidden entities and ones with entity category (unless specified).""" return bool( (entry := ent_reg.async_get(entity_id)) and ((not include_hidden and entry.hidden_by is not None) or (not include_entity_category and entry.entity_category is not None)))
async def test_get_triggers(hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry) -> None: """Test we get the expected triggers from a update entity.""" config_entry = MockConfigEntry(domain="test", data={}) config_entry.add_to_hass(hass) device_entry = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) expected_triggers = [ { "platform": "device", "domain": DOMAIN, "type": "changed_states", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, { "platform": "device", "domain": DOMAIN, "type": "turned_off", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, { "platform": "device", "domain": DOMAIN, "type": "turned_on", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", }, ] triggers = await async_get_device_automations(hass, DeviceAutomationType.TRIGGER, device_entry.id) assert triggers == expected_triggers
async def test_migration_device_online_end_to_end(hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry): """Test migration from single config entry.""" config_entry = MockConfigEntry(domain=DOMAIN, title="LEGACY", data={}, unique_id=DOMAIN) config_entry.add_to_hass(hass) device = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, SERIAL)}, connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, name=LABEL, ) light_entity_reg = entity_reg.async_get_or_create( config_entry=config_entry, platform=DOMAIN, domain="light", unique_id=dr.format_mac(SERIAL), original_name=LABEL, device_id=device.id, ) with _patch_discovery(), _patch_config_flow_try_connect(), _patch_device(): await setup.async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() migrated_entry = None for entry in hass.config_entries.async_entries(DOMAIN): if entry.unique_id == DOMAIN: migrated_entry = entry break assert migrated_entry is not None assert device.config_entries == {migrated_entry.entry_id} assert light_entity_reg.config_entry_id == migrated_entry.entry_id assert er.async_entries_for_config_entry(entity_reg, config_entry) == [] hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=20)) await hass.async_block_till_done() legacy_entry = None for entry in hass.config_entries.async_entries(DOMAIN): if entry.unique_id == DOMAIN: legacy_entry = entry break assert legacy_entry is None
def async_migrate_entity( ent_reg: EntityRegistry, platform: str, old_unique_id: str, new_unique_id: str ) -> None: """Check if entity with old unique ID exists, and if so migrate it to new ID.""" if entity_id := ent_reg.async_get_entity_id(platform, DOMAIN, old_unique_id): _LOGGER.debug( "Migrating entity %s from old unique ID '%s' to new unique ID '%s'", entity_id, old_unique_id, new_unique_id, ) try: ent_reg.async_update_entity( entity_id, new_unique_id=new_unique_id, ) except ValueError: _LOGGER.debug( ( "Entity %s can't be migrated because the unique ID is taken; " "Cleaning it up since it is likely no longer valid" ), entity_id, ) ent_reg.async_remove(entity_id)
async def code_slots_changed( ent_reg: EntityRegistry, platform: entity_platform.EntityPlatform, config_entry: ConfigEntry, old_slots: List[int], new_slots: List[int], ): """Handle code slots changed.""" slots_to_add = list(set(new_slots) - set(old_slots)) slots_to_remove = list(set(old_slots) - set(new_slots)) for slot in slots_to_remove: sensor_name = slugify( f"{config_entry.data[CONF_LOCK_NAME]}_code_slot_{slot}" ) entity_id = f"sensor.{sensor_name}" if ent_reg.async_get(entity_id): await platform.async_remove_entity(entity_id) ent_reg.async_remove(entity_id) async_add_entities( [CodesSensor(hass, entry, x) for x in slots_to_add], True, )
def async_migrate_old_entity( hass: HomeAssistant, ent_reg: EntityRegistry, registered_unique_ids: set[str], platform: str, device: DeviceEntry, unique_id: str, ) -> None: """Migrate existing entity if current one can't be found and an old one exists.""" # If we can find an existing entity with this unique ID, there's nothing to migrate if ent_reg.async_get_entity_id(platform, DOMAIN, unique_id): return value_id = ValueID.from_unique_id(unique_id) # Look for existing entities in the registry that could be the same value but on # a different endpoint existing_entity_entries: list[RegistryEntry] = [] for entry in async_entries_for_device(ent_reg, device.id): # If entity is not in the domain for this discovery info or entity has already # been processed, skip it if entry.domain != platform or entry.unique_id in registered_unique_ids: continue try: old_ent_value_id = ValueID.from_unique_id(entry.unique_id) # Skip non value ID based unique ID's (e.g. node status sensor) except IndexError: continue if value_id.is_same_value_different_endpoints(old_ent_value_id): existing_entity_entries.append(entry) # We can return early if we get more than one result if len(existing_entity_entries) > 1: return # If we couldn't find any results, return early if not existing_entity_entries: return entry = existing_entity_entries[0] state = hass.states.get(entry.entity_id) if not state or state.state == STATE_UNAVAILABLE: async_migrate_unique_id(ent_reg, platform, entry.unique_id, unique_id)
async def test_migration_device_online_end_to_end_ignores_other_devices( hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry): """Test migration from single config entry.""" config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN) config_entry.add_to_hass(hass) other_domain_config_entry = MockConfigEntry(domain="other_domain", data={}, unique_id="other_domain") other_domain_config_entry.add_to_hass(hass) device = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, name=ALIAS, ) other_device = device_reg.async_get_or_create( config_entry_id=other_domain_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "556655665566")}, name=ALIAS, ) light_entity_reg = entity_reg.async_get_or_create( config_entry=config_entry, platform=DOMAIN, domain="light", unique_id=MAC_ADDRESS, original_name=ALIAS, device_id=device.id, ) power_sensor_entity_reg = entity_reg.async_get_or_create( config_entry=config_entry, platform=DOMAIN, domain="sensor", unique_id=f"{MAC_ADDRESS}_sensor", original_name=ALIAS, device_id=device.id, ) ignored_entity_reg = entity_reg.async_get_or_create( config_entry=other_domain_config_entry, platform=DOMAIN, domain="sensor", unique_id="00:00:00:00:00:00_sensor", original_name=ALIAS, device_id=device.id, ) garbage_entity_reg = entity_reg.async_get_or_create( config_entry=config_entry, platform=DOMAIN, domain="sensor", unique_id="garbage", original_name=ALIAS, device_id=other_device.id, ) with _patch_discovery(), _patch_single_discovery(): await setup.async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() migrated_entry = None for entry in hass.config_entries.async_entries(DOMAIN): if entry.unique_id == DOMAIN: migrated_entry = entry break assert migrated_entry is not None assert device.config_entries == {migrated_entry.entry_id} assert light_entity_reg.config_entry_id == migrated_entry.entry_id assert power_sensor_entity_reg.config_entry_id == migrated_entry.entry_id assert ignored_entity_reg.config_entry_id == other_domain_config_entry.entry_id assert garbage_entity_reg.config_entry_id == config_entry.entry_id assert er.async_entries_for_config_entry(entity_reg, config_entry) == [] hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() legacy_entry = None for entry in hass.config_entries.async_entries(DOMAIN): if entry.unique_id == DOMAIN: legacy_entry = entry break assert legacy_entry is not None
async def devices_serialize(self, entity_reg: EntityRegistry, dev_reg: DeviceRegistry): """Serialize entity for a devices response. https://yandex.ru/dev/dialogs/alice/doc/smart-home/reference/get-devices-docpage/ """ state = self.state # When a state is unavailable, the attributes that describe # capabilities will be stripped. For example, a light entity will miss # the min/max mireds. Therefore they will be excluded from a sync. if state.state == STATE_UNAVAILABLE: return None entity_config = self.config.entity_config.get(state.entity_id, {}) name = (entity_config.get(CONF_NAME) or state.name).strip() domain = state.domain device_class = state.attributes.get(ATTR_DEVICE_CLASS) # If an empty string if not name: return None capabilities = self.capabilities() properties = self.properties() # Found no supported capabilities for this entity if not capabilities and not properties: return None device_type = get_yandex_type(domain, device_class) entry = entity_reg.async_get(state.entity_id) device = dev_reg.async_get(getattr(entry, 'device_id', "")) manufacturer = state.entity_id + ' | ' + getattr( device, "manufacturer", "Yandex Smart Home") model = getattr(device, "model", "") device_info = {'manufacturer': manufacturer, 'model': model} device = { 'id': state.entity_id, 'name': name, 'type': device_type, 'capabilities': [], 'properties': [], 'device_info': device_info, } for cpb in capabilities: description = cpb.description() if description not in device['capabilities']: device['capabilities'].append(description) for ppt in properties: description = ppt.description() if description not in device['properties']: device['properties'].append(description) override_type = entity_config.get(CONF_TYPE) if override_type: device['type'] = override_type room = entity_config.get(CONF_ROOM) if room: device['room'] = room return device dev_reg, ent_reg, area_reg = await gather( self.hass.helpers.device_registry.async_get_registry(), self.hass.helpers.entity_registry.async_get_registry(), self.hass.helpers.area_registry.async_get_registry(), ) entity_entry = ent_reg.async_get(state.entity_id) if not (entity_entry and entity_entry.device_id): return device device_entry = dev_reg.devices.get(entity_entry.device_id) if not (device_entry and device_entry.area_id): return device area_entry = area_reg.areas.get(device_entry.area_id) if area_entry and area_entry.name: device['room'] = area_entry.name return device
async def test_migration_device_online_end_to_end_ignores_other_devices( hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry): """Test migration from single config entry.""" legacy_config_entry = MockConfigEntry(domain=DOMAIN, title="LEGACY", data={}, unique_id=DOMAIN) legacy_config_entry.add_to_hass(hass) other_domain_config_entry = MockConfigEntry(domain="other_domain", data={}, unique_id="other_domain") other_domain_config_entry.add_to_hass(hass) device = device_reg.async_get_or_create( config_entry_id=legacy_config_entry.entry_id, identifiers={(DOMAIN, SERIAL)}, connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, name=LABEL, ) other_device = device_reg.async_get_or_create( config_entry_id=other_domain_config_entry.entry_id, connections={(dr.CONNECTION_NETWORK_MAC, "556655665566")}, name=LABEL, ) light_entity_reg = entity_reg.async_get_or_create( config_entry=legacy_config_entry, platform=DOMAIN, domain="light", unique_id=SERIAL, original_name=LABEL, device_id=device.id, ) ignored_entity_reg = entity_reg.async_get_or_create( config_entry=other_domain_config_entry, platform=DOMAIN, domain="sensor", unique_id="00:00:00:00:00:00_sensor", original_name=LABEL, device_id=device.id, ) garbage_entity_reg = entity_reg.async_get_or_create( config_entry=legacy_config_entry, platform=DOMAIN, domain="sensor", unique_id="garbage", original_name=LABEL, device_id=other_device.id, ) with _patch_discovery(), _patch_config_flow_try_connect(), _patch_device(): await setup.async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=20)) await hass.async_block_till_done() new_entry = None legacy_entry = None for entry in hass.config_entries.async_entries(DOMAIN): if entry.unique_id == DOMAIN: legacy_entry = entry else: new_entry = entry assert new_entry is not None assert legacy_entry is None assert device.config_entries == {legacy_config_entry.entry_id} assert light_entity_reg.config_entry_id == legacy_config_entry.entry_id assert ignored_entity_reg.config_entry_id == other_domain_config_entry.entry_id assert garbage_entity_reg.config_entry_id == legacy_config_entry.entry_id assert er.async_entries_for_config_entry(entity_reg, legacy_config_entry) == [] assert dr.async_entries_for_config_entry(device_reg, legacy_config_entry) == []
async def test_discovery_is_more_frequent_during_migration( hass: HomeAssistant, device_reg: DeviceRegistry, entity_reg: EntityRegistry): """Test that discovery is more frequent during migration.""" config_entry = MockConfigEntry(domain=DOMAIN, title="LEGACY", data={}, unique_id=DOMAIN) config_entry.add_to_hass(hass) device = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, SERIAL)}, connections={(dr.CONNECTION_NETWORK_MAC, MAC_ADDRESS)}, name=LABEL, ) entity_reg.async_get_or_create( config_entry=config_entry, platform=DOMAIN, domain="light", unique_id=dr.format_mac(SERIAL), original_name=LABEL, device_id=device.id, ) bulb = _mocked_bulb() start_calls = 0 class MockLifxDiscovery: """Mock lifx discovery.""" def __init__(self, *args, **kwargs): """Init discovery.""" self.bulb = bulb self.lights = {} def start(self): """Mock start.""" nonlocal start_calls start_calls += 1 # Discover the bulb so we can complete migration # and verify we switch back to normal discovery # interval if start_calls == 4: self.lights = {self.bulb.mac_addr: self.bulb} def cleanup(self): """Mock cleanup.""" with _patch_device(device=bulb), _patch_config_flow_try_connect( device=bulb), patch.object(discovery, "DEFAULT_TIMEOUT", 0), patch( "homeassistant.components.lifx.discovery.LifxDiscovery", MockLifxDiscovery): await async_setup_component(hass, lifx.DOMAIN, {lifx.DOMAIN: {}}) await hass.async_block_till_done() assert start_calls == 0 hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() assert start_calls == 1 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5)) await hass.async_block_till_done() assert start_calls == 3 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=10)) await hass.async_block_till_done() assert start_calls == 4 async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=15)) await hass.async_block_till_done() assert start_calls == 5