inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
739 lines (676 loc) • 20.7 kB
text/typescript
import type {
CommandClasses,
CommandClassInfo,
ValueID,
} from "@zwave-js/core/safe";
import { JSONObject, pick } from "@zwave-js/shared/safe";
import { isArray, isObject } from "alcalzone-shared/typeguards";
import { hexKeyRegex2Digits, throwInvalidConfig } from "../utils_safe";
import { ConditionalItem, conditionApplies } from "./ConditionalItem";
import type { DeviceID } from "./shared";
export class ConditionalCompatConfig implements ConditionalItem<CompatConfig> {
private valueIdRegex = /^\$value\$\[.+\]$/;
public constructor(filename: string, definition: JSONObject) {
this.condition = definition.$if;
if (definition.queryOnWakeup != undefined) {
if (
!isArray(definition.queryOnWakeup) ||
!definition.queryOnWakeup.every(
(cmd: unknown) =>
isArray(cmd) &&
cmd.length >= 2 &&
typeof cmd[0] === "string" &&
typeof cmd[1] === "string" &&
cmd
.slice(2)
.every(
(arg) =>
typeof arg === "string" ||
typeof arg === "number" ||
typeof arg === "boolean",
),
)
) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option queryOnWakeup`,
);
}
// Parse "smart" values into partial Value IDs
this.queryOnWakeup = (definition.queryOnWakeup as any[][]).map(
(cmd) =>
cmd.map((arg) => {
if (
typeof arg === "string" &&
this.valueIdRegex.test(arg)
) {
const tuple = JSON.parse(
arg.substr("$value$".length),
);
return {
property: tuple[0],
propertyKey: tuple[1],
};
}
return arg;
}),
) as any;
}
if (definition.disableBasicMapping != undefined) {
if (definition.disableBasicMapping !== true) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option disableBasicMapping`,
);
}
this.disableBasicMapping = definition.disableBasicMapping;
}
if (definition.disableStrictEntryControlDataValidation != undefined) {
if (definition.disableStrictEntryControlDataValidation !== true) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option disableStrictEntryControlDataValidation`,
);
}
this.disableStrictEntryControlDataValidation =
definition.disableStrictEntryControlDataValidation;
}
if (definition.disableStrictMeasurementValidation != undefined) {
if (definition.disableStrictMeasurementValidation !== true) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option disableStrictMeasurementValidation`,
);
}
this.disableStrictMeasurementValidation =
definition.disableStrictMeasurementValidation;
}
if (definition.enableBasicSetMapping != undefined) {
if (definition.enableBasicSetMapping !== true) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option enableBasicSetMapping`,
);
}
this.enableBasicSetMapping = definition.enableBasicSetMapping;
}
if (definition.forceNotificationIdleReset != undefined) {
if (definition.forceNotificationIdleReset !== true) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option forceNotificationIdleReset`,
);
}
this.forceNotificationIdleReset =
definition.forceNotificationIdleReset;
}
if (definition.forceSceneControllerGroupCount != undefined) {
if (typeof definition.forceSceneControllerGroupCount !== "number") {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
compat option forceSceneControllerGroupCount must be a number!`,
);
}
if (
definition.forceSceneControllerGroupCount < 0 ||
definition.forceSceneControllerGroupCount > 255
) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
compat option forceSceneControllerGroupCount must be between 0 and 255!`,
);
}
this.forceSceneControllerGroupCount =
definition.forceSceneControllerGroupCount;
}
if (definition.preserveRootApplicationCCValueIDs != undefined) {
if (definition.preserveRootApplicationCCValueIDs !== true) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option preserveRootApplicationCCValueIDs`,
);
}
this.preserveRootApplicationCCValueIDs =
definition.preserveRootApplicationCCValueIDs;
}
if (definition.preserveEndpoints != undefined) {
if (
definition.preserveEndpoints !== "*" &&
!(
isArray(definition.preserveEndpoints) &&
definition.preserveEndpoints.every(
(d: any) =>
typeof d === "number" && d % 1 === 0 && d > 0,
)
)
) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
compat option preserveEndpoints must be "*" or an array of positive integers`,
);
}
this.preserveEndpoints = definition.preserveEndpoints;
}
if (definition.skipConfigurationNameQuery != undefined) {
if (definition.skipConfigurationNameQuery !== true) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option skipConfigurationNameQuery`,
);
}
this.skipConfigurationNameQuery =
definition.skipConfigurationNameQuery;
}
if (definition.skipConfigurationInfoQuery != undefined) {
if (definition.skipConfigurationInfoQuery !== true) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option skipConfigurationInfoQuery`,
);
}
this.skipConfigurationInfoQuery =
definition.skipConfigurationInfoQuery;
}
if (definition.treatBasicSetAsEvent != undefined) {
if (definition.treatBasicSetAsEvent !== true) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option treatBasicSetAsEvent`,
);
}
this.treatBasicSetAsEvent = definition.treatBasicSetAsEvent;
}
if (definition.treatMultilevelSwitchSetAsEvent != undefined) {
if (definition.treatMultilevelSwitchSetAsEvent !== true) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option treatMultilevelSwitchSetAsEvent`,
);
}
this.treatMultilevelSwitchSetAsEvent =
definition.treatMultilevelSwitchSetAsEvent;
}
if (definition.treatDestinationEndpointAsSource != undefined) {
if (definition.treatDestinationEndpointAsSource !== true) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option treatDestinationEndpointAsSource`,
);
}
this.treatDestinationEndpointAsSource =
definition.treatDestinationEndpointAsSource;
}
if (definition.manualValueRefreshDelayMs != undefined) {
if (typeof definition.manualValueRefreshDelayMs !== "number") {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
compat option manualValueRefreshDelayMs must be a number!`,
);
}
if (
definition.manualValueRefreshDelayMs % 1 !== 0 ||
definition.manualValueRefreshDelayMs < 0
) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
compat option manualValueRefreshDelayMs must be a non-negative integer!`,
);
}
this.manualValueRefreshDelayMs =
definition.manualValueRefreshDelayMs;
}
if (definition.mapRootReportsToEndpoint != undefined) {
if (typeof definition.mapRootReportsToEndpoint !== "number") {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
compat option mapRootReportsToEndpoint must be a number!`,
);
}
if (
definition.mapRootReportsToEndpoint % 1 !== 0 ||
definition.mapRootReportsToEndpoint < 1
) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
compat option mapRootReportsToEndpoint must be a positive integer!`,
);
}
this.mapRootReportsToEndpoint = definition.mapRootReportsToEndpoint;
}
if (definition.overrideFloatEncoding != undefined) {
if (!isObject(definition.overrideFloatEncoding)) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option overrideFloatEncoding`,
);
}
this.overrideFloatEncoding = {};
if ("precision" in definition.overrideFloatEncoding) {
if (
typeof definition.overrideFloatEncoding.precision !=
"number"
) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
compat option overrideFloatEncoding.precision must be a number!`,
);
}
if (
definition.overrideFloatEncoding.precision % 1 !== 0 ||
definition.overrideFloatEncoding.precision < 0
) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
compat option overrideFloatEncoding.precision must be a positive integer!`,
);
}
this.overrideFloatEncoding.precision =
definition.overrideFloatEncoding.precision;
}
if ("size" in definition.overrideFloatEncoding) {
if (typeof definition.overrideFloatEncoding.size != "number") {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
compat option overrideFloatEncoding.size must be a number!`,
);
}
if (
definition.overrideFloatEncoding.size % 1 !== 0 ||
definition.overrideFloatEncoding.size < 1 ||
definition.overrideFloatEncoding.size > 4
) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
compat option overrideFloatEncoding.size must be an integer between 1 and 4!`,
);
}
this.overrideFloatEncoding.size =
definition.overrideFloatEncoding.size;
}
if (Object.keys(this.overrideFloatEncoding).length === 0) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option overrideFloatEncoding: size and/or precision must be specified!`,
);
}
}
if (definition.commandClasses != undefined) {
if (!isObject(definition.commandClasses)) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option commandClasses`,
);
}
if (definition.commandClasses.add != undefined) {
if (!isObject(definition.commandClasses.add)) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option commandClasses.add`,
);
} else if (
!Object.keys(definition.commandClasses.add).every((k) =>
hexKeyRegex2Digits.test(k),
)
) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
All keys in compat option commandClasses.add must be 2-digit lowercase hex numbers!`,
);
} else if (
!Object.values(definition.commandClasses.add).every((v) =>
isObject(v),
)
) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
All values in compat option commandClasses.add must be objects`,
);
}
const addCCs = new Map<CommandClasses, CompatAddCC>();
for (const [cc, info] of Object.entries(
definition.commandClasses.add,
)) {
addCCs.set(
parseInt(cc),
new CompatAddCC(filename, info as any),
);
}
this.addCCs = addCCs;
}
if (definition.commandClasses.remove != undefined) {
if (!isObject(definition.commandClasses.remove)) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option commandClasses.remove`,
);
} else if (
!Object.keys(definition.commandClasses.remove).every((k) =>
hexKeyRegex2Digits.test(k),
)
) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
All keys in compat option commandClasses.remove must be 2-digit lowercase hex numbers!`,
);
}
const removeCCs = new Map<
CommandClasses,
"*" | readonly number[]
>();
for (const [cc, info] of Object.entries(
definition.commandClasses.remove,
)) {
if (isObject(info) && "endpoints" in info) {
if (
info.endpoints === "*" ||
(isArray(info.endpoints) &&
info.endpoints.every(
(i) => typeof i === "number",
))
) {
removeCCs.set(parseInt(cc), info.endpoints as any);
} else {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
Compat option commandClasses.remove has an invalid "endpoints" property. Only "*" and numeric arrays are allowed!`,
);
}
} else {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
All values in compat option commandClasses.remove must be objects with an "endpoints" property!`,
);
}
}
this.removeCCs = removeCCs;
}
}
if (definition.alarmMapping != undefined) {
if (
!isArray(definition.alarmMapping) ||
!definition.alarmMapping.every((m: any) => isObject(m))
) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
compat option alarmMapping must be an array where all items are objects!`,
);
}
this.alarmMapping = (definition.alarmMapping as any[]).map(
(m, i) => new CompatMapAlarm(filename, m, i + 1),
);
}
}
public readonly alarmMapping?: readonly CompatMapAlarm[];
public readonly addCCs?: ReadonlyMap<CommandClasses, CompatAddCC>;
public readonly removeCCs?: ReadonlyMap<
CommandClasses,
"*" | readonly number[]
>;
public readonly disableBasicMapping?: boolean;
public readonly disableStrictEntryControlDataValidation?: boolean;
public readonly disableStrictMeasurementValidation?: boolean;
public readonly enableBasicSetMapping?: boolean;
public readonly forceNotificationIdleReset?: boolean;
public readonly forceSceneControllerGroupCount?: number;
public readonly manualValueRefreshDelayMs?: number;
public readonly mapRootReportsToEndpoint?: number;
public readonly overrideFloatEncoding?: {
size?: number;
precision?: number;
};
public readonly preserveRootApplicationCCValueIDs?: boolean;
public readonly preserveEndpoints?: "*" | readonly number[];
public readonly skipConfigurationNameQuery?: boolean;
public readonly skipConfigurationInfoQuery?: boolean;
public readonly treatBasicSetAsEvent?: boolean;
public readonly treatMultilevelSwitchSetAsEvent?: boolean;
public readonly treatDestinationEndpointAsSource?: boolean;
public readonly queryOnWakeup?: readonly [
string,
string,
...(
| string
| number
| boolean
| Pick<ValueID, "property" | "propertyKey">
)[],
][];
public readonly condition?: string | undefined;
public evaluateCondition(deviceId?: DeviceID): CompatConfig | undefined {
if (!conditionApplies(this, deviceId)) return;
return pick(this, [
"alarmMapping",
"addCCs",
"removeCCs",
"disableBasicMapping",
"disableStrictEntryControlDataValidation",
"disableStrictMeasurementValidation",
"enableBasicSetMapping",
"forceNotificationIdleReset",
"forceSceneControllerGroupCount",
"manualValueRefreshDelayMs",
"mapRootReportsToEndpoint",
"overrideFloatEncoding",
"preserveRootApplicationCCValueIDs",
"preserveEndpoints",
"skipConfigurationNameQuery",
"skipConfigurationInfoQuery",
"treatBasicSetAsEvent",
"treatMultilevelSwitchSetAsEvent",
"treatDestinationEndpointAsSource",
"queryOnWakeup",
]);
}
}
export type CompatConfig = Omit<
ConditionalCompatConfig,
"condition" | "evaluateCondition"
>;
export class CompatAddCC {
public constructor(filename: string, definition: JSONObject) {
const endpoints = new Map<number, Partial<CommandClassInfo>>();
const parseEndpointInfo = (endpoint: number, info: JSONObject) => {
const parsed: Partial<CommandClassInfo> = {};
if (info.isSupported != undefined) {
if (typeof info.isSupported !== "boolean") {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
Property isSupported in compat option commandClasses.add, endpoint ${endpoint} must be a boolean!`,
);
} else {
parsed.isSupported = info.isSupported;
}
}
if (info.isControlled != undefined) {
if (typeof info.isControlled !== "boolean") {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
Property isControlled in compat option commandClasses.add, endpoint ${endpoint} must be a boolean!`,
);
} else {
parsed.isControlled = info.isControlled;
}
}
if (info.secure != undefined) {
if (typeof info.secure !== "boolean") {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
Property secure in compat option commandClasses.add, endpoint ${endpoint} must be a boolean!`,
);
} else {
parsed.secure = info.secure;
}
}
if (info.version != undefined) {
if (typeof info.version !== "number") {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
Property version in compat option commandClasses.add, endpoint ${endpoint} must be a number!`,
);
} else {
parsed.version = info.version;
}
}
endpoints.set(endpoint, parsed);
};
// Parse root endpoint info if given
if (
definition.isSupported != undefined ||
definition.isControlled != undefined ||
definition.version != undefined ||
definition.secure != undefined
) {
// We have info for the root endpoint
parseEndpointInfo(0, definition);
}
// Parse all other endpoints
if (isObject(definition.endpoints)) {
if (
!Object.keys(definition.endpoints).every((k) => /^\d+$/.test(k))
) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
invalid endpoint index in compat option commandClasses.add`,
);
} else {
for (const [ep, info] of Object.entries(definition.endpoints)) {
parseEndpointInfo(parseInt(ep), info as any);
}
}
}
this.endpoints = endpoints;
}
public readonly endpoints: ReadonlyMap<number, Partial<CommandClassInfo>>;
}
export interface CompatMapAlarmFrom {
alarmType: number;
alarmLevel?: number;
}
export interface CompatMapAlarmTo {
notificationType: number;
notificationEvent: number;
eventParameters?: Record<string, number | "alarmLevel">;
}
export class CompatMapAlarm {
public constructor(
filename: string,
definition: JSONObject,
index: number,
) {
if (!isObject(definition.from)) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option alarmMapping, mapping #${index}: property "from" must be an object!`,
);
} else {
if (typeof definition.from.alarmType !== "number") {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option alarmMapping, mapping #${index}: property "from.alarmType" must be a number!`,
);
}
if (
definition.from.alarmLevel != undefined &&
typeof definition.from.alarmLevel !== "number"
) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option alarmMapping, mapping #${index}: if property "from.alarmLevel" is given, it must be a number!`,
);
}
}
if (!isObject(definition.to)) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option alarmMapping, mapping #${index}: property "to" must be an object!`,
);
} else {
if (typeof definition.to.notificationType !== "number") {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option alarmMapping, mapping #${index}: property "to.notificationType" must be a number!`,
);
}
if (typeof definition.to.notificationEvent !== "number") {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option alarmMapping, mapping #${index}: property "to.notificationEvent" must be a number!`,
);
}
if (definition.to.eventParameters != undefined) {
if (!isObject(definition.to.eventParameters)) {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option alarmMapping, mapping #${index}: property "to.eventParameters" must be an object!`,
);
} else {
for (const [key, val] of Object.entries(
definition.to.eventParameters,
)) {
if (typeof val !== "number" && val !== "alarmLevel") {
throwInvalidConfig(
"devices",
`config/devices/${filename}:
error in compat option alarmMapping, mapping #${index}: property "to.eventParameters.${key}" must be a number or the literal "alarmLevel"!`,
);
}
}
}
}
}
this.from = pick(definition.from, ["alarmType", "alarmLevel"]);
this.to = pick(definition.to, [
"notificationType",
"notificationEvent",
"eventParameters",
]);
}
public readonly from: CompatMapAlarmFrom;
public readonly to: CompatMapAlarmTo;
}