import type { CommandClasses , CommandClassInfo , ValueID } from "@zwave-js/core" ; import { JSONObject , pick } from "@zwave-js/shared" ; import { isArray , isObject } from "alcalzone-shared/typeguards" ; import { hexKeyRegex2Digits , throwInvalidConfig } from "./utils" ; export class CompatConfig { private valueIdRegex = /^\$value\$\[.+\]$/ ; public constructor ( filename , definition ) { if ( definition . queryOnWakeup != undefined ) { if ( ! isArray ( definition . queryOnWakeup ) || ! definition . queryOnWakeup . every ( ( cmd ) => 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` ) ; } 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 . 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 ) => 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 . 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 . 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 ) => 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 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 skipConfigurationInfoQuery ? : boolean ; public readonly treatBasicSetAsEvent ? : boolean ; public readonly treatDestinationEndpointAsSource ? : boolean ; public readonly queryOnWakeup ? : readonly [ string , string , ... ( string | number | boolean | Pick < ValueID , "property" | "propertyKey" > ) [ ] ] [ ] ; } export class CompatAddCC { public constructor ( filename , definition ) { const endpoints = new Map < number , Partial < CommandClassInfo > > ( ) ; const parseEndpointInfo = ( endpoint , info ) => { const parsed = { } ; 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 ) ; } ; if ( definition . isSupported != undefined || definition . isControlled != undefined || definition . version != undefined || definition . secure != undefined ) { parseEndpointInfo ( 0 , definition ) ; } 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 , definition , index ) { 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 ; }