2021-03-29 12:51:48 +02:00
const { EventEmitter } = require ( 'events' ) ;
2021-02-20 07:55:26 +01:00
const Configuration = require ( './Configuration' ) ;
2025-09-12 08:36:02 +02:00
const { COMMANDS } = require ( './constants' ) ;
2021-02-20 07:55:26 +01:00
const Logger = require ( './Logger' ) ;
const PlejBLEHandler = require ( './PlejdBLEHandler' ) ;
const logger = Logger . getLogger ( 'device-comm' ) ;
const MAX _TRANSITION _STEPS _PER _SECOND = 5 ; // Could be made a setting
2021-02-22 09:50:06 +01:00
const MAX _RETRY _COUNT = 10 ; // Could be made a setting
2021-02-20 07:55:26 +01:00
class PlejdDeviceCommunication extends EventEmitter {
2021-02-22 09:50:06 +01:00
bleConnected ;
2021-03-31 20:04:45 +02:00
bleOutputTransitionTimers = { } ;
2021-02-20 07:55:26 +01:00
plejdBleHandler ;
config ;
2021-03-29 12:51:48 +02:00
/** @type {import('./DeviceRegistry')} */
2021-02-20 07:55:26 +01:00
deviceRegistry ;
2021-03-31 20:04:45 +02:00
// eslint-disable-next-line max-len
2025-09-12 08:36:02 +02:00
/** @type {{uniqueOutputId: string, command: {command: keyof typeof COMMANDS, brightness: number?, color_temp: number? }, shouldRetry: boolean, retryCount?: number}[]} */
2021-02-20 07:55:26 +01:00
writeQueue = [ ] ;
writeQueueRef = null ;
static EVENTS = {
sceneTriggered : 'sceneTriggered' ,
stateChanged : 'stateChanged' ,
} ;
constructor ( deviceRegistry ) {
super ( ) ;
logger . info ( 'Starting Plejd communication handler.' ) ;
this . plejdBleHandler = new PlejBLEHandler ( deviceRegistry ) ;
this . config = Configuration . getOptions ( ) ;
this . deviceRegistry = deviceRegistry ;
2021-02-20 15:33:06 +01:00
}
2021-02-20 07:55:26 +01:00
2021-02-20 15:33:06 +01:00
cleanup ( ) {
2021-03-31 20:04:45 +02:00
Object . values ( this . bleOutputTransitionTimers ) . forEach ( ( t ) => clearTimeout ( t ) ) ;
2021-02-20 15:33:06 +01:00
this . plejdBleHandler . cleanup ( ) ;
this . plejdBleHandler . removeAllListeners ( PlejBLEHandler . EVENTS . commandReceived ) ;
this . plejdBleHandler . removeAllListeners ( PlejBLEHandler . EVENTS . connected ) ;
this . plejdBleHandler . removeAllListeners ( PlejBLEHandler . EVENTS . reconnecting ) ;
2021-02-20 07:55:26 +01:00
}
async init ( ) {
try {
2021-02-22 09:50:06 +01:00
this . cleanup ( ) ;
this . bleConnected = false ;
2021-02-20 15:33:06 +01:00
// eslint-disable-next-line max-len
2021-03-31 20:04:45 +02:00
this . plejdBleHandler . on (
PlejBLEHandler . EVENTS . commandReceived ,
( uniqueOutputId , command , data ) => this . _bleCommandReceived ( uniqueOutputId , command , data ) ,
) ;
2021-02-20 15:33:06 +01:00
this . plejdBleHandler . on ( PlejBLEHandler . EVENTS . connected , ( ) => {
logger . info ( 'Bluetooth connected. Plejd BLE up and running!' ) ;
2021-02-22 09:50:06 +01:00
logger . verbose ( ` Starting writeQueue loop. Write queue length: ${ this . writeQueue . length } ` ) ;
this . bleConnected = true ;
this . _startWriteQueue ( ) ;
2021-02-20 15:33:06 +01:00
} ) ;
this . plejdBleHandler . on ( PlejBLEHandler . EVENTS . reconnecting , ( ) => {
logger . info ( 'Bluetooth reconnecting...' ) ;
2021-02-27 09:57:29 +01:00
logger . verbose (
` Stopping writeQueue loop until connection is established. Write queue length: ${ this . writeQueue . length } ` ,
) ;
2021-02-22 09:50:06 +01:00
this . bleConnected = false ;
2021-02-20 15:33:06 +01:00
clearTimeout ( this . writeQueueRef ) ;
} ) ;
2021-02-20 07:55:26 +01:00
await this . plejdBleHandler . init ( ) ;
} catch ( err ) {
logger . error ( 'Failed init() of BLE. Starting reconnect loop.' ) ;
await this . plejdBleHandler . startReconnectPeriodicallyLoop ( ) ;
}
}
2021-03-31 20:04:45 +02:00
turnOn ( uniqueOutputId , command ) {
const deviceName = this . deviceRegistry . getOutputDeviceName ( uniqueOutputId ) ;
2021-02-20 07:55:26 +01:00
logger . info (
2025-09-12 08:36:02 +02:00
` Plejd got turn on command for ${ deviceName } ( ${ uniqueOutputId } ) ${ JSON . stringify ( command ) } ` ,
2021-02-20 07:55:26 +01:00
) ;
2025-09-12 08:36:02 +02:00
this . _transitionTo ( uniqueOutputId , command , deviceName ) ;
2021-02-20 07:55:26 +01:00
}
2021-03-31 20:04:45 +02:00
turnOff ( uniqueOutputId , command ) {
const deviceName = this . deviceRegistry . getOutputDeviceName ( uniqueOutputId ) ;
2021-02-20 07:55:26 +01:00
logger . info (
2021-03-31 20:04:45 +02:00
` Plejd got turn off command for ${ deviceName } ( ${ uniqueOutputId } ) ${
2021-02-20 07:55:26 +01:00
command . transition ? ` , transition: ${ command . transition } ` : ''
} ` ,
) ;
2025-09-12 08:36:02 +02:00
this . _transitionTo ( uniqueOutputId , { ... command , brightness : 0 } , deviceName ) ;
2021-02-20 07:55:26 +01:00
}
2021-03-31 20:04:45 +02:00
_bleCommandReceived ( uniqueOutputId , command , data ) {
2021-02-20 07:55:26 +01:00
try {
if ( command === COMMANDS . DIM ) {
2025-09-12 08:36:02 +02:00
if ( data . dim === 0 && data . state === 1 ) {
data . dim = 1 ; // Transform BLE brightness value 0 to 1, which is the minimum MQTT brightness value
}
2021-03-31 20:04:45 +02:00
this . deviceRegistry . setOutputState ( uniqueOutputId , data . state , data . dim ) ;
this . emit ( PlejdDeviceCommunication . EVENTS . stateChanged , uniqueOutputId , {
2021-03-29 12:51:48 +02:00
state : ! ! data . state ,
2021-02-20 07:55:26 +01:00
brightness : data . dim ,
} ) ;
2025-09-12 08:36:02 +02:00
} else if ( command === COMMANDS . COLOR ) {
this . deviceRegistry . setOutputState ( uniqueOutputId , data . state , null , data . color ) ;
logger . verbose ( ` Set color state to ${ data . color } . Emitting EVENTS.stateChanged ` ) ;
this . emit ( PlejdDeviceCommunication . EVENTS . stateChanged , uniqueOutputId , {
state : ! ! data . state ,
color : data . color ,
} ) ;
2021-02-20 07:55:26 +01:00
} else if ( command === COMMANDS . TURN _ON ) {
2021-03-31 20:04:45 +02:00
this . deviceRegistry . setOutputState ( uniqueOutputId , true ) ;
this . emit ( PlejdDeviceCommunication . EVENTS . stateChanged , uniqueOutputId , {
2021-02-20 10:49:00 +01:00
state : 1 ,
} ) ;
} else if ( command === COMMANDS . TURN _OFF ) {
2021-03-31 20:04:45 +02:00
this . deviceRegistry . setOutputState ( uniqueOutputId , false ) ;
this . emit ( PlejdDeviceCommunication . EVENTS . stateChanged , uniqueOutputId , {
2021-02-20 10:49:00 +01:00
state : 0 ,
2021-02-20 07:55:26 +01:00
} ) ;
} else if ( command === COMMANDS . TRIGGER _SCENE ) {
2021-02-22 09:50:06 +01:00
this . emit ( PlejdDeviceCommunication . EVENTS . sceneTriggered , data . sceneId ) ;
2021-05-01 19:41:29 +02:00
} else if ( command === COMMANDS . BUTTON _CLICK ) {
2021-05-06 07:58:06 +02:00
this . emit ( PlejdDeviceCommunication . EVENTS . buttonPressed , data . deviceId , data . deviceInput ) ;
2021-02-20 07:55:26 +01:00
} else {
logger . warn ( ` Unknown ble command ${ command } ` ) ;
}
} catch ( error ) {
logger . error ( 'Error processing ble command' , error ) ;
}
}
2021-03-31 20:04:45 +02:00
_clearDeviceTransitionTimer ( uniqueOutputId ) {
if ( this . bleOutputTransitionTimers [ uniqueOutputId ] ) {
clearInterval ( this . bleOutputTransitionTimers [ uniqueOutputId ] ) ;
2021-02-20 07:55:26 +01:00
}
}
2025-09-12 08:36:02 +02:00
/ * *
* @ param { string } uniqueOutputId
* @ param { { transition : number , brightness : number , color _temp : number ? } } command
* @ param { string } deviceName
* /
_transitionTo ( uniqueOutputId , command , deviceName ) {
2021-03-31 20:04:45 +02:00
const device = this . deviceRegistry . getOutputDevice ( uniqueOutputId ) ;
2021-02-20 15:33:06 +01:00
const initialBrightness = device ? device . state && device . dim : null ;
2021-03-31 20:04:45 +02:00
this . _clearDeviceTransitionTimer ( uniqueOutputId ) ;
2021-02-20 07:55:26 +01:00
2021-03-31 20:04:45 +02:00
const isDimmable = this . deviceRegistry . getOutputDevice ( uniqueOutputId ) . dimmable ;
2021-02-20 07:55:26 +01:00
if (
2025-09-12 08:36:02 +02:00
command . transition > 1 &&
2023-08-16 15:32:53 +02:00
isDimmable &&
( initialBrightness || initialBrightness === 0 ) &&
2025-09-12 08:36:02 +02:00
( command . brightness || command . brightness === 0 ) &&
command . brightness !== initialBrightness
2021-02-20 07:55:26 +01:00
) {
// Transition time set, known initial and target brightness
// Calculate transition interval time based on delta brightness and max steps per second
// During transition, measure actual transition interval time and adjust stepping continously
// If transition <= 1 second, Plejd will do a better job
// than we can in transitioning so transitioning will be skipped
2025-09-12 08:36:02 +02:00
const deltaBrightness = command . brightness - initialBrightness ;
2021-02-20 07:55:26 +01:00
const transitionSteps = Math . min (
Math . abs ( deltaBrightness ) ,
2025-09-12 08:36:02 +02:00
MAX _TRANSITION _STEPS _PER _SECOND * command . transition ,
2021-02-20 07:55:26 +01:00
) ;
2025-09-12 08:36:02 +02:00
const transitionInterval = ( command . transition * 1000 ) / transitionSteps ;
2021-02-20 07:55:26 +01:00
logger . debug (
2025-09-12 08:36:02 +02:00
` transitioning from ${ initialBrightness } to ${ command . brightness } ${
command . transition ? ` in ${ command . transition } seconds ` : ''
2021-02-20 07:55:26 +01:00
} . ` ,
) ;
logger . verbose (
` delta brightness ${ deltaBrightness } , steps ${ transitionSteps } , interval ${ transitionInterval } ms ` ,
) ;
const dtStart = new Date ( ) ;
let nSteps = 0 ;
2021-03-31 20:04:45 +02:00
this . bleOutputTransitionTimers [ uniqueOutputId ] = setInterval ( ( ) => {
2021-02-20 07:55:26 +01:00
const tElapsedMs = new Date ( ) . getTime ( ) - dtStart . getTime ( ) ;
let tElapsed = tElapsedMs / 1000 ;
2025-09-12 08:36:02 +02:00
if ( tElapsed > command . transition || tElapsed < 0 ) {
tElapsed = command . transition ;
2021-02-20 07:55:26 +01:00
}
let newBrightness = Math . round (
2025-09-12 08:36:02 +02:00
initialBrightness + ( deltaBrightness * tElapsed ) / command . transition ,
2021-02-20 07:55:26 +01:00
) ;
2025-09-12 08:36:02 +02:00
if ( tElapsed === command . transition ) {
2021-02-20 07:55:26 +01:00
nSteps ++ ;
2021-03-31 20:04:45 +02:00
this . _clearDeviceTransitionTimer ( uniqueOutputId ) ;
2025-09-12 08:36:02 +02:00
newBrightness = command . brightness ;
2021-02-20 07:55:26 +01:00
logger . debug (
2025-09-12 08:36:02 +02:00
` Queueing finalize ${ deviceName } ( ${ uniqueOutputId } ) transition from ${ initialBrightness } to ${
command . brightness
} in $ { tElapsedMs } ms . Done steps $ { nSteps } . Average interval $ {
2021-02-20 07:55:26 +01:00
tElapsedMs / ( nSteps || 1 )
} ms . ` ,
) ;
2025-09-12 08:36:02 +02:00
this . _setLightState (
uniqueOutputId ,
{ ... command , brightness : newBrightness } ,
true ,
deviceName ,
) ;
2021-02-20 07:55:26 +01:00
} else {
nSteps ++ ;
logger . verbose (
2021-03-31 20:04:45 +02:00
` Queueing dim transition for ${ deviceName } ( ${ uniqueOutputId } ) to ${ newBrightness } . Total queue length ${ this . writeQueue . length } ` ,
2021-02-20 07:55:26 +01:00
) ;
2025-09-12 08:36:02 +02:00
this . _setLightState (
uniqueOutputId ,
{ ... command , brightness : newBrightness } ,
false ,
deviceName ,
) ;
2021-02-20 07:55:26 +01:00
}
} , transitionInterval ) ;
} else {
2025-09-12 08:36:02 +02:00
if ( command . transition && isDimmable ) {
2021-02-20 07:55:26 +01:00
logger . debug (
2025-09-12 08:36:02 +02:00
` Could not transition light change. Either initial value is unknown or change is too small. Requested from ${ initialBrightness } to ${ command . brightness } ` ,
2021-02-20 07:55:26 +01:00
) ;
}
2025-09-12 08:36:02 +02:00
this . _setLightState ( uniqueOutputId , command , true , deviceName ) ;
2021-02-20 07:55:26 +01:00
}
}
2025-09-12 08:36:02 +02:00
/ * *
* @ param { string } uniqueOutputId
* @ param { { brightness : number , color _temp : number ? } } command
* @ param { boolean } shouldRetry
* @ param { string } deviceName
* /
_setLightState ( uniqueOutputId , command , shouldRetry , deviceName ) {
const lightCommand = { } ;
if ( ! command . brightness && command . brightness !== 0 ) {
2021-02-20 07:55:26 +01:00
logger . debug (
2025-09-12 08:36:02 +02:00
` Queueing turn on ${ deviceName } ( ${ uniqueOutputId } ). No brightness specified, setting DIM to previous. ` ,
2021-02-20 07:55:26 +01:00
) ;
2025-09-12 08:36:02 +02:00
lightCommand . command = COMMANDS . TURN _ON ;
} else if ( command . brightness <= 0 ) {
logger . debug ( ` Queueing turn off ${ uniqueOutputId } ` ) ;
lightCommand . command = COMMANDS . TURN _OFF ;
2021-02-20 07:55:26 +01:00
} else {
2025-09-12 08:36:02 +02:00
if ( command . brightness > 255 ) {
2021-02-20 07:55:26 +01:00
// eslint-disable-next-line no-param-reassign
2025-09-12 08:36:02 +02:00
command . brightness = 255 ;
2021-02-20 07:55:26 +01:00
}
2025-09-12 08:36:02 +02:00
logger . debug ( ` Queueing ${ uniqueOutputId } set brightness to ${ command . brightness } ` ) ;
lightCommand . command = COMMANDS . DIM ;
lightCommand . brightness = command . brightness ;
}
if ( command . color _temp ) {
lightCommand . command = COMMANDS . COLOR ;
lightCommand . color _temp = command . color _temp ;
2021-02-20 07:55:26 +01:00
}
2025-09-12 08:36:02 +02:00
this . _appendCommandToWriteQueue (
uniqueOutputId ,
// @ts-ignore
lightCommand ,
shouldRetry ,
) ;
2021-02-20 07:55:26 +01:00
}
2025-09-12 08:36:02 +02:00
/ * *
* @ param { string } uniqueOutputId
* @ param { { command : keyof typeof COMMANDS , brightness : number ? , color _temp : number ? } } command
* @ param { boolean } shouldRetry
* /
_appendCommandToWriteQueue ( uniqueOutputId , command , shouldRetry ) {
2021-02-20 07:55:26 +01:00
this . writeQueue . unshift ( {
2021-03-31 20:04:45 +02:00
uniqueOutputId ,
2021-02-20 07:55:26 +01:00
command ,
shouldRetry ,
} ) ;
}
2021-02-22 09:50:06 +01:00
_startWriteQueue ( ) {
2021-02-20 07:55:26 +01:00
logger . info ( 'startWriteQueue()' ) ;
clearTimeout ( this . writeQueueRef ) ;
2021-02-22 09:50:06 +01:00
this . writeQueueRef = setTimeout ( ( ) => this . _runWriteQueue ( ) , this . config . writeQueueWaitTime ) ;
2021-02-20 07:55:26 +01:00
}
2021-02-22 09:50:06 +01:00
async _runWriteQueue ( ) {
2021-02-20 07:55:26 +01:00
try {
while ( this . writeQueue . length > 0 ) {
2021-02-22 09:50:06 +01:00
if ( ! this . bleConnected ) {
logger . warn ( 'BLE not connected, stopping write queue until connection is up again.' ) ;
return ;
}
2021-02-20 07:55:26 +01:00
const queueItem = this . writeQueue . pop ( ) ;
2021-03-31 23:28:25 +02:00
const device = this . deviceRegistry . getOutputDevice ( queueItem . uniqueOutputId ) ;
2021-02-20 07:55:26 +01:00
logger . debug (
2025-09-12 08:36:02 +02:00
` Write queue: Processing ${ device . name } ( ${
queueItem . uniqueOutputId
} ) . Command $ { JSON . stringify ( queueItem . command ) } . Total queue length : $ {
2021-02-20 10:49:00 +01:00
this . writeQueue . length
} ` ,
2021-02-20 07:55:26 +01:00
) ;
2021-03-31 20:04:45 +02:00
if ( this . writeQueue . some ( ( item ) => item . uniqueOutputId === queueItem . uniqueOutputId ) ) {
2021-02-20 07:55:26 +01:00
logger . verbose (
2023-08-16 15:32:53 +02:00
` Skipping ${ device . name } ( ${ queueItem . uniqueOutputId } ) ` +
` ${ queueItem . command } due to more recent command in queue. ` ,
2021-02-20 07:55:26 +01:00
) ;
2021-03-31 20:04:45 +02:00
// Skip commands if new ones exist for the same uniqueOutputId
2021-02-20 07:55:26 +01:00
// still process all messages in order
} else {
/* eslint-disable no-await-in-loop */
try {
await this . plejdBleHandler . sendCommand (
2025-09-12 08:36:02 +02:00
queueItem . command . command ,
2021-03-31 23:28:25 +02:00
device . bleOutputAddress ,
2025-09-12 08:36:02 +02:00
queueItem . command . brightness ,
queueItem . command . color _temp ,
2021-02-20 07:55:26 +01:00
) ;
} catch ( err ) {
if ( queueItem . shouldRetry ) {
queueItem . retryCount = ( queueItem . retryCount || 0 ) + 1 ;
logger . debug ( ` Will retry command, count failed so far ${ queueItem . retryCount } ` ) ;
if ( queueItem . retryCount <= MAX _RETRY _COUNT ) {
this . writeQueue . push ( queueItem ) ; // Add back to top of queue to be processed next;
} else {
logger . error (
2021-03-31 23:28:25 +02:00
` Write queue: Exceeed max retry count ( ${ MAX _RETRY _COUNT } ) for ${ device . name } ( ${ queueItem . uniqueOutputId } ). Command ${ queueItem . command } failed. ` ,
2021-02-20 07:55:26 +01:00
) ;
break ;
}
if ( queueItem . retryCount > 1 ) {
break ; // First retry directly, consecutive after writeQueueWaitTime ms
}
}
}
/* eslint-enable no-await-in-loop */
}
}
} catch ( e ) {
logger . error ( 'Error in writeQueue loop, values probably not written to Plejd' , e ) ;
}
2021-02-22 09:50:06 +01:00
this . writeQueueRef = setTimeout ( ( ) => this . _runWriteQueue ( ) , this . config . writeQueueWaitTime ) ;
2021-02-20 07:55:26 +01:00
}
}
module . exports = PlejdDeviceCommunication ;