inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
258 lines (237 loc) • 7.92 kB
text/typescript
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;
}
}