zwave-js
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
395 lines • 16.6 kB
JavaScript
import { CCAPI, CommandClass, getCommandClassStatic, normalizeCCNameOrId, } from "@zwave-js/cc";
import { ZWavePlusCCValues } from "@zwave-js/cc/ZWavePlusCC";
import { BasicDeviceClass, CacheBackedMap, CommandClasses, GraphNode, ZWaveError, ZWaveErrorCodes, actuatorCCs, getCCName, isCCInfoEqual, } from "@zwave-js/core";
import { getEnumMemberName, num2hex } from "@zwave-js/shared";
import { cacheKeys } from "../driver/NetworkCache.js";
/**
* Represents a physical endpoint of a Z-Wave node. This can either be the root
* device itself (index 0) or a more specific endpoint like a single plug.
*
* Each endpoint may have different capabilities (device class/supported CCs)
*/
export class Endpoint {
nodeId;
driver;
index;
constructor(
/** The id of the node this endpoint belongs to */
nodeId,
/** The driver instance this endpoint belongs to */
driver,
/** The index of this endpoint. 0 for the root device, 1+ otherwise */
index, deviceClass, supportedCCs) {
this.nodeId = nodeId;
this.driver = driver;
this.index = index;
// Initialize class fields
this._implementedCommandClasses = new CacheBackedMap(this.driver.networkCache, {
prefix: cacheKeys.node(this.nodeId).endpoint(this.index)._ccBaseKey,
suffixSerializer: (cc) => num2hex(cc),
suffixDeserializer: (key) => {
const ccId = parseInt(key, 16);
if (ccId in CommandClasses)
return ccId;
},
});
// Optionally initialize the device class
if (deviceClass)
this.deviceClass = deviceClass;
// Add optional CCs
if (supportedCCs != undefined) {
for (const cc of supportedCCs) {
if (cc === CommandClasses.Basic) {
// This codepath is only taken when we construct
// a node instance with info from a NIF.
//
// Basic CC MUST not be in the NIF. If it is anyways, we ignore it.
// If we blindly add it here as supported, it will always be exposed.
//
// Whether or not it should be exposed is determined at a later stage.
continue;
}
this.addCC(cc, { isSupported: true });
}
}
}
/** Required by {@link IZWaveEndpoint} */
virtual = false;
/**
* Only used for endpoints which store their device class differently than nodes.
* DO NOT ACCESS directly!
*/
_deviceClass;
get deviceClass() {
if (this.index > 0) {
return this._deviceClass;
}
else {
return this.driver.cacheGet(cacheKeys.node(this.nodeId).deviceClass);
}
}
set deviceClass(deviceClass) {
if (this.index > 0) {
this._deviceClass = deviceClass;
}
else {
this.driver.cacheSet(cacheKeys.node(this.nodeId).deviceClass, deviceClass);
}
}
/** Can be used to distinguish multiple endpoints of a node */
get endpointLabel() {
return this.tryGetNode()?.deviceConfig?.endpoints?.get(this.index)
?.label;
}
/** Resets all stored information of this endpoint */
reset() {
this._implementedCommandClasses.clear();
this._commandClassAPIs.clear();
}
_implementedCommandClasses;
/**
* @internal
* Information about the implemented Command Classes of this endpoint.
*/
get implementedCommandClasses() {
return this._implementedCommandClasses;
}
getCCs() {
return this._implementedCommandClasses.entries();
}
/**
* Adds a CC to the list of command classes implemented by the endpoint or updates the information.
* You shouldn't need to call this yourself.
* @param info The information about the command class. This is merged with existing information.
*/
addCC(cc, info) {
// Endpoints cannot support Multi Channel CC
if (this.index > 0 && cc === CommandClasses["Multi Channel"])
return;
const original = this._implementedCommandClasses.get(cc);
const updated = Object.assign({}, original ?? {
isSupported: false,
isControlled: false,
secure: false,
version: 0,
}, info);
if (original == undefined || !isCCInfoEqual(original, updated)) {
this._implementedCommandClasses.set(cc, updated);
}
}
/** Removes a CC from the list of command classes implemented by the endpoint */
removeCC(cc) {
this._implementedCommandClasses.delete(cc);
}
/** Tests if this endpoint supports the given CommandClass */
supportsCC(cc) {
return !!this._implementedCommandClasses.get(cc)?.isSupported;
}
/** Tests if this endpoint supports or controls the given CC only securely */
isCCSecure(cc) {
return !!this._implementedCommandClasses.get(cc)?.secure;
}
/** Tests if this endpoint controls the given CommandClass */
controlsCC(cc) {
return !!this._implementedCommandClasses.get(cc)?.isControlled;
}
/**
* Checks if this endpoint is allowed to support Basic CC per the specification.
* This depends on the device type and the other supported CCs
*/
maySupportBasicCC() {
// Basic CC must not be offered if any other actuator CC is supported
if (actuatorCCs.some((cc) => this.supportsCC(cc))) {
return false;
}
// ...or the device class forbids it
return this.deviceClass?.specific.maySupportBasicCC
?? this.deviceClass?.generic.maySupportBasicCC
?? true;
}
/** Determines if support for a CC was force-removed via config file */
wasCCRemovedViaConfig(cc) {
if (this.supportsCC(cc))
return false;
const compatConfig = this.tryGetNode()?.deviceConfig?.compat;
if (!compatConfig)
return false;
const removedEndpoints = compatConfig.removeCCs?.get(cc);
if (!removedEndpoints)
return false;
return removedEndpoints == "*" || removedEndpoints.includes(this.index);
}
/**
* Determines if support for a CC was force-added via config file. */
wasCCSupportAddedViaConfig(cc) {
const compatConfig = this.tryGetNode()?.deviceConfig?.compat;
if (!compatConfig)
return false;
const addedCC = compatConfig.addCCs?.get(cc);
if (!addedCC)
return false;
const endpointInfo = addedCC.endpoints.get(this.index);
if (!endpointInfo)
return false;
return endpointInfo.isSupported === true;
}
/**
* Retrieves the version of the given CommandClass this endpoint implements.
* Returns 0 if the CC is not supported.
*/
getCCVersion(cc) {
const ccInfo = this._implementedCommandClasses.get(cc);
const ret = ccInfo?.version ?? 0;
// The specs are contracting themselves here...
//
// CC Control Specification:
// A controlling node interviewing a Multi Channel End Point
// MUST request the End Point’s Command Class version from the Root Device
// if the End Point does not advertise support for the Version Command Class.
// - vs -
// Management CC Specification:
// [...] the Version Command Class SHOULD NOT be supported by individual End Points
// The Root Device MUST respond to Version requests for any Command Class
// implemented by the Multi Channel device; also in cases where the actual
// Command Class is only provided by an End Point.
//
// We go with the 2nd interpretation since the other either results in
// an unnecessary Version CC interview for each endpoint or an incorrect V1 for endpoints
if (ret === 0 && this.index > 0) {
return this.tryGetNode().getCCVersion(cc);
}
return ret;
}
/**
* Creates an instance of the given CC and links it to this endpoint.
* Throws if the CC is neither supported nor controlled by the endpoint.
*/
createCCInstance(cc) {
const ccId = typeof cc === "number" ? cc : getCommandClassStatic(cc);
if (!this.supportsCC(ccId) && !this.controlsCC(ccId)) {
throw new ZWaveError(`Cannot create an instance of the unsupported CC ${CommandClasses[ccId]} (${num2hex(ccId)})`, ZWaveErrorCodes.CC_NotSupported);
}
return CommandClass.createInstanceUnchecked(this, cc);
}
/**
* Creates an instance of the given CC and links it to this endpoint.
* Returns `undefined` if the CC is neither supported nor controlled by the endpoint.
*/
createCCInstanceUnsafe(cc) {
const ccId = typeof cc === "number" ? cc : getCommandClassStatic(cc);
if (this.supportsCC(ccId) || this.controlsCC(ccId)) {
return CommandClass.createInstanceUnchecked(this, cc);
}
}
/** Returns instances for all CCs this endpoint supports, that should be interviewed, and that are implemented in this library */
getSupportedCCInstances() {
let supportedCCInstances = [...this.implementedCommandClasses.keys()]
// Don't interview CCs the node or endpoint only controls
.filter((cc) => this.supportsCC(cc))
// Filter out CCs we don't implement
.map((cc) => this.createCCInstance(cc))
.filter((instance) => !!instance);
// For endpoint interviews, we skip some CCs
if (this.index > 0) {
supportedCCInstances = supportedCCInstances.filter((instance) => !instance.skipEndpointInterview());
}
return supportedCCInstances;
}
/** Builds the dependency graph used to automatically determine the order of CC interviews */
buildCCInterviewGraph(skipCCs) {
const supportedCCs = this.getSupportedCCInstances()
.map((instance) => instance.ccId)
.filter((ccId) => !skipCCs.includes(ccId));
// Create GraphNodes from all supported CCs that should not be skipped
const ret = supportedCCs.map((cc) => new GraphNode(cc));
// Create the dependencies
for (const node of ret) {
const instance = this.createCCInstance(node.value);
for (const requiredCCId of instance.determineRequiredCCInterviews()) {
const requiredCC = ret.find((instance) => instance.value === requiredCCId);
if (requiredCC)
node.edges.add(requiredCC);
}
}
return ret;
}
/**
* @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
* @param requireSupport Whether accessing the API should throw if it is not supported by the node.
*/
createAPI(ccId, requireSupport = true) {
// Trust me on this, TypeScript :)
return CCAPI.create(ccId, this.driver, this, requireSupport);
}
_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 = 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
*/
commandClassesIterator = function* () {
for (const cc of this.implementedCommandClasses.keys()) {
if (this.supportsCC(cc))
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 Endpoint.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 endpoint by CC ID and method name.
* Use {@link Endpoint.supportsCCAPI} to check support first.
*/
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);
}
/**
* Returns the node this endpoint belongs to (or undefined if the node doesn't exist)
*/
tryGetNode() {
return this.driver.controller.nodes.get(this.nodeId);
}
/** Z-Wave+ Icon (for management) */
get installerIcon() {
return this.tryGetNode()?.getValue(ZWavePlusCCValues.installerIcon.endpoint(this.index));
}
/** Z-Wave+ Icon (for end users) */
get userIcon() {
return this.tryGetNode()?.getValue(ZWavePlusCCValues.userIcon.endpoint(this.index));
}
/**
* @internal
* Returns a dump of this endpoint's information for debugging purposes
*/
createEndpointDump() {
const ret = {
index: this.index,
deviceClass: "unknown",
commandClasses: {},
maySupportBasicCC: this.maySupportBasicCC(),
};
if (this.deviceClass) {
ret.deviceClass = {
basic: {
key: this.deviceClass.basic,
label: getEnumMemberName(BasicDeviceClass, this.deviceClass.basic),
},
generic: {
key: this.deviceClass.generic.key,
label: this.deviceClass.generic.label,
},
specific: {
key: this.deviceClass.specific.key,
label: this.deviceClass.specific.label,
},
};
}
for (const [ccId, info] of this._implementedCommandClasses) {
ret.commandClasses[getCCName(ccId)] = { ...info, values: [] };
}
for (const [prop, value] of Object.entries(ret)) {
// @ts-expect-error
if (value === undefined)
delete ret[prop];
}
return ret;
}
}
//# sourceMappingURL=Endpoint.js.map