2021-03-29 12:51:48 +02:00
const { EventEmitter } = require ( 'events' ) ;
2021-02-20 07:55:26 +01:00
const Configuration = require ( './Configuration' ) ;
const constants = require ( './constants' ) ;
const Logger = require ( './Logger' ) ;
const PlejBLEHandler = require ( './PlejdBLEHandler' ) ;
const { COMMANDS } = constants ;
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
/** @type {{uniqueOutputId: string, command: string, data: any, 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 (
2021-03-31 20:04:45 +02:00
` Plejd got turn on command for ${ deviceName } ( ${ uniqueOutputId } ), brightness ${
command . brightness
} $ { command . transition ? ` , transition: ${ command . transition } ` : '' } ` ,
2021-02-20 07:55:26 +01:00
) ;
2021-03-31 20:04:45 +02:00
this . _transitionTo ( uniqueOutputId , command . brightness , command . transition , 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 } ` : ''
} ` ,
) ;
2021-03-31 20:04:45 +02:00
this . _transitionTo ( uniqueOutputId , 0 , command . transition , 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 ) {
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 ,
} ) ;
} 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
}
}
2021-03-31 20:04:45 +02:00
_transitionTo ( uniqueOutputId , targetBrightness , transition , deviceName ) {
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 (
2023-08-16 15:32:53 +02:00
transition > 1 &&
isDimmable &&
( initialBrightness || initialBrightness === 0 ) &&
( targetBrightness || targetBrightness === 0 ) &&
targetBrightness !== 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
const deltaBrightness = targetBrightness - initialBrightness ;
const transitionSteps = Math . min (
Math . abs ( deltaBrightness ) ,
MAX _TRANSITION _STEPS _PER _SECOND * transition ,
) ;
const transitionInterval = ( transition * 1000 ) / transitionSteps ;
logger . debug (
` transitioning from ${ initialBrightness } to ${ targetBrightness } ${
transition ? ` in ${ transition } seconds ` : ''
} . ` ,
) ;
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 ;
if ( tElapsed > transition || tElapsed < 0 ) {
tElapsed = transition ;
}
let newBrightness = Math . round (
initialBrightness + ( deltaBrightness * tElapsed ) / transition ,
) ;
if ( tElapsed === transition ) {
nSteps ++ ;
2021-03-31 20:04:45 +02:00
this . _clearDeviceTransitionTimer ( uniqueOutputId ) ;
2021-02-20 07:55:26 +01:00
newBrightness = targetBrightness ;
logger . debug (
2021-03-31 20:04:45 +02:00
` Queueing finalize ${ deviceName } ( ${ uniqueOutputId } ) transition from ${ initialBrightness } to ${ targetBrightness } in ${ tElapsedMs } ms. Done steps ${ nSteps } . Average interval ${
2021-02-20 07:55:26 +01:00
tElapsedMs / ( nSteps || 1 )
} ms . ` ,
) ;
2021-03-31 20:04:45 +02:00
this . _setBrightness ( uniqueOutputId , 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
) ;
2021-03-31 20:04:45 +02:00
this . _setBrightness ( uniqueOutputId , newBrightness , false , deviceName ) ;
2021-02-20 07:55:26 +01:00
}
} , transitionInterval ) ;
} else {
if ( transition && isDimmable ) {
logger . debug (
` Could not transition light change. Either initial value is unknown or change is too small. Requested from ${ initialBrightness } to ${ targetBrightness } ` ,
) ;
}
2021-03-31 20:04:45 +02:00
this . _setBrightness ( uniqueOutputId , targetBrightness , true , deviceName ) ;
2021-02-20 07:55:26 +01:00
}
}
2021-03-31 20:04:45 +02:00
_setBrightness ( unqiueOutputId , brightness , shouldRetry , deviceName ) {
2021-02-20 07:55:26 +01:00
if ( ! brightness && brightness !== 0 ) {
logger . debug (
2021-03-31 20:04:45 +02:00
` Queueing turn on ${ deviceName } ( ${ unqiueOutputId } ). No brightness specified, setting DIM to previous. ` ,
2021-02-20 07:55:26 +01:00
) ;
2021-03-31 20:04:45 +02:00
this . _appendCommandToWriteQueue ( unqiueOutputId , COMMANDS . TURN _ON , null , shouldRetry ) ;
2021-02-20 07:55:26 +01:00
} else if ( brightness <= 0 ) {
2021-03-31 20:04:45 +02:00
logger . debug ( ` Queueing turn off ${ unqiueOutputId } ` ) ;
this . _appendCommandToWriteQueue ( unqiueOutputId , COMMANDS . TURN _OFF , null , shouldRetry ) ;
2021-02-20 07:55:26 +01:00
} else {
if ( brightness > 255 ) {
// eslint-disable-next-line no-param-reassign
brightness = 255 ;
}
2021-03-31 20:04:45 +02:00
logger . debug ( ` Queueing ${ unqiueOutputId } set brightness to ${ brightness } ` ) ;
2021-02-20 07:55:26 +01:00
// eslint-disable-next-line no-bitwise
2021-03-31 20:04:45 +02:00
this . _appendCommandToWriteQueue ( unqiueOutputId , COMMANDS . DIM , brightness , shouldRetry ) ;
2021-02-20 07:55:26 +01:00
}
}
2021-03-31 20:04:45 +02:00
_appendCommandToWriteQueue ( uniqueOutputId , command , data , 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 ,
data ,
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 (
2021-03-31 23:28:25 +02:00
` Write queue: Processing ${ device . name } ( ${ queueItem . uniqueOutputId } ). Command ${
2021-02-20 07:55:26 +01:00
queueItem . command
2021-02-20 10:49:00 +01:00
} $ { queueItem . data ? ` ${ queueItem . data } ` : '' } . Total queue length : $ {
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 (
queueItem . command ,
2021-03-31 23:28:25 +02:00
device . bleOutputAddress ,
2021-02-20 07:55:26 +01:00
queueItem . data ,
) ;
} 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 ;