From 6b35559acc40622766f5f6f0323767258ef9151a Mon Sep 17 00:00:00 2001 From: AlCalzone Date: Tue, 8 Oct 2024 13:03:02 +0200 Subject: [PATCH] feat: skip rebuilding routes for nodes with priority return routes (#7252) --- docs/api/controller.md | 6 +++ .../zwave-js/src/lib/controller/Controller.ts | 39 +++++++++++++++++++ .../zwave-js/src/lib/controller/_Types.ts | 2 + packages/zwave-js/src/lib/driver/Driver.ts | 29 ++++++++++++++ .../zwave-js/src/lib/driver/NetworkCache.ts | 9 +++++ 5 files changed, 85 insertions(+) diff --git a/docs/api/controller.md b/docs/api/controller.md index 833fce5e239d..b8d11ad84da6 100644 --- a/docs/api/controller.md +++ b/docs/api/controller.md @@ -428,6 +428,8 @@ async rebuildNodeRoutes(nodeId: number): Promise Rebuilds routes for a single alive node in the network, updating the neighbor list and assigning fresh routes to association targets. The returned promise resolves to `true` if the process was completed, or `false` if it was unsuccessful. +> [!ATTENTION] Rebuilding routes for a single node will delete existing priority return routes to end nodes and the SUC. It is recommended to first check if priority return routes are known to exist using `getPriorityReturnRoutesCached` and `getPrioritySUCReturnRouteCached` and asking for confirmation before proceeding. + #### `beginRebuildingRoutes` ```ts @@ -449,6 +451,8 @@ The `options` argument can be used to skip sleeping nodes: interface RebuildRoutesOptions { /** Whether the routes of sleeping nodes should be rebuilt too at the end of the process. Default: true */ includeSleeping?: boolean; + /** Whether nodes with priority return routes should be included, as those will be deleted. Default: false */ + deletePriorityReturnRoutes?: boolean; } ``` @@ -628,10 +632,12 @@ As mentioned before, there is unfortunately no way to query return routes from a ```ts getPriorityReturnRouteCached(nodeId: number, destinationNodeId: number): MaybeUnknown | undefined; +getPriorityReturnRoutesCached(nodeId: number): Record; getPrioritySUCReturnRouteCached(nodeId: number): MaybeUnknown | undefined; ``` - `getPriorityReturnRouteCached` returns a priority return route that was set using `assignPriorityReturnRoute`. If a non-priority return route has been set since assigning the priority route, this will return `UNKNOWN_STATE` (`null`). +- `getPriorityReturnRoutesCached` returns an object containing the IDs of all known end node destinations a node has priority return routes for and their respective routes. - `getPrioritySUCReturnRouteCached` does the same for a route set through `assignPrioritySUCReturnRoute`. The return type `Route` has the following shape: diff --git a/packages/zwave-js/src/lib/controller/Controller.ts b/packages/zwave-js/src/lib/controller/Controller.ts index ff6f02295059..80e3b7048459 100644 --- a/packages/zwave-js/src/lib/controller/Controller.ts +++ b/packages/zwave-js/src/lib/controller/Controller.ts @@ -4579,6 +4579,7 @@ export class ZWaveController if (existingTask) return false; options.includeSleeping ??= true; + options.deletePriorityReturnRoutes ??= false; this.driver.controllerLog.print( `rebuilding routes${ @@ -4605,6 +4606,21 @@ export class ZWaveController ); this._rebuildRoutesProgress.set(id, "skipped"); } else if (!options.includeSleeping && node.canSleep) { + this.driver.controllerLog.logNode( + id, + `Skipping route rebuild because the node is sleeping.`, + ); + this._rebuildRoutesProgress.set(id, "skipped"); + } else if ( + !options.deletePriorityReturnRoutes + && (this.getPrioritySUCReturnRouteCached(id) + || Object.keys(this.getPriorityReturnRoutesCached(id)) + .length > 0) + ) { + this.driver.controllerLog.logNode( + id, + `Skipping route rebuild because the node has priority return routes.`, + ); this._rebuildRoutesProgress.set(id, "skipped"); } else { this._rebuildRoutesProgress.set(id, "pending"); @@ -5804,6 +5820,29 @@ export class ZWaveController ); } + /** + * For the given node, returns all end node destinations and the priority routes to them. + * + * **Note:** This is using cached information, since there's no way to query priority routes from a node. + * If another controller has assigned routes in the meantime, this information may be out of date. + */ + public getPriorityReturnRoutesCached( + nodeId: number, + ): Record { + const ret: Record = {}; + + const routes = this.driver.cacheList( + cacheKeys.node(nodeId)._priorityReturnRouteBaseKey, + ); + for (const [key, route] of Object.entries(routes)) { + const destination = cacheKeyUtils + .destinationFromPriorityReturnRouteKey(key); + if (destination !== undefined) ret[destination] = route; + } + + return ret; + } + /** * Assigns a priority route from an end node to the SUC. This route will always be used for the first transmission attempt. * @param nodeId The ID of the end node for which to assign the route diff --git a/packages/zwave-js/src/lib/controller/_Types.ts b/packages/zwave-js/src/lib/controller/_Types.ts index b67cda4e34b9..02f21a9e9f1a 100644 --- a/packages/zwave-js/src/lib/controller/_Types.ts +++ b/packages/zwave-js/src/lib/controller/_Types.ts @@ -7,6 +7,8 @@ export type RebuildRoutesStatus = "pending" | "done" | "failed" | "skipped"; export interface RebuildRoutesOptions { /** Whether the routes of sleeping nodes should be rebuilt too at the end of the process. Default: true */ includeSleeping?: boolean; + /** Whether nodes with priority return routes should be included, as those will be deleted. Default: false */ + deletePriorityReturnRoutes?: boolean; } export type SDKVersion = diff --git a/packages/zwave-js/src/lib/driver/Driver.ts b/packages/zwave-js/src/lib/driver/Driver.ts index c565b8ef18ec..202132d2adbd 100644 --- a/packages/zwave-js/src/lib/driver/Driver.ts +++ b/packages/zwave-js/src/lib/driver/Driver.ts @@ -6731,6 +6731,35 @@ ${handlers.length} left`, } } + /** + * @internal + * Helper function to find multiple existing values from the network cache + */ + public cacheList( + prefix: string, + options?: { + reviver?: (value: any) => T; + }, + ): Record { + const ret: Record = {}; + for (const entry of this.networkCache.entries()) { + const key = entry[0]; + if (!key.startsWith(prefix)) continue; + let value = entry[1]; + if (value === undefined) continue; + if (typeof options?.reviver === "function") { + try { + value = options.reviver(value); + } catch { + // invalid entry + continue; + } + } + ret[key] = value; + } + return ret; + } + private cachePurge(prefix: string): void { for (const key of this.networkCache.keys()) { if (key.startsWith(prefix)) { diff --git a/packages/zwave-js/src/lib/driver/NetworkCache.ts b/packages/zwave-js/src/lib/driver/NetworkCache.ts index 8e1de262c92e..31d74e2117f0 100644 --- a/packages/zwave-js/src/lib/driver/NetworkCache.ts +++ b/packages/zwave-js/src/lib/driver/NetworkCache.ts @@ -51,6 +51,7 @@ export const cacheKeys = { return { _baseKey: nodeBaseKey, _securityClassBaseKey: `${nodeBaseKey}securityClasses`, + _priorityReturnRouteBaseKey: `${nodeBaseKey}priorityReturnRoute`, interviewStage: `${nodeBaseKey}interviewStage`, deviceClass: `${nodeBaseKey}deviceClass`, isListening: `${nodeBaseKey}isListening`, @@ -117,6 +118,14 @@ export const cacheKeyUtils = { return parseInt(match.groups!.index, 10); } }, + destinationFromPriorityReturnRouteKey: ( + key: string, + ): number | undefined => { + const match = /\.priorityReturnRoute\.(?\d+)$/.exec(key); + if (match) { + return parseInt(match.groups!.nodeId, 10); + } + }, } as const; function tryParseInterviewStage(value: unknown): InterviewStage | undefined {