Grouped accessories: parent/child model, location backfill, Device Manager UI#11
Merged
Merged
Conversation
Children of a grouped device (e.g. AirPods Case/Left/Right Bud whose groupIdentifier matches a parent's baUUID) carry their parent's id so upstream can reason about hierarchy. Defaulted to nil so existing call sites compile unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
parseDeviceArray accepts an optional groupParentIDs map (child's groupIdentifier -> parent's id). Children whose groupIdentifier resolves to a known parent receive parentID; everything else stays nil. The function remains pure — naming/visibility decisions live upstream. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
readAndParseCaches now reads+decrypts all candidates into raw dicts in phase 1, builds a groupIdentifier->parentID map from Devices.data parent groups in phase 2, then parses to DevicePoint with the map applied to items in phase 3. Children of grouped accessories now carry parentID. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Controls whether unaliased grouped children (e.g. AirPods Case/Left/Right Bud) appear in the Device Manager and publish to HA. Aliased children always remain visible/published. Defaulted on so new and upgrading users see a tidy Device Manager out of the box. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds filterHiddenGroupedChildren helper and applies it inside buildPlanAndLog before constructing PlanPhase. Aliased children are always kept; only unaliased entries with parentID set are dropped. Logs the dropped count at debug level. New SyncEngineGroupingTests target file (added to project.pbxproj since the test group is an explicit PBXGroup, not a synchronized one). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a parent group's location is flagged isOld=true or is older than its freshest child by 60s+, replace it with the freshest child's lat/ lon/accuracy. Preserves parent's id, name, battery, prsId, parentID. Wired into readAndParseCaches as a Phase 4 step after parsing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds baseUnassignedAfterGroupHide derived property and routes filteredUnassigned through it. Aliased children are already excluded from baseUnassigned, so the toggle only drops remaining (unaliased) children with parentID set. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds ParentDisclosureRow view; ephemeral collapsedParents @State on DeviceManagerView. unassignedList partitions filteredUnassigned into top-level entries and children grouped by parentID; parents with kids get a chevron and indented children below. Orphans (children whose parent isn't in the filtered list, e.g. when the parent has been aliased and is no longer in the unassigned list) fall back to flat top-level rendering. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Compact checkbox bound to settings.hideGroupedChildren, placed alongside the existing source filter picker in the Unassigned section header. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ildren The per-parent disclosure chevron in the Device Manager already covers the UI hide/show concern, so the global toggle was redundant. Posting behavior is now unconditional: unaliased grouped children are always dropped from the post list. Aliased children continue to publish, so existing user setups keep working. Removed: - SettingsStore.hideGroupedChildren - DeviceManagerView "Hide sub-items" toggle and its onChange - baseUnassignedAfterGroupHide derived property - filterHiddenGroupedChildren's hideEnabled param Added/changed: - SyncEngine.filterUnaliasedGroupedChildren (pure unconditional filter) - DeviceManagerView state inverted: expandedParents (default empty) so parents are collapsed by default. Click chevron to expand. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parent row chevron is hidden at rest and appears in a faint gray pill on hover, with an accent-blue chevron inside. Pill stays visible while expanded so users can collapse without re-hovering. Childless rows unchanged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CLAUDE.md: bump test count 58->70, add SyncEngineGroupingTests, note parentID on DevicePoint, add convention line on grouped accessory recognition. - README.md: add Features bullet under Device Manager. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When a grouped accessory's parent is aliased, the parent stays in the Unassigned list (instead of disappearing and orphaning its children). The row gets a green "Assigned" pill next to the source badge, the Assign button is disabled, and the disclosure pill still works so users can expand and individually alias children (e.g. for finding a lost AirPod). Suppresses the rotated-UUID "Update" label when the UUID is already in the alias — clicking would no-op and reading "Update" was misleading. The actual UUID rotation flow still works because rotation produces a new UUID, which is not aliased. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Grouped accessory support is a new feature, not a patch — bump the minor version to mark the 1.4 line. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
AccessSettingsView had two pre-existing long tooltip/message strings exceeding the 200-char line_length error threshold. Splits each across multiple lines via string concatenation. No behavior change. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Apple's Find My data models accessory pairs (e.g. AirPods Pro) as a parent device in
Devices.dataplus N children inItems.datajoined bygroupIdentifier. FMS+ historically threw the relationship away — children surfaced as flat top-level entities ("Case", "Left Bud", "Right Bud") and parent groups appeared as a separate fourth entry, with no signal connecting them. Users with multiple pairs saw indistinguishable duplicates, and parent entities reported stale (isOld: true) locations even when their children had fresh GPS data (closes #10).This change makes the parent/child relationship first-class:
CacheDecryptor.parseDeviceArray): accepts agroupParentIDsmap and stampsparentIDonto childDevicePoints whosegroupIdentifierresolves to a known parent.DevicePoint: gains aparentID: String?field, preserved throughwith().SyncEngine.readAndParseCaches: restructured into 4 phases — read+decrypt all caches into raw, build the parent map fromDevices.dataitemGroupentries, parse toDevicePointwith the map applied to items, then backfill any parent whoselocationis stale (isOld: trueor older than the freshest child by ≥ 60 s) with the freshest child's lat/lon/accuracy.MARKETING_VERSIONto 1.4.0b — this is a feature line, not a patch.Backward compatibility
hideGroupedChildrenwas prototyped, then dropped — the per-parent disclosure pill plus the unconditional posting filter cover both UI hide and posting concerns more cleanly than a global toggle.Test plan
parentIDplumbing onDevicePoint, parser,with()filterUnaliasedGroupedChildren: keeps parents, drops unaliased children, keeps aliased children, ignores ungrouped itemsbackfillParentLocations: replacesisOldparent location, leaves fresh parent alone, replaces parent older than child by ≥ 60 s, no-op when no childrenDocumentation
Docs/DEVICE-MANAGEMENT.md— new "Grouped Accessories" sectionCLAUDE.md— test count, parentID note, convention lineREADME.md— Features bullet