import { getDefaultScale , Scale } from "@zwave-js/config" ; import type { MessageOrCCLogEntry , MessageRecord , ValueID , } from "@zwave-js/core" ; import { CommandClasses , encodeFloatWithScale , Maybe , parseBitMask , parseFloatWithScale , validatePayload , ValueMetadata , ZWaveError , ZWaveErrorCodes , } from "@zwave-js/core" ; import { num2hex , pick } from "@zwave-js/shared" ; import type { Driver } from "../driver/Driver" ; import { MessagePriority } from "../message/Constants" ; import { PhysicalCCAPI , PollValueImplementation , POLL_VALUE , throwUnsupportedProperty , } from "./API" ; import { API , CCCommand , CCCommandOptions , ccKeyValuePair , CCResponsePredicate , ccValue , CommandClass , commandClass , CommandClassDeserializationOptions , expectedCCResponse , gotDeserializationOptions , implementedVersion , } from "./CommandClass" ; export enum MultilevelSensorCommand { GetSupportedSensor = 0x01 , SupportedSensorReport = 0x02 , GetSupportedScale = 0x03 , Get = 0x04 , Report = 0x05 , SupportedScaleReport = 0x06 } publicAPI export interface MultilevelSensorValue { value : number ; scale : Scale ; } publicAPI export type MultilevelSensorValueMetadata = ValueMetadata & { ccSpecific : { sensorType : number ; scale : number ; } ; } ; /**
 * Determine the scale to use to query a sensor reading. Uses the user-preferred scale if given,
 * otherwise falls back to the first supported one.
 */ function getPreferredSensorScale ( driver , nodeId , endpointIndex , sensorType , supportedScales ) { const scaleGroup = driver . configManager . lookupSensorType ( sensorType ) ?. scales ; if ( ! scaleGroup ) { const preferred = driver . options . preferences . scales [ sensorType ] ; if ( typeof preferred !== "number" ) return 0 ; return preferred ; } let preferred ; preferred = driver . options . preferences . scales [ sensorType ] ; if ( preferred == undefined && scaleGroup . name ) { preferred = driver . options . preferences . scales [ scaleGroup . name ] ; } if ( preferred == undefined ) { preferred = supportedScales [ 0 ] ?? 0 ; driver . controllerLog . logNode ( nodeId , { endpoint : endpointIndex , message : `No scale preference for sensor type ${ sensorType }, using the first supported scale ${ preferred }` } ) ; return preferred ; } if ( typeof preferred === "string" ) { for ( const scale of scaleGroup . values ( ) ) { if ( scale . label === preferred || scale . unit === preferred ) { preferred = scale . key ; break ; } } } if ( typeof preferred === "string" ) { driver . controllerLog . logNode ( nodeId , { endpoint : endpointIndex , message : `Preferred scale "${ preferred }" for sensor type ${ sensorType } not found, using the first supported scale ${ supportedScales [ 0 ] ?? 0 }` } ) ; return supportedScales [ 0 ] ?? 0 ; } if ( ! supportedScales . length ) { return preferred ; } else if ( ! supportedScales . includes ( preferred ) ) { driver . controllerLog . logNode ( nodeId , { endpoint : endpointIndex , message : `Preferred scale ${ preferred } not supported for sensor type ${ sensorType }, using the first supported scale` } ) ; return supportedScales [ 0 ] ; } else { return preferred ; } } export function getSupportedSensorTypesValueId ( endpoint ) { return { commandClass : CommandClasses [ "Multilevel Sensor" ] , endpoint : endpoint , property : "supportedSensorTypes" } ; } export function getSupportedScalesValueId ( endpoint , sensorType ) { return { commandClass : CommandClasses [ "Multilevel Sensor" ] , endpoint : endpoint , property : "supportedScales" , propertyKey : sensorType } ; } @ API ( CommandClasses [ "Multilevel Sensor" ] ) export class MultilevelSensorCCAPI extends PhysicalCCAPI { public supportsCommand ( cmd ) { switch ( cmd ) { case MultilevelSensorCommand . Get : case MultilevelSensorCommand . Report : return true ; case MultilevelSensorCommand . GetSupportedSensor : case MultilevelSensorCommand . GetSupportedScale : return this . version >= 5 ; } return super . supportsCommand ( cmd ) ; } protected [ POLL_VALUE ] : PollValueImplementation = async ( { property , } ) : Promise < unknown > => { const valueId = { commandClass : CommandClasses [ "Multilevel Sensor" ] , endpoint : this . endpoint . index , property } ; const ccSpecific = this . endpoint . getNodeUnsafe ( ) ?. valueDB . getMetadata ( valueId ) ?. ccSpecific ; if ( ! ccSpecific ) { throwUnsupportedProperty ( this . ccId , property ) ; } const { sensorType , scale } = ccSpecific ; return this . get ( sensorType , scale ) ; } ; /** Query the default sensor value */ public async get ( ) ; /** Query the sensor value for the given sensor type using the preferred sensor scale */ public async get ( sensorType ) ; /** Query the sensor value for the given sensor type using the given sensor scale */ public async get ( sensorType , scale ) ; public async get ( sensorType ? , scale ? ) { this . assertSupportsCommand ( MultilevelSensorCommand , MultilevelSensorCommand . Get ) ; let preferredScale ; if ( sensorType != undefined && scale == undefined ) { const supportedScales = this . endpoint . getNodeUnsafe ( ) ?. getValue ( { commandClass : this . ccId , endpoint : this . endpoint . index , property : "supportedScales" , propertyKey : sensorType } ) ?? [ ] ; preferredScale = getPreferredSensorScale ( this . driver , this . endpoint . nodeId , this . endpoint . index , sensorType , supportedScales ) ; } const cc = new MultilevelSensorCCGet ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , sensorType , scale : scale ?? preferredScale } ) ; const response = await this . driver . sendCommand < MultilevelSensorCCReport > ( cc , this . commandOptions ) ; if ( ! response ) return ; if ( sensorType == undefined ) { return { type : response . type , value : response . value , scale : response . scale } ; } else if ( scale == undefined ) { return pick ( response , [ "value" , "scale" ] ) ; } else { return response . value ; } } public async getSupportedSensorTypes ( ) { this . assertSupportsCommand ( MultilevelSensorCommand , MultilevelSensorCommand . GetSupportedSensor ) ; const cc = new MultilevelSensorCCGetSupportedSensor ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index } ) ; const response = await this . driver . sendCommand < MultilevelSensorCCSupportedSensorReport > ( cc , this . commandOptions ) ; return response ?. supportedSensorTypes ; } public async getSupportedScales ( sensorType ) { this . assertSupportsCommand ( MultilevelSensorCommand , MultilevelSensorCommand . GetSupportedScale ) ; const cc = new MultilevelSensorCCGetSupportedScale ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , sensorType } ) ; const response = await this . driver . sendCommand < MultilevelSensorCCSupportedScaleReport > ( cc , this . commandOptions ) ; return response ?. sensorSupportedScales ; } public async sendReport ( sensorType , scale , value ) { this . assertSupportsCommand ( MultilevelSensorCommand , MultilevelSensorCommand . Report ) ; const cc = new MultilevelSensorCCReport ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , type : sensorType , scale , value } ) ; await this . driver . sendCommand ( cc , this . commandOptions ) ; } } @ commandClass ( CommandClasses [ "Multilevel Sensor" ] ) @ implementedVersion ( 11 ) export class MultilevelSensorCC extends CommandClass { declare ccCommand : MultilevelSensorCommand ; public async interview ( ) { const node = this . getNode ( ) ! ; const endpoint = this . getEndpoint ( ) ! ; const api = endpoint . commandClasses [ "Multilevel Sensor" ] . withOptions ( { priority : MessagePriority . NodeQuery } ) ; this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : `Interviewing ${ this . ccName }...` , direction : "none" } ) ; if ( this . version >= 5 ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : "retrieving supported sensor types..." , direction : "outbound" } ) ; const sensorTypes = await api . getSupportedSensorTypes ( ) ; if ( sensorTypes ) { const logMessage = "received supported sensor types:\n" + sensorTypes . map ( ( t ) => this . driver . configManager . getSensorTypeName ( t ) ) . map ( ( name ) => `· ${ name }` ) . join ( "\n" ) ; this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : logMessage , direction : "inbound" } ) ; } else { this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : "Querying supported sensor types timed out, skipping interview..." , level : "warn" } ) ; return ; } for ( const type of sensorTypes ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : `querying supported scales for ${ this . driver . configManager . getSensorTypeName ( type ) } sensor` , direction : "outbound" } ) ; const sensorScales = await api . getSupportedScales ( type ) ; if ( sensorScales ) { const logMessage = "received supported scales:\n" + sensorScales . map ( ( s ) => this . driver . configManager . lookupSensorScale ( type , s ) . label ) . map ( ( name ) => `· ${ name }` ) . join ( "\n" ) ; this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : logMessage , direction : "inbound" } ) ; } else { this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : "Querying supported scales timed out, skipping interview..." , level : "warn" } ) ; return ; } } } await this . refreshValues ( ) ; this . interviewComplete = true ; } public async refreshValues ( ) { const node = this . getNode ( ) ! ; const endpoint = this . getEndpoint ( ) ! ; const api = endpoint . commandClasses [ "Multilevel Sensor" ] . withOptions ( { priority : MessagePriority . NodeQuery } ) ; if ( this . version <= 4 ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : "querying current sensor reading..." , direction : "outbound" } ) ; const mlsResponse = await api . get ( ) ; if ( mlsResponse ) { const sensorScale = this . driver . configManager . lookupSensorScale ( mlsResponse . type , mlsResponse . scale . key ) ; const logMessage = `received current sensor reading:
sensor type: ${ this . driver . configManager . getSensorTypeName ( mlsResponse . type ) }
value:       ${ mlsResponse . value } ${ sensorScale . unit || "" }` ; this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : logMessage , direction : "inbound" } ) ; } } else { const sensorTypes = this . getValueDB ( ) . getValue ( { commandClass : this . ccId , property : "supportedSensorTypes" , endpoint : this . endpointIndex } ) || [ ] ; for ( const type of sensorTypes ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : `querying ${ this . driver . configManager . getSensorTypeName ( type ) } sensor reading...` , direction : "outbound" } ) ; const value = await api . get ( type ) ; if ( value ) { const logMessage = `received current ${ this . driver . configManager . getSensorTypeName ( type ) } sensor reading: ${ value . value } ${ value . scale . unit || "" }` ; this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : logMessage , direction : "inbound" } ) ; } } } } public translatePropertyKey ( property , propertyKey ) { if ( property === "values" && typeof propertyKey === "number" ) { const type = this . driver . configManager . lookupSensorType ( propertyKey ) ; if ( type ) return type . label ; } return super . translatePropertyKey ( property , propertyKey ) ; } } export interface MultilevelSensorCCReportOptions extends CCCommandOptions { type : number ; scale : number | Scale ; value : number ; } @ CCCommand ( MultilevelSensorCommand . Report ) export class MultilevelSensorCCReport extends MultilevelSensorCC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { validatePayload ( this . payload . length >= 1 ) ; this . type = this . payload [ 0 ] ; const sensorType = this . driver . configManager . lookupSensorType ( this . type ) ; const { value , scale } = parseFloatWithScale ( this . payload . slice ( 1 ) ) ; this . value = value ; this . scale = this . driver . configManager . lookupSensorScale ( this . type , scale ) ; validatePayload . withReason ( `Unknown sensor type ${ num2hex ( this . type ) } or corrupted data` ) ( ! ! sensorType ) ; validatePayload . withReason ( `Unknown scale ${ num2hex ( scale ) } or corrupted data` ) ( this . scale . label !== getDefaultScale ( scale ) . label ) ; if ( this . version >= 5 ) { const valueDB = this . getValueDB ( ) ; const supportedSensorTypes = valueDB . getValue < number [ ] > ( getSupportedSensorTypesValueId ( this . endpointIndex ) ) ; if ( supportedSensorTypes ?. length ) { validatePayload . withReason ( `Unsupported sensor type ${ sensorType ! . label } or corrupted data` ) ( supportedSensorTypes . includes ( this . type ) ) ; } const supportedScales = valueDB . getValue < number [ ] > ( getSupportedScalesValueId ( this . endpointIndex , this . type ) ) ; if ( supportedScales ?. length ) { validatePayload . withReason ( `Unsupported sensor type ${ this . scale . label } or corrupted data` ) ( supportedScales . includes ( this . scale . key ) ) ; } } this . persistValues ( ) ; } else { this . type = options . type ; this . value = options . value ; this . scale = options . scale instanceof Scale ? options . scale : this . driver . configManager . lookupSensorScale ( this . type , options . scale ) ; } } public persistValues ( ) { const typeName = this . driver . configManager . getSensorTypeName ( this . type ) ; const valueId = { commandClass : this . ccId , endpoint : this . endpointIndex , property : typeName } ; this . getValueDB ( ) . setMetadata ( valueId , { ... ValueMetadata . ReadOnlyNumber , unit : this . scale . unit , label : typeName , ccSpecific : { sensorType : this . type , scale : this . scale . key } } ) ; this . getValueDB ( ) . setValue ( valueId , this . value ) ; return true ; } public type : number ; public scale : Scale ; public value : number ; public serialize ( ) { this . payload = Buffer . concat ( [ Buffer . from ( [ this . type ] ) , encodeFloatWithScale ( this . value , this . scale . key ) , ] ) ; return super . serialize ( ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { type : this . driver . configManager . getSensorTypeName ( this . type ) , scale : this . scale . label , value : this . value } } ; } } const testResponseForMultilevelSensorGet = ( sent , received ) => { return sent . sensorType == undefined || received . type === sent . sensorType ; } ; interface MultilevelSensorCCGetSpecificOptions { sensorType : number ; scale : number ; } type MultilevelSensorCCGetOptions = CCCommandOptions | ( CCCommandOptions & MultilevelSensorCCGetSpecificOptions ) ; @ CCCommand ( MultilevelSensorCommand . Get ) @ expectedCCResponse ( MultilevelSensorCCReport , testResponseForMultilevelSensorGet ) export class MultilevelSensorCCGet extends MultilevelSensorCC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { throw new ZWaveError ( `${ this . constructor . name }: deserialization not implemented` , ZWaveErrorCodes . Deserialization_NotImplemented ) ; } else { if ( "sensorType" in options ) { this . sensorType = options . sensorType ; this . scale = options . scale ; } } } public sensorType : number | undefined ; public scale : number | undefined ; public serialize ( ) { if ( this . version >= 5 && this . sensorType != undefined && this . scale != undefined ) { this . payload = Buffer . from ( [ this . sensorType , ( this . scale & 0b11 ) << 3 , ] ) ; } return super . serialize ( ) ; } public toLogEntry ( ) { let message = { } ; if ( this . version >= 5 && this . sensorType != undefined && this . scale != undefined ) { message = { "sensor type" : this . driver . configManager . getSensorTypeName ( this . sensorType ) , scale : this . driver . configManager . lookupSensorScale ( this . sensorType , this . scale ) . label } ; } return { ... super . toLogEntry ( ) , message } ; } } @ CCCommand ( MultilevelSensorCommand . SupportedSensorReport ) export class MultilevelSensorCCSupportedSensorReport extends MultilevelSensorCC { public constructor ( driver , options ) { super ( driver , options ) ; validatePayload ( this . payload . length >= 1 ) ; this . _supportedSensorTypes = parseBitMask ( this . payload ) ; this . persistValues ( ) ; } private _supportedSensorTypes : number [ ] ; @ ccValue ( { internal : true } ) public get supportedSensorTypes ( ) : readonly number [ ] { return this . _supportedSensorTypes ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { "supported sensor types" : this . supportedSensorTypes . map ( ( t ) => `\n· ${ this . driver . configManager . getSensorTypeName ( t ) }` ) . join ( "" ) } } ; } } @ CCCommand ( MultilevelSensorCommand . GetSupportedSensor ) @ expectedCCResponse ( MultilevelSensorCCSupportedSensorReport ) export class MultilevelSensorCCGetSupportedSensor extends MultilevelSensorCC { } @ CCCommand ( MultilevelSensorCommand . SupportedScaleReport ) export class MultilevelSensorCCSupportedScaleReport extends MultilevelSensorCC { public constructor ( driver , options ) { super ( driver , options ) ; validatePayload ( this . payload . length >= 2 ) ; const sensorType = this . payload [ 0 ] ; const supportedScales = parseBitMask ( Buffer . from ( [ this . payload [ 1 ] & 0b1111 ] ) , 0 ) ; this . supportedScales = [ sensorType , supportedScales ] ; this . persistValues ( ) ; } @ ccKeyValuePair ( { internal : true } ) private supportedScales : [ number , number [ ] ] ; public get sensorType ( ) : number { return this . supportedScales [ 0 ] ; } public get sensorSupportedScales ( ) : readonly number [ ] { return this . supportedScales [ 1 ] ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { "sensor type" : this . driver . configManager . getSensorTypeName ( this . sensorType ) , "supported scales" : this . sensorSupportedScales . map ( ( s ) => `\n· ${ this . driver . configManager . lookupSensorScale ( this . sensorType , s ) . label }` ) . join ( "" ) } } ; } } interface MultilevelSensorCCGetSupportedScaleOptions extends CCCommandOptions { sensorType : number ; } @ CCCommand ( MultilevelSensorCommand . GetSupportedScale ) @ expectedCCResponse ( MultilevelSensorCCSupportedScaleReport ) export class MultilevelSensorCCGetSupportedScale extends MultilevelSensorCC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { throw new ZWaveError ( `${ this . constructor . name }: deserialization not implemented` , ZWaveErrorCodes . Deserialization_NotImplemented ) ; } else { this . sensorType = options . sensorType ; } } public sensorType : number ; public serialize ( ) { this . payload = Buffer . from ( [ this . sensorType ] ) ; return super . serialize ( ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { "sensor type" : this . driver . configManager . getSensorTypeName ( this . sensorType ) } } ; } }