feat: add contextual Zigbee topology hover

Issue: #54
User-Visible: yes
This commit is contained in:
Sergey Matyunin
2026-09-04 14:40:09 +03:00
parent e5587aed7c
commit 85702afc0c
56 changed files with 2481 additions and 564 deletions
+16
View File
@@ -100,6 +100,22 @@ export const CONFIG_FIELD_REGISTRY = Object.freeze([
migration: 'safe point-in-room backfill; cross-source full import drops it',
compatibility: 'older frontends ignore it; same-source backup preserves it',
},
{
id: 'settings.zigbee_topology',
schema: 'allow-extra',
enforcedBy: 'zigbee-topology-settings.ts + explicit admin provider controls; #54',
selector: { path: ['settings', 'zigbee_topology'] },
storage: 'House Plan server config',
type: '{ enabled?: boolean, z2m_base_topics?: string[] }',
default: 'unset (= disabled; default Zigbee2MQTT base topic is presented in UI)',
level: 'global',
ui: 'General settings → Zigbee links',
runtime: 'allows a mouse-only incident-neighbour overlay after explicit provider refresh',
introduced: '#54',
status: 'current',
migration: 'none — topology snapshots remain memory-only',
compatibility: 'older frontends ignore it; backend preserves it through allow-extra',
},
{
id: 'markers[].display=ripple',
enforcedBy: 'logic.ts normalizeDeviceDisplay (read) + editor-runtime _dropLegacySegments rewrites on every write',
+44
View File
@@ -5607,6 +5607,50 @@ const MUTANT_DEFINITIONS = [
+ ' && fingerprint(current) !== attempt.attemptedFingerprint)) return false;',
}],
},
{
id: 'zigbee-topology-zha-read-starts-scan',
guard: 'node --test test/zigbee-topology.test.mjs',
because: 'hover diagnostics may read the existing ZHA snapshot but must never turn a read '
+ 'into an implicit radio scan (#54 AC4)',
patches: [{
file: 'src/zigbee-topology-runtime.ts',
find: " return normalizeZhaTopology(await hass.callWS({ type: 'zha/devices' }));",
replace: " return normalizeZhaTopology(await hass.callWS({ type: 'zha/topology/update' }));",
}],
},
{
id: 'zigbee-topology-ambiguous-marker-selected',
guard: 'node --test test/zigbee-topology.test.mjs',
because: 'a Zigbee node with multiple drawable placements must fail closed instead of '
+ 'drawing a plausible but false neighbour line (#54 AC8)',
patches: [{
file: 'src/zigbee-topology.ts',
find: ' if (candidates.length === 1) placements.set(node.key, {',
replace: ' if (candidates.length >= 1) placements.set(node.key, {',
}],
},
{
id: 'zigbee-topology-z2m-foreign-response-accepted',
guard: 'node --test test/zigbee-topology.test.mjs',
because: 'parallel Zigbee2MQTT requests share one response topic; only the matching '
+ 'transaction may complete this snapshot (#54 AC6)',
patches: [{
file: 'src/zigbee-topology-runtime.ts',
find: ' if (value && transactionOf(value) === transaction) responseResolve?.(value);',
replace: ' if (value) responseResolve?.(value);',
}],
},
{
id: 'zigbee-topology-z2m-subscriptions-leak',
guard: 'node --test test/zigbee-topology.test.mjs',
because: 'both MQTT subscriptions must be released after success or failure so one manual '
+ 'refresh cannot leave listeners processing later payloads (#54 AC6)',
patches: [{
file: 'src/zigbee-topology-runtime.ts',
find: ' try { unsubscribe(); } catch { /* cleanup is best effort */ }',
replace: ' try { void unsubscribe; } catch { /* cleanup is best effort */ }',
}],
},
];
const mutationCardSource = readFileSync(join(repoRoot, 'src/houseplan-card.ts'), 'utf8');
+10
View File
@@ -27,6 +27,16 @@
/** @type {SmokeLink[]} */
export const SMOKE_LINKS = [
{
symbols: [
'renderZigbeeTopologyOverlay', 'resolveTopologyHover', 'mapTopologyNodes',
'readZhaTopology', 'refreshZ2mTopology', 'zigbeeTopologySettingsOf',
],
smokes: ['smoke_zigbee_topology_hover.mjs'],
because: 'the #54 production-bundle scenario proves default-off lazy loading, explicit provider '
+ 'refresh, incident-only mouse hover, cross-space counting, pointer transparency and immediate '
+ 'cleanup across leave, touch, editor and non-admin boundaries',
},
{
symbols: [
'beginVacuumRouteDraft', 'chooseVacuumRouteSpace', 'commitVacuumRouteDraft',