UNPKG

inventoresed

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

258 lines (237 loc) 7.92 kB
import { BasicCCValues, CCAPI, SetValueAPIOptions } from "@zwave-js/cc"; import { actuatorCCs, isZWaveError, IVirtualNode, TranslatedValueID, ValueID, valueIdToString, ValueMetadata, ValueMetadataNumeric, ZWaveError, ZWaveErrorCodes, } from "@zwave-js/core"; import { distinct } from "alcalzone-shared/arrays"; import type { Driver } from "../driver/Driver"; import type { ZWaveNode } from "./Node"; import { VirtualEndpoint } from "./VirtualEndpoint"; export interface VirtualValueID extends TranslatedValueID { /** The metadata that belongs to this virtual value ID */ metadata: ValueMetadata; /** The maximum supported CC version among all nodes targeted by this virtual value ID */ ccVersion: number; } export class VirtualNode extends VirtualEndpoint implements IVirtualNode { public constructor( public readonly id: number | undefined, driver: Driver, /** The references to the physical node this virtual node abstracts */ physicalNodes: Iterable<ZWaveNode>, ) { // Define this node's intrinsic endpoint as the root device (0) super(undefined, driver, 0); // Set the reference to this and the physical nodes super.setNode(this); this.physicalNodes = [...physicalNodes].filter( // And avoid including the controller node in the support checks (n) => n.id !== driver.controller.ownNodeId, ); } public readonly physicalNodes: ZWaveNode[]; /** * Updates a value for a given property of a given CommandClass. * This will communicate with the physical node(s) this virtual node represents! */ public async setValue( valueId: ValueID, value: unknown, options?: SetValueAPIOptions, ): Promise<boolean> { // Try to retrieve the corresponding CC API try { // Access the CC API by name const endpointInstance = this.getEndpoint(valueId.endpoint || 0); if (!endpointInstance) return false; const api = (endpointInstance.commandClasses as any)[ valueId.commandClass ] as CCAPI; // Check if the setValue method is implemented if (!api.setValue) return false; // And call it await api.setValue( { property: valueId.property, propertyKey: valueId.propertyKey, }, value, options, ); // api.setValue could technically return a SupervisionResult // but supervision isn't used for multicast / broadcast 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 = endpointInstance.node.physicalNodes.filter((node) => node .getEndpoint(endpointInstance.index) ?.supportsCC(valueId.commandClass), ); for (const node of affectedNodes) { node.valueDB.setValue(valueId, value); } } return true; } catch (e) { // Define which errors during setValue are expected and won't crash // the driver: if (isZWaveError(e)) { let handled = false; let emitErrorEvent = false; switch (e.code) { // This CC or API is not implemented case ZWaveErrorCodes.CC_NotImplemented: case ZWaveErrorCodes.CC_NoAPI: handled = true; break; // A user tried to set an invalid value case ZWaveErrorCodes.Argument_Invalid: handled = true; emitErrorEvent = true; break; } if (emitErrorEvent) this.driver.emit("error", e); if (handled) return false; } 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. */ public getDefinedValueIDs(): VirtualValueID[] { // If all nodes are secure, we can't use broadcast/multicast commands if (this.physicalNodes.every((n) => n.isSecure === true)) return []; // In order to compare value ids, we need them to be strings const ret = new Map<string, VirtualValueID>(); for (const pNode of this.physicalNodes) { // Secure nodes cannot be used for broadcast if (pNode.isSecure === true) continue; // Take only the actuator values const valueIDs: TranslatedValueID[] = 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 is number => e !== undefined), ); for (const endpoint of exposedEndpoints) { // TODO: This should be defined in the Basic CC file const valueId: TranslatedValueID = { ...BasicCCValues.targetValue.endpoint(endpoint), commandClassName: "Basic", propertyName: "Target value", }; const ccVersion = 1; const metadata: ValueMetadataNumeric = { ...BasicCCValues.targetValue.meta, readable: false, }; ret.set(valueIdToString(valueId), { ...valueId, ccVersion, metadata, }); } return [...ret.values()]; } /** Cache for this node's endpoint instances */ private _endpointInstances = new Map<number, VirtualEndpoint>(); /** * Returns an endpoint of this node with the given index. 0 returns the node itself. */ public getEndpoint(index: 0): VirtualEndpoint; public getEndpoint(index: number): VirtualEndpoint | undefined; public getEndpoint(index: number): VirtualEndpoint | undefined { if (index < 0) throw new ZWaveError( "The endpoint index must be positive!", ZWaveErrorCodes.Argument_Invalid, ); // Zero is the root endpoint - i.e. this node if (index === 0) 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)!; } public getEndpointOrThrow(index: number): VirtualEndpoint { 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) */ public getEndpointCount(): number { let ret = 0; for (const node of this.physicalNodes) { const count = node.getEndpointCount(); ret = Math.max(ret, count); } return ret; } private get isMultiChannelInterviewComplete(): boolean { for (const node of this.physicalNodes) { if (!node["isMultiChannelInterviewComplete"]) return false; } return true; } }