zwave-js
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
294 lines • 13.9 kB
JavaScript
import { BasicCCValues, SetValueStatus, supervisionResultToSetValueResult, } from "@zwave-js/cc";
import { SecurityClass, SupervisionStatus, ZWaveError, ZWaveErrorCodes, actuatorCCs, getCCName, isSupervisionResult, isZWaveError, normalizeValueID, supervisedCommandSucceeded, valueIdToString, } from "@zwave-js/core";
import { distinct } from "alcalzone-shared/arrays";
import { VirtualEndpoint } from "./VirtualEndpoint.js";
export var CommunicationProfile;
(function (CommunicationProfile) {
CommunicationProfile[CommunicationProfile["Mesh_S2_Unauthenticated"] = 0] = "Mesh_S2_Unauthenticated";
CommunicationProfile[CommunicationProfile["Mesh_S2_Authenticated"] = 1] = "Mesh_S2_Authenticated";
CommunicationProfile[CommunicationProfile["Mesh_S2_AccessControl"] = 2] = "Mesh_S2_AccessControl";
CommunicationProfile[CommunicationProfile["Mesh_S0_Legacy"] = 7] = "Mesh_S0_Legacy";
CommunicationProfile[CommunicationProfile["LR_S2_Authenticated"] = 17] = "LR_S2_Authenticated";
CommunicationProfile[CommunicationProfile["LR_S2_AccessControl"] = 18] = "LR_S2_AccessControl";
})(CommunicationProfile || (CommunicationProfile = {}));
export function getCommunicationProfile(protocol, securityClass) {
// We assume that only valid combinations are passed
return (protocol << 4) | (securityClass & 0x0f);
}
export function getSecurityClassFromCommunicationProfile(profile) {
return profile & 0x0f;
}
function groupNodesByCommunicationProfile(nodes) {
const ret = new Map();
for (const node of nodes) {
const secClass = node.getHighestSecurityClass();
if (secClass === SecurityClass.Temporary || secClass == undefined) {
continue;
}
const profile = getCommunicationProfile(node.protocol, secClass);
if (!ret.has(profile)) {
ret.set(profile, []);
}
ret.get(profile).push(node);
}
return ret;
}
export class VirtualNode extends VirtualEndpoint {
id;
constructor(id, driver,
/** The references to the physical node this virtual node abstracts */
physicalNodes) {
// Define this node's intrinsic endpoint as the root device (0)
super(undefined, driver, 0);
this.id = id;
// Set the reference to this and the physical nodes
super.setNode(this);
this.physicalNodes = [...physicalNodes].filter((n) =>
// And avoid including the controller node in the support checks
n.id !== driver.controller.ownNodeId
// And omit nodes using Security S0 which does not support broadcast / multicast
&& n.getHighestSecurityClass() !== SecurityClass.S0_Legacy);
this.nodesByCommunicationProfile = groupNodesByCommunicationProfile(this.physicalNodes);
// If broadcasting is attempted with mixed security classes or protocols, automatically fall back to multicast
if (this.hasMixedCommunicationProfiles)
this.id = undefined;
}
physicalNodes;
nodesByCommunicationProfile;
get hasMixedCommunicationProfiles() {
return this.nodesByCommunicationProfile.size > 1;
}
/**
* Updates a value for a given property of a given CommandClass.
* This will communicate with the physical node(s) this virtual node represents!
*/
async setValue(valueId, value, options) {
// Ensure we're dealing with a valid value ID, with no extra properties
valueId = normalizeValueID(valueId);
// Try to retrieve the corresponding CC API
try {
// Access the CC API by name
const endpointInstance = this.getEndpoint(valueId.endpoint || 0);
if (!endpointInstance) {
return {
status: SetValueStatus.EndpointNotFound,
message: `Endpoint ${valueId.endpoint} does not exist on virtual node ${this.id ?? "??"}`,
};
}
let api = endpointInstance.commandClasses[valueId.commandClass];
// Check if the setValue method is implemented
if (!api.setValue) {
return {
status: SetValueStatus.NotImplemented,
message: `The ${getCCName(valueId.commandClass)} CC does not support setting values`,
};
}
const valueIdProps = {
property: valueId.property,
propertyKey: valueId.propertyKey,
};
const hooks = api.setValueHooks?.(valueIdProps, value, options);
if (hooks?.supervisionDelayedUpdates) {
api = api.withOptions({
requestStatusUpdates: true,
onUpdate: async (update) => {
try {
if (update.status === SupervisionStatus.Success) {
await hooks.supervisionOnSuccess();
}
else if (update.status === SupervisionStatus.Fail) {
await hooks.supervisionOnFailure();
}
}
catch {
// TODO: Log error?
}
},
});
}
// If the caller wants progress updates, they shall have them
if (typeof options?.onProgress === "function") {
api = api.withOptions({
onProgress: options.onProgress,
});
}
// And call it
const result = await api.setValue.call(api, valueIdProps, value, options);
if (api.isSetValueOptimistic(valueId)) {
// If the call did not throw, assume that the call was successful and remember the new value
// for each node that was affected by this command
const affectedNodes = this.physicalNodes
.filter((node) => node
.getEndpoint(endpointInstance.index)
?.supportsCC(valueId.commandClass));
for (const node of affectedNodes) {
node.valueDB.setValue(valueId, value);
}
}
// Depending on the settings of the SET_VALUE implementation, we may have to
// optimistically update a different value and/or verify the changes
if (hooks) {
const supervisedAndSuccessful = isSupervisionResult(result)
&& result.status === SupervisionStatus.Success;
const shouldUpdateOptimistically = api.isSetValueOptimistic(valueId)
// For successful supervised commands, we know that an optimistic update is ok
&& (supervisedAndSuccessful
// For unsupervised commands that did not fail, we let the application decide whether
// to update related value optimistically
|| (!this.driver.options.disableOptimisticValueUpdate
&& result == undefined));
// The actual API implementation handles additional optimistic updates
if (shouldUpdateOptimistically) {
hooks.optimisticallyUpdateRelatedValues?.(supervisedAndSuccessful);
}
// Verify the current value after a delay, unless...
// ...the command was supervised and successful
// ...and the CC API decides not to verify anyway
if (!supervisedCommandSucceeded(result)
|| hooks.forceVerifyChanges?.()) {
// Let the CC API implementation handle the verification.
// It may still decide not to do it.
await hooks.verifyChanges?.(result);
}
}
return supervisionResultToSetValueResult(result);
}
catch (e) {
// Define which errors during setValue are expected and won't throw an error
if (isZWaveError(e)) {
let result;
switch (e.code) {
// This CC or API is not implemented
case ZWaveErrorCodes.CC_NotImplemented:
case ZWaveErrorCodes.CC_NoAPI:
result = {
status: SetValueStatus.NotImplemented,
message: e.message,
};
break;
// A user tried to set an invalid value
case ZWaveErrorCodes.Argument_Invalid:
result = {
status: SetValueStatus.InvalidValue,
message: e.message,
};
break;
}
if (result)
return result;
}
throw e;
}
}
/**
* Returns a list of all value IDs and their metadata that can be used to
* control the physical node(s) this virtual node represents.
*/
getDefinedValueIDs() {
// In order to compare value ids, we need them to be strings
const ret = new Map();
for (const pNode of this.physicalNodes) {
// // Nodes using Security S0 cannot be used for broadcast
// if (pNode.getHighestSecurityClass() === SecurityClass.S0_Legacy) {
// continue;
// }
// Take only the actuator values
const valueIDs = pNode
.getDefinedValueIDs()
.filter((v) => actuatorCCs.includes(v.commandClass));
// And add them to the returned array if they aren't included yet or if the version is higher
for (const valueId of valueIDs) {
const mapKey = valueIdToString(valueId);
const ccVersion = pNode.getCCVersion(valueId.commandClass);
const metadata = pNode.getValueMetadata(valueId);
// Don't expose read-only values for virtual nodes, they won't ever have any value
if (!metadata.writeable)
continue;
const needsUpdate = !ret.has(mapKey)
|| ret.get(mapKey).ccVersion < ccVersion;
if (needsUpdate) {
ret.set(mapKey, {
...valueId,
ccVersion,
metadata: {
...metadata,
// Metadata of virtual nodes is only writable
readable: false,
},
});
}
}
}
// Basic CC is not exposed, but virtual nodes need it to control multiple different devices together
const exposedEndpoints = distinct([...ret.values()]
.map((v) => v.endpoint)
.filter((e) => e !== undefined));
for (const endpoint of exposedEndpoints) {
// TODO: This should be defined in the Basic CC file
const valueId = {
...BasicCCValues.targetValue.endpoint(endpoint),
commandClassName: "Basic",
propertyName: "Target value",
};
const ccVersion = 1;
const metadata = {
...BasicCCValues.targetValue.meta,
readable: false,
};
ret.set(valueIdToString(valueId), {
...valueId,
ccVersion,
metadata,
});
}
return [...ret.values()];
}
/** Cache for this node's endpoint instances */
_endpointInstances = new Map();
getEndpoint(index) {
if (index < 0) {
throw new ZWaveError("The endpoint index must be positive!", ZWaveErrorCodes.Argument_Invalid);
}
// Zero is the root endpoint - i.e. this node. Also accept undefined if an application misbehaves
if (!index)
return this;
// Check if the Multi Channel CC interviews for all nodes are completed,
// because we don't have all the information before that
if (!this.isMultiChannelInterviewComplete) {
this.driver.driverLog.print(`Virtual node ${this.id ?? "??"}, Endpoint ${index}: Trying to access endpoint instance before the Multi Channel interview of all nodes was completed!`, "error");
return undefined;
}
// Check if the requested endpoint exists on any physical node
if (index > this.getEndpointCount())
return undefined;
// Create an endpoint instance if it does not exist
if (!this._endpointInstances.has(index)) {
this._endpointInstances.set(index, new VirtualEndpoint(this, this.driver, index));
}
return this._endpointInstances.get(index);
}
getEndpointOrThrow(index) {
const ret = this.getEndpoint(index);
if (!ret) {
throw new ZWaveError(`Endpoint ${index} does not exist on virtual node ${this.id ?? "??"}`, ZWaveErrorCodes.Controller_EndpointNotFound);
}
return ret;
}
/** Returns the current endpoint count of this virtual node (the maximum in the list of physical nodes) */
getEndpointCount() {
let ret = 0;
for (const node of this.physicalNodes) {
const count = node.getEndpointCount();
ret = Math.max(ret, count);
}
return ret;
}
get isMultiChannelInterviewComplete() {
for (const node of this.physicalNodes) {
if (!node["isMultiChannelInterviewComplete"])
return false;
}
return true;
}
}
//# sourceMappingURL=VirtualNode.js.map