import type { ConfigManager , Scale } from "@zwave-js/config" ; import type { MessageOrCCLogEntry , ValueID , ValueMetadataNumeric , } from "@zwave-js/core" ; import { CommandClasses , encodeFloatWithScale , Maybe , parseBitMask , parseFloatWithScale , validatePayload , ValueMetadata , ZWaveError , ZWaveErrorCodes , } from "@zwave-js/core" ; import { getEnumMemberName , pick } 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 , ccValue , CommandClass , commandClass , CommandClassDeserializationOptions , CommandClassOptions , expectedCCResponse , gotDeserializationOptions , implementedVersion , } from "./CommandClass" ; export enum ThermostatSetpointCommand { Set = 0x01 , Get = 0x02 , Report = 0x03 , SupportedGet = 0x04 , SupportedReport = 0x05 , CapabilitiesGet = 0x09 , CapabilitiesReport = 0x0a } publicAPI export enum ThermostatSetpointType { "N/A" = 0x00 , "Heating" = 0x01 , "Cooling" = 0x02 , "Furnace" = 0x07 , "Dry Air" = 0x08 , "Moist Air" = 0x09 , "Auto Changeover" = 0x0a , "Energy Save Heating" = 0x0b , "Energy Save Cooling" = 0x0c , "Away Heating" = 0x0d , "Away Cooling" = 0x0e , "Full Power" = 0x0f } const thermostatSetpointTypeMap = [ 0x00 , 0x01 , 0x02 , 0x07 , 0x08 , 0x09 , 0x0a , 0x0b , 0x0c , 0x0d , 0x0e , 0x0f ] ; const thermostatSetpointScaleName = "temperature" ; function getScale ( configManager , scale ) { return configManager . lookupNamedScale ( thermostatSetpointScaleName , scale ) ; } function getSetpointUnit ( configManager , scale ) { return getScale ( configManager , scale ) . unit ?? "" ; } export interface ThermostatSetpointValue { value : number ; scale : number ; } export interface ThermostatSetpointCapabilities { minValue : number ; minValueScale : number ; maxValue : number ; maxValueScale : number ; } publicAPI export type ThermostatSetpointMetadata = ValueMetadata & { ccSpecific : { setpointType : ThermostatSetpointType ; } ; } ; function getSupportedSetpointTypesValueID ( endpoint ) { return { commandClass : CommandClasses [ "Thermostat Setpoint" ] , property : "supportedSetpointTypes" , endpoint } ; } function getSetpointTypesInterpretationValueID ( endpoint ) { return { commandClass : CommandClasses [ "Thermostat Setpoint" ] , property : "setpointTypesInterpretation" , endpoint } ; } function getSetpointValueID ( endpoint , setpointType ) { return { commandClass : CommandClasses [ "Thermostat Setpoint" ] , endpoint , property : "setpoint" , propertyKey : setpointType } ; } function getSetpointScaleValueID ( endpoint , setpointType ) { return { commandClass : CommandClasses [ "Thermostat Setpoint" ] , endpoint , property : "setpointScale" , propertyKey : setpointType } ; } @ API ( CommandClasses [ "Thermostat Setpoint" ] ) export class ThermostatSetpointCCAPI extends CCAPI { public supportsCommand ( cmd ) { switch ( cmd ) { case ThermostatSetpointCommand . Get : case ThermostatSetpointCommand . SupportedGet : return this . isSinglecast ( ) ; case ThermostatSetpointCommand . Set : return true ; case ThermostatSetpointCommand . CapabilitiesGet : return this . version >= 3 && this . isSinglecast ( ) ; } return super . supportsCommand ( cmd ) ; } protected [ SET_VALUE ] : SetValueImplementation = async ( { property , propertyKey } , value ) : Promise < void > => { if ( property !== "setpoint" ) { throwUnsupportedProperty ( this . ccId , property ) ; } if ( typeof propertyKey !== "number" ) { throw new ZWaveError ( `${ CommandClasses [ this . ccId ] }: "${ property }" must be further specified by a numeric property key` , ZWaveErrorCodes . Argument_Invalid ) ; } if ( typeof value !== "number" ) { throwWrongValueType ( this . ccId , property , "number" , typeof value ) ; } const preferredScale = this . endpoint . getNodeUnsafe ( ) ! . getValue < number > ( getSetpointScaleValueID ( this . endpoint . index , propertyKey ) ) ; await this . set ( propertyKey , value , preferredScale ?? 0 ) ; if ( this . isSinglecast ( ) ) { this . schedulePoll ( { property , propertyKey } ) ; } } ; protected [ POLL_VALUE ] : PollValueImplementation = async ( { property , propertyKey , } ) : Promise < unknown > => { switch ( property ) { case "setpoint" : if ( typeof propertyKey !== "number" ) { throw new ZWaveError ( `${ CommandClasses [ this . ccId ] }: "${ property }" must be further specified by a numeric property key` , ZWaveErrorCodes . Argument_Invalid ) ; } return ( await this . get ( propertyKey ) ) ?. value ; default : throwUnsupportedProperty ( this . ccId , property ) ; } } ; public async get ( setpointType ) { this . assertSupportsCommand ( ThermostatSetpointCommand , ThermostatSetpointCommand . Get ) ; const cc = new ThermostatSetpointCCGet ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , setpointType } ) ; const response = await this . driver . sendCommand < ThermostatSetpointCCReport > ( cc , this . commandOptions ) ; if ( ! response ) return ; return response . type === ThermostatSetpointType [ "N/A" ] ? undefined : { value : response . value , scale : response . scale } ; } public async set ( setpointType , value , scale ) { this . assertSupportsCommand ( ThermostatSetpointCommand , ThermostatSetpointCommand . Set ) ; const cc = new ThermostatSetpointCCSet ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , setpointType , value , scale } ) ; await this . driver . sendCommand ( cc , this . commandOptions ) ; } public async getCapabilities ( setpointType ) { this . assertSupportsCommand ( ThermostatSetpointCommand , ThermostatSetpointCommand . CapabilitiesGet ) ; const cc = new ThermostatSetpointCCCapabilitiesGet ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , setpointType } ) ; const response = await this . driver . sendCommand < ThermostatSetpointCCCapabilitiesReport > ( cc , this . commandOptions ) ; if ( response ) { return pick ( response , [ "minValue" , "maxValue" , "minValueScale" , "maxValueScale" , ] ) ; } } /**
     * Requests the supported setpoint types from the node. Due to inconsistencies it is NOT recommended
     * to use this method on nodes with CC versions 1 and 2. Instead rely on the information determined
     * during node interview.
     */ public async getSupportedSetpointTypes ( ) { this . assertSupportsCommand ( ThermostatSetpointCommand , ThermostatSetpointCommand . SupportedGet ) ; const cc = new ThermostatSetpointCCSupportedGet ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index } ) ; const response = await this . driver . sendCommand < ThermostatSetpointCCSupportedReport > ( cc , this . commandOptions ) ; return response ?. supportedSetpointTypes ; } } @ commandClass ( CommandClasses [ "Thermostat Setpoint" ] ) @ implementedVersion ( 3 ) export class ThermostatSetpointCC extends CommandClass { declare ccCommand : ThermostatSetpointCommand ; public constructor ( driver , options ) { super ( driver , options ) ; this . registerValue ( getSetpointTypesInterpretationValueID ( 0 ) . property , true ) ; this . registerValue ( getSetpointScaleValueID ( 0 , 0 ) . property , true ) ; } public translatePropertyKey ( property , propertyKey ) { if ( property === "setpoint" ) { return getEnumMemberName ( ThermostatSetpointType , propertyKey as any ) ; } else { return super . translatePropertyKey ( property , propertyKey ) ; } } public async interview ( ) { const node = this . getNode ( ) ! ; const endpoint = this . getEndpoint ( ) ! ; const api = endpoint . commandClasses [ "Thermostat Setpoint" ] . withOptions ( { priority : MessagePriority . NodeQuery } ) ; this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : `Interviewing ${ this . ccName }...` , direction : "none" } ) ; if ( this . version <= 2 ) { let setpointTypes ; let interpretation ; let interpretationChanged = false ; const supportedSetpointTypesValueId = getSupportedSetpointTypesValueID ( this . endpointIndex ) ; this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : "retrieving supported setpoint types..." , direction : "outbound" } ) ; const resp = await api . getSupportedSetpointTypes ( ) ; if ( ! resp ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : "Querying supported setpoint types timed out, skipping interview..." , level : "warn" } ) ; return ; } setpointTypes = [ ... resp ] ; interpretation = undefined ; function switchToInterpretationA ( ) { setpointTypes = setpointTypes . map ( ( i ) => thermostatSetpointTypeMap [ i ] ) ; interpretation = "A" ; interpretationChanged = true ; } if ( [ 3 , 4 , 5 , 6 ] . some ( ( type ) => setpointTypes . includes ( type ) ) ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : "uses Thermostat Setpoint bitmap interpretation A" , direction : "none" } ) ; switchToInterpretationA ( ) ; } else { this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : "Thermostat Setpoint bitmap interpretation is unknown, assuming B for now" , direction : "none" } ) ; } const supportedSetpointTypes = [ ] ; for ( let i = 0 ; i < setpointTypes . length ; i ++ ) { const type = setpointTypes [ i ] ; const setpointName = getEnumMemberName ( ThermostatSetpointType , type ) ; this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : `querying current value of setpoint ${ setpointName }...` , direction : "outbound" } ) ; const setpoint = await api . get ( type ) ; let logMessage ; if ( setpoint ) { supportedSetpointTypes . push ( type ) ; logMessage = `received current value of setpoint ${ setpointName }: ${ setpoint . value } ${ setpoint . scale . unit ?? "" }` ; } else if ( ! interpretation ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : `the setpoint type ${ type } is unsupported, switching to interpretation A` , direction : "none" } ) ; switchToInterpretationA ( ) ; i -- ; continue ; } else { logMessage = `Setpoint ${ setpointName } is not supported` ; } this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : logMessage , direction : "inbound" } ) ; } if ( ! interpretation && ! interpretationChanged ) { interpretation = "B" ; interpretationChanged = true ; } this . getValueDB ( ) . setValue ( supportedSetpointTypesValueId , supportedSetpointTypes ) ; if ( interpretationChanged ) { this . getValueDB ( ) . setValue ( getSetpointTypesInterpretationValueID ( this . endpointIndex ) , interpretation ) ; } } else { let setpointTypes = [ ] ; this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : "retrieving supported setpoint types..." , direction : "outbound" } ) ; const resp = await api . getSupportedSetpointTypes ( ) ; if ( resp ) { setpointTypes = [ ... resp ] ; const logMessage = "received supported setpoint types:\n" + setpointTypes . map ( ( type ) => getEnumMemberName ( ThermostatSetpointType , type ) ) . 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 setpoint types timed out, skipping interview..." , level : "warn" } ) ; return ; } for ( const type of setpointTypes ) { const setpointName = getEnumMemberName ( ThermostatSetpointType , type ) ; this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : `retrieving capabilities for setpoint ${ setpointName }...` , direction : "outbound" } ) ; const setpointCaps = await api . getCapabilities ( type ) ; if ( setpointCaps ) { const minValueUnit = getSetpointUnit ( this . driver . configManager , setpointCaps . minValueScale ) ; const maxValueUnit = getSetpointUnit ( this . driver . configManager , setpointCaps . maxValueScale ) ; const logMessage = `received capabilities for setpoint ${ setpointName }:
minimum value: ${ setpointCaps . minValue } ${ minValueUnit }
maximum value: ${ setpointCaps . maxValue } ${ maxValueUnit }` ; 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 [ "Thermostat Setpoint" ] . withOptions ( { priority : MessagePriority . NodeQuery } ) ; const setpointTypes = this . getValueDB ( ) . getValue ( getSupportedSetpointTypesValueID ( this . endpointIndex ) ) ?? [ ] ; for ( const type of setpointTypes ) { const setpointName = getEnumMemberName ( ThermostatSetpointType , type ) ; this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : `querying current value of setpoint ${ setpointName }...` , direction : "outbound" } ) ; const setpoint = await api . get ( type ) ; if ( setpoint ) { const logMessage = `received current value of setpoint ${ setpointName }: ${ setpoint . value } ${ setpoint . scale . unit ?? "" }` ; this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : logMessage , direction : "inbound" } ) ; } } } } interface ThermostatSetpointCCSetOptions extends CCCommandOptions { setpointType : ThermostatSetpointType ; value : number ; scale : number ; } @ CCCommand ( ThermostatSetpointCommand . Set ) export class ThermostatSetpointCCSet extends ThermostatSetpointCC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { throw new ZWaveError ( `${ this . constructor . name }: deserialization not implemented` , ZWaveErrorCodes . Deserialization_NotImplemented ) ; } else { this . setpointType = options . setpointType ; this . value = options . value ; this . scale = options . scale ; } } public setpointType : ThermostatSetpointType ; public value : number ; public scale : number ; public serialize ( ) { const override = this . getNodeUnsafe ( ) ?. deviceConfig ?. compat ?. overrideFloatEncoding ; this . payload = Buffer . concat ( [ Buffer . from ( [ this . setpointType & 0b1111 ] ) , encodeFloatWithScale ( this . value , this . scale , override ) , ] ) ; return super . serialize ( ) ; } public toLogEntry ( ) { const scale = getScale ( this . driver . configManager , this . scale ) ; return { ... super . toLogEntry ( ) , message : { "setpoint type" : getEnumMemberName ( ThermostatSetpointType , this . setpointType ) , value : `${ this . value } ${ scale . unit }` } } ; } } @ CCCommand ( ThermostatSetpointCommand . Report ) export class ThermostatSetpointCCReport extends ThermostatSetpointCC { public constructor ( driver , options ) { super ( driver , options ) ; validatePayload ( this . payload . length >= 1 ) ; this . _type = this . payload [ 0 ] & 0b1111 ; if ( this . _type === 0 ) { this . _value = 0 ; this . _scale = getScale ( this . driver . configManager , 0 ) ; return ; } const { value , scale } = parseFloatWithScale ( this . payload . slice ( 1 ) ) ; this . _value = value ; this . _scale = getScale ( this . driver . configManager , scale ) ; this . persistValues ( ) ; } public persistValues ( ) { const valueDB = this . getValueDB ( ) ; const setpointValueId = getSetpointValueID ( this . endpointIndex , this . _type ) ; if ( ( valueDB . getMetadata ( setpointValueId ) as ValueMetadataNumeric | undefined ) ?. unit !== this . _scale . unit ) { valueDB . setMetadata ( setpointValueId , { ... ValueMetadata . Number , unit : this . _scale . unit , ccSpecific : { setpointType : this . _type } } ) ; } valueDB . setValue ( setpointValueId , this . _value ) ; const scaleValueId = getSetpointScaleValueID ( this . endpointIndex , this . _type ) ; valueDB . setValue ( scaleValueId , this . _scale . key ) ; return true ; } private _type : ThermostatSetpointType ; public get type ( ) : ThermostatSetpointType { return this . _type ; } private _scale : Scale ; public get scale ( ) : Scale { return this . _scale ; } private _value : number ; public get value ( ) : number { return this . _value ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { "setpoint type" : getEnumMemberName ( ThermostatSetpointType , this . type ) , value : `${ this . value } ${ this . scale . unit }` } } ; } } function testResponseForThermostatSetpointGet ( sent , received ) { return received . type === sent . setpointType ; } interface ThermostatSetpointCCGetOptions extends CCCommandOptions { setpointType : ThermostatSetpointType ; } @ CCCommand ( ThermostatSetpointCommand . Get ) @ expectedCCResponse ( ThermostatSetpointCCReport , testResponseForThermostatSetpointGet ) export class ThermostatSetpointCCGet extends ThermostatSetpointCC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { throw new ZWaveError ( `${ this . constructor . name }: deserialization not implemented` , ZWaveErrorCodes . Deserialization_NotImplemented ) ; } else { this . setpointType = options . setpointType ; } } public setpointType : ThermostatSetpointType ; public serialize ( ) { this . payload = Buffer . from ( [ this . setpointType & 0b1111 ] ) ; return super . serialize ( ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { "setpoint type" : getEnumMemberName ( ThermostatSetpointType , this . setpointType ) } } ; } } @ CCCommand ( ThermostatSetpointCommand . CapabilitiesReport ) export class ThermostatSetpointCCCapabilitiesReport extends ThermostatSetpointCC { public constructor ( driver , options ) { super ( driver , options ) ; validatePayload ( this . payload . length >= 1 ) ; this . _type = this . payload [ 0 ] ; let bytesRead ; ( { value : this . _minValue , scale : this . _minValueScale , bytesRead } = parseFloatWithScale ( this . payload . slice ( 1 ) ) ) ; ( { value : this . _maxValue , scale : this . _maxValueScale } = parseFloatWithScale ( this . payload . slice ( 1 + bytesRead ) ) ) ; const valueId = getSetpointValueID ( this . endpointIndex , this . _type ) ; this . getValueDB ( ) . setMetadata ( valueId , { ... ValueMetadata . Number , min : this . _minValue , max : this . _maxValue , unit : getSetpointUnit ( this . driver . configManager , this . _minValueScale ) || getSetpointUnit ( this . driver . configManager , this . _maxValueScale ) , ccSpecific : { setpointType : this . _type } } ) ; this . persistValues ( ) ; } private _type : ThermostatSetpointType ; public get type ( ) : ThermostatSetpointType { return this . _type ; } private _minValue : number ; public get minValue ( ) : number { return this . _minValue ; } private _maxValue : number ; public get maxValue ( ) : number { return this . _maxValue ; } private _minValueScale : number ; public get minValueScale ( ) : number { return this . _minValueScale ; } private _maxValueScale : number ; public get maxValueScale ( ) : number { return this . _maxValueScale ; } public toLogEntry ( ) { const minValueScale = getScale ( this . driver . configManager , this . minValueScale ) ; const maxValueScale = getScale ( this . driver . configManager , this . maxValueScale ) ; return { ... super . toLogEntry ( ) , message : { "setpoint type" : getEnumMemberName ( ThermostatSetpointType , this . _type ) , "min value" : `${ this . minValue } ${ minValueScale . unit }` , "max value" : `${ this . maxValue } ${ maxValueScale . unit }` } } ; } } interface ThermostatSetpointCCCapabilitiesGetOptions extends CCCommandOptions { setpointType : ThermostatSetpointType ; } @ CCCommand ( ThermostatSetpointCommand . CapabilitiesGet ) @ expectedCCResponse ( ThermostatSetpointCCCapabilitiesReport ) export class ThermostatSetpointCCCapabilitiesGet extends ThermostatSetpointCC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { throw new ZWaveError ( `${ this . constructor . name }: deserialization not implemented` , ZWaveErrorCodes . Deserialization_NotImplemented ) ; } else { this . setpointType = options . setpointType ; } } public setpointType : ThermostatSetpointType ; public serialize ( ) { this . payload = Buffer . from ( [ this . setpointType & 0b1111 ] ) ; return super . serialize ( ) ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { "setpoint type" : getEnumMemberName ( ThermostatSetpointType , this . setpointType ) } } ; } } @ CCCommand ( ThermostatSetpointCommand . SupportedReport ) export class ThermostatSetpointCCSupportedReport extends ThermostatSetpointCC { public constructor ( driver , options ) { super ( driver , options ) ; validatePayload ( this . payload . length >= 1 ) ; const bitMask = this . payload ; const supported = parseBitMask ( bitMask , ThermostatSetpointType [ "N/A" ] ) ; if ( this . version >= 3 ) { this . _supportedSetpointTypes = supported . map ( ( i ) => thermostatSetpointTypeMap [ i ] ) ; } else { this . _supportedSetpointTypes = supported ; } this . persistValues ( ) ; } private _supportedSetpointTypes : ThermostatSetpointType [ ] ; @ ccValue ( { internal : true } ) public get supportedSetpointTypes ( ) : readonly ThermostatSetpointType [ ] { return this . _supportedSetpointTypes ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { "supported setpoint types" : this . supportedSetpointTypes . map ( ( t ) => `\n· ${ getEnumMemberName ( ThermostatSetpointType , t ) }` ) . join ( "" ) } } ; } } @ CCCommand ( ThermostatSetpointCommand . SupportedGet ) @ expectedCCResponse ( ThermostatSetpointCCSupportedReport ) export class ThermostatSetpointCCSupportedGet extends ThermostatSetpointCC { }