Skip to content

Commit

Permalink
feat(mock-server): support node dumps as input (#6907)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlCalzone authored Jun 7, 2024
1 parent ef28407 commit 2c11420
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 6 deletions.
6 changes: 5 additions & 1 deletion packages/cc/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export {
export * from "./lib/Security2/shared";
export * from "./lib/SetValueResult";
export { defaultCCValueOptions } from "./lib/Values";
export type { CCValueOptions } from "./lib/Values";
export type {
CCValueOptions,
CCValuePredicate,
PartialCCValuePredicate,
} from "./lib/Values";
export * from "./lib/_Types";
export { utils };
5 changes: 4 additions & 1 deletion packages/testing/src/MockNodeCapabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ export function getDefaultMockNodeCapabilities(): MockNodeCapabilities {
}

export function getDefaultMockEndpointCapabilities(
nodeCaps: MockNodeCapabilities,
nodeCaps: {
genericDeviceClass: number;
specificDeviceClass: number;
},
): MockEndpointCapabilities {
return {
genericDeviceClass: nodeCaps.genericDeviceClass,
Expand Down
6 changes: 5 additions & 1 deletion packages/testing/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ export * from "./CCSpecificCapabilities";
export * from "./MockController";
export { getDefaultSupportedFunctionTypes } from "./MockControllerCapabilities";
export * from "./MockNode";
export { ccCaps } from "./MockNodeCapabilities";
export {
ccCaps,
getDefaultMockEndpointCapabilities,
getDefaultMockNodeCapabilities,
} from "./MockNodeCapabilities";
export type { PartialCCCapabilities } from "./MockNodeCapabilities";
export * from "./MockZWaveFrame";
18 changes: 16 additions & 2 deletions packages/zwave-js/bin/mock-server.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// @ts-check
const { MockServer } = require("../build/mockServer");
const { MockServer, createMockNodeOptionsFromDump } = require(
"../build/mockServer",
);
const { readFileSync, statSync, readdirSync } = require("fs");
const path = require("path");

Expand Down Expand Up @@ -91,6 +93,16 @@ function getConfig(filename) {
} else if (filename.endsWith(".json")) {
// TODO: JSON5 support
return JSON.parse(readFileSync(filename, "utf8"));
} else if (filename.endsWith(".dump")) {
const node = createMockNodeOptionsFromDump(
JSON.parse(
readFileSync(
filename,
"utf8",
),
),
);
return { nodes: [node] };
}
}

Expand All @@ -114,7 +126,9 @@ if (configPath) {
const files = readdirSync(absolutePath)
.filter(
(filename) =>
filename.endsWith(".js") || filename.endsWith(".json"),
filename.endsWith(".js")
|| filename.endsWith(".json")
|| filename.endsWith(".dump"),
)
.map((filename) => {
const fullPath = path.join(absolutePath, filename);
Expand Down
2 changes: 1 addition & 1 deletion packages/zwave-js/src/Testing.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { createAndStartDriverWithMockPort } from "./lib/driver/DriverMock";
export { MockServer } from "./mockServer";
export { MockServer, createMockNodeOptionsFromDump } from "./mockServer";
export type {
MockServerControllerOptions,
MockServerNodeOptions,
Expand Down
1 change: 1 addition & 0 deletions packages/zwave-js/src/lib/node/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7482,6 +7482,7 @@ ${formatRouteHealthCheckSummary(this.id, otherNode.id, summary)}`,
collectValues(0, (ccId) => ret.commandClasses[getCCName(ccId)]?.values);

for (const endpoint of this.getAllEndpoints()) {
if (endpoint.index === 0) continue;
ret.endpoints ??= {};
const endpointDump = endpoint.createEndpointDump();
collectValues(
Expand Down
226 changes: 226 additions & 0 deletions packages/zwave-js/src/mockServer.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
import { NotificationCCValues } from "@zwave-js/cc";
import {
CommandClasses,
type ConfigurationMetadata,
type ValueID,
} from "@zwave-js/core";
import type { ZWaveSerialPort } from "@zwave-js/serial";
import {
type MockPortBinding,
createAndOpenMockedZWaveSerialPort,
} from "@zwave-js/serial/mock";
import {
type ConfigurationCCCapabilities,
MockController,
type MockControllerBehavior,
type MockControllerOptions,
MockNode,
type MockNodeBehavior,
type MockNodeOptions,
type NotificationCCCapabilities,
type PartialCCCapabilities,
getDefaultMockEndpointCapabilities,
getDefaultMockNodeCapabilities,
} from "@zwave-js/testing";
import { createDeferredPromise } from "alcalzone-shared/deferred-promise";
import { type AddressInfo, type Server, createServer } from "node:net";
import {
ProtocolVersion,
createDefaultMockControllerBehaviors,
createDefaultMockNodeBehaviors,
} from "./Utils";
import { type CommandClassDump, type NodeDump } from "./lib/node/Dump";

export type MockServerControllerOptions =
& Pick<
Expand Down Expand Up @@ -184,3 +197,216 @@ function prepareMocks(
mockNodes,
};
}

export function createMockNodeOptionsFromDump(
dump: NodeDump,
): MockServerNodeOptions {
const ret: MockServerNodeOptions = {
id: dump.id,
};

ret.capabilities = getDefaultMockNodeCapabilities();

if (typeof dump.isListening === "boolean") {
ret.capabilities.isListening = dump.isListening;
}
if (dump.isFrequentListening !== "unknown") {
ret.capabilities.isFrequentListening = dump.isFrequentListening;
}
if (typeof dump.isRouting === "boolean") {
ret.capabilities.isRouting = dump.isRouting;
}
if (typeof dump.supportsBeaming === "boolean") {
ret.capabilities.supportsBeaming = dump.supportsBeaming;
}
if (typeof dump.supportsSecurity === "boolean") {
ret.capabilities.supportsSecurity = dump.supportsSecurity;
}
if (typeof dump.supportedDataRates === "boolean") {
ret.capabilities.supportedDataRates = dump.supportedDataRates;
}
if ((ProtocolVersion as any)[dump.protocol] !== undefined) {
ret.capabilities.protocolVersion =
(ProtocolVersion as any)[dump.protocol];
}

if (dump.deviceClass !== "unknown") {
ret.capabilities.basicDeviceClass = dump.deviceClass.basic.key;
ret.capabilities.genericDeviceClass = dump.deviceClass.generic.key;
ret.capabilities.specificDeviceClass = dump.deviceClass.specific.key;
}

ret.capabilities.firmwareVersion = dump.fingerprint.firmwareVersion;
ret.capabilities.manufacturerId = parseInt(
dump.fingerprint.manufacturerId,
16,
);
ret.capabilities.productType = parseInt(dump.fingerprint.productType, 16);
ret.capabilities.productId = parseInt(dump.fingerprint.productId, 16);

for (const [ccName, ccDump] of Object.entries(dump.commandClasses)) {
const ccId = (CommandClasses as any)[ccName];
if (ccId == undefined) continue;
// FIXME: Security encapsulation is not supported yet in mocks
if (
ccId === CommandClasses.Security
|| ccId === CommandClasses["Security 2"]
) {
continue;
}

ret.capabilities.commandClasses ??= [];
ret.capabilities.commandClasses.push(
createCCCapabilitiesFromDump(ccId, ccDump),
);
}

if (dump.endpoints) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const [indexStr, endpointDump] of Object.entries(dump.endpoints)) {
// FIXME: The mocks expect endpoints to be consecutive
// const index = parseInt(indexStr);

const epCaps = getDefaultMockEndpointCapabilities(
// @ts-expect-error We are initializing the device classes above
ret.capabilities,
);
let epCCs: PartialCCCapabilities[] | undefined;
if (endpointDump.deviceClass !== "unknown") {
epCaps.genericDeviceClass =
endpointDump.deviceClass.generic.key;
epCaps.specificDeviceClass =
endpointDump.deviceClass.specific.key;
}

for (
const [ccName, ccDump] of Object.entries(
endpointDump.commandClasses,
)
) {
const ccId = (CommandClasses as any)[ccName];
if (ccId == undefined) continue;
// FIXME: Security encapsulation is not supported yet in mocks
if (
ccId === CommandClasses.Security
|| ccId === CommandClasses["Security 2"]
) {
continue;
}

epCCs ??= [];
epCCs.push(
createCCCapabilitiesFromDump(ccId, ccDump),
);
}

ret.capabilities.endpoints ??= [];
ret.capabilities.endpoints.push({
...epCaps,
commandClasses: epCCs,
});
}
}

return ret;
}

function createCCCapabilitiesFromDump(
ccId: CommandClasses,
dump: CommandClassDump,
): PartialCCCapabilities {
const ret: PartialCCCapabilities = {
ccId,
isSupported: dump.isSupported,
isControlled: dump.isControlled,
secure: dump.secure,
version: dump.version,
};

// Parse CC specific info from values
if (ccId === CommandClasses.Configuration) {
Object.assign(ret, createConfigurationCCCapabilitiesFromDump(dump));
} else if (ccId === CommandClasses.Notification) {
Object.assign(ret, createNotificationCCCapabilitiesFromDump(dump));
}

return ret;
}

function createConfigurationCCCapabilitiesFromDump(
dump: CommandClassDump,
): ConfigurationCCCapabilities {
const ret: ConfigurationCCCapabilities = {
bulkSupport: false,
parameters: [],
};

for (const val of dump.values) {
if (typeof val.property !== "number") continue;
// Mocks don't support partial parameters
if (val.propertyKey != undefined) continue;
// Metadata contains the param information
if (!val.metadata) continue;
const meta = val.metadata as ConfigurationMetadata;

ret.parameters.push({
"#": val.property,
valueSize: meta.valueSize ?? 1,
name: meta.label,
info: meta.description,
format: meta.format,
minValue: meta.min,
maxValue: meta.max,
defaultValue: meta.default,
readonly: !meta.writeable,
});
}

return ret;
}

function createNotificationCCCapabilitiesFromDump(
dump: CommandClassDump,
): NotificationCCCapabilities {
const supportsV1Alarm = findDumpedValue(
dump,
CommandClasses.Notification,
NotificationCCValues.supportsV1Alarm.id,
false,
);
const ret: NotificationCCCapabilities = {
supportsV1Alarm,
notificationTypesAndEvents: {},
};

const supportedNotificationTypes: number[] = findDumpedValue(
dump,
CommandClasses.Notification,
NotificationCCValues.supportedNotificationTypes.id,
[],
);

for (const type of supportedNotificationTypes) {
const supportedEvents: number[] = findDumpedValue(
dump,
CommandClasses.Notification,
NotificationCCValues.supportedNotificationEvents(type).id,
[],
);
ret.notificationTypesAndEvents[type] = supportedEvents;
}

return ret;
}

function findDumpedValue<T>(
dump: CommandClassDump,
commandClass: CommandClasses,
valueId: ValueID,
defaultValue: T,
): T {
return (dump.values.find((id) =>
id.property === valueId.property
&& id.propertyKey === valueId.propertyKey
)?.value) as (T | undefined) ?? defaultValue;
}

0 comments on commit 2c11420

Please sign in to comment.