import type { MessageRecord , ValueID } from "@zwave-js/core" ; import { CommandClasses , Duration , Maybe , MessageOrCCLogEntry , parseMaybeNumber , parseNumber , 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 { VirtualEndpoint } from "../node/VirtualEndpoint" ; import { CCAPI , PollValueImplementation , POLL_VALUE , SetValueImplementation , SET_VALUE , throwUnsupportedProperty , throwWrongValueType , } from "./API" ; import { API , CCCommand , CCCommandOptions , ccValue , ccValueMetadata , CommandClass , commandClass , CommandClassDeserializationOptions , CommandClassOptions , expectedCCResponse , gotDeserializationOptions , implementedVersion , } from "./CommandClass" ; import { SupervisionStatus } from "./SupervisionCC" ; export enum MultilevelSwitchCommand { Set = 0x01 , Get = 0x02 , Report = 0x03 , StartLevelChange = 0x04 , StopLevelChange = 0x05 , SupportedGet = 0x06 , SupportedReport = 0x07 } publicAPI export enum LevelChangeDirection { "up" = 0b0 , "down" = 0b1 } publicAPI export enum SwitchType { "not supported" = 0x00 , "Off/On" = 0x01 , "Down/Up" = 0x02 , "Close/Open" = 0x03 , "CCW/CW" = 0x04 , "Left/Right" = 0x05 , "Reverse/Forward" = 0x06 , "Pull/Push" = 0x07 } /**
 * Translates a switch type into two actions that may be performed. Unknown types default to Down/Up
 */ function switchTypeToActions ( switchType ) { if ( ! switchType . includes ( "/" ) ) switchType = SwitchType [ 0x02 ] ; return switchType . split ( "/" , 2 ) as any ; } const switchTypeProperties = Object . keys ( SwitchType ) . filter ( ( key ) => key . indexOf ( "/" ) > - 1 ) . map ( ( key ) => switchTypeToActions ( key ) ) . reduce < string [ ] > ( ( acc , cur ) => acc . concat ( ... cur ) , [ ] ) ; publicAPI export type MultilevelSwitchLevelChangeMetadata = ValueMetadata & { ccSpecific : { switchType : SwitchType ; } ; } ; function getCurrentValueValueId ( endpoint ) { return { commandClass : CommandClasses [ "Multilevel Switch" ] , endpoint , property : "currentValue" } ; } /** Returns the ValueID used to remember whether a node supports supervision on the start/stop level change commands*/ function getSuperviseStartStopLevelChangeValueId ( ) { return { commandClass : CommandClasses [ "Multilevel Switch" ] , property : "superviseStartStopLevelChange" } ; } @ API ( CommandClasses [ "Multilevel Switch" ] ) export class MultilevelSwitchCCAPI extends CCAPI { public supportsCommand ( cmd ) { switch ( cmd ) { case MultilevelSwitchCommand . Get : return this . isSinglecast ( ) ; case MultilevelSwitchCommand . Set : case MultilevelSwitchCommand . StartLevelChange : case MultilevelSwitchCommand . StopLevelChange : return true ; case MultilevelSwitchCommand . SupportedGet : return this . version >= 3 && this . isSinglecast ( ) ; } return super . supportsCommand ( cmd ) ; } public async get ( ) { this . assertSupportsCommand ( MultilevelSwitchCommand , MultilevelSwitchCommand . Get ) ; const cc = new MultilevelSwitchCCGet ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index } ) ; const response = await this . driver . sendCommand < MultilevelSwitchCCReport > ( cc , this . commandOptions ) ; if ( response ) { return pick ( response , [ "currentValue" , "targetValue" , "duration" ] ) ; } } param targetValue param duration returns public async set ( targetValue , duration ? ) { this . assertSupportsCommand ( MultilevelSwitchCommand , MultilevelSwitchCommand . Set ) ; const cc = new MultilevelSwitchCCSet ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , targetValue , duration : Duration . from ( duration ) } ) ; const supervisionResult = await this . driver . trySendCommandSupervised ( cc , { requestStatusUpdates : true , onUpdate : ( status ) => { if ( status === SupervisionStatus . Working || status === SupervisionStatus . Success ) { void this . get ( ) . catch ( ( ) => { } ) ; } } } ) ; return ( ! supervisionResult || supervisionResult . status === SupervisionStatus . Success ) ; } public async startLevelChange ( options ) { this . assertSupportsCommand ( MultilevelSwitchCommand , MultilevelSwitchCommand . StartLevelChange ) ; const cc = new MultilevelSwitchCCStartLevelChange ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index , ... options } ) ; let mayUseSupervision ; if ( this . endpoint instanceof VirtualEndpoint ) { mayUseSupervision = false ; } else { const superviseValueId = getSuperviseStartStopLevelChangeValueId ( ) ; const node = this . endpoint . getNodeUnsafe ( ) ! ; mayUseSupervision = node . getValue ( superviseValueId ) !== false ; if ( mayUseSupervision ) { const supervisionResult = await this . driver . trySendCommandSupervised ( cc ) ; if ( supervisionResult ?. status === SupervisionStatus . Fail ) { throw new ZWaveError ( "startLevelChange failed" , ZWaveErrorCodes . SupervisionCC_CommandFailed ) ; } else if ( supervisionResult ?. status === SupervisionStatus . NoSupport ) { node . valueDB . setValue ( superviseValueId , false ) ; mayUseSupervision = false ; } } } if ( ! mayUseSupervision ) { await this . driver . sendCommand ( cc ) ; } } public async stopLevelChange ( ) { this . assertSupportsCommand ( MultilevelSwitchCommand , MultilevelSwitchCommand . StopLevelChange ) ; const cc = new MultilevelSwitchCCStopLevelChange ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index } ) ; let mayUseSupervision ; if ( this . endpoint instanceof VirtualEndpoint ) { mayUseSupervision = false ; } else { const superviseValueId = getSuperviseStartStopLevelChangeValueId ( ) ; const node = this . endpoint . getNodeUnsafe ( ) ! ; mayUseSupervision = node . getValue ( superviseValueId ) !== false ; if ( mayUseSupervision ) { const supervisionResult = await this . driver . trySendCommandSupervised ( cc ) ; if ( supervisionResult ?. status === SupervisionStatus . Fail ) { throw new ZWaveError ( "stopLevelChange failed" , ZWaveErrorCodes . SupervisionCC_CommandFailed ) ; } else if ( supervisionResult ?. status === SupervisionStatus . NoSupport ) { node . valueDB . setValue ( superviseValueId , false ) ; mayUseSupervision = false ; } } } if ( ! mayUseSupervision ) { await this . driver . sendCommand ( cc ) ; } } public async getSupported ( ) { this . assertSupportsCommand ( MultilevelSwitchCommand , MultilevelSwitchCommand . SupportedGet ) ; const cc = new MultilevelSwitchCCSupportedGet ( this . driver , { nodeId : this . endpoint . nodeId , endpoint : this . endpoint . index } ) ; const response = await this . driver . sendCommand < MultilevelSwitchCCSupportedReport > ( cc , this . commandOptions ) ; return response ?. switchType ; } protected [ SET_VALUE ] : SetValueImplementation = async ( { property } , value , options ) : Promise < void > => { if ( property === "targetValue" ) { if ( typeof value !== "number" ) { throwWrongValueType ( this . ccId , property , "number" , typeof value ) ; } const duration = Duration . from ( options ?. transitionDuration ) ; const completed = await this . set ( value , duration ) ; if ( completed ) { if ( this . isSinglecast ( ) ) { if ( ! this . driver . options . disableOptimisticValueUpdate && value >= 0 && value <= 99 ) { const valueDB = this . endpoint . getNodeUnsafe ( ) ?. valueDB ; valueDB ?. setValue ( getCurrentValueValueId ( this . endpoint . index ) , value ) ; } if ( value === 255 || ! this . endpoint . getNodeUnsafe ( ) ?. supportsCC ( CommandClasses . Supervision ) ) { if ( property === "targetValue" ) property = "currentValue" ; this . schedulePoll ( { property } , { duration } ) ; } } else if ( this . isMulticast ( ) ) { if ( ! this . driver . options . disableOptimisticValueUpdate && value >= 0 && value <= 99 ) { const affectedNodes = this . endpoint . node . physicalNodes . filter ( ( node ) => node . getEndpoint ( this . endpoint . index ) ?. supportsCC ( this . ccId ) ) ; for ( const node of affectedNodes ) { node . valueDB ?. setValue ( getCurrentValueValueId ( this . endpoint . index ) , value ) ; } } else if ( value === 255 ) { if ( property === "targetValue" ) property = "currentValue" ; this . schedulePoll ( { property } , { duration } ) ; } } } } else if ( switchTypeProperties . includes ( property as string ) ) { if ( typeof value !== "boolean" ) { throwWrongValueType ( this . ccId , property , "number" , typeof value ) ; } if ( value ) { const direction = switchTypeProperties . indexOf ( property as string ) % 2 === 0 ? "down" : "up" ; const startLevel = this . endpoint instanceof VirtualEndpoint ? undefined : this . endpoint . getNodeUnsafe ( ) ?. getValue < number > ( getCurrentValueValueId ( this . endpoint . index ) ) ; const duration = Duration . from ( options ?. transitionDuration ) ; await this . startLevelChange ( { direction , ignoreStartLevel : true , startLevel , duration } ) ; } else { await this . stopLevelChange ( ) ; } } else { throwUnsupportedProperty ( this . ccId , property ) ; } } ; protected [ POLL_VALUE ] : PollValueImplementation = async ( { property , } ) : Promise < unknown > => { switch ( property ) { case "currentValue" : case "targetValue" : case "duration" : return ( await this . get ( ) ) ?. [ property ] ; default : throwUnsupportedProperty ( this . ccId , property ) ; } } ; } @ commandClass ( CommandClasses [ "Multilevel Switch" ] ) @ implementedVersion ( 4 ) export class MultilevelSwitchCC extends CommandClass { declare ccCommand : MultilevelSwitchCommand ; public constructor ( driver , options ) { super ( driver , options ) ; this . registerValue ( getSuperviseStartStopLevelChangeValueId ( ) . property , true ) ; } public async interview ( ) { const node = this . getNode ( ) ! ; const endpoint = this . getEndpoint ( ) ! ; const api = endpoint . commandClasses [ "Multilevel Switch" ] . withOptions ( { priority : MessagePriority . NodeQuery } ) ; this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : `Interviewing ${ this . ccName }...` , direction : "none" } ) ; if ( this . version >= 3 ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : "requesting switch type..." , direction : "outbound" } ) ; const switchType = await api . getSupported ( ) ; if ( switchType != undefined ) { this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : `has switch type ${ getEnumMemberName ( SwitchType , switchType ) }` , direction : "inbound" } ) ; } } else { this . createMetadataForLevelChangeActions ( ) ; } await this . refreshValues ( ) ; this . interviewComplete = true ; } public async refreshValues ( ) { const node = this . getNode ( ) ! ; const endpoint = this . getEndpoint ( ) ! ; const api = endpoint . commandClasses [ "Multilevel Switch" ] . withOptions ( { priority : MessagePriority . NodeQuery } ) ; this . driver . controllerLog . logNode ( node . id , { endpoint : this . endpointIndex , message : "requesting current switch state..." , direction : "outbound" } ) ; await api . get ( ) ; } public setMappedBasicValue ( value ) { this . getValueDB ( ) . setValue ( { commandClass : this . ccId , endpoint : this . endpointIndex , property : "currentValue" } , value ) ; return true ; } protected createMetadataForLevelChangeActions ( switchType = SwitchType [ "Down/Up" ] ) { const valueDb = this . getValueDB ( ) ; const switchTypeName = getEnumMemberName ( SwitchType , switchType ) ; const [ down , up ] = switchTypeToActions ( switchTypeName ) ; const upValueId = { commandClass : this . ccId , endpoint : this . endpointIndex , property : up } ; const downValueId = { commandClass : this . ccId , endpoint : this . endpointIndex , property : down } ; if ( ! valueDb . hasMetadata ( upValueId ) ) { this . getValueDB ( ) . setMetadata ( upValueId , { ... ValueMetadata . Boolean , label : `Perform a level change (${ up })` , ccSpecific : { switchType } } ) ; } if ( ! valueDb . hasMetadata ( downValueId ) ) { this . getValueDB ( ) . setMetadata ( downValueId , { ... ValueMetadata . Boolean , label : `Perform a level change (${ down })` , ccSpecific : { switchType } } ) ; } } } interface MultilevelSwitchCCSetOptions extends CCCommandOptions { targetValue : number ; duration ? : Duration ; } @ CCCommand ( MultilevelSwitchCommand . Set ) export class MultilevelSwitchCCSet extends MultilevelSwitchCC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { throw new ZWaveError ( `${ this . constructor . name }: deserialization not implemented` , ZWaveErrorCodes . Deserialization_NotImplemented ) ; } else { this . targetValue = options . targetValue ; this . duration = options . duration ; } } public targetValue : number ; public duration : Duration | undefined ; public serialize ( ) { const payload = [ this . targetValue ] ; if ( this . version >= 2 && this . duration ) { payload . push ( this . duration . serializeSet ( ) ) ; } this . payload = Buffer . from ( payload ) ; return super . serialize ( ) ; } public toLogEntry ( ) { const message = { "target value" : this . targetValue } ; if ( this . duration ) { message . duration = this . duration . toString ( ) ; } return { ... super . toLogEntry ( ) , message } ; } } @ CCCommand ( MultilevelSwitchCommand . Report ) export class MultilevelSwitchCCReport extends MultilevelSwitchCC { public constructor ( driver , options ) { super ( driver , options ) ; validatePayload ( this . payload . length >= 1 ) ; this . currentValue = parseMaybeNumber ( this . payload [ 0 ] , driver . options . preserveUnknownValues ) ; if ( this . version >= 4 && this . payload . length >= 3 ) { this . targetValue = parseNumber ( this . payload [ 1 ] ) ; this . duration = Duration . parseReport ( this . payload [ 2 ] ) ; } this . persistValues ( ) ; } @ ccValue ( { forceCreation : true } ) @ ccValueMetadata ( { ... ValueMetadata . Level , label : "Target value" , valueChangeOptions : [ "transitionDuration" ] } ) public readonly targetValue : number | undefined ; @ ccValue ( ) @ ccValueMetadata ( { ... ValueMetadata . Duration , label : "Transition duration" } ) public readonly duration : Duration | undefined ; @ ccValue ( ) @ ccValueMetadata ( { ... ValueMetadata . ReadOnlyLevel , label : "Current value" } ) public readonly currentValue : Maybe < number > | undefined ; public toLogEntry ( ) { const message = { "current value" : this . currentValue } ; if ( this . targetValue != undefined && this . duration ) { message [ "target value" ] = this . targetValue ; message . duration = this . duration . toString ( ) ; } return { ... super . toLogEntry ( ) , message } ; } } @ CCCommand ( MultilevelSwitchCommand . Get ) @ expectedCCResponse ( MultilevelSwitchCCReport ) export class MultilevelSwitchCCGet extends MultilevelSwitchCC { } type MultilevelSwitchCCStartLevelChangeOptions = { direction : keyof typeof LevelChangeDirection ; } & ( { ignoreStartLevel : true ; startLevel ? : number ; } | { ignoreStartLevel : false ; startLevel : number ; } ) & { duration ? : Duration ; } ; @ CCCommand ( MultilevelSwitchCommand . StartLevelChange ) export class MultilevelSwitchCCStartLevelChange extends MultilevelSwitchCC { public constructor ( driver , options ) { super ( driver , options ) ; if ( gotDeserializationOptions ( options ) ) { throw new ZWaveError ( `${ this . constructor . name }: deserialization not implemented` , ZWaveErrorCodes . Deserialization_NotImplemented ) ; } else { this . duration = options . duration ; this . ignoreStartLevel = options . ignoreStartLevel ; this . startLevel = options . startLevel ?? 0 ; this . direction = options . direction ; } } public duration : Duration | undefined ; public startLevel : number ; public ignoreStartLevel : boolean ; public direction : keyof typeof LevelChangeDirection ; public serialize ( ) { const controlByte = ( LevelChangeDirection [ this . direction ] << 6 ) | ( this . ignoreStartLevel ? 32 : 0 ) ; const payload = [ controlByte , this . startLevel ] ; if ( this . version >= 2 && this . duration ) { payload . push ( this . duration . serializeSet ( ) ) ; } this . payload = Buffer . from ( payload ) ; return super . serialize ( ) ; } public toLogEntry ( ) { const message = { startLevel : `${ this . startLevel }${ this . ignoreStartLevel ? " (ignored)" : "" }` , direction : this . direction } ; if ( this . duration ) { message . duration = this . duration . toString ( ) ; } return { ... super . toLogEntry ( ) , message } ; } } @ CCCommand ( MultilevelSwitchCommand . StopLevelChange ) export class MultilevelSwitchCCStopLevelChange extends MultilevelSwitchCC { } @ CCCommand ( MultilevelSwitchCommand . SupportedReport ) export class MultilevelSwitchCCSupportedReport extends MultilevelSwitchCC { public constructor ( driver , options ) { super ( driver , options ) ; validatePayload ( this . payload . length >= 2 ) ; this . _switchType = this . payload [ 0 ] & 0b11111 ; this . persistValues ( ) ; } private _switchType : SwitchType ; @ ccValue ( { internal : true } ) public get switchType ( ) : SwitchType { return this . _switchType ; } public persistValues ( ) { if ( ! super . persistValues ( ) ) return false ; this . createMetadataForLevelChangeActions ( this . _switchType ) ; return true ; } public toLogEntry ( ) { return { ... super . toLogEntry ( ) , message : { "switch type" : getEnumMemberName ( SwitchType , this . switchType ) } } ; } } @ CCCommand ( MultilevelSwitchCommand . SupportedGet ) @ expectedCCResponse ( MultilevelSwitchCCSupportedReport ) export class MultilevelSwitchCCSupportedGet extends MultilevelSwitchCC { }