import type { ConfigManager } from "@zwave-js/config" ; import type { MessageOrCCLogEntry , MessageRecord , ValueID , } from "@zwave-js/core" ; import { CommandClasses , Maybe , parseBitMask , validatePayload , ValueMetadata , ZWaveError , ZWaveErrorCodes , } from "@zwave-js/core" ; import { num2hex } from "@zwave-js/shared" ; import type { Driver } from "../driver/Driver" ; import { MessagePriority } from "../message/Constants" ; import { CCAPI , PollValueImplementation , POLL_VALUE , SetValueImplementation , SET_VALUE , throwUnsupportedProperty , throwWrongValueType , } from "./API" ; import { API , CCCommand , CCCommandOptions , CommandClass , commandClass , CommandClassDeserializationOptions , CommandClassOptions , expectedCCResponse , gotDeserializationOptions , implementedVersion , } from "./CommandClass" ; publicAPI export type IndicatorMetadata = ValueMetadata & { ccSpecific : { indicatorId : number ; propertyId ? : number ; } ; } ; export function getSupportedIndicatorIDsValueID ( endpoint ) { return { commandClass : CommandClasses . Indicator , endpoint , property : "supportedIndicatorIds" } ; } export function getSupportedPropertyIDsValueID ( endpoint , indicatorId ) { return { commandClass : CommandClasses . Indicator , endpoint , property : "supportedPropertyIDs" , propertyKey : indicatorId } ; } export function getIndicatorValueValueID ( endpoint , indicatorId , propertyId ) { if ( indicatorId === 0 ) { return { commandClass : CommandClasses . Indicator , endpoint , property : "value" } ; } else { return { commandClass : CommandClasses . Indicator , endpoint , property : indicatorId , propertyKey : propertyId } ; } } /**
 * Looks up the configured metadata for the given indicator and property
 */ function getIndicatorMetadata ( configManager , indicatorId , propertyId ) { const label = configManager . lookupIndicator ( indicatorId ) ; const prop = configManager . lookupProperty ( propertyId ) ; if ( ! label && ! prop ) { return { ... ValueMetadata . UInt8 , ccSpecific : { indicatorId , propertyId } } ; } else if ( ! prop ) { return { ... ValueMetadata . UInt8 , label , ccSpecific : { indicatorId , propertyId } } ; } else { if ( prop . type === "boolean" ) { return { ... ValueMetadata . Boolean , label : `${ label } - ${ prop . label }` , description : prop . description , readable : ! prop . readonly , ccSpecific : { indicatorId , propertyId } } ; } else { return { ... ValueMetadata . UInt8 , label : `${ label } - ${ prop . label }` , description : prop . description , min : prop . min , max : prop . max , readable : ! prop . readonly , ccSpecific : { indicatorId , propertyId } } ; } } } function getIndicatorName ( configManager , indicatorId ) { let indicatorName = "0 (default)" ; if ( indicatorId ) { indicatorName = `${ num2hex ( indicatorId ) } (${ configManager . lookupIndicator ( indicatorId ) ?? `Unknown` })` ; } return indicatorName ; } export enum IndicatorCommand { Set = 0x01 , Get = 0x02 , Report = 0x03 , SupportedGet = 0x04 , SupportedReport = 0x05 } const MAX_INDICATOR_OBJECTS = 31 ; @ API ( CommandClasses . Indicator ) export class IndicatorCCAPI extends CCAPI { public supportsCommand ( cmd ) { switch ( cmd ) { case IndicatorCommand . Get : return this . isSinglecast ( ) ; case IndicatorCommand . Set : return true ; case IndicatorCommand . SupportedGet : return this . version >= 2 && this . isSinglecast ( ) ; } return super . supportsCommand ( cmd ) ; } protected [ SET_VALUE ] : SetValueImplementation = async ( { property , propertyKey } , value ) : Promise < void > => { if ( property === "value" ) { if ( typeof value !== "number" ) { throwWrongValueType ( this . ccId , property , "number" , typeof value ) ; } await this . set ( value ) ; } else if ( typeof property === "number" && typeof propertyKey === "number" ) { const indicatorId = property ; const propertyId = propertyKey ; const expectedType = getIndicatorMetadata ( this . driver . configManager , indicatorId , propertyId ) . type as "number" | "boolean" ; if ( typeof value !== expectedType ) { throwWrongValueType ( this . ccId , property , expectedType , typeof value ) ; } await this . set ( [ { indicatorId : property , propertyId : propertyKey , value : value as any } , ] ) ; } else { throwUnsupportedProperty ( this . ccId , property ) ; } } ; protected [ POLL_VALUE ] : PollValueImplementation = async ( { property , } ) : Promise < unknown > => { if ( property === "value" ) return this . get ( ) ; if ( typeof property === "number" ) { return this . get ( property ) ; } throwUnsupportedProperty ( this . ccId , property ) ; } ; public async get ( indicatorId ? ) { this . assertSupportsCommand ( IndicatorCommand , IndicatorCommand . Get ) ; const cc = new IndicatorCCGet ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , indicatorId } ) ; const response = await this . driver . sendCommand < IndicatorCCReport > ( cc , this . commandOptions ) ; if ( ! response ) return ; if ( response . values ) return response . values ; return response . value ! ; } public async set ( value ) { this . assertSupportsCommand ( IndicatorCommand , IndicatorCommand . Set ) ; const cc = new IndicatorCCSet ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , ... ( typeof value === "number" ? { value } : { values : value } ) } ) ; await this . driver . sendCommand ( cc , this . commandOptions ) ; } public async getSupported ( indicatorId ) { this . assertSupportsCommand ( IndicatorCommand , IndicatorCommand . SupportedGet ) ; const cc = new IndicatorCCSupportedGet ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , indicatorId } ) ; const response = await this . driver . sendCommand < IndicatorCCSupportedReport > ( cc , this . commandOptions ) ; if ( response ) { return { ... ( indicatorId === 0x00 ? { indicatorId : response . indicatorId } : undefined ) , supportedProperties : response . supportedProperties , nextIndicatorId : response . nextIndicatorId } ; } } /**
     * Instructs the node to identify itself. Available starting with V3 of this CC.
     */ public async identify ( ) { if ( this . version <= 3 ) { throw new ZWaveError ( `The identify command is only supported in Version 3 and above` , ZWaveErrorCodes . CC_NotSupported ) ; } await this . set ( [ { indicatorId : 0x50 , propertyId : 0x03 , value : 0x08 } , { indicatorId : 0x50 , propertyId : 0x04 , value : 0x03 } , { indicatorId : 0x50 , propertyId : 0x05 , value : 0x06 } , ] ) ; } } @ commandClass ( CommandClasses . Indicator ) @ implementedVersion ( 3 ) export class IndicatorCC extends CommandClass { declare ccCommand : IndicatorCommand ; public constructor ( driver , options ) { super ( driver , options ) ; this . registerValue ( getSupportedIndicatorIDsValueID ( undefined ) . property , true ) ; this . registerValue ( getSupportedPropertyIDsValueID ( undefined , 0 ) . property , true ) ; } public async interview ( ) { const node = this . getNode ( ) ! ; const endpoint = this . getEndpoint ( ) ! ; const api = endpoint . commandClasses . Indicator . withOptions ( { priority : MessagePriority . NodeQuery } ) ; this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : `Interviewing ${ this . ccName }...` , direction : "none" } ) ; if ( this . version > 1 ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : "scanning supported indicator IDs..." , direction : "outbound" } ) ; let curId = 0x00 ; const supportedIndicatorIds = [ ] ; do { const supportedResponse = await api . getSupported ( curId ) ; if ( ! supportedResponse ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : "Time out while scanning supported indicator IDs, skipping interview..." , level : "warn" } ) ; return ; } supportedIndicatorIds . push ( supportedResponse . indicatorId ?? curId ) ; curId = supportedResponse . nextIndicatorId ; } while ( curId !== 0x00 ) ; this . getValueDB ( ) . setValue ( getSupportedIndicatorIDsValueID ( this . endpointIndex ) , supportedIndicatorIds ) ; const logMessage = `supported indicator IDs: ${ supportedIndicatorIds . join ( ", " ) }` ; this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : logMessage , direction : "inbound" } ) ; } await this . refreshValues ( ) ; this . interviewComplete = true ; } public async refreshValues ( ) { const node = this . getNode ( ) ! ; const endpoint = this . getEndpoint ( ) ! ; const api = endpoint . commandClasses . Indicator . withOptions ( { priority : MessagePriority . NodeQuery } ) ; if ( this . version === 1 ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : "requesting current indicator value..." , direction : "outbound" } ) ; await api . get ( ) ; } else { const supportedIndicatorIds = this . getValueDB ( ) . getValue ( getSupportedIndicatorIDsValueID ( this . endpointIndex ) ) ?? [ ] ; for ( const indicatorId of supportedIndicatorIds ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : `requesting current indicator value (id = ${ num2hex ( indicatorId ) })...` , direction : "outbound" } ) ; await api . get ( indicatorId ) ; } } } public translatePropertyKey ( property , propertyKey ) { if ( property === "value" ) { return undefined ; } else if ( typeof property === "number" && typeof propertyKey === "number" ) { const prop = this . driver . configManager . lookupProperty ( propertyKey ) ; if ( prop ) return prop . label ; } return super . translatePropertyKey ( property , propertyKey ) ; } public translateProperty ( property , propertyKey ? ) { if ( typeof property === "number" && typeof propertyKey === "number" ) { const label = this . driver . configManager . lookupIndicator ( property ) ; if ( label ) return label ; } return super . translateProperty ( property , propertyKey ) ; } protected supportsV2Indicators ( ) { const supportedIndicatorIds = this . getValueDB ( ) . getValue < number [ ] > ( getSupportedIndicatorIDsValueID ( this . endpointIndex ) ) ; if ( ! supportedIndicatorIds ?. length ) return false ; return supportedIndicatorIds . some ( ( indicatorId ) => ! ! this . getValueDB ( ) . getValue < number [ ] > ( getSupportedPropertyIDsValueID ( this . endpointIndex , indicatorId ) ) ?. length ) ; } } export interface IndicatorObject { indicatorId : number ; propertyId : number ; value : number | boolean ; } type IndicatorCCSetOptions = { value : number ; } | { values : IndicatorObject [ ] ; } ; @ CCCommand ( IndicatorCommand . Set ) export class IndicatorCCSet extends IndicatorCC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { throw new ZWaveError ( `${ this . constructor . name }: deserialization not implemented` , ZWaveErrorCodes . Deserialization_NotImplemented ) ; } else { if ( this . version === 1 ) { if ( ! ( "value" in options ) ) { throw new ZWaveError ( `Node ${ this . nodeId as number } only supports IndicatorCC V1 which requires a single value to be set` , ZWaveErrorCodes . Argument_Invalid ) ; } this . indicator0Value = options . value ; } else { if ( "value" in options ) { this . indicator0Value = options . value ; } else { if ( options . values . length > MAX_INDICATOR_OBJECTS ) { throw new ZWaveError ( `Only ${ MAX_INDICATOR_OBJECTS } indicator values can be set at a time!` , ZWaveErrorCodes . Argument_Invalid ) ; } this . values = options . values ; } } } } public indicator0Value : number | undefined ; public values : IndicatorObject [ ] | undefined ; public serialize ( ) { if ( this . indicator0Value != undefined ) { this . payload = Buffer . from ( [ this . indicator0Value ] ) ; } else { const values = this . values ! ; const objCount = values . length & MAX_INDICATOR_OBJECTS ; const valuesFlat = values . slice ( 0 , objCount + 1 ) . map ( ( o ) => [ o . indicatorId , o . propertyId , typeof o . value === "number" ? o . value : o . value ? 0xff : 0x00 , ] as const ) . reduce ( ( acc , cur ) => acc . concat ( ... cur ) , [ ] as number [ ] ) ; this . payload = Buffer . concat ( [ Buffer . from ( [ 0 , objCount ] ) , Buffer . from ( valuesFlat ) , ] ) ; } return super . serialize ( ) ; } public toLogEntry ( ) { const message = { } ; if ( this . indicator0Value != undefined ) { message [ "indicator 0 value" ] = this . indicator0Value ; } if ( this . values != undefined ) { message . values = `${ this . values . map ( ( v ) => `
· indicatorId: ${ v . indicatorId }
  propertyId:  ${ v . propertyId }
  value:       ${ v . value }` ) . join ( "" ) }` ; } return { ... super . toLogEntry ( ) , message } ; } } @ CCCommand ( IndicatorCommand . Report ) export class IndicatorCCReport extends IndicatorCC { public constructor ( driver , options ) { super ( driver , options ) ; validatePayload ( this . payload . length >= 1 ) ; const valueDB = this . getValueDB ( ) ; const objCount = this . payload . length >= 2 ? this . payload [ 1 ] & 0b11111 : 0 ; if ( objCount === 0 ) { this . value = this . payload [ 0 ] ; if ( ! this . supportsV2Indicators ( ) ) { const valueId = getIndicatorValueValueID ( this . endpointIndex , 0 , 1 ) ; valueDB . setMetadata ( valueId , { ... ValueMetadata . UInt8 , label : "Indicator value" , ccSpecific : { indicatorId : 0 } } ) ; valueDB . setValue ( valueId , this . value ) ; } else { if ( this . isSinglecast ( ) ) { this . driver . controllerLog . logNode ( this . nodeId , { message : `ignoring V1 indicator report because the node supports V2 indicators` , direction : "none" , endpoint : this . endpointIndex } ) ; } } } else { validatePayload ( this . payload . length >= 2 + 3 * objCount ) ; this . values = [ ] ; for ( let i = 0 ; i < objCount ; i ++ ) { const offset = 2 + 3 * i ; const value = { indicatorId : this . payload [ offset ] , propertyId : this . payload [ offset + 1 ] , value : this . payload [ offset + 2 ] } ; this . setIndicatorValue ( value ) ; } } } public readonly value : number | undefined ; public readonly values : IndicatorObject [ ] | undefined ; private setIndicatorValue ( value ) { const valueDB = this . getValueDB ( ) ; const metadata = getIndicatorMetadata ( this . driver . configManager , value . indicatorId , value . propertyId ) ; if ( metadata . type === "boolean" ) { value . value = ! ! value . value ; } this . values ! . push ( value ) ; const valueId = getIndicatorValueValueID ( this . endpointIndex , value . indicatorId , value . propertyId ) ; valueDB . setMetadata ( valueId , metadata ) ; valueDB . setValue ( valueId , value . value ) ; } public toLogEntry ( ) { const message = { } ; if ( this . value != undefined ) { message [ "indicator 0 value" ] = this . value ; } if ( this . values != undefined ) { message . values = `${ this . values . map ( ( v ) => `
· indicatorId: ${ v . indicatorId }
  propertyId:  ${ v . propertyId }
  value:       ${ v . value }` ) . join ( "" ) }` ; } return { ... super . toLogEntry ( ) , message } ; } } interface IndicatorCCGetOptions extends CCCommandOptions { indicatorId ? : number ; } @ CCCommand ( IndicatorCommand . Get ) @ expectedCCResponse ( IndicatorCCReport ) export class IndicatorCCGet extends IndicatorCC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { throw new ZWaveError ( `${ this . constructor . name }: deserialization not implemented` , ZWaveErrorCodes . Deserialization_NotImplemented ) ; } else { this . indicatorId = options . indicatorId ; } } public indicatorId : number | undefined ; public serialize ( ) { if ( this . indicatorId != undefined ) { this . payload = Buffer . from ( [ this . indicatorId ] ) ; } return super . serialize ( ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { indicator : getIndicatorName ( this . driver . configManager , this . indicatorId ) } } ; } } @ CCCommand ( IndicatorCommand . SupportedReport ) export class IndicatorCCSupportedReport extends IndicatorCC { public constructor ( driver , options ) { super ( driver , options ) ; validatePayload ( this . payload . length >= 3 ) ; this . indicatorId = this . payload [ 0 ] ; this . nextIndicatorId = this . payload [ 1 ] ; const bitMaskLength = this . payload [ 2 ] & 0b11111 ; if ( bitMaskLength === 0 ) { this . supportedProperties = [ ] ; } else { validatePayload ( this . payload . length >= 3 + bitMaskLength ) ; this . supportedProperties = parseBitMask ( this . payload . slice ( 3 , 3 + bitMaskLength ) , 0 ) . filter ( ( v ) => v !== 0 ) ; } this . persistValues ( ) ; } public persistValues ( ) { if ( this . indicatorId !== 0x00 ) { this . getValueDB ( ) . setValue ( getSupportedPropertyIDsValueID ( this . endpointIndex , this . indicatorId ) , this . supportedProperties ) ; } return true ; } public readonly indicatorId : number ; public readonly nextIndicatorId : number ; public readonly supportedProperties : readonly number [ ] ; public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { indicator : getIndicatorName ( this . driver . configManager , this . indicatorId ) , "supported properties" : `${ this . supportedProperties . map ( ( id ) => this . driver . configManager . lookupProperty ( id ) ?. label ?? `Unknown (${ num2hex ( id ) })` ) . join ( ", " ) }` , "next indicator" : getIndicatorName ( this . driver . configManager , this . nextIndicatorId ) } } ; } } interface IndicatorCCSupportedGetOptions extends CCCommandOptions { indicatorId : number ; } @ CCCommand ( IndicatorCommand . SupportedGet ) @ expectedCCResponse ( IndicatorCCSupportedReport ) export class IndicatorCCSupportedGet extends IndicatorCC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { throw new ZWaveError ( `${ this . constructor . name }: deserialization not implemented` , ZWaveErrorCodes . Deserialization_NotImplemented ) ; } else { this . indicatorId = options . indicatorId ; } } public indicatorId : number ; public serialize ( ) { this . payload = Buffer . from ( [ this . indicatorId ] ) ; return super . serialize ( ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { indicator : getIndicatorName ( this . driver . configManager , this . indicatorId ) } } ; } }