UNPKG

zwave-js

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

199 lines 8.8 kB
import { CCAPI, PhysicalCCAPI, getAPI, normalizeCCNameOrId, } from "@zwave-js/cc"; import { ZWaveError, ZWaveErrorCodes, getCCName, securityClassIsS2, } from "@zwave-js/core"; import { staticExtends } from "@zwave-js/shared"; import { distinct } from "alcalzone-shared/arrays"; import { createMultiCCAPIWrapper } from "./MultiCCAPIWrapper.js"; import { VirtualNode, getSecurityClassFromCommunicationProfile, } from "./VirtualNode.js"; /** * 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 { driver; index; constructor( /** The virtual node this endpoint belongs to */ node, /** The driver instance this endpoint belongs to */ driver, /** The index of this endpoint. 0 for the root device, 1+ otherwise */ index) { this.driver = driver; this.index = index; if (node) this._node = node; } /** Required by {@link IZWaveEndpoint} */ virtual = true; /** The virtual node this endpoint belongs to */ _node; get node() { return this._node; } setNode(node) { this._node = node; } get nodeId() { // 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; } /** Tests if this endpoint supports the given CommandClass */ supportsCC(cc) { // A virtual endpoints supports a CC if any of the physical endpoints it targets supports the CC non-securely return this.node.physicalNodes.some((n) => { const endpoint = n.getEndpoint(this.index); return endpoint?.supportsCC(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. */ getCCVersion(cc) { const nonZeroVersions = this.node.physicalNodes .map((n) => n.getEndpoint(this.index)?.getCCVersion(cc)) .filter((v) => 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 */ createAPI(ccId) { const createCCAPI = (endpoint, secClass) => { if (securityClassIsS2(secClass) // No need to do multicast if there is only one node && endpoint.node.physicalNodes.length > 1) { // The API for S2 needs to know the multicast group ID const secMan = this.driver.getSecurityManager2(endpoint.node.physicalNodes[0].id); return CCAPI.create(ccId, this.driver, endpoint).withOptions({ s2MulticastGroupId: secMan?.createMulticastGroup(endpoint.node.physicalNodes.map((n) => n.id), secClass), }); } else { return CCAPI.create(ccId, this.driver, endpoint); } }; // For mixed security classes and/or mixed protocols (LR and non-LR), we need to create a wrapper // that handles calling multiple API instances if (this.node.hasMixedCommunicationProfiles) { const apiInstances = [ ...this.node.nodesByCommunicationProfile.entries(), ].map(([profile, nodes]) => { // We need a separate virtual endpoint for each security class and protocol, // so the API instances access the correct nodes. const node = new VirtualNode(this.node.id, this.driver, nodes); const endpoint = node.getEndpoint(this.index) ?? node; const secClass = getSecurityClassFromCommunicationProfile(profile); return createCCAPI(endpoint, secClass); }); return createMultiCCAPIWrapper(apiInstances); } else { const profile = [...this.node.nodesByCommunicationProfile.keys()][0]; const securityClass = getSecurityClassFromCommunicationProfile(profile); return createCCAPI(this, securityClass); } } _commandClassAPIs = new Map(); _commandClassAPIsProxy = new Proxy(this._commandClassAPIs, { get: (target, ccNameOrId) => { // 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 { // The command classes are exposed to library users by their name or the ID const ccId = normalizeCCNameOrId(ccNameOrId); if (ccId == undefined) { throw new ZWaveError(`Command Class ${ccNameOrId} is not implemented!`, ZWaveErrorCodes.CC_NotImplemented); } // When accessing a CC API for the first time, we need to create it if (!target.has(ccId)) { const api = this.createAPI(ccId); target.set(ccId, api); } return target.get(ccId); } }, }); /** * Used to iterate over the commandClasses API without throwing errors by accessing unsupported CCs */ commandClassesIterator = function* () { const allCCs = distinct(this._node.physicalNodes .map((n) => n.getEndpoint(this.index)) .filter((e) => !!e) .flatMap((e) => [...e.implementedCommandClasses.keys()])); 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[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 */ get commandClasses() { return this._commandClassAPIsProxy; } /** Allows checking whether a CC API is supported before calling it with {@link VirtualEndpoint.invokeCCAPI} */ supportsCCAPI(cc) { // No need to validate the `cc` parameter, the following line will throw for invalid CCs return this.commandClasses[cc].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. */ invokeCCAPI(cc, method, ...args) { // No need to validate the `cc` parameter, the following line will throw for invalid CCs const CCAPI = this.commandClasses[cc]; const ccId = normalizeCCNameOrId(cc); const ccName = getCCName(ccId); if (!CCAPI) { throw new ZWaveError(`The API for the ${ccName} CC does not exist or is not implemented!`, ZWaveErrorCodes.CC_NoAPI); } const apiMethod = CCAPI[method]; if (typeof apiMethod !== "function") { throw new ZWaveError(`Method "${method}" does not exist on the API for the ${ccName} CC!`, ZWaveErrorCodes.CC_NotImplemented); } return apiMethod.apply(CCAPI, args); } } //# sourceMappingURL=VirtualEndpoint.js.map