UNPKG

zwave-js

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

294 lines 13.9 kB
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