inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
218 lines (202 loc) • 7.17 kB
text/typescript
import {
APIMethodsOf,
CCAPI,
CCAPIs,
getAPI,
PhysicalCCAPI,
} from "@zwave-js/cc";
import {
CommandClasses,
IVirtualEndpoint,
MulticastDestination,
ZWaveError,
ZWaveErrorCodes,
} from "@zwave-js/core/safe";
import { staticExtends } from "@zwave-js/shared/safe";
import { distinct } from "alcalzone-shared/arrays";
import type { Driver } from "../driver/Driver";
import type { Endpoint } from "./Endpoint";
import type { VirtualNode } from "./VirtualNode";
/**
* Represents an endpoint of a virtual (broadcast, multicast) Z-Wave node.
* This can either be the root device itself (index 0) or a more specific endpoint like a single plug.
*
* The endpoint's capabilities are determined by the capabilities of the individual nodes' endpoints.
*/
export class VirtualEndpoint implements IVirtualEndpoint {
public constructor(
/** The virtual node this endpoint belongs to (or undefined if it set later) */
node: VirtualNode | undefined,
/** The driver instance this endpoint belongs to */
protected readonly driver: Driver,
/** The index of this endpoint. 0 for the root device, 1+ otherwise */
public readonly index: number,
) {
if (node) this._node = node;
}
/** Required by {@link IZWaveEndpoint} */
public readonly virtual = true;
/** The virtual node this endpoint belongs to */
private _node!: VirtualNode;
public get node(): VirtualNode {
return this._node;
}
/** @internal */
protected setNode(node: VirtualNode): void {
this._node = node;
}
public get nodeId(): number | MulticastDestination {
// Use the defined node ID if it exists
if (this.node.id != undefined) return this.node.id;
// Otherwise deduce it from the physical nodes
const ret = this.node.physicalNodes.map((n) => n.id);
if (ret.length === 1) return ret[0];
return ret as MulticastDestination;
}
/** Tests if this endpoint supports the given CommandClass */
public supportsCC(cc: CommandClasses): boolean {
// A virtual endpoints supports a CC if any of the physical endpoints it targets supports the CC non-securely
// Security S0 does not support broadcast / multicast!
return this.node.physicalNodes.some((n) => {
const endpoint = n.getEndpoint(this.index);
return endpoint?.supportsCC(cc) && !endpoint?.isCCSecure(cc);
});
}
/**
* Retrieves the minimum non-zero version of the given CommandClass the physical endpoints implement
* Returns 0 if the CC is not supported at all.
*/
public getCCVersion(cc: CommandClasses): number {
const nonZeroVersions = this.node.physicalNodes
.map((n) => n.getEndpoint(this.index)?.getCCVersion(cc))
.filter((v): v is number => v != undefined && v > 0);
if (!nonZeroVersions.length) return 0;
return Math.min(...nonZeroVersions);
}
/**
* @internal
* Creates an API instance for a given command class. Throws if no API is defined.
* @param ccId The command class to create an API instance for
*/
public createAPI(ccId: CommandClasses): CCAPI {
// Trust me on this, TypeScript :)
return CCAPI.create(ccId, this.driver, this) as any;
}
private _commandClassAPIs = new Map<CommandClasses, CCAPI>();
private _commandClassAPIsProxy = new Proxy(this._commandClassAPIs, {
get: (target, ccNameOrId: string | symbol) => {
// Avoid ultra-weird error messages during testing
if (
process.env.NODE_ENV === "test" &&
typeof ccNameOrId === "string" &&
(ccNameOrId === "$$typeof" ||
ccNameOrId === "constructor" ||
ccNameOrId.includes("@@__IMMUTABLE"))
) {
return undefined;
}
if (typeof ccNameOrId === "symbol") {
// Allow access to the iterator symbol
if (ccNameOrId === Symbol.iterator) {
return this.commandClassesIterator;
} else if (ccNameOrId === Symbol.toStringTag) {
return "[object Object]";
}
// ignore all other symbols
return undefined;
} else {
// typeof ccNameOrId === "string"
let ccId: CommandClasses | undefined;
// The command classes are exposed to library users by their name or the ID
if (/^\d+$/.test(ccNameOrId)) {
// Since this is a property accessor, ccNameOrID is passed as a string,
// even when it was a number (CommandClasses)
ccId = +ccNameOrId;
} else {
// If a name was given, retrieve the corresponding ID
ccId = CommandClasses[ccNameOrId as any] as unknown as
| CommandClasses
| undefined;
if (ccId == undefined) {
throw new ZWaveError(
`Command Class ${ccNameOrId} is not implemented! If you are sure that the name/id is correct, consider opening an issue at https://github.com/AlCalzone/node-zwave-js`,
ZWaveErrorCodes.CC_NotImplemented,
);
}
}
// When accessing a CC API for the first time, we need to create it
if (!target.has(ccId)) {
const api = CCAPI.create(ccId, this.driver, this);
target.set(ccId, api);
}
return target.get(ccId);
}
},
});
/**
* Used to iterate over the commandClasses API without throwing errors by accessing unsupported CCs
*/
private readonly commandClassesIterator: () => Iterator<CCAPI> = function* (
this: VirtualEndpoint,
) {
const allCCs = distinct(
this._node.physicalNodes
.map((n) => n.getEndpoint(this.index))
.filter((e): e is Endpoint => !!e)
.map((e) => [...e.implementedCommandClasses.keys()])
.reduce((acc, cur) => [...acc, ...cur], []),
);
for (const cc of allCCs) {
if (this.supportsCC(cc)) {
// When a CC is supported, it can still happen that the CC API
// cannot be created for virtual endpoints
const APIConstructor = getAPI(cc);
if (staticExtends(APIConstructor, PhysicalCCAPI)) continue;
yield (this.commandClasses as any)[cc];
}
}
}.bind(this);
/**
* Provides access to simplified APIs that are tailored to specific CCs.
* Make sure to check support of each API using `API.isSupported()` since
* all other API calls will throw if the API is not supported
*/
public get commandClasses(): CCAPIs {
return this._commandClassAPIsProxy as unknown as CCAPIs;
}
/** Allows checking whether a CC API is supported before calling it with {@link VirtualEndpoint.invokeCCAPI} */
public supportsCCAPI(cc: CommandClasses): boolean {
return ((this.commandClasses as any)[cc] as CCAPI).isSupported();
}
/**
* Allows dynamically calling any CC API method on this virtual endpoint by CC ID and method name.
* Use {@link VirtualEndpoint.supportsCCAPI} to check support first.
*
* **Warning:** Get-type commands are not supported, even if auto-completion indicates that they are.
*/
public invokeCCAPI<
CC extends CommandClasses,
TMethod extends keyof TAPI,
TAPI extends Record<
string,
(...args: any[]) => any
> = CommandClasses extends CC ? any : APIMethodsOf<CC>,
>(
cc: CC,
method: TMethod,
...args: Parameters<TAPI[TMethod]>
): ReturnType<TAPI[TMethod]> {
const CCAPI = (this.commandClasses as any)[cc];
return CCAPI[method](...args);
}
/**
* @internal
* DO NOT CALL THIS!
*/
public getNodeUnsafe(): never {
throw new ZWaveError(
`The node of a virtual endpoint cannot be accessed this way!`,
ZWaveErrorCodes.CC_NoNodeID,
);
}
}