zwave-js
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
199 lines • 8.8 kB
JavaScript
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