UNPKG

inventoresed

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

739 lines (676 loc) 20.7 kB
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; }