UNPKG

inventoresed

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

218 lines (202 loc) 7.17 kB
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, ); } }