inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
333 lines (306 loc) • 8.38 kB
text/typescript
import { CommandClass } from "@zwave-js/cc";
import { MultiChannelCCValues } from "@zwave-js/cc/MultiChannelCC";
import {
allCCs,
applicationCCs,
CommandClasses,
getCCName,
IZWaveEndpoint,
IZWaveNode,
SetValueOptions,
TranslatedValueID,
ValueID,
ZWaveError,
ZWaveErrorCodes,
} from "@zwave-js/core";
import type { ZWaveApplicationHost } from "@zwave-js/host";
function getValue<T>(
applHost: ZWaveApplicationHost,
node: IZWaveNode,
valueId: ValueID,
): T | undefined {
return applHost.getValueDB(node.id).getValue(valueId);
}
function setValue(
applHost: ZWaveApplicationHost,
node: IZWaveNode,
valueId: ValueID,
value: unknown,
options?: SetValueOptions,
): void {
return applHost.getValueDB(node.id).setValue(valueId, value, options);
}
export function endpointCountIsDynamic(
applHost: ZWaveApplicationHost,
node: IZWaveNode,
): boolean | undefined {
return getValue(
applHost,
node,
MultiChannelCCValues.endpointCountIsDynamic.id,
);
}
export function endpointsHaveIdenticalCapabilities(
applHost: ZWaveApplicationHost,
node: IZWaveNode,
): boolean | undefined {
return getValue(
applHost,
node,
MultiChannelCCValues.endpointsHaveIdenticalCapabilities.id,
);
}
export function getIndividualEndpointCount(
applHost: ZWaveApplicationHost,
node: IZWaveNode,
): number | undefined {
return getValue(
applHost,
node,
MultiChannelCCValues.individualEndpointCount.id,
);
}
export function getAggregatedEndpointCount(
applHost: ZWaveApplicationHost,
node: IZWaveNode,
): number | undefined {
return getValue(
applHost,
node,
MultiChannelCCValues.aggregatedEndpointCount.id,
);
}
export function getEndpointCount(
applHost: ZWaveApplicationHost,
node: IZWaveNode,
): number {
return (
(getIndividualEndpointCount(applHost, node) || 0) +
(getAggregatedEndpointCount(applHost, node) || 0)
);
}
export function setIndividualEndpointCount(
applHost: ZWaveApplicationHost,
node: IZWaveNode,
count: number,
): void {
setValue(
applHost,
node,
MultiChannelCCValues.individualEndpointCount.id,
count,
);
}
export function setAggregatedEndpointCount(
applHost: ZWaveApplicationHost,
node: IZWaveNode,
count: number,
): void {
setValue(
applHost,
node,
MultiChannelCCValues.aggregatedEndpointCount.id,
count,
);
}
export function getEndpointIndizes(
applHost: ZWaveApplicationHost,
node: IZWaveNode,
): number[] {
let ret = getValue<number[]>(
applHost,
node,
MultiChannelCCValues.endpointIndizes.id,
);
if (!ret) {
// Endpoint indizes not stored, assume sequential endpoints
ret = [];
for (let i = 1; i <= getEndpointCount(applHost, node); i++) {
ret.push(i);
}
}
return ret;
}
export function setEndpointIndizes(
applHost: ZWaveApplicationHost,
node: IZWaveNode,
indizes: number[],
): void {
setValue(applHost, node, MultiChannelCCValues.endpointIndizes.id, indizes);
}
export function isMultiChannelInterviewComplete(
applHost: ZWaveApplicationHost,
node: IZWaveNode,
): boolean {
return !!getValue(applHost, node, {
commandClass: CommandClasses["Multi Channel"],
endpoint: 0,
property: "interviewComplete",
});
}
export function setMultiChannelInterviewComplete(
applHost: ZWaveApplicationHost,
node: IZWaveNode,
complete: boolean,
): void {
setValue(
applHost,
node,
{
commandClass: CommandClasses["Multi Channel"],
endpoint: 0,
property: "interviewComplete",
},
complete,
);
}
export function getAllEndpoints(
applHost: ZWaveApplicationHost,
node: IZWaveNode,
): IZWaveEndpoint[] {
const ret: IZWaveEndpoint[] = [node];
// Check if the Multi Channel CC interview for this node is completed,
// because we don't have all the endpoint information before that
if (isMultiChannelInterviewComplete(applHost, node)) {
for (const i of getEndpointIndizes(applHost, node)) {
const endpoint = node.getEndpoint(i);
if (endpoint) ret.push(endpoint);
}
}
return ret;
}
/** Determines whether the root application CC values should be hidden in favor of endpoint values */
export function shouldHideRootApplicationCCValues(
applHost: ZWaveApplicationHost,
node: IZWaveNode,
): boolean {
// This is not the case when the root values should explicitly be preserved
const compatConfig = applHost.getDeviceConfig?.(node.id)?.compat;
if (compatConfig?.preserveRootApplicationCCValueIDs) return false;
// This is not the case when there are no endpoints
const endpointIndizes = getEndpointIndizes(applHost, node);
if (endpointIndizes.length === 0) return false;
// This is not the case when only individual endpoints should be preserved in addition to the root
const preserveEndpoints = compatConfig?.preserveEndpoints;
if (
preserveEndpoints != undefined &&
preserveEndpoints !== "*" &&
preserveEndpoints.length !== endpointIndizes.length
) {
return false;
}
// Otherwise they should be hidden
return true;
}
/**
* Enhances a value id so it can be consumed better by applications
*/
export function translateValueID<T extends ValueID>(
applHost: ZWaveApplicationHost,
node: IZWaveNode,
valueId: T,
): T & TranslatedValueID {
// Try to retrieve the speaking CC name
const commandClassName = getCCName(valueId.commandClass);
const ret: T & TranslatedValueID = {
commandClassName,
...valueId,
};
const ccInstance = CommandClass.createInstanceUnchecked(
applHost,
node,
valueId.commandClass,
);
if (!ccInstance) {
throw new ZWaveError(
`Cannot translate a value ID for the non-implemented CC ${getCCName(
valueId.commandClass,
)}`,
ZWaveErrorCodes.CC_NotImplemented,
);
}
// Retrieve the speaking property name
ret.propertyName = ccInstance.translateProperty(
applHost,
valueId.property,
valueId.propertyKey,
);
// Try to retrieve the speaking property key
if (valueId.propertyKey != undefined) {
const propertyKey = ccInstance.translatePropertyKey(
applHost,
valueId.property,
valueId.propertyKey,
);
ret.propertyKeyName = propertyKey;
}
return ret;
}
/**
* Removes all Value IDs from an array that belong to a root endpoint and have a corresponding
* Value ID on a non-root endpoint
*/
export function filterRootApplicationCCValueIDs(
allValueIds: ValueID[],
): ValueID[] {
const shouldHideRootValueID = (
valueId: ValueID,
allValueIds: ValueID[],
): boolean => {
// Non-root endpoint values don't need to be filtered
if (!!valueId.endpoint) return false;
// Non-application CCs don't need to be filtered
if (!applicationCCs.includes(valueId.commandClass)) return false;
// Filter out root values if an identical value ID exists for another endpoint
const valueExistsOnAnotherEndpoint = allValueIds.some(
(other) =>
// same CC
other.commandClass === valueId.commandClass &&
// non-root endpoint
!!other.endpoint &&
// same property and key
other.property === valueId.property &&
other.propertyKey === valueId.propertyKey,
);
return valueExistsOnAnotherEndpoint;
};
return allValueIds.filter(
(vid) => !shouldHideRootValueID(vid, allValueIds),
);
}
/** Returns a list of all value names that are defined on all endpoints of this node */
export function getDefinedValueIDs(
applHost: ZWaveApplicationHost,
node: IZWaveNode,
): TranslatedValueID[] {
let ret: ValueID[] = [];
const allowControlled: CommandClasses[] = [
CommandClasses["Scene Activation"],
];
for (const endpoint of getAllEndpoints(applHost, node)) {
for (const cc of allCCs) {
if (
endpoint.supportsCC(cc) ||
(endpoint.controlsCC(cc) && allowControlled.includes(cc))
) {
const ccInstance = CommandClass.createInstanceUnchecked(
applHost,
endpoint,
cc,
);
if (ccInstance) {
ret.push(...ccInstance.getDefinedValueIDs(applHost));
}
}
}
}
// Application command classes of the Root Device capabilities that are also advertised by at
// least one End Point SHOULD be filtered out by controlling nodes before presenting the functionalities
// via service discovery mechanisms like mDNS or to users in a GUI.
// We do this when there are endpoints that were explicitly preserved
if (shouldHideRootApplicationCCValues(applHost, node)) {
ret = filterRootApplicationCCValueIDs(ret);
}
// Translate the remaining value IDs before exposing them to applications
return ret.map((id) => translateValueID(applHost, node, id));
}