2020-01-18 15:50:46 +00:00
const dbus = require ( 'dbus-next' ) ;
2020-01-17 14:50:58 +00:00
const crypto = require ( 'crypto' ) ;
const xor = require ( 'buffer-xor' ) ;
2021-03-29 12:48:27 +02:00
const { EventEmitter } = require ( 'events' ) ;
2020-01-17 14:50:58 +00:00
2021-02-01 21:29:44 +01:00
const Configuration = require ( './Configuration' ) ;
2021-02-20 07:55:26 +01:00
const constants = require ( './constants' ) ;
const Logger = require ( './Logger' ) ;
2021-02-01 21:29:44 +01:00
2021-02-20 07:55:26 +01:00
const { COMMANDS } = constants ;
2021-01-22 15:49:02 +01:00
const logger = Logger . getLogger ( 'plejd-ble' ) ;
2020-01-17 14:50:58 +00:00
// UUIDs
2021-02-08 07:38:31 +01:00
const BLE _UUID _SUFFIX = '6085-4726-be45-040c957391b5' ;
const PLEJD _SERVICE = ` 31ba0001- ${ BLE _UUID _SUFFIX } ` ;
const DATA _UUID = ` 31ba0004- ${ BLE _UUID _SUFFIX } ` ;
const LAST _DATA _UUID = ` 31ba0005- ${ BLE _UUID _SUFFIX } ` ;
const AUTH _UUID = ` 31ba0009- ${ BLE _UUID _SUFFIX } ` ;
const PING _UUID = ` 31ba000a- ${ BLE _UUID _SUFFIX } ` ;
2020-01-17 14:50:58 +00:00
2021-02-18 10:47:33 +01:00
const BLE _CMD _DIM _CHANGE = 0x00c8 ;
const BLE _CMD _DIM2 _CHANGE = 0x0098 ;
const BLE _CMD _STATE _CHANGE = 0x0097 ;
const BLE _CMD _SCENE _TRIG = 0x0021 ;
const BLE _CMD _TIME _UPDATE = 0x001b ;
const BLE _BROADCAST _DEVICE _ID = 0x01 ;
2021-02-18 22:24:20 +01:00
const BLE _REQUEST _NO _RESPONSE = 0x0110 ;
const BLE _REQUEST _RESPONSE = 0x0102 ;
// const BLE_REQUEST_READ_VALUE = 0x0103;
2020-01-17 14:50:58 +00:00
const BLUEZ _SERVICE _NAME = 'org.bluez' ;
2020-01-18 15:50:46 +00:00
const DBUS _OM _INTERFACE = 'org.freedesktop.DBus.ObjectManager' ;
const DBUS _PROP _INTERFACE = 'org.freedesktop.DBus.Properties' ;
2020-01-17 14:50:58 +00:00
const BLUEZ _ADAPTER _ID = 'org.bluez.Adapter1' ;
const BLUEZ _DEVICE _ID = 'org.bluez.Device1' ;
const GATT _SERVICE _ID = 'org.bluez.GattService1' ;
const GATT _CHRC _ID = 'org.bluez.GattCharacteristic1' ;
2021-02-01 21:29:44 +01:00
const delay = ( timeout ) => new Promise ( ( resolve ) => setTimeout ( resolve , timeout ) ) ;
2021-02-08 07:38:31 +01:00
class PlejBLEHandler extends EventEmitter {
adapter ;
adapterProperties ;
2021-02-01 21:29:44 +01:00
config ;
2021-02-09 19:20:09 +01:00
bleDevices = [ ] ;
bus = null ;
connectedDevice = null ;
2021-02-08 19:54:24 +01:00
consecutiveWriteFails ;
2021-02-27 13:46:41 +01:00
consecutiveReconnectAttempts = 0 ;
2021-03-31 20:04:45 +02:00
/** @type {import('./DeviceRegistry')} */
deviceRegistry ;
2021-02-09 19:20:09 +01:00
discoveryTimeout = null ;
plejdService = null ;
pingRef = null ;
2021-02-20 07:55:26 +01:00
requestCurrentPlejdTimeRef = null ;
2021-02-09 19:20:09 +01:00
reconnectInProgress = false ;
2021-02-27 09:57:29 +01:00
emergencyReconnectTimeout = null ;
2021-02-01 21:29:44 +01:00
2021-02-08 07:38:31 +01:00
// Refer to BLE-states.md regarding the internal BLE/bluez state machine of Bluetooth states
// These states refer to the state machine of this file
static STATES = [ 'MAIN_INIT' , 'GET_ADAPTER_PROXY' ] ;
2021-02-20 07:55:26 +01:00
static EVENTS = {
connected : 'connected' ,
reconnecting : 'reconnecting' ,
commandReceived : 'commandReceived' ,
2021-02-20 15:33:06 +01:00
writeFailed : 'writeFailed' ,
writeSuccess : 'writeSuccess' ,
2021-02-20 07:55:26 +01:00
} ;
2021-02-08 19:54:24 +01:00
2021-02-01 21:29:44 +01:00
constructor ( deviceRegistry ) {
2020-01-17 14:50:58 +00:00
super ( ) ;
2021-02-08 07:38:31 +01:00
logger . info ( 'Starting Plejd BLE Handler, resetting all device states.' ) ;
2021-01-18 17:21:37 +01:00
2021-02-01 21:29:44 +01:00
this . config = Configuration . getOptions ( ) ;
2021-02-08 07:38:31 +01:00
this . deviceRegistry = deviceRegistry ;
2020-01-17 14:50:58 +00:00
// Holds a reference to all characteristics
this . characteristics = {
data : null ,
lastData : null ,
2020-01-19 20:08:48 +00:00
lastDataProperties : null ,
2020-01-17 14:50:58 +00:00
auth : null ,
2021-01-22 15:49:02 +01:00
ping : null ,
2020-01-17 14:50:58 +00:00
} ;
2021-02-27 09:57:29 +01:00
this . bus = dbus . systemBus ( ) ;
2021-02-20 15:33:06 +01:00
}
cleanup ( ) {
2021-02-22 09:50:06 +01:00
logger . verbose ( 'cleanup() - Clearing ping interval and clock update timer' ) ;
clearInterval ( this . pingRef ) ;
clearTimeout ( this . requestCurrentPlejdTimeRef ) ;
logger . verbose ( 'Removing listeners to write events, bus events and objectManager...' ) ;
2021-02-20 15:33:06 +01:00
this . removeAllListeners ( PlejBLEHandler . EVENTS . writeFailed ) ;
this . removeAllListeners ( PlejBLEHandler . EVENTS . writeSuccess ) ;
2020-01-17 14:50:58 +00:00
2021-02-20 15:33:06 +01:00
if ( this . bus ) {
this . bus . removeAllListeners ( 'error' ) ;
this . bus . removeAllListeners ( 'connect' ) ;
}
if ( this . characteristics . lastDataProperties ) {
this . characteristics . lastDataProperties . removeAllListeners ( 'PropertiesChanged' ) ;
}
if ( this . objectManager ) {
this . objectManager . removeAllListeners ( 'InterfacesAdded' ) ;
}
2021-02-01 21:29:44 +01:00
}
async init ( ) {
logger . info ( 'init()' ) ;
2021-02-09 19:20:09 +01:00
2021-02-20 15:33:06 +01:00
this . on ( PlejBLEHandler . EVENTS . writeFailed , ( error ) => this . _onWriteFailed ( error ) ) ;
this . on ( PlejBLEHandler . EVENTS . writeSuccess , ( ) => this . _onWriteSuccess ( ) ) ;
2021-02-09 19:20:09 +01:00
this . bus . on ( 'error' , ( err ) => {
// Uncaught error events will show UnhandledPromiseRejection logs
logger . verbose ( ` dbus-next error event: ${ err . message } ` ) ;
} ) ;
2021-02-18 10:47:33 +01:00
this . bus . on ( 'connect' , ( ) => {
logger . verbose ( 'dbus-next connected' ) ;
} ) ;
2021-02-09 19:20:09 +01:00
// this.bus also has a 'message' event that gets emitted _very_ frequently
2021-02-08 07:38:31 +01:00
this . adapter = null ;
this . adapterProperties = null ;
2021-02-08 19:54:24 +01:00
this . consecutiveWriteFails = 0 ;
2020-01-17 14:50:58 +00:00
2021-02-01 21:29:44 +01:00
this . cryptoKey = Buffer . from ( this . deviceRegistry . cryptoKey . replace ( /-/g , '' ) , 'hex' ) ;
2020-01-17 14:50:58 +00:00
if ( this . objectManager ) {
this . objectManager . removeAllListeners ( ) ;
}
2021-01-13 01:47:05 +01:00
this . bleDevices = [ ] ;
2020-02-29 15:54:08 +00:00
this . connectedDevice = null ;
2021-01-13 01:47:05 +01:00
2020-01-21 14:24:02 +00:00
this . characteristics = {
data : null ,
lastData : null ,
lastDataProperties : null ,
auth : null ,
2021-01-22 15:49:02 +01:00
ping : null ,
2020-01-21 14:24:02 +00:00
} ;
2020-01-27 20:43:52 +00:00
2021-02-08 07:38:31 +01:00
await this . _getInterface ( ) ;
await this . _startGetPlejdDevice ( ) ;
2021-02-08 19:54:24 +01:00
logger . info ( 'BLE init done, waiting for devices.' ) ;
2021-02-08 07:38:31 +01:00
}
2021-03-31 23:28:25 +02:00
/ * *
* @ param { string } command
* @ param { number } bleOutputAddress
* @ param { number } data
* /
async sendCommand ( command , bleOutputAddress , data ) {
2021-02-20 07:55:26 +01:00
let payload ;
let brightnessVal ;
switch ( command ) {
case COMMANDS . TURN _ON :
2021-03-31 23:28:25 +02:00
payload = this . _createHexPayload ( bleOutputAddress , BLE _CMD _STATE _CHANGE , '01' ) ;
2021-02-20 07:55:26 +01:00
break ;
case COMMANDS . TURN _OFF :
2021-03-31 23:28:25 +02:00
payload = this . _createHexPayload ( bleOutputAddress , BLE _CMD _STATE _CHANGE , '00' ) ;
2021-02-20 07:55:26 +01:00
break ;
case COMMANDS . DIM :
// eslint-disable-next-line no-bitwise
brightnessVal = ( data << 8 ) | data ;
payload = this . _createHexPayload (
2021-03-31 23:28:25 +02:00
bleOutputAddress ,
2021-02-20 07:55:26 +01:00
BLE _CMD _DIM2 _CHANGE ,
` 01 ${ brightnessVal . toString ( 16 ) . padStart ( 4 , '0' ) } ` ,
) ;
break ;
default :
logger . error ( ` Unknown command ${ command } ` ) ;
throw new Error ( ` Unknown command ${ command } ` ) ;
}
await this . _write ( payload ) ;
}
2021-02-08 07:38:31 +01:00
async _initDiscoveredPlejdDevice ( path ) {
logger . debug ( ` initDiscoveredPlejdDevice(). Got ${ path } device ` ) ;
logger . debug ( ` Inspecting ${ path } ` ) ;
try {
const proxyObject = await this . bus . getProxyObject ( BLUEZ _SERVICE _NAME , path ) ;
const device = await proxyObject . getInterface ( BLUEZ _DEVICE _ID ) ;
const properties = await proxyObject . getInterface ( DBUS _PROP _INTERFACE ) ;
const plejd = { path } ;
plejd . rssi = ( await properties . Get ( BLUEZ _DEVICE _ID , 'RSSI' ) ) . value ;
plejd . instance = device ;
const segments = plejd . path . split ( '/' ) ;
2021-03-31 20:04:45 +02:00
let plejdSerialNumber = segments [ segments . length - 1 ] . replace ( 'dev_' , '' ) ;
plejdSerialNumber = plejdSerialNumber . replace ( /_/g , '' ) ;
plejd . device = this . deviceRegistry . getPhysicalDevice ( plejdSerialNumber ) ;
2021-02-08 07:38:31 +01:00
2021-02-20 12:34:30 +01:00
if ( plejd . device ) {
2021-03-23 13:45:12 +01:00
logger . debug (
` Discovered ${ plejd . path } with rssi ${ plejd . rssi } dBm, name ${ plejd . device . name } ` ,
) ;
2021-02-20 12:34:30 +01:00
this . bleDevices . push ( plejd ) ;
} else {
2021-03-31 20:04:45 +02:00
logger . warn ( ` Device registry does not contain device with serial ${ plejdSerialNumber } ` ) ;
2021-02-20 12:34:30 +01:00
}
2021-02-08 07:38:31 +01:00
} catch ( err ) {
logger . error ( ` Failed inspecting ${ path } . ` , err ) ;
}
}
2020-01-20 10:58:03 +00:00
2021-02-08 07:38:31 +01:00
async _inspectDevicesDiscovered ( ) {
2021-02-08 20:08:40 +01:00
try {
if ( this . bleDevices . length === 0 ) {
logger . error ( 'Discovery timeout elapsed, no devices found. Starting reconnect loop...' ) ;
throw new Error ( 'Discovery timeout elapsed' ) ;
}
2021-02-08 07:38:31 +01:00
2021-02-08 20:08:40 +01:00
logger . info ( ` Device discovery done, found ${ this . bleDevices . length } Plejd devices ` ) ;
2021-02-08 07:38:31 +01:00
2021-02-08 20:08:40 +01:00
const sortedDevices = this . bleDevices . sort ( ( a , b ) => b . rssi - a . rssi ) ;
2021-02-08 07:38:31 +01:00
2021-02-08 20:08:40 +01:00
// eslint-disable-next-line no-restricted-syntax
for ( const plejd of sortedDevices ) {
try {
logger . verbose ( ` Inspecting ${ plejd . path } ` ) ;
if ( plejd . instance ) {
logger . info ( ` Connecting to ${ plejd . path } ` ) ;
// eslint-disable-next-line no-await-in-loop
await plejd . instance . Connect ( ) ;
logger . verbose ( 'Connected. Waiting for timeout before reading characteristics...' ) ;
// eslint-disable-next-line no-await-in-loop
await delay ( this . config . connectionTimeout * 1000 ) ;
// eslint-disable-next-line no-await-in-loop
const connectedPlejdDevice = await this . _onDeviceConnected ( plejd ) ;
if ( connectedPlejdDevice ) {
break ;
}
2021-02-08 07:38:31 +01:00
}
2021-02-08 20:08:40 +01:00
} catch ( err ) {
2021-02-09 19:20:09 +01:00
logger . warn ( 'Unable to connect. ' , err ) ;
2021-02-08 07:38:31 +01:00
}
}
2021-02-08 20:08:40 +01:00
try {
logger . verbose ( 'Stopping discovery...' ) ;
await this . adapter . StopDiscovery ( ) ;
logger . verbose ( 'Stopped BLE discovery' ) ;
} catch ( err ) {
logger . error ( 'Failed to stop discovery.' , err ) ;
if ( err . message . includes ( 'Operation already in progress' ) ) {
logger . info (
'If you continue to get "operation already in progress" error, you can try power cycling the bluetooth adapter. Get root console access, run "bluetoothctl" => "power off" => "power on" => "exit" => restart addon.' ,
) ;
try {
await delay ( 250 ) ;
logger . verbose ( 'Power cycling...' ) ;
await this . _powerCycleAdapter ( ) ;
logger . verbose ( 'Trying again...' ) ;
await this . _startGetPlejdDevice ( ) ;
} catch ( errInner ) {
logger . error ( 'Failed to retry internalInit. Starting reconnect loop' , errInner ) ;
throw new Error ( 'Failed to retry internalInit' ) ;
}
2021-02-08 07:38:31 +01:00
}
2021-02-08 20:08:40 +01:00
logger . error ( 'Failed to start discovery. Make sure no other add-on is currently scanning.' ) ;
throw new Error ( 'Failed to start discovery' ) ;
}
if ( ! this . connectedDevice ) {
logger . error ( 'Could not connect to any Plejd device. Starting reconnect loop...' ) ;
throw new Error ( 'Could not connect to any Plejd device' ) ;
2021-02-08 07:38:31 +01:00
}
2021-02-08 20:08:40 +01:00
logger . info ( ` BLE Connected to ${ this . connectedDevice . name } ` ) ;
2021-02-18 10:47:33 +01:00
// Connected and authenticated, request current time and start ping
2021-02-18 21:17:29 +01:00
if ( this . config . updatePlejdClock ) {
this . _requestCurrentPlejdTime ( ) ;
} else {
logger . info ( 'Plejd clock updates disabled in configuration.' ) ;
}
2021-02-20 07:55:26 +01:00
this . _startPing ( ) ;
2021-02-08 20:08:40 +01:00
// After we've authenticated, we need to hook up the event listener
// for changes to lastData.
this . characteristics . lastDataProperties . on ( 'PropertiesChanged' , (
iface ,
properties ,
// invalidated (third param),
2021-02-20 07:55:26 +01:00
) => this . _onLastDataUpdated ( iface , properties ) ) ;
2021-02-08 20:08:40 +01:00
this . characteristics . lastData . StartNotify ( ) ;
2021-02-27 13:46:41 +01:00
this . consecutiveReconnectAttempts = 0 ;
2021-02-20 07:55:26 +01:00
this . emit ( PlejBLEHandler . EVENTS . connected ) ;
2021-02-27 09:57:29 +01:00
clearTimeout ( this . emergencyReconnectTimeout ) ;
this . emergencyReconnectTimeout = null ;
2021-02-08 20:08:40 +01:00
} catch ( err ) {
// This method is run on a timer, so errors can't e re-thrown.
// Start reconnect loop if errors occur here
logger . debug ( ` Starting reconnect loop due to ${ err . message } ` ) ;
2021-02-08 19:54:24 +01:00
this . startReconnectPeriodicallyLoop ( ) ;
2021-02-08 07:38:31 +01:00
}
}
async _getInterface ( ) {
2020-01-18 15:50:46 +00:00
const bluez = await this . bus . getProxyObject ( BLUEZ _SERVICE _NAME , '/' ) ;
2021-02-20 15:33:06 +01:00
2020-01-18 15:50:46 +00:00
this . objectManager = await bluez . getInterface ( DBUS _OM _INTERFACE ) ;
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
// We need to find the ble interface which implements the Adapter1 interface
const managedObjects = await this . objectManager . GetManagedObjects ( ) ;
2021-02-08 07:38:31 +01:00
const managedPaths = Object . keys ( managedObjects ) ;
2020-01-17 14:50:58 +00:00
2021-02-08 19:54:24 +01:00
logger . verbose ( ` Managed paths ${ JSON . stringify ( managedPaths , null , 2 ) } ` ) ;
2020-01-17 14:50:58 +00:00
2021-02-08 07:38:31 +01:00
// eslint-disable-next-line no-restricted-syntax
for ( const path of managedPaths ) {
const pathInterfaces = Object . keys ( managedObjects [ path ] ) ;
if ( pathInterfaces . indexOf ( BLUEZ _ADAPTER _ID ) > - 1 ) {
logger . debug ( ` Found BLE interface ' ${ BLUEZ _ADAPTER _ID } ' at ${ path } ` ) ;
try {
// eslint-disable-next-line no-await-in-loop
const adapterObject = await this . bus . getProxyObject ( BLUEZ _SERVICE _NAME , path ) ;
// eslint-disable-next-line no-await-in-loop
this . adapterProperties = await adapterObject . getInterface ( DBUS _PROP _INTERFACE ) ;
// eslint-disable-next-line no-await-in-loop
await this . _powerOnAdapter ( ) ;
this . adapter = adapterObject . getInterface ( BLUEZ _ADAPTER _ID ) ;
// eslint-disable-next-line no-await-in-loop
await this . _cleanExistingConnections ( managedObjects ) ;
2021-02-08 19:54:24 +01:00
logger . verbose ( ` Got adapter ${ this . adapter . path } ` ) ;
2021-02-08 07:38:31 +01:00
return this . adapter ;
} catch ( err ) {
logger . error ( ` Failed to get interface ' ${ BLUEZ _ADAPTER _ID } '. ` , err ) ;
}
}
2020-01-17 14:50:58 +00:00
}
2021-02-08 07:38:31 +01:00
this . adapter = null ;
logger . error ( 'Unable to find a bluetooth adapter that is compatible.' ) ;
throw new Error ( 'Unable to find a bluetooth adapter that is compatible.' ) ;
}
async _powerCycleAdapter ( ) {
2021-02-27 13:46:41 +01:00
logger . verbose ( 'Power cycling BLE adapter' ) ;
2021-02-08 07:38:31 +01:00
await this . _powerOffAdapter ( ) ;
await this . _powerOnAdapter ( ) ;
}
async _powerOnAdapter ( ) {
2021-02-27 13:46:41 +01:00
logger . verbose ( 'Powering on BLE adapter and waiting 5 seconds' ) ;
2021-02-08 07:38:31 +01:00
await this . adapterProperties . Set ( BLUEZ _ADAPTER _ID , 'Powered' , new dbus . Variant ( 'b' , 1 ) ) ;
2021-02-27 13:46:41 +01:00
await delay ( 5000 ) ;
2021-02-08 07:38:31 +01:00
}
async _powerOffAdapter ( ) {
2021-03-23 13:45:12 +01:00
logger . verbose ( 'Powering off BLE adapter and waiting 30 seconds' ) ;
2021-02-08 07:38:31 +01:00
await this . adapterProperties . Set ( BLUEZ _ADAPTER _ID , 'Powered' , new dbus . Variant ( 'b' , 0 ) ) ;
2021-03-23 13:45:12 +01:00
await delay ( 30000 ) ;
2021-02-08 07:38:31 +01:00
}
async _cleanExistingConnections ( managedObjects ) {
2021-02-01 21:29:44 +01:00
logger . verbose (
` Iterating ${
Object . keys ( managedObjects ) . length
} BLE managedObjects looking for $ { BLUEZ _DEVICE _ID } ` ,
) ;
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line no-restricted-syntax
for ( const path of Object . keys ( managedObjects ) ) {
/* eslint-disable no-await-in-loop */
2021-02-01 21:29:44 +01:00
try {
const interfaces = Object . keys ( managedObjects [ path ] ) ;
2020-01-20 07:58:23 +00:00
2021-02-01 21:29:44 +01:00
if ( interfaces . indexOf ( BLUEZ _DEVICE _ID ) > - 1 ) {
const proxyObject = await this . bus . getProxyObject ( BLUEZ _SERVICE _NAME , path ) ;
const device = await proxyObject . getInterface ( BLUEZ _DEVICE _ID ) ;
2020-01-17 14:50:58 +00:00
2021-02-08 19:54:24 +01:00
logger . verbose ( ` Found ${ path } ` ) ;
2020-01-19 20:08:48 +00:00
2021-02-01 21:29:44 +01:00
const connected = managedObjects [ path ] [ BLUEZ _DEVICE _ID ] . Connected . value ;
2020-01-19 20:08:48 +00:00
2021-02-01 21:29:44 +01:00
if ( connected ) {
logger . info ( ` disconnecting ${ path } . This can take up to 180 seconds ` ) ;
await device . Disconnect ( ) ;
}
logger . verbose ( ` Removing ${ path } from adapter. ` ) ;
await this . adapter . RemoveDevice ( path ) ;
}
} catch ( err ) {
logger . error ( ` Error handling ${ path } ` , err ) ;
2020-01-19 20:08:48 +00:00
}
2021-01-22 15:49:02 +01:00
/* eslint-enable no-await-in-loop */
2020-01-17 14:50:58 +00:00
}
2021-02-01 21:29:44 +01:00
logger . verbose ( 'All active BLE device connections cleaned up.' ) ;
2021-02-08 07:38:31 +01:00
}
2021-02-01 21:29:44 +01:00
2021-02-08 07:38:31 +01:00
async _startGetPlejdDevice ( ) {
2021-02-01 21:29:44 +01:00
logger . verbose ( 'Setting up interfacesAdded subscription and discovery filter' ) ;
2021-02-20 07:55:26 +01:00
this . objectManager . on ( 'InterfacesAdded' , ( path , interfaces ) => this . _onInterfacesAdded ( path , interfaces ) ) ;
2020-01-19 20:08:48 +00:00
this . adapter . SetDiscoveryFilter ( {
2021-01-22 15:49:02 +01:00
UUIDs : new dbus . Variant ( 'as' , [ PLEJD _SERVICE ] ) ,
Transport : new dbus . Variant ( 's' , 'le' ) ,
2020-01-19 20:08:48 +00:00
} ) ;
2020-01-27 20:43:52 +00:00
try {
2021-02-01 21:29:44 +01:00
logger . verbose ( 'Starting BLE discovery... This can take up to 180 seconds.' ) ;
2021-02-08 07:38:31 +01:00
this . _scheduleInternalInit ( ) ;
2020-01-27 20:43:52 +00:00
await this . adapter . StartDiscovery ( ) ;
2021-02-01 21:29:44 +01:00
logger . verbose ( 'Started BLE discovery' ) ;
2020-06-12 11:15:00 +02:00
} catch ( err ) {
2021-02-01 21:29:44 +01:00
logger . error ( 'Failed to start discovery.' , err ) ;
if ( err . message . includes ( 'Operation already in progress' ) ) {
logger . info (
'If you continue to get "operation already in progress" error, you can try power cycling the bluetooth adapter. Get root console access, run "bluetoothctl" => "power off" => "power on" => "exit" => restart addon.' ,
) ;
}
throw new Error (
'Failed to start discovery. Make sure no other add-on is currently scanning.' ,
2021-01-22 15:49:02 +01:00
) ;
}
2020-01-19 20:08:48 +00:00
}
2020-01-17 14:50:58 +00:00
2021-02-08 07:38:31 +01:00
_scheduleInternalInit ( ) {
clearTimeout ( this . discoveryTimeout ) ;
2021-02-09 19:20:09 +01:00
this . discoveryTimeout = setTimeout (
( ) => this . _inspectDevicesDiscovered ( ) ,
this . config . connectionTimeout * 1000 ,
) ;
2020-01-19 20:08:48 +00:00
}
2020-01-17 14:50:58 +00:00
2021-02-20 07:55:26 +01:00
async _onInterfacesAdded ( path , interfaces ) {
2021-02-01 21:29:44 +01:00
logger . silly ( ` Interface added ${ path } , inspecting... ` ) ;
2020-01-19 20:08:48 +00:00
const interfaceKeys = Object . keys ( interfaces ) ;
if ( interfaceKeys . indexOf ( BLUEZ _DEVICE _ID ) > - 1 ) {
2021-01-22 15:49:02 +01:00
if ( interfaces [ BLUEZ _DEVICE _ID ] . UUIDs . value . indexOf ( PLEJD _SERVICE ) > - 1 ) {
2021-01-21 21:31:37 +01:00
logger . debug ( ` Found Plejd service on ${ path } ` ) ;
2021-02-20 15:33:06 +01:00
this . objectManager . removeAllListeners ( 'InterfacesAdded' ) ;
2021-02-08 07:38:31 +01:00
await this . _initDiscoveredPlejdDevice ( path ) ;
2020-01-19 20:08:48 +00:00
} else {
2021-01-21 21:31:37 +01:00
logger . error ( 'Uh oh, no Plejd device!' ) ;
2020-01-17 14:50:58 +00:00
}
2021-02-01 21:29:44 +01:00
} else {
logger . silly ( 'Not the right device id' ) ;
2020-01-17 14:50:58 +00:00
}
}
2021-02-20 07:55:26 +01:00
async _authenticate ( ) {
2021-01-21 21:31:37 +01:00
logger . info ( 'authenticate()' ) ;
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
try {
2021-01-21 21:31:37 +01:00
logger . debug ( 'Sending challenge to device' ) ;
2020-01-19 20:08:48 +00:00
await this . characteristics . auth . WriteValue ( [ 0 ] , { } ) ;
2021-01-21 21:31:37 +01:00
logger . debug ( 'Reading response from device' ) ;
2020-01-19 20:08:48 +00:00
const challenge = await this . characteristics . auth . ReadValue ( { } ) ;
2020-01-20 21:32:46 +00:00
const response = this . _createChallengeResponse ( this . cryptoKey , Buffer . from ( challenge ) ) ;
2021-01-21 21:31:37 +01:00
logger . debug ( 'Responding to authenticate' ) ;
2020-01-19 20:08:48 +00:00
await this . characteristics . auth . WriteValue ( [ ... response ] , { } ) ;
2020-06-12 11:15:00 +02:00
} catch ( err ) {
2021-01-21 21:31:37 +01:00
logger . error ( 'Failed to authenticate: ' , err ) ;
2021-02-08 07:38:31 +01:00
throw new Error ( 'Failed to authenticate' ) ;
2020-01-19 20:08:48 +00:00
}
2020-01-17 14:50:58 +00:00
}
2021-02-08 19:54:24 +01:00
async startReconnectPeriodicallyLoop ( ) {
2021-02-22 09:50:06 +01:00
logger . info ( 'Starting reconnect loop...' ) ;
2021-02-27 09:57:29 +01:00
clearTimeout ( this . emergencyReconnectTimeout ) ;
this . emergencyReconnectTimeout = null ;
await this . _startReconnectPeriodicallyLoopInternal ( ) ;
}
async _startReconnectPeriodicallyLoopInternal ( ) {
logger . verbose ( 'Starting internal reconnect loop...' ) ;
if ( this . reconnectInProgress && ! this . emergencyReconnectTimeout ) {
2021-02-09 19:20:09 +01:00
logger . debug ( 'Reconnect already in progress. Skipping this call.' ) ;
2021-02-08 19:54:24 +01:00
return ;
}
2021-02-27 09:57:29 +01:00
if ( this . emergencyReconnectTimeout ) {
logger . warn (
'Restarting reconnect loop due to emergency reconnect timer elapsed. This should very rarely happen!' ,
) ;
}
2021-02-08 19:54:24 +01:00
this . reconnectInProgress = true ;
/* eslint-disable no-await-in-loop */
// eslint-disable-next-line no-constant-condition
while ( true ) {
try {
2021-02-22 09:50:06 +01:00
logger . verbose ( 'Reconnect: Clean up, emit reconnect event, wait 5s and the re-init...' ) ;
2021-02-20 15:33:06 +01:00
this . cleanup ( ) ;
2021-02-27 13:46:41 +01:00
this . consecutiveReconnectAttempts ++ ;
2021-03-23 13:45:12 +01:00
if ( this . consecutiveReconnectAttempts % 100 === 0 ) {
logger . error ( 'Failed reconnecting 100 times. Creating a new dbus instance...' ) ;
this . bus = dbus . systemBus ( ) ;
}
2021-02-27 13:46:41 +01:00
if ( this . consecutiveReconnectAttempts % 10 === 0 ) {
logger . warn (
2021-03-23 13:45:12 +01:00
` Tried reconnecting ${ this . consecutiveReconnectAttempts } times. Will power cycle the BLE adapter now... ` ,
2021-02-27 13:46:41 +01:00
) ;
await this . _powerCycleAdapter ( ) ;
} else {
logger . verbose (
` Reconnect attempt ${ this . consecutiveReconnectAttempts } in a row. Will power cycle every 10th time. ` ,
) ;
}
2021-02-20 07:55:26 +01:00
this . emit ( PlejBLEHandler . EVENTS . reconnecting ) ;
2021-02-27 09:57:29 +01:00
// Emergency 2 minute timer if reconnect silently fails somewhere
clearTimeout ( this . emergencyReconnectTimeout ) ;
this . emergencyReconnectTimeout = setTimeout (
( ) => this . _startReconnectPeriodicallyLoopInternal ( ) ,
120 * 1000 ,
) ;
2021-02-22 09:50:06 +01:00
await delay ( 5000 ) ;
2021-02-08 19:54:24 +01:00
logger . info ( 'Reconnecting BLE...' ) ;
await this . init ( ) ;
break ;
} catch ( err ) {
2021-02-09 19:20:09 +01:00
logger . warn ( 'Failed reconnecting.' , err ) ;
2021-02-08 19:54:24 +01:00
}
}
/* eslint-enable no-await-in-loop */
this . reconnectInProgress = false ;
2021-01-13 01:47:05 +01:00
}
2021-02-20 07:55:26 +01:00
async _write ( payload ) {
if ( ! payload || ! this . plejdService || ! this . characteristics . data ) {
2021-01-21 21:31:37 +01:00
logger . debug ( 'data, plejdService or characteristics not available. Cannot write()' ) ;
2021-02-20 07:55:26 +01:00
throw new Error ( 'data, plejdService or characteristics not available. Cannot write()' ) ;
2020-01-24 10:20:39 +01:00
}
2020-01-19 20:08:48 +00:00
try {
2021-02-20 07:55:26 +01:00
logger . verbose (
` Sending ${ payload . length } byte(s) of data to Plejd. ${ payload . toString ( 'hex' ) } ` ,
) ;
const encryptedData = this . _encryptDecrypt ( this . cryptoKey , this . plejdService . addr , payload ) ;
2020-01-19 20:08:48 +00:00
await this . characteristics . data . WriteValue ( [ ... encryptedData ] , { } ) ;
2021-02-20 07:55:26 +01:00
await this . _onWriteSuccess ( ) ;
2020-06-12 11:15:00 +02:00
} catch ( err ) {
2021-02-20 07:55:26 +01:00
await this . _onWriteFailed ( err ) ;
2020-03-03 15:59:10 +01:00
if ( err . message === 'In Progress' ) {
2021-01-22 15:49:02 +01:00
logger . debug ( "Write failed due to 'In progress' " , err ) ;
2021-02-20 07:55:26 +01:00
throw new Error ( "Write failed due to 'In progress'" ) ;
2020-03-03 15:59:10 +01:00
}
2021-02-20 07:55:26 +01:00
logger . debug ( 'Write failed ' , err ) ;
throw new Error ( ` Write failed due to ${ err . message } ` ) ;
2020-01-19 20:08:48 +00:00
}
2020-01-17 14:50:58 +00:00
}
2021-02-20 07:55:26 +01:00
_startPing ( ) {
2021-01-21 21:31:37 +01:00
logger . info ( 'startPing()' ) ;
2020-01-17 14:50:58 +00:00
clearInterval ( this . pingRef ) ;
this . pingRef = setInterval ( async ( ) => {
2021-01-21 21:31:37 +01:00
logger . silly ( 'ping' ) ;
2021-02-20 07:55:26 +01:00
await this . _ping ( ) ;
2020-01-20 21:32:46 +00:00
} , 3000 ) ;
2020-01-17 14:50:58 +00:00
}
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line class-methods-use-this
2021-02-20 07:55:26 +01:00
_onWriteSuccess ( ) {
2021-02-08 19:54:24 +01:00
this . consecutiveWriteFails = 0 ;
2020-01-17 14:50:58 +00:00
}
2021-02-20 07:55:26 +01:00
async _onWriteFailed ( error ) {
2021-02-08 19:54:24 +01:00
this . consecutiveWriteFails ++ ;
logger . debug ( ` onWriteFailed # ${ this . consecutiveWriteFails } in a row. ` , error ) ;
2021-02-09 19:20:09 +01:00
logger . verbose ( ` Error message: ${ error . message } ` ) ;
2021-02-08 19:54:24 +01:00
let errorIndicatesDisconnected = false ;
2021-02-11 22:45:13 +01:00
if ( error . message . includes ( 'error: 0x0e' ) ) {
2021-02-09 19:20:09 +01:00
logger . error ( "'Unlikely error' (0x0e) writing to Plejd. Will retry." , error ) ;
2021-02-11 22:45:13 +01:00
} else if ( error . message . includes ( 'Not connected' ) ) {
logger . error ( "'Not connected' writing to Plejd. Plejd device is probably disconnected." ) ;
2021-02-08 19:54:24 +01:00
errorIndicatesDisconnected = true ;
2021-02-11 22:45:13 +01:00
} else if ( error . message . includes ( 'Method "WriteValue" with signature' ) ) {
2021-02-08 19:54:24 +01:00
logger . error ( "'Method \"WriteValue\" doesn't exist'. Plejd device is probably disconnected." ) ;
errorIndicatesDisconnected = true ;
}
2020-01-17 14:50:58 +00:00
2021-02-09 19:20:09 +01:00
if ( errorIndicatesDisconnected || this . consecutiveWriteFails >= 5 ) {
logger . warn (
` Write error indicates BLE is disconnected. Retry count ${ this . consecutiveWriteFails } . Reconnecting... ` ,
) ;
this . startReconnectPeriodicallyLoop ( ) ;
2021-02-08 19:54:24 +01:00
}
2020-01-17 14:50:58 +00:00
}
2021-02-20 07:55:26 +01:00
async _ping ( ) {
2021-01-21 21:31:37 +01:00
logger . silly ( 'ping()' ) ;
2020-01-17 14:50:58 +00:00
2021-01-22 15:49:02 +01:00
const ping = crypto . randomBytes ( 1 ) ;
2020-01-19 20:08:48 +00:00
let pong = null ;
try {
await this . characteristics . ping . WriteValue ( [ ... ping ] , { } ) ;
pong = await this . characteristics . ping . ReadValue ( { } ) ;
2020-06-12 11:15:00 +02:00
} catch ( err ) {
2021-02-11 22:45:13 +01:00
logger . verbose ( ` Error pinging Plejd, calling onWriteFailed... ${ err . message } ` ) ;
2021-02-20 07:55:26 +01:00
await this . _onWriteFailed ( err ) ;
2020-01-17 14:50:58 +00:00
return ;
}
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line no-bitwise
2020-01-19 20:08:48 +00:00
if ( ( ( ping [ 0 ] + 1 ) & 0xff ) !== pong [ 0 ] ) {
2021-02-11 22:45:13 +01:00
logger . verbose ( 'Plejd ping failed, pong contains wrong data. Calling onWriteFailed...' ) ;
2021-02-20 07:55:26 +01:00
await this . _onWriteFailed ( new Error ( ` plejd ping failed ${ ping [ 0 ] } - ${ pong [ 0 ] } ` ) ) ;
2020-01-19 20:08:48 +00:00
return ;
}
2020-01-17 14:50:58 +00:00
2021-02-08 19:54:24 +01:00
logger . silly ( ` pong: ${ pong [ 0 ] } ` ) ;
2021-02-20 07:55:26 +01:00
await this . _onWriteSuccess ( ) ;
2020-01-17 14:50:58 +00:00
}
2021-02-18 20:21:57 +01:00
async _requestCurrentPlejdTime ( ) {
2021-02-18 22:24:20 +01:00
if ( ! this . connectedDevice ) {
logger . warn ( 'Cannot request current Plejd time, not connected.' ) ;
return ;
}
logger . info ( 'Requesting current Plejd time...' ) ;
2021-02-20 07:55:26 +01:00
const payload = this . _createHexPayload (
2021-02-18 22:24:20 +01:00
this . connectedDevice . id ,
BLE _CMD _TIME _UPDATE ,
'' ,
BLE _REQUEST _RESPONSE ,
2021-02-18 10:47:33 +01:00
) ;
2021-01-18 17:21:37 +01:00
try {
2021-02-20 07:55:26 +01:00
this . _write ( payload ) ;
} catch ( error ) {
logger . warn ( 'Failed requesting time update from Plejd' ) ;
2020-03-03 15:59:10 +01:00
}
2021-02-20 07:55:26 +01:00
clearTimeout ( this . requestCurrentPlejdTimeRef ) ;
this . requestCurrentPlejdTimeRef = setTimeout (
( ) => this . _requestCurrentPlejdTime ( ) ,
1000 * 3600 ,
) ; // Once per hour
2020-02-29 15:54:08 +00:00
}
2020-01-19 20:08:48 +00:00
async _processPlejdService ( path , characteristics ) {
const proxyObject = await this . bus . getProxyObject ( BLUEZ _SERVICE _NAME , path ) ;
const properties = await proxyObject . getInterface ( DBUS _PROP _INTERFACE ) ;
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
const uuid = ( await properties . Get ( GATT _SERVICE _ID , 'UUID' ) ) . value ;
if ( uuid !== PLEJD _SERVICE ) {
2021-01-21 21:31:37 +01:00
logger . error ( 'not a Plejd device.' ) ;
2020-01-19 20:08:48 +00:00
return null ;
}
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
const dev = ( await properties . Get ( GATT _SERVICE _ID , 'Device' ) ) . value ;
const regex = /dev_([0-9A-F_]+)$/ ;
const dirtyAddr = regex . exec ( dev ) ;
const addr = this . _reverseBuffer (
2021-01-25 08:06:28 +01:00
Buffer . from (
String ( dirtyAddr [ 1 ] ) . replace ( /-/g , '' ) . replace ( /_/g , '' ) . replace ( /:/g , '' ) ,
'hex' ,
) ,
2020-01-19 20:08:48 +00:00
) ;
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line no-restricted-syntax
2020-01-19 20:08:48 +00:00
for ( const chPath of characteristics ) {
2021-01-22 15:49:02 +01:00
/* eslint-disable no-await-in-loop */
2020-01-19 20:08:48 +00:00
const chProxyObject = await this . bus . getProxyObject ( BLUEZ _SERVICE _NAME , chPath ) ;
const ch = await chProxyObject . getInterface ( GATT _CHRC _ID ) ;
const prop = await chProxyObject . getInterface ( DBUS _PROP _INTERFACE ) ;
const chUuid = ( await prop . Get ( GATT _CHRC _ID , 'UUID' ) ) . value ;
if ( chUuid === DATA _UUID ) {
2021-02-08 19:54:24 +01:00
logger . verbose ( 'found DATA characteristic.' ) ;
2020-01-19 20:08:48 +00:00
this . characteristics . data = ch ;
2020-06-12 11:15:00 +02:00
} else if ( chUuid === LAST _DATA _UUID ) {
2021-02-08 19:54:24 +01:00
logger . verbose ( 'found LAST_DATA characteristic.' ) ;
2020-01-19 20:08:48 +00:00
this . characteristics . lastData = ch ;
this . characteristics . lastDataProperties = prop ;
2020-06-12 11:15:00 +02:00
} else if ( chUuid === AUTH _UUID ) {
2021-02-08 19:54:24 +01:00
logger . verbose ( 'found AUTH characteristic.' ) ;
2020-01-19 20:08:48 +00:00
this . characteristics . auth = ch ;
2020-06-12 11:15:00 +02:00
} else if ( chUuid === PING _UUID ) {
2021-02-08 19:54:24 +01:00
logger . verbose ( 'found PING characteristic.' ) ;
2020-01-19 20:08:48 +00:00
this . characteristics . ping = ch ;
}
2021-01-22 15:49:02 +01:00
/* eslint-eslint no-await-in-loop */
2020-01-19 20:08:48 +00:00
}
return {
2021-01-22 15:49:02 +01:00
addr ,
2020-01-19 20:08:48 +00:00
} ;
2020-01-17 14:50:58 +00:00
}
2021-02-08 07:38:31 +01:00
async _onDeviceConnected ( device ) {
this . connectedDevice = null ;
2021-01-21 21:31:37 +01:00
logger . info ( 'onDeviceConnected()' ) ;
2021-02-08 19:54:24 +01:00
logger . debug ( ` Device ${ device . path } , ${ JSON . stringify ( device . device ) } ` ) ;
2020-01-19 20:08:48 +00:00
const objects = await this . objectManager . GetManagedObjects ( ) ;
2020-01-24 17:30:17 +00:00
const paths = Object . keys ( objects ) ;
2021-01-22 15:49:02 +01:00
const characteristics = [ ] ;
2020-01-19 20:08:48 +00:00
2021-02-08 19:54:24 +01:00
logger . verbose ( ` Iterating connected devices looking for ${ GATT _CHRC _ID } ` ) ;
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line no-restricted-syntax
2020-01-24 17:30:17 +00:00
for ( const path of paths ) {
2020-01-19 20:08:48 +00:00
const interfaces = Object . keys ( objects [ path ] ) ;
2021-02-08 19:54:24 +01:00
logger . verbose ( ` Interfaces ${ path } : ${ JSON . stringify ( interfaces ) } ` ) ;
2020-01-19 20:08:48 +00:00
if ( interfaces . indexOf ( GATT _CHRC _ID ) > - 1 ) {
characteristics . push ( path ) ;
}
2020-01-17 14:50:58 +00:00
}
2021-02-08 19:54:24 +01:00
logger . verbose ( ` Characteristics found: ${ JSON . stringify ( characteristics ) } ` ) ;
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line no-restricted-syntax
2020-01-24 17:30:17 +00:00
for ( const path of paths ) {
2020-01-19 20:08:48 +00:00
const interfaces = Object . keys ( objects [ path ] ) ;
if ( interfaces . indexOf ( GATT _SERVICE _ID ) > - 1 ) {
2021-01-22 15:49:02 +01:00
const chPaths = [ ] ;
// eslint-disable-next-line no-restricted-syntax
2020-01-19 20:08:48 +00:00
for ( const c of characteristics ) {
2021-01-22 15:49:02 +01:00
if ( c . startsWith ( ` ${ path } / ` ) ) {
2020-01-19 20:08:48 +00:00
chPaths . push ( c ) ;
}
}
2021-02-08 19:54:24 +01:00
logger . verbose ( ` Trying ${ chPaths . length } characteristics on ${ path } ... ` ) ;
2020-01-17 14:50:58 +00:00
2020-01-19 20:08:48 +00:00
this . plejdService = await this . _processPlejdService ( path , chPaths ) ;
if ( this . plejdService ) {
break ;
}
}
}
if ( ! this . plejdService ) {
2021-02-08 07:38:31 +01:00
logger . warn ( "Wasn't able to connect to Plejd, will retry." ) ;
return null ;
2020-01-17 14:50:58 +00:00
}
2020-01-24 10:20:39 +01:00
if ( ! this . characteristics . auth ) {
2021-01-21 21:31:37 +01:00
logger . error ( 'unable to enumerate characteristics.' ) ;
2021-02-08 07:38:31 +01:00
return null ;
2020-01-24 10:20:39 +01:00
}
2021-02-08 19:54:24 +01:00
logger . info ( 'Connected device is a Plejd device with the right characteristics.' ) ;
2021-01-22 15:49:02 +01:00
this . connectedDevice = device . device ;
2021-02-20 07:55:26 +01:00
await this . _authenticate ( ) ;
2021-02-08 07:38:31 +01:00
return this . connectedDevice ;
2020-01-17 14:50:58 +00:00
}
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line no-unused-vars
2021-02-20 07:55:26 +01:00
async _onLastDataUpdated ( iface , properties ) {
2020-01-20 07:58:23 +00:00
if ( iface !== GATT _CHRC _ID ) {
return ;
}
2020-01-20 10:58:03 +00:00
2020-01-20 07:58:23 +00:00
const changedKeys = Object . keys ( properties ) ;
if ( changedKeys . length === 0 ) {
return ;
}
2021-01-22 15:49:02 +01:00
const value = await properties . Value ;
2020-01-20 10:58:03 +00:00
if ( ! value ) {
return ;
}
2021-02-20 07:55:26 +01:00
const encryptedData = value . value ;
const decoded = this . _encryptDecrypt ( this . cryptoKey , this . plejdService . addr , encryptedData ) ;
2020-01-17 14:50:58 +00:00
if ( decoded . length < 5 ) {
2021-02-02 19:23:19 +01:00
if ( Logger . shouldLog ( 'debug' ) ) {
// decoded.toString() could potentially be expensive
2021-02-08 19:54:24 +01:00
logger . verbose ( ` Too short raw event ignored: ${ decoded . toString ( 'hex' ) } ` ) ;
2021-02-02 19:23:19 +01:00
}
2020-01-17 14:50:58 +00:00
// ignore the notification since too small
return ;
}
2021-03-31 20:04:45 +02:00
const bleOutputAddress = decoded . readUInt8 ( 0 ) ;
2021-02-18 10:47:33 +01:00
// Bytes 2-3 is Command/Request
const cmd = decoded . readUInt16BE ( 3 ) ;
2021-02-02 19:23:19 +01:00
const state = decoded . length > 5 ? decoded . readUInt8 ( 5 ) : 0 ;
2021-02-18 10:47:33 +01:00
2021-02-02 19:23:19 +01:00
const dim = decoded . length > 7 ? decoded . readUInt8 ( 7 ) : 0 ;
2021-02-18 10:47:33 +01:00
if ( Logger . shouldLog ( 'silly' ) ) {
// Full dim level is 2 bytes, we could potentially use this
const dimFull = decoded . length > 7 ? decoded . readUInt16LE ( 6 ) : 0 ;
logger . silly ( ` Dim: ${ dim . toString ( 16 ) } , full precision: ${ dimFull . toString ( 16 ) } ` ) ;
}
2021-02-02 19:23:19 +01:00
2021-03-31 20:04:45 +02:00
const device = this . deviceRegistry . getOutputDeviceByBleOutputAddress ( bleOutputAddress ) ;
const deviceName = device ? device . name : 'Unknown' ;
const outputUniqueId = device ? device . uniqueId : null ;
2021-02-18 10:47:33 +01:00
if ( Logger . shouldLog ( 'verbose' ) ) {
2021-02-02 19:23:19 +01:00
// decoded.toString() could potentially be expensive
2021-02-08 19:54:24 +01:00
logger . verbose ( ` Raw event received: ${ decoded . toString ( 'hex' ) } ` ) ;
logger . verbose (
2021-03-31 23:28:25 +02:00
` Decoded: Device ${ outputUniqueId } (BLE address ${ bleOutputAddress } ), cmd ${ cmd . toString (
16 ,
) } , state $ { state } , dim $ { dim } ` ,
2021-02-08 19:54:24 +01:00
) ;
2021-02-02 19:23:19 +01:00
}
2020-01-17 14:50:58 +00:00
2021-02-20 07:55:26 +01:00
let command ;
let data = { } ;
2020-01-17 14:50:58 +00:00
if ( cmd === BLE _CMD _DIM _CHANGE || cmd === BLE _CMD _DIM2 _CHANGE ) {
2021-03-31 20:04:45 +02:00
logger . debug (
` ${ deviceName } ( ${ outputUniqueId } ) got state+dim update. S: ${ state } , D: ${ dim } ` ,
) ;
2020-06-12 11:15:00 +02:00
2021-02-20 07:55:26 +01:00
command = COMMANDS . DIM ;
data = { state , dim } ;
2021-03-31 20:04:45 +02:00
this . emit ( PlejBLEHandler . EVENTS . commandReceived , outputUniqueId , command , data ) ;
2020-06-12 11:15:00 +02:00
} else if ( cmd === BLE _CMD _STATE _CHANGE ) {
2021-03-31 20:04:45 +02:00
logger . debug ( ` ${ deviceName } ( ${ outputUniqueId } ) got state update. S: ${ state } ` ) ;
2021-02-20 07:55:26 +01:00
command = state ? COMMANDS . TURN _ON : COMMANDS . TURN _OFF ;
2021-03-31 20:04:45 +02:00
this . emit ( PlejBLEHandler . EVENTS . commandReceived , outputUniqueId , command , data ) ;
2020-06-12 11:15:00 +02:00
} else if ( cmd === BLE _CMD _SCENE _TRIG ) {
2021-04-01 13:19:02 +02:00
const sceneBleAddress = state ;
const scene = this . deviceRegistry . getSceneByBleAddress ( sceneBleAddress ) ;
2020-01-17 14:50:58 +00:00
2021-04-01 13:19:02 +02:00
if ( ! scene ) {
2021-04-07 10:26:33 +02:00
logger . warn (
` Scene with BLE address ${ sceneBleAddress } could not be found, can't process message ` ,
) ;
2021-04-01 13:19:02 +02:00
return ;
}
2021-04-07 10:26:33 +02:00
logger . debug (
` ${ scene . name } ( ${ sceneBleAddress } ) scene triggered (device id ${ outputUniqueId } ). ` ,
) ;
2021-01-18 17:21:37 +01:00
2021-02-20 07:55:26 +01:00
command = COMMANDS . TRIGGER _SCENE ;
2021-04-01 13:19:02 +02:00
data = { sceneId : scene . uniqueId } ;
2021-03-31 20:04:45 +02:00
this . emit ( PlejBLEHandler . EVENTS . commandReceived , outputUniqueId , command , data ) ;
2021-02-18 10:47:33 +01:00
} else if ( cmd === BLE _CMD _TIME _UPDATE ) {
const now = new Date ( ) ;
// Guess Plejd timezone based on HA time zone
2021-02-18 22:24:20 +01:00
const offsetSecondsGuess = now . getTimezoneOffset ( ) * 60 + 250 ; // Todo: 4 min off
2021-02-18 10:47:33 +01:00
// Plejd reports local unix timestamp adjust to local time zone
const plejdTimestampUTC = ( decoded . readInt32LE ( 5 ) + offsetSecondsGuess ) * 1000 ;
const diffSeconds = Math . round ( ( plejdTimestampUTC - now . getTime ( ) ) / 1000 ) ;
if (
2021-03-31 20:04:45 +02:00
bleOutputAddress !== BLE _BROADCAST _DEVICE _ID
2021-02-18 10:47:33 +01:00
|| Logger . shouldLog ( 'verbose' )
|| Math . abs ( diffSeconds ) > 60
) {
const plejdTime = new Date ( plejdTimestampUTC ) ;
2021-02-18 21:17:29 +01:00
logger . debug (
` Plejd clock time update ${ plejdTime . toString ( ) } , diff ${ diffSeconds } seconds ` ,
) ;
if ( this . config . updatePlejdClock && Math . abs ( diffSeconds ) > 60 ) {
2021-02-18 10:47:33 +01:00
logger . warn (
2021-02-18 21:17:29 +01:00
` Plejd clock time off by more than 1 minute. Reported time: ${ plejdTime . toString ( ) } , diff ${ diffSeconds } seconds. Time will be set hourly. ` ,
2021-02-18 10:47:33 +01:00
) ;
2021-03-31 20:04:45 +02:00
if ( this . connectedDevice && bleOutputAddress === this . connectedDevice . id ) {
2021-02-18 22:24:20 +01:00
// Requested time sync by us
2021-02-18 21:38:11 +01:00
const newLocalTimestamp = now . getTime ( ) / 1000 - offsetSecondsGuess ;
2021-02-18 20:21:57 +01:00
logger . info ( ` Setting time to ${ now . toString ( ) } ` ) ;
2021-02-20 07:55:26 +01:00
const payload = this . _createPayload (
2021-02-18 22:24:20 +01:00
this . connectedDevice . id ,
BLE _CMD _TIME _UPDATE ,
10 ,
2021-02-20 07:55:26 +01:00
( pl ) => pl . writeInt32LE ( Math . trunc ( newLocalTimestamp ) , 5 ) ,
2021-02-18 22:24:20 +01:00
) ;
2021-02-20 07:55:26 +01:00
try {
2021-03-29 12:48:27 +02:00
this . _write ( payload ) ;
2021-02-20 07:55:26 +01:00
} catch ( err ) {
2021-02-20 10:49:00 +01:00
logger . error (
'Failed writing new time to Plejd. Will try again in one hour or at restart.' ,
) ;
2021-02-20 07:55:26 +01:00
}
2021-02-18 20:21:57 +01:00
}
2021-03-31 20:04:45 +02:00
} else if ( bleOutputAddress !== BLE _BROADCAST _DEVICE _ID ) {
2021-02-18 21:17:29 +01:00
logger . info ( 'Got time response. Plejd clock time in sync with Home Assistant time' ) ;
2021-02-18 10:47:33 +01:00
}
}
2021-01-22 15:49:02 +01:00
} else {
2021-02-02 19:23:19 +01:00
logger . verbose (
` Command ${ cmd . toString ( 16 ) } unknown. ${ decoded . toString (
'hex' ,
2021-03-31 20:04:45 +02:00
) } . Device $ { deviceName } ( $ { bleOutputAddress } : $ { outputUniqueId } ) ` ,
2021-02-02 19:23:19 +01:00
) ;
2021-01-18 17:21:37 +01:00
}
2020-01-17 14:50:58 +00:00
}
2021-02-20 07:55:26 +01:00
_createHexPayload (
2021-03-31 20:04:45 +02:00
bleOutputAddress ,
2021-02-18 22:24:20 +01:00
command ,
hexDataString ,
requestResponseCommand = BLE _REQUEST _NO _RESPONSE ,
) {
2021-02-20 07:55:26 +01:00
return this . _createPayload (
2021-03-31 20:04:45 +02:00
bleOutputAddress ,
2021-02-18 22:24:20 +01:00
command ,
5 + Math . ceil ( hexDataString . length / 2 ) ,
( payload ) => payload . write ( hexDataString , 5 , 'hex' ) ,
requestResponseCommand ,
) ;
}
2021-02-20 07:55:26 +01:00
// eslint-disable-next-line class-methods-use-this
_createPayload (
2021-03-31 20:04:45 +02:00
bleOutputAddress ,
2021-02-18 22:24:20 +01:00
command ,
bufferLength ,
payloadBufferAddDataFunc ,
requestResponseCommand = BLE _REQUEST _NO _RESPONSE ,
) {
const payload = Buffer . alloc ( bufferLength ) ;
2021-03-31 20:04:45 +02:00
payload . writeUInt8 ( bleOutputAddress ) ;
2021-02-18 22:24:20 +01:00
payload . writeUInt16BE ( requestResponseCommand , 1 ) ;
payload . writeUInt16BE ( command , 3 ) ;
payloadBufferAddDataFunc ( payload ) ;
2021-02-20 07:55:26 +01:00
return payload ;
2021-02-18 22:24:20 +01:00
}
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line class-methods-use-this
2020-01-17 14:50:58 +00:00
_createChallengeResponse ( key , challenge ) {
const intermediate = crypto . createHash ( 'sha256' ) . update ( xor ( key , challenge ) ) . digest ( ) ;
const part1 = intermediate . subarray ( 0 , 16 ) ;
const part2 = intermediate . subarray ( 16 ) ;
const resp = xor ( part1 , part2 ) ;
return resp ;
}
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line class-methods-use-this
2020-01-17 14:50:58 +00:00
_encryptDecrypt ( key , addr , data ) {
2021-01-22 15:49:02 +01:00
const buf = Buffer . concat ( [ addr , addr , addr . subarray ( 0 , 4 ) ] ) ;
2020-01-17 14:50:58 +00:00
2021-01-22 15:49:02 +01:00
const cipher = crypto . createCipheriv ( 'aes-128-ecb' , key , '' ) ;
2020-01-17 14:50:58 +00:00
cipher . setAutoPadding ( false ) ;
2021-01-22 15:49:02 +01:00
let ct = cipher . update ( buf ) . toString ( 'hex' ) ;
2020-01-17 14:50:58 +00:00
ct += cipher . final ( ) . toString ( 'hex' ) ;
2021-03-31 20:04:45 +02:00
const ctBuf = Buffer . from ( ct , 'hex' ) ;
2020-01-17 14:50:58 +00:00
2021-01-22 15:49:02 +01:00
let output = '' ;
for ( let i = 0 , { length } = data ; i < length ; i ++ ) {
// eslint-disable-next-line no-bitwise
2021-03-31 20:04:45 +02:00
output += String . fromCharCode ( data [ i ] ^ ctBuf [ i % 16 ] ) ;
2020-01-17 14:50:58 +00:00
}
return Buffer . from ( output , 'ascii' ) ;
}
2021-01-22 15:49:02 +01:00
// eslint-disable-next-line class-methods-use-this
2020-01-17 14:50:58 +00:00
_reverseBuffer ( src ) {
2021-01-22 15:49:02 +01:00
const buffer = Buffer . allocUnsafe ( src . length ) ;
2020-01-17 14:50:58 +00:00
2021-01-22 15:49:02 +01:00
for ( let i = 0 , j = src . length - 1 ; i <= j ; ++ i , -- j ) {
buffer [ i ] = src [ j ] ;
buffer [ j ] = src [ i ] ;
2020-01-17 14:50:58 +00:00
}
2021-01-22 15:49:02 +01:00
return buffer ;
2020-01-17 14:50:58 +00:00
}
}
2021-02-08 07:38:31 +01:00
module . exports = PlejBLEHandler ;