|
|
|
@@ -7,8 +7,17 @@ const space = (id, rooms = []) => ({
|
|
|
|
|
id, title: id, cell_cm: 5, view_box: [0, 0, 1, 1], rooms,
|
|
|
|
|
});
|
|
|
|
|
const room = (id, area = null) => ({ id, name: id, area, x: 0, y: 0, w: 1, h: 1 });
|
|
|
|
|
const emptyOwnerReport = {
|
|
|
|
|
orphanRoomLabelsRemoved: 0,
|
|
|
|
|
orphanDevicePositionsRemoved: 0,
|
|
|
|
|
orphanGroupPositionsRemoved: 0,
|
|
|
|
|
liveMissingPositionsRemoved: 0,
|
|
|
|
|
removedPositions: [],
|
|
|
|
|
liveMissingPositions: [],
|
|
|
|
|
unverifiedPositions: [],
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
test('issue 244 detaches only dead active placement and drops its stale coordinates', () => {
|
|
|
|
|
test('issue 252 detaches a live marker but preserves its stale coordinates until explicit cleanup', () => {
|
|
|
|
|
const marker = {
|
|
|
|
|
id: 'washer', binding: 'entity:sensor.washer', space: 'gone', room_id: 'gone_room',
|
|
|
|
|
name: 'Washer', icon: 'mdi:washing-machine', tap_action: 'more-info',
|
|
|
|
@@ -23,7 +32,11 @@ test('issue 244 detaches only dead active placement and drops its stale coordina
|
|
|
|
|
assert.equal(result.report.spaceRefsRemapped, 0);
|
|
|
|
|
assert.equal(result.config.markers[0].space, undefined);
|
|
|
|
|
assert.equal(result.config.markers[0].room_id, undefined);
|
|
|
|
|
assert.equal(result.layout.washer, undefined);
|
|
|
|
|
assert.deepEqual(result.layout.washer, { s: 'gone', x: 0.25, y: 0.5, k: 1.2 });
|
|
|
|
|
assert.equal(result.report.positionsUnresolved, 1);
|
|
|
|
|
assert.deepEqual(result.report.liveMissingPositions, [{
|
|
|
|
|
id: 'washer', spaceId: 'gone', kind: 'device', name: 'Washer',
|
|
|
|
|
}]);
|
|
|
|
|
assert.equal(result.config.markers[0].binding, marker.binding);
|
|
|
|
|
assert.equal(result.config.markers[0].icon, marker.icon);
|
|
|
|
|
assert.deepEqual(result.config.markers[0].pdfs, marker.pdfs);
|
|
|
|
@@ -62,6 +75,7 @@ test('issue 244 exact import signatures repair the full reference graph and are
|
|
|
|
|
positionsUnresolved: 0,
|
|
|
|
|
nestedRefsUnresolved: 0,
|
|
|
|
|
deadSpaceIds: [],
|
|
|
|
|
...emptyOwnerReport,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const again = repairSpaceReferences(result.config, result.layout);
|
|
|
|
@@ -74,10 +88,11 @@ test('issue 244 exact import signatures repair the full reference graph and are
|
|
|
|
|
positionsUnresolved: 0,
|
|
|
|
|
nestedRefsUnresolved: 0,
|
|
|
|
|
deadSpaceIds: [],
|
|
|
|
|
...emptyOwnerReport,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('issue 244 Area remap uses a unique production Area and never transplants coordinates', () => {
|
|
|
|
|
test('issue 252 Area remap never transplants or silently deletes old coordinates', () => {
|
|
|
|
|
const input = {
|
|
|
|
|
spaces: [space('ground', [room('utility', 'utility_area')])],
|
|
|
|
|
markers: [{
|
|
|
|
@@ -91,7 +106,9 @@ test('issue 244 Area remap uses a unique production Area and never transplants c
|
|
|
|
|
|
|
|
|
|
assert.equal(result.config.markers[0].space, 'ground');
|
|
|
|
|
assert.equal(result.config.markers[0].room_id, 'utility');
|
|
|
|
|
assert.equal(result.layout.washer, undefined);
|
|
|
|
|
assert.deepEqual(result.layout.washer, { s: 'gone', x: 0.9, y: 0.9 });
|
|
|
|
|
assert.equal(result.report.positionsUnresolved, 1);
|
|
|
|
|
assert.equal(result.report.liveMissingPositions.length, 1);
|
|
|
|
|
assert.equal(result.report.spaceRefsRemapped, 1);
|
|
|
|
|
assert.equal(result.report.roomRefsRemapped, 1);
|
|
|
|
|
assert.equal(result.report.markersDetached, 0);
|
|
|
|
@@ -121,7 +138,7 @@ test('issue 244 ambiguous, truncated and malformed signatures are never guessed'
|
|
|
|
|
assert.equal(result.config.markers[2].space, undefined);
|
|
|
|
|
assert.equal(result.config.markers[3].space, 'f1', 'removed tombstone is preserved');
|
|
|
|
|
assert.equal(result.report.markersDetached, 3);
|
|
|
|
|
assert.deepEqual(result.report.deadSpaceIds, ['f1']);
|
|
|
|
|
assert.deepEqual(result.report.deadSpaceIds, []);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('issue 244 preserves unresolved layout and nested calibration while reporting it', () => {
|
|
|
|
@@ -141,6 +158,9 @@ test('issue 244 preserves unresolved layout and nested calibration while reporti
|
|
|
|
|
assert.deepEqual(result.layout, layout);
|
|
|
|
|
assert.equal(result.report.nestedRefsUnresolved, 1);
|
|
|
|
|
assert.equal(result.report.positionsUnresolved, 1);
|
|
|
|
|
assert.deepEqual(result.report.unverifiedPositions, [{
|
|
|
|
|
id: 'opaque_owner', spaceId: 'gone', kind: 'unknown', name: '', reason: 'unknown_owner',
|
|
|
|
|
}]);
|
|
|
|
|
assert.deepEqual(result.report.deadSpaceIds, ['gone']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -175,7 +195,8 @@ test('issue 244 removed tombstones accept only an exact signature map', () => {
|
|
|
|
|
assert.deepEqual(result.layout.exact, { s: 'space_f1_deadbeef', x: 0.2, y: 0.3 });
|
|
|
|
|
assert.equal(result.config.markers[1].space, 'other');
|
|
|
|
|
assert.equal(result.config.markers[1].room_id, 'other-room');
|
|
|
|
|
assert.deepEqual(result.layout.guess, { s: 'other', x: 0.4, y: 0.5 });
|
|
|
|
|
assert.equal(result.layout.guess, undefined);
|
|
|
|
|
assert.equal(result.report.orphanDevicePositionsRemoved, 1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('issue 244 large valid reference graph stays unchanged in one indexed pass', () => {
|
|
|
|
@@ -200,3 +221,126 @@ test('issue 244 large valid reference graph stays unchanged in one indexed pass'
|
|
|
|
|
assert.equal(result.config.markers.length, markers.length);
|
|
|
|
|
assert.equal(Object.keys(result.layout).length, markers.length);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('issue 252 removes only proven room, device and group orphans and is idempotent', () => {
|
|
|
|
|
const removedMarkers = Array.from({ length: 3 }, (_, index) => ({
|
|
|
|
|
id: `removed-device-${index}`, binding: 'virtual', removed: true, space: 'deleted-floor',
|
|
|
|
|
}));
|
|
|
|
|
const roomLabels = Object.fromEntries(Array.from({ length: 32 }, (_, index) => [
|
|
|
|
|
`rl_deleted-room-${index}`, { s: 'deleted-floor', x: 0.1, y: 0.2 },
|
|
|
|
|
]));
|
|
|
|
|
const devicePositions = Object.fromEntries(removedMarkers.map((marker) => [
|
|
|
|
|
marker.id, { s: 'deleted-floor', x: 0.3, y: 0.4 },
|
|
|
|
|
]));
|
|
|
|
|
const layout = {
|
|
|
|
|
...roomLabels,
|
|
|
|
|
...devicePositions,
|
|
|
|
|
'lg_light.deleted_one': { s: 'deleted-floor', x: 0.5, y: 0.6 },
|
|
|
|
|
'lg_light.deleted_two': { s: 'deleted-floor', x: 0.7, y: 0.8 },
|
|
|
|
|
};
|
|
|
|
|
const config = {
|
|
|
|
|
spaces: [space('home', [room('living')])], markers: removedMarkers, settings: {},
|
|
|
|
|
};
|
|
|
|
|
const result = repairSpaceReferences(config, layout, {
|
|
|
|
|
ownerRoster: { authoritative: true, deviceIds: [], entityIds: [] },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert.equal(result.changed, true);
|
|
|
|
|
assert.equal(Object.keys(result.layout).length, 0);
|
|
|
|
|
assert.equal(result.report.orphanRoomLabelsRemoved, 32);
|
|
|
|
|
assert.equal(result.report.orphanDevicePositionsRemoved, 3);
|
|
|
|
|
assert.equal(result.report.orphanGroupPositionsRemoved, 2);
|
|
|
|
|
assert.equal(result.report.positionsUnresolved, 0);
|
|
|
|
|
assert.deepEqual(result.report.deadSpaceIds, []);
|
|
|
|
|
assert.deepEqual(layout, {
|
|
|
|
|
...roomLabels,
|
|
|
|
|
...devicePositions,
|
|
|
|
|
'lg_light.deleted_one': { s: 'deleted-floor', x: 0.5, y: 0.6 },
|
|
|
|
|
'lg_light.deleted_two': { s: 'deleted-floor', x: 0.7, y: 0.8 },
|
|
|
|
|
}, 'preview must not mutate the source layout');
|
|
|
|
|
|
|
|
|
|
const again = repairSpaceReferences(result.config, result.layout, {
|
|
|
|
|
ownerRoster: { authoritative: true, deviceIds: [], entityIds: [] },
|
|
|
|
|
});
|
|
|
|
|
assert.equal(again.changed, false);
|
|
|
|
|
assert.equal(again.report.orphanRoomLabelsRemoved, 0);
|
|
|
|
|
assert.equal(again.report.orphanDevicePositionsRemoved, 0);
|
|
|
|
|
assert.equal(again.report.orphanGroupPositionsRemoved, 0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('issue 252 preserves live owners by default and removes only them after explicit opt-in', () => {
|
|
|
|
|
const config = {
|
|
|
|
|
spaces: [space('home', [room('living')])],
|
|
|
|
|
markers: [{ id: 'virtual-live', binding: 'virtual', name: 'Hall note' }],
|
|
|
|
|
settings: { known_devices: ['auto-live'] },
|
|
|
|
|
};
|
|
|
|
|
const layout = {
|
|
|
|
|
'virtual-live': { s: 'gone', x: 0.1, y: 0.2 },
|
|
|
|
|
'auto-live': { s: 'gone', x: 0.3, y: 0.4 },
|
|
|
|
|
'lg_light.live': { s: 'gone', x: 0.5, y: 0.6 },
|
|
|
|
|
rl_living: { s: 'gone', x: 0.7, y: 0.8 },
|
|
|
|
|
};
|
|
|
|
|
const context = {
|
|
|
|
|
ownerRoster: {
|
|
|
|
|
authoritative: true,
|
|
|
|
|
deviceIds: ['auto-live'],
|
|
|
|
|
entityIds: ['light.live'],
|
|
|
|
|
names: {
|
|
|
|
|
'virtual-live': 'Hall note',
|
|
|
|
|
'auto-live': 'Washing machine',
|
|
|
|
|
'lg_light.live': 'Living lights',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const kept = repairSpaceReferences(config, layout, context);
|
|
|
|
|
assert.deepEqual(kept.layout, layout);
|
|
|
|
|
assert.equal(kept.report.positionsUnresolved, 4);
|
|
|
|
|
assert.equal(kept.report.liveMissingPositions.length, 4);
|
|
|
|
|
assert.deepEqual(kept.report.liveMissingPositions.map((item) => item.name).sort(), [
|
|
|
|
|
'Hall note', 'Living lights', 'Washing machine', 'living',
|
|
|
|
|
].sort());
|
|
|
|
|
assert.equal(kept.report.orphanDevicePositionsRemoved, 0);
|
|
|
|
|
|
|
|
|
|
const removed = repairSpaceReferences(config, layout, {
|
|
|
|
|
...context, removeLiveMissingPositions: true,
|
|
|
|
|
});
|
|
|
|
|
assert.deepEqual(removed.layout, {});
|
|
|
|
|
assert.equal(removed.report.liveMissingPositionsRemoved, 4);
|
|
|
|
|
assert.equal(removed.report.orphanRoomLabelsRemoved, 1);
|
|
|
|
|
assert.equal(removed.report.orphanDevicePositionsRemoved, 2);
|
|
|
|
|
assert.equal(removed.report.orphanGroupPositionsRemoved, 1);
|
|
|
|
|
assert.equal(removed.report.positionsUnresolved, 0);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test('issue 252 fails closed for limited registry and unknown future layout owners', () => {
|
|
|
|
|
const config = {
|
|
|
|
|
spaces: [space('home')], markers: [], settings: { known_devices: ['known-old-device'] },
|
|
|
|
|
};
|
|
|
|
|
const layout = {
|
|
|
|
|
'known-old-device': { s: 'gone', x: 0.1, y: 0.2 },
|
|
|
|
|
'lg_light.maybe': { s: 'gone', x: 0.3, y: 0.4 },
|
|
|
|
|
'future_widget:one': { s: 'gone', x: 0.5, y: 0.6 },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const limited = repairSpaceReferences(config, layout, {
|
|
|
|
|
ownerRoster: { authoritative: false, deviceIds: [], entityIds: [] },
|
|
|
|
|
removeLiveMissingPositions: true,
|
|
|
|
|
});
|
|
|
|
|
assert.deepEqual(limited.layout, layout);
|
|
|
|
|
assert.equal(limited.report.unverifiedPositions.length, 3);
|
|
|
|
|
assert.equal(limited.report.positionsUnresolved, 3);
|
|
|
|
|
assert.deepEqual(limited.report.unverifiedPositions.map((item) => item.reason).sort(), [
|
|
|
|
|
'registry_unavailable', 'registry_unavailable', 'unknown_owner',
|
|
|
|
|
].sort());
|
|
|
|
|
|
|
|
|
|
const authoritative = repairSpaceReferences(config, layout, {
|
|
|
|
|
ownerRoster: { authoritative: true, deviceIds: [], entityIds: [] },
|
|
|
|
|
});
|
|
|
|
|
assert.equal(authoritative.layout['known-old-device'], undefined);
|
|
|
|
|
assert.equal(authoritative.layout['lg_light.maybe'], undefined);
|
|
|
|
|
assert.deepEqual(authoritative.layout['future_widget:one'], layout['future_widget:one']);
|
|
|
|
|
assert.equal(authoritative.report.orphanDevicePositionsRemoved, 1);
|
|
|
|
|
assert.equal(authoritative.report.orphanGroupPositionsRemoved, 1);
|
|
|
|
|
assert.equal(authoritative.report.unverifiedPositions.length, 1);
|
|
|
|
|
});
|
|
|
|
|