def register_node_in_dev_reg( hass: HomeAssistant, entry: ConfigEntry, dev_reg: device_registry.DeviceRegistry, client: ZwaveClient, node: ZwaveNode, remove_device_func: Callable[[device_registry.DeviceEntry], None], ) -> device_registry.DeviceEntry: """Register node in dev reg.""" device_id = get_device_id(client, node) # If a device already exists but it doesn't match the new node, it means the node # was replaced with a different device and the device needs to be removeed so the # new device can be created. Otherwise if the device exists and the node is the same, # the node was replaced with the same device model and we can reuse the device. if (device := dev_reg.async_get_device({ device_id })) and (device.model != node.device_config.label or device.manufacturer != node.device_config.manufacturer): remove_device_func(device)
async def _remove_device( hass: HomeAssistant, config_entry: ConfigEntry, mac: str, tasmota_mqtt: TasmotaMQTTClient, device_registry: DeviceRegistry, ) -> None: """Remove device from device registry.""" device = device_registry.async_get_device(set(), {(CONNECTION_NETWORK_MAC, mac)}) if device is None or config_entry.entry_id not in device.config_entries: return _LOGGER.debug("Removing tasmota from device %s", mac) device_registry.async_update_device( device.id, remove_config_entry_id=config_entry.entry_id) await clear_discovery_topic(mac, config_entry.data[CONF_DISCOVERY_PREFIX], tasmota_mqtt)
def register_node_in_dev_reg( hass: HomeAssistant, entry: ConfigEntry, dev_reg: device_registry.DeviceRegistry, driver: Driver, node: ZwaveNode, remove_device_func: Callable[[device_registry.DeviceEntry], None], ) -> device_registry.DeviceEntry: """Register node in dev reg.""" device_id = get_device_id(driver, node) device_id_ext = get_device_id_ext(driver, node) device = dev_reg.async_get_device({device_id}) # Replace the device if it can be determined that this node is not the # same product as it was previously. if ( device_id_ext and device and len(device.identifiers) == 2 and device_id_ext not in device.identifiers ): remove_device_func(device) device = None if device_id_ext: ids = {device_id, device_id_ext} else: ids = {device_id} device = dev_reg.async_get_or_create( config_entry_id=entry.entry_id, identifiers=ids, sw_version=node.firmware_version, name=node.name or node.device_config.description or f"Node {node.node_id}", model=node.device_config.label, manufacturer=node.device_config.manufacturer, suggested_area=node.location if node.location else UNDEFINED, ) async_dispatcher_send(hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device) return device
def check_device_registry( device_registry: DeviceRegistry, expected_devices: list[MappingProxyType] ) -> None: """Ensure that the expected_devices are correctly registered.""" for expected_device in expected_devices: registry_entry = device_registry.async_get_device( expected_device[ATTR_IDENTIFIERS] ) assert registry_entry is not None assert registry_entry.identifiers == expected_device[ATTR_IDENTIFIERS] assert registry_entry.manufacturer == expected_device[ATTR_MANUFACTURER] assert registry_entry.name == expected_device[ATTR_NAME] assert registry_entry.model == expected_device[ATTR_MODEL] if expected_via_device := expected_device.get(ATTR_VIA_DEVICE): assert registry_entry.via_device_id is not None parent_entry = device_registry.async_get_device({expected_via_device}) assert parent_entry is not None assert registry_entry.via_device_id == parent_entry.id else: assert registry_entry.via_device_id is None
def register_node_in_dev_reg( hass: HomeAssistant, entry: ConfigEntry, dev_reg: device_registry.DeviceRegistry, client: ZwaveClient, node: ZwaveNode, remove_device_func: Callable[[device_registry.DeviceEntry], None], ) -> device_registry.DeviceEntry: """Register node in dev reg.""" device_id = get_device_id(client, node) device_id_ext = get_device_id_ext(client, node) device = dev_reg.async_get_device({device_id}) # Replace the device if it can be determined that this node is not the # same product as it was previously. if (device_id_ext and device and len(device.identifiers) == 2 and device_id_ext not in device.identifiers): remove_device_func(device) device = None if device_id_ext: ids = {device_id, device_id_ext} else: ids = {device_id} params = { ATTR_IDENTIFIERS: ids, ATTR_SW_VERSION: node.firmware_version, ATTR_NAME: node.name or node.device_config.description or f"Node {node.node_id}", ATTR_MODEL: node.device_config.label, ATTR_MANUFACTURER: node.device_config.manufacturer, } if node.location: params[ATTR_SUGGESTED_AREA] = node.location device = dev_reg.async_get_or_create(config_entry_id=entry.entry_id, **params) async_dispatcher_send(hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device) return device
def check_device_registry( device_registry: DeviceRegistry, expected_device: dict[str, Any] ) -> None: """Ensure that the expected_device is correctly registered.""" assert len(device_registry.devices) == 1 registry_entry = device_registry.async_get_device(expected_device[ATTR_IDENTIFIERS]) assert registry_entry is not None assert registry_entry.identifiers == expected_device[ATTR_IDENTIFIERS] assert registry_entry.manufacturer == expected_device[ATTR_MANUFACTURER] assert registry_entry.name == expected_device[ATTR_NAME] assert registry_entry.model == expected_device[ATTR_MODEL] assert registry_entry.sw_version == expected_device[ATTR_SW_VERSION]
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
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
def _add_camera( hass: HomeAssistant, device_registry: dr.DeviceRegistry, client: MotionEyeClient, entry: ConfigEntry, camera_id: int, camera: dict[str, Any], device_identifier: tuple[str, str], ) -> None: """Add a motionEye camera to hass.""" device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={device_identifier}, manufacturer=MOTIONEYE_MANUFACTURER, model=MOTIONEYE_MANUFACTURER, name=camera[KEY_NAME], ) async_dispatcher_send( hass, SIGNAL_CAMERA_ADD.format(entry.entry_id), camera, )
async def test_get_actions(hass, device_reg: DeviceRegistry, device, expected): """Test we get the expected actions from a rfxtrx.""" await setup_entry(hass, {device.code: {"signal_repetitions": 1}}) device_entry = device_reg.async_get_device(device.device_identifiers, set()) assert device_entry actions = await async_get_device_automations(hass, "action", device_entry.id) actions = [action for action in actions if action["domain"] == DOMAIN] expected_actions = [ {"domain": DOMAIN, "device_id": device_entry.id, **action_type} for action_type in expected ] assert_lists_same(actions, expected_actions)
def register_device( hass: HomeAssistant, entry: ConfigEntry, dev_reg: device_registry.DeviceRegistry, device: Device, ) -> None: """Register device in device registry.""" params = DeviceInfo( identifiers={(DOMAIN, device.address)}, name=device.name, model=device.model, manufacturer="Govee", ) device = dev_reg.async_get_or_create(config_entry_id=entry.entry_id, **params) async_dispatcher_send(hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device)
def register_node_in_dev_reg( hass: HomeAssistant, entry: ConfigEntry, dev_reg: device_registry.DeviceRegistry, client: ZwaveClient, node: ZwaveNode, ) -> None: """Register node in dev reg.""" device = dev_reg.async_get_or_create( config_entry_id=entry.entry_id, identifiers={(DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}")}, sw_version=node.firmware_version, name=node.name or node.device_config.description or f"Node {node.node_id}", model=node.device_config.label, manufacturer=node.device_config.manufacturer, ) async_dispatcher_send(hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device)
async def test_invalid_trigger(hass, device_reg: DeviceRegistry): """Test for invalid actions.""" event = EVENT_LIGHTING_1 notification_calls = async_mock_service(hass, "persistent_notification", "create") await setup_entry( hass, {event.code: { "fire_event": True, "signal_repetitions": 1 }}) device_identifers: Any = event.device_identifiers device_entry = device_reg.async_get_device(device_identifers, set()) assert device_entry assert await async_setup_component( hass, automation.DOMAIN, { automation.DOMAIN: [ { "trigger": { "platform": "device", "domain": DOMAIN, "device_id": device_entry.id, "type": event.type, "subtype": "invalid", }, "action": { "service": "test.automation", "data_template": { "some": ("{{trigger.platform}}") }, }, }, ] }, ) await hass.async_block_till_done() assert len(notification_calls) == 1 assert ("The following integrations and platforms could not be set up" in notification_calls[0].data["message"])
async def test_firing_event(hass, device_reg: DeviceRegistry, rfxtrx, event): """Test for turn_on and turn_off triggers firing.""" await setup_entry( hass, {event.code: { "fire_event": True, "signal_repetitions": 1 }}) device_entry = device_reg.async_get_device(event.device_identifiers, set()) assert device_entry calls = async_mock_service(hass, "test", "automation") assert await async_setup_component( hass, automation.DOMAIN, { automation.DOMAIN: [ { "trigger": { "platform": "device", "domain": DOMAIN, "device_id": device_entry.id, "type": event.type, "subtype": event.subtype, }, "action": { "service": "test.automation", "data_template": { "some": ("{{trigger.platform}}") }, }, }, ] }, ) await hass.async_block_till_done() await rfxtrx.signal(event.code) assert len(calls) == 1 assert calls[0].data["some"] == "device"
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
def register_node_in_dev_reg( hass: HomeAssistant, entry: ConfigEntry, dev_reg: device_registry.DeviceRegistry, client: ZwaveClient, node: ZwaveNode, ) -> None: """Register node in dev reg.""" params = { "config_entry_id": entry.entry_id, "identifiers": {get_device_id(client, node)}, "sw_version": node.firmware_version, "name": node.name or node.device_config.description or f"Node {node.node_id}", "model": node.device_config.label, "manufacturer": node.device_config.manufacturer, } if node.location: params["suggested_area"] = node.location device = dev_reg.async_get_or_create(**params) async_dispatcher_send(hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device)
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, DeviceAutomationType.CONDITION, device_entry.id) assert_lists_same(conditions, expected_conditions)
async def test_invalid_action(hass, device_reg: DeviceRegistry): """Test for invalid actions.""" device = DEVICE_LIGHTING_1 await setup_entry(hass, {device.code: {"signal_repetitions": 1}}) device_identifers: Any = device.device_identifiers device_entry = device_reg.async_get_device(device_identifers, set()) assert device_entry assert await async_setup_component( hass, automation.DOMAIN, { automation.DOMAIN: [ { "trigger": { "platform": "event", "event_type": "test_event", }, "action": { "domain": DOMAIN, "device_id": device_entry.id, "type": "send_command", "subtype": "invalid", }, }, ] }, ) await hass.async_block_till_done() assert len( notifications := hass.states.async_all("persistent_notification")) == 1 assert ("The following integrations and platforms could not be set up" in notifications[0].attributes["message"])
async def test_get_triggers( hass: HomeAssistant, device_reg: device_registry.DeviceRegistry, entity_reg: EntityRegistry, ) -> None: """Test we get the expected triggers from a select.""" 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": "current_option_changed", "device_id": device_entry.id, "entity_id": f"{DOMAIN}.test_5678", } ] triggers = await async_get_device_automations(hass, "trigger", device_entry.id) assert_lists_same(triggers, expected_triggers)
def _get_device_long_name(device_registry: DeviceRegistry, current_device: str) -> str: device = device_registry.async_get_device({(DOMAIN, current_device)}) if device and device.name_by_user: return f"{device.name_by_user} ({current_device})" return current_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.""" 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
def async_remove_addons_from_dev_reg(dev_reg: DeviceRegistry, addons: set[str]) -> None: """Remove addons from the device registry.""" for addon_slug in addons: if dev := dev_reg.async_get_device({(DOMAIN, addon_slug)}): dev_reg.async_remove_device(dev.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.""" 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) == []
def _add_camera( hass: HomeAssistant, device_registry: dr.DeviceRegistry, client: MotionEyeClient, entry: ConfigEntry, camera_id: int, camera: dict[str, Any], device_identifier: tuple[str, str], ) -> None: """Add a motionEye camera to hass.""" def _is_recognized_web_hook(url: str) -> bool: """Determine whether this integration set a web hook.""" return f"{WEB_HOOK_SENTINEL_KEY}={WEB_HOOK_SENTINEL_VALUE}" in url def _set_webhook( url: str, key_url: str, key_method: str, key_enabled: str, camera: dict[str, Any], ) -> bool: """Set a web hook.""" if (entry.options.get( CONF_WEBHOOK_SET_OVERWRITE, DEFAULT_WEBHOOK_SET_OVERWRITE, ) or not camera.get(key_url) or _is_recognized_web_hook(camera[key_url])) and ( not camera.get(key_enabled, False) or camera.get(key_method) != KEY_HTTP_METHOD_GET or camera.get(key_url) != url): camera[key_enabled] = True camera[key_method] = KEY_HTTP_METHOD_GET camera[key_url] = url return True return False def _build_url(base: str, keys: list[str]) -> str: """Build a motionEye webhook URL.""" return (base + "?" + urlencode( { **{ k: KEY_WEB_HOOK_CONVERSION_SPECIFIERS[k] for k in sorted(keys) }, WEB_HOOK_SENTINEL_KEY: WEB_HOOK_SENTINEL_VALUE, }, safe="%{}", )) device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={device_identifier}, manufacturer=MOTIONEYE_MANUFACTURER, model=MOTIONEYE_MANUFACTURER, name=camera[KEY_NAME], ) if entry.options.get(CONF_WEBHOOK_SET, DEFAULT_WEBHOOK_SET): url = None try: url = get_url(hass) except NoURLAvailableError: pass if url: if _set_webhook( _build_url( f"{url}{API_PATH_DEVICE_ROOT}{device.id}/{EVENT_MOTION_DETECTED}", EVENT_MOTION_DETECTED_KEYS, ), KEY_WEB_HOOK_NOTIFICATIONS_URL, KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD, KEY_WEB_HOOK_NOTIFICATIONS_ENABLED, camera, ) | _set_webhook( _build_url( f"{url}{API_PATH_DEVICE_ROOT}{device.id}/{EVENT_FILE_STORED}", EVENT_FILE_STORED_KEYS, ), KEY_WEB_HOOK_STORAGE_URL, KEY_WEB_HOOK_STORAGE_HTTP_METHOD, KEY_WEB_HOOK_STORAGE_ENABLED, camera, ): hass.async_create_task( client.async_set_camera(camera_id, camera)) async_dispatcher_send( hass, SIGNAL_CAMERA_ADD.format(entry.entry_id), camera, )
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
def _add_camera( hass: HomeAssistant, device_registry: dr.DeviceRegistry, client: MotionEyeClient, entry: ConfigEntry, camera_id: int, camera: dict[str, Any], device_identifier: tuple[str, str], ) -> None: """Add a motionEye camera to hass.""" def _is_recognized_web_hook(url: str) -> bool: """Determine whether this integration set a web hook.""" return f"{WEB_HOOK_SENTINEL_KEY}={WEB_HOOK_SENTINEL_VALUE}" in url def _set_webhook( url: str, key_url: str, key_method: str, key_enabled: str, camera: dict[str, Any], ) -> bool: """Set a web hook.""" if (entry.options.get( CONF_WEBHOOK_SET_OVERWRITE, DEFAULT_WEBHOOK_SET_OVERWRITE, ) or not camera.get(key_url) or _is_recognized_web_hook(camera[key_url])) and ( not camera.get(key_enabled, False) or camera.get(key_method) != KEY_HTTP_METHOD_POST_JSON or camera.get(key_url) != url): camera[key_enabled] = True camera[key_method] = KEY_HTTP_METHOD_POST_JSON camera[key_url] = url return True return False def _build_url(device: dr.DeviceEntry, base: str, event_type: str, keys: list[str]) -> str: """Build a motionEye webhook URL.""" # This URL-surgery cannot use YARL because the output must NOT be # url-encoded. This is because motionEye will do further string # manipulation/substitution on this value before ultimately fetching it, # and it cannot deal with URL-encoded input to that string manipulation. return urljoin( base, "?" + urlencode( { **{ k: KEY_WEB_HOOK_CONVERSION_SPECIFIERS[k] for k in sorted(keys) }, WEB_HOOK_SENTINEL_KEY: WEB_HOOK_SENTINEL_VALUE, ATTR_EVENT_TYPE: event_type, ATTR_DEVICE_ID: device.id, }, safe="%{}", ), ) device = device_registry.async_get_or_create( config_entry_id=entry.entry_id, identifiers={device_identifier}, manufacturer=MOTIONEYE_MANUFACTURER, model=MOTIONEYE_MANUFACTURER, name=camera[KEY_NAME], ) if entry.options.get(CONF_WEBHOOK_SET, DEFAULT_WEBHOOK_SET): url = async_generate_motioneye_webhook(hass, entry.data[CONF_WEBHOOK_ID]) if url: set_motion_event = _set_webhook( _build_url( device, url, EVENT_MOTION_DETECTED, EVENT_MOTION_DETECTED_KEYS, ), KEY_WEB_HOOK_NOTIFICATIONS_URL, KEY_WEB_HOOK_NOTIFICATIONS_HTTP_METHOD, KEY_WEB_HOOK_NOTIFICATIONS_ENABLED, camera, ) set_storage_event = _set_webhook( _build_url( device, url, EVENT_FILE_STORED, EVENT_FILE_STORED_KEYS, ), KEY_WEB_HOOK_STORAGE_URL, KEY_WEB_HOOK_STORAGE_HTTP_METHOD, KEY_WEB_HOOK_STORAGE_ENABLED, camera, ) if set_motion_event or set_storage_event: hass.async_create_task( client.async_set_camera(camera_id, camera)) async_dispatcher_send( hass, SIGNAL_CAMERA_ADD.format(entry.entry_id), camera, )