Skip to content

Commit 84b967c

Browse files
Fix Scenario B migration: keep legacy accessory to preserve HomeKit customisations (#246)
* Initial plan * Fix Scenario B: keep legacy accessory to preserve HomeKit customisations Agent-Logs-Url: https://github.com/homebridge-plugins/homebridge-updater/sessions/979794f8-58d7-4fbb-9b49-3283a8a12e03 Co-authored-by: donavanbecker <9875439+donavanbecker@users.noreply.github.com> * Tighten Scenario B assertions: use toHaveBeenLastCalledWith and call count Agent-Logs-Url: https://github.com/homebridge-plugins/homebridge-updater/sessions/01011440-32cb-45fb-a605-53e4eb523985 Co-authored-by: donavanbecker <9875439+donavanbecker@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: donavanbecker <9875439+donavanbecker@users.noreply.github.com> Co-authored-by: Donavan Becker <beckersmarthome@icloud.com>
1 parent 61990d2 commit 84b967c

3 files changed

Lines changed: 45 additions & 18 deletions

File tree

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Platform.HAP.test.ts

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,15 @@ describe('PluginUpdatePlatform legacy UUID migration', () => {
8989
const legacyUpdateSensorUuid = mockApi.hap.uuid.generate(LEGACY_UPDATE_SENSOR_UUID_KEY)
9090

9191
let cachedLegacyUpdateAccessory: any = undefined
92+
let cachedCurrentUpdateAccessory: any = undefined
9293
let updateSensorCached = false
9394

9495
// Simulate configureAccessory for each cached accessory
9596
for (const accessory of cachedAccessories) {
9697
if (accessory.UUID === updateSensorUuid) {
9798
mockUpdateSensor.configureAccessory(accessory)
9899
updateSensorCached = true
100+
cachedCurrentUpdateAccessory = accessory
99101
continue
100102
}
101103
if (accessory.UUID === legacyUpdateSensorUuid) {
@@ -112,9 +114,15 @@ describe('PluginUpdatePlatform legacy UUID migration', () => {
112114
if (!cachedLegacyUpdateAccessory) return
113115

114116
if (updateSensorCached) {
115-
mockApi.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [cachedLegacyUpdateAccessory])
117+
// Both UUIDs found — remove the newer empty accessory, promote the legacy one
118+
// so the user's HomeKit customisations are preserved.
119+
if (cachedCurrentUpdateAccessory) {
120+
mockApi.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [cachedCurrentUpdateAccessory])
121+
cachedCurrentUpdateAccessory = undefined
122+
}
123+
mockUpdateSensor.configureAccessory(cachedLegacyUpdateAccessory)
116124
cachedLegacyUpdateAccessory = undefined
117-
mockLog.info('Removed stale legacy update sensor accessory (migrated to current UUID)')
125+
mockLog.info('Migrated update sensor to legacy cached accessory (preserved HomeKit customisations)')
118126
return
119127
}
120128

@@ -160,21 +168,24 @@ describe('PluginUpdatePlatform legacy UUID migration', () => {
160168
expect(registered).toBe(true)
161169
})
162170

163-
it('Scenario B: both legacy and current UUID in cache — must unregister the stale legacy accessory', () => {
171+
it('Scenario B: both legacy and current UUID in cache — must preserve legacy accessory and remove the newer empty duplicate', () => {
164172
const { mockLog, mockApi, mockUpdateSensor, uuidMap } = buildMocks()
165173

166174
const v3Accessory = { UUID: uuidMap[UPDATE_SENSOR_UUID_KEY], displayName: 'PluginUpdate' }
167175
const legacyAccessory = { UUID: uuidMap[LEGACY_UPDATE_SENSOR_UUID_KEY], displayName: 'Plugin Update Check' }
168176

169177
runMigrationSimulation(mockApi, mockLog, mockUpdateSensor, uuidMap, [v3Accessory, legacyAccessory])
170178

171-
// v3 accessory configured as update sensor
172-
expect(mockUpdateSensor.configureAccessory).toHaveBeenCalledWith(v3Accessory)
173-
// Legacy unregistered
174-
expect(mockApi.unregisterPlatformAccessories).toHaveBeenCalledWith(PLUGIN_NAME, PLATFORM_NAME, [legacyAccessory])
175-
// No new accessory created (v3 already registered)
179+
// configureAccessory called exactly twice: once for the v3 (during restore) then
180+
// again for the legacy during migration — legacy must be the final (last) call.
181+
expect(mockUpdateSensor.configureAccessory).toHaveBeenCalledTimes(2)
182+
expect(mockUpdateSensor.configureAccessory).toHaveBeenNthCalledWith(1, v3Accessory)
183+
expect(mockUpdateSensor.configureAccessory).toHaveBeenLastCalledWith(legacyAccessory)
184+
// Current-UUID empty duplicate is removed
185+
expect(mockApi.unregisterPlatformAccessories).toHaveBeenCalledWith(PLUGIN_NAME, PLATFORM_NAME, [v3Accessory])
186+
// No new accessory created
176187
expect(mockApi.registerPlatformAccessories).not.toHaveBeenCalled()
177-
expect(mockLog.info).toHaveBeenCalledWith(expect.stringContaining('Removed stale legacy update sensor accessory'))
188+
expect(mockLog.info).toHaveBeenCalledWith(expect.stringContaining('Migrated update sensor to legacy cached accessory'))
178189
})
179190

180191
it('Scenario B (legacy processed first): same result regardless of configureAccessory order', () => {
@@ -186,8 +197,12 @@ describe('PluginUpdatePlatform legacy UUID migration', () => {
186197
// Legacy accessory arrives first in configureAccessory order
187198
runMigrationSimulation(mockApi, mockLog, mockUpdateSensor, uuidMap, [legacyAccessory, v3Accessory])
188199

189-
expect(mockUpdateSensor.configureAccessory).toHaveBeenCalledWith(v3Accessory)
190-
expect(mockApi.unregisterPlatformAccessories).toHaveBeenCalledWith(PLUGIN_NAME, PLATFORM_NAME, [legacyAccessory])
200+
// Only the v3 accessory is processed during restore (legacy is held), then the
201+
// migration promotes the legacy one — so configureAccessory is still called twice
202+
// and the legacy accessory is always the last call.
203+
expect(mockUpdateSensor.configureAccessory).toHaveBeenCalledTimes(2)
204+
expect(mockUpdateSensor.configureAccessory).toHaveBeenLastCalledWith(legacyAccessory)
205+
expect(mockApi.unregisterPlatformAccessories).toHaveBeenCalledWith(PLUGIN_NAME, PLATFORM_NAME, [v3Accessory])
191206
expect(mockApi.registerPlatformAccessories).not.toHaveBeenCalled()
192207
})
193208

src/Platform.HAP.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export class PluginUpdatePlatform implements DynamicPlatformPlugin {
1818
private cachedFailureAccessory?: PlatformAccessory
1919
/** Cached accessory with the legacy v2.x UUID — resolved in handleLegacyMigration */
2020
private cachedLegacyUpdateAccessory?: PlatformAccessory
21+
/** Cached accessory with the current UUID — tracked so it can be replaced by the legacy one in Scenario B */
22+
private cachedCurrentUpdateAccessory?: PlatformAccessory
2123
/** True when a v3 update-sensor accessory was found in the cache (current UUID) */
2224
private updateSensorCached = false
2325

@@ -46,19 +48,28 @@ export class PluginUpdatePlatform implements DynamicPlatformPlugin {
4648
* 1. Only the legacy UUID is in the cache (fresh v2→v3 migration): configure the
4749
* update sensor on the legacy accessory so that {@link UpdateSensor.addUpdateSensor}
4850
* sees `registered = true` and does NOT create a duplicate.
49-
* 2. Both legacy and current UUIDs are in the cache (user already ran v3 once):
50-
* unregister the stale legacy accessory and let the current-UUID accessory continue.
51+
* 2. Both legacy and current UUIDs are in the cache (user already ran a newer version
52+
* once before this migration existed, creating a duplicate): remove the newer
53+
* empty accessory and promote the legacy one so that the user's HomeKit
54+
* customisations (room, name, automations) that are tied to the legacy UUID are
55+
* preserved.
5156
*/
5257
private handleLegacyMigration(): void {
5358
if (!this.cachedLegacyUpdateAccessory) {
5459
return
5560
}
5661

5762
if (this.updateSensorCached) {
58-
// Current-UUID accessory already recognised — remove the stale legacy one
59-
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [this.cachedLegacyUpdateAccessory])
63+
// Both UUIDs found — the current-UUID accessory was created empty when the legacy
64+
// one was first ignored. Prefer the legacy accessory so that any HomeKit
65+
// customisations the user applied to it are retained.
66+
if (this.cachedCurrentUpdateAccessory) {
67+
this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [this.cachedCurrentUpdateAccessory])
68+
this.cachedCurrentUpdateAccessory = undefined
69+
}
70+
this.updateSensor.configureAccessory(this.cachedLegacyUpdateAccessory)
6071
this.cachedLegacyUpdateAccessory = undefined
61-
this.log.info('Removed stale legacy update sensor accessory (migrated to current UUID)')
72+
this.log.info('Migrated update sensor to legacy cached accessory (preserved HomeKit customisations)')
6273
return
6374
}
6475

@@ -103,6 +114,7 @@ export class PluginUpdatePlatform implements DynamicPlatformPlugin {
103114
if (accessory.UUID === this.updateSensorUuid) {
104115
this.updateSensor.configureAccessory(accessory)
105116
this.updateSensorCached = true
117+
this.cachedCurrentUpdateAccessory = accessory
106118
return
107119
}
108120
// Restore for failure sensor

0 commit comments

Comments
 (0)