homebridge-myplace
Version:
Exec Plugin bringing Advanatge Air MyPlace system to Homekit
1,183 lines (940 loc) • 50.9 kB
JavaScript
'use strict';
// 3rd Party includes
const exec = require( "child_process" ).exec;
// These would already be initialized by index.js
let CMD5_ACC_TYPE_ENUM = require( "./lib/CMD5_ACC_TYPE_ENUM" ).CMD5_ACC_TYPE_ENUM;
// Settings, Globals and Constants
let settings = require( "./cmd5Settings" );
const constants = require( "./cmd5Constants" );
// Pretty Colors
var chalk = require( "chalk" );
let trueTypeOf = require( "./utils/trueTypeOf" );
let lcFirst = require( "./utils/lcFirst" );
// For changing validValue Constants to Values and back again
var { transposeConstantToValidValue,
transposeValueToValidConstant,
transposeBoolToValue
} = require( "./utils/transposeCMD5Props" );
let HIGH_PRIORITY_SET = 0;
let HIGH_PRIORITY_GET = 1;
let LOW_PRIORITY_GET = 2;
class Cmd5PriorityPollingQueue
{
constructor( log, queueName, queueType = constants.DEFAULT_QUEUE_TYPE, queueRetryCount = constants.DEFAULT_WORM_QUEUE_RETRY_COUNT )
{
this.log = log;
// This works better for Unit testing
settings.cmd5Dbg = log.debugEnabled;
this.queueName = queueName;
this.queueType = queueType;
this.queueRetryCount = queueRetryCount;
this.queueStarted = false;
this.highPriorityQueue = [ ];
this.lowPriorityQueue = [ ];
this.lowPriorityQueueIndex = 0 ;
this.inProgressGets = 0;
this.inProgressSets = 0;
this.listOfRunningPolls = {};
// This is not a sanity timer.
// This controls when it is safe to do a "Get" of the Aircon
// after a failed condition. It does happen to fix the queue
// when something is wrong, but this is not the purpose of
// this timer.
this.pauseTimer = null;
this.lastGoodTransactionTime = Date.now( );
this.errorCountSinceLastGoodTransaction = 0;
// - Not a const so it can be manipulated during unit testing
this.pauseTimerTimeout = constants.DEFAULT_QUEUE_PAUSE_TIMEOUT;
// The WoRm queue needs error messages to be silenced as
// they are inevitable, but are handled through retries
// By default non WoRm queues are allowed to echo errors
if ( this.queueRetryCount == 0 || settings.debug )
this.echoE = true;
else
this.echoE = true;
this.changeQueueType( this, queueType );
}
echoRetryErrors( currentRetryCount )
{
// If debug then the default is true
if ( settings.cmd5Debug )
return true;
// Since this is the last retry, echo the error
if ( currentRetryCount == this.queueRetryCount )
return true;
return false;
}
// This function is called by homebridge to *PUT AN ENTRY INTO THE HIGHEST PRIORITY SET QUEUE*.
// We immediately return success if the device is accessible and the Set request will be sent,
// if not the Set request will be aborted.
prioritySetValue( accTypeEnumIndex, characteristicString, timeout, stateChangeResponseTime, value, homebridgeCallback )
{
// this is Accessory
//
//if ( settings.cmd5Dbg ) this.log.debug(`prioritySetValue, asked to set: ${ characteristicString } to ${ value }`);
// Save the value to cache. The set will come later
// this.cmd5Storage.setStoredValueForIndex( accTypeEnumIndex, value );
if ( this.errorValue != 0 )
{
if ( settings.cmd5Dbg ) this.log.debug(`prioritySetValue for ${ this.displayName }, homebridgeCallback returning error ${ this.errorValue } ${ this.errorString }`);
// retrieved and reset to the storedValue on Homekit, then abort the setValue request
let storedValue = this.cmd5Storage.getStoredValueForIndex( accTypeEnumIndex );
this.service.getCharacteristic( CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ].characteristic ).updateValue( storedValue );
homebridgeCallback( this.errorValue );
this.log.error(`prioritySetValue: ${ this.displayName } not accessible, Setting ${ this.displayName } ${ characteristicString } ${ value } aborted!`);
return; // abort the setValue request
} else
{
if ( settings.cmd5Dbg ) this.log.debug(`prioritySetValue for ${ this.displayName }, homebridgeCallback returning default success 0`);
homebridgeCallback( 0 );
// For homebridge-myplace users only
if ( this.state_cmd.match( /MyPlace.sh'$/ ) )
{
// Reject Set requests to change Fanv2 rotationSpeed or to turn off myZone for zones with temperature sensors
if ( this.typeIndex == 20 && this.displayName.match ( / Zone$/ ) &&
( characteristicString == 'RotationSpeed' ||
( characteristicString == 'RotationDirection' && value == 1 )
)
)
{
let storedValue = this.cmd5Storage.getStoredValueForIndex( accTypeEnumIndex );
this.service.getCharacteristic( CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ].characteristic ).updateValue( storedValue );
// Abort this Set request
return;
}
// Reject Set requests to change Thermostat targetHeatingCoolingState for Zone Thermostat
if ( this.typeIndex == 57 && this.displayName.match ( / Thermostat$/ ) &&
characteristicString == 'TargetHeatingCoolingState'
)
{
let storedValue = this.cmd5Storage.getStoredValueForIndex( accTypeEnumIndex );
this.service.getCharacteristic( CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ].characteristic ).updateValue( storedValue );
// Abort this Set request
return;
}
// Turn on the Zone when this Zone is set as myZone
else if ( this.typeIndex == 20 && this.displayName.match ( / Zone$/ ) &&
characteristicString == 'RotationDirection' && value == 0
)
{
this.service.getCharacteristic( 'Active' ).updateValue( 1 );
}
else if ( this.displayName.match( / FanSpeed$/ ) )
{
// Abort if 'on' or 'rotationSpeed' value = 0, this accessory is always on
if ( value == 0 )
{
let storedValue = this.cmd5Storage.getStoredValueForIndex( accTypeEnumIndex );
this.service.getCharacteristic( CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ].characteristic ).updateValue( storedValue );
return;
}
// set the value to 25% if value <= 33% else 50% if value <=67% else 90% if value <=99% for Aircon RotationSpeed
else if ( characteristicString == 'RotationSpeed' && value < 100 )
{
value = Math.ceil( value / 33.8 ) * 25;
value = value >= 75 ? 90 : value;
this.service.getCharacteristic( CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ].characteristic ).updateValue( value );
}
}
}
}
let newEntry = { [ constants.IS_SET_lv ]: true, [ constants.ACCESSORY_lv ]: this, [ constants.ACC_TYPE_ENUM_INDEX_lv ]: accTypeEnumIndex, [ constants.CHARACTERISTIC_STRING_lv ]: characteristicString, [ constants.TIMEOUT_lv ]: timeout, [ constants.STATE_CHANGE_RESPONSE_TIME_lv ]: stateChangeResponseTime, [ constants.CALLBACK_lv ]: homebridgeCallback, [ constants.VALUE_lv ]: value };
// Determine where to put the entry in the queue
if ( this.queue.highPriorityQueue.length == 0 )
{
// No entries, then it goes on top
this.queue.highPriorityQueue.push( newEntry );
} else {
// Make sure that this is the latest "Set" of this entry
let index = this.queue.highPriorityQueue.findIndex( ( entry ) => entry.accessory.uuid == this.uuid && entry.isSet == true && entry.accTypeEnumIndex == accTypeEnumIndex );
if ( index == -1 )
{
// It doesn't exist in the queue, It needs to be placed after any "Sets".
// First Determine the first "Get"
let getIndex = this.queue.highPriorityQueue.findIndex( ( entry ) => entry.isSet == false );
if ( getIndex == -1 )
{
// No "Get" entrys, it goes at the end after everything.
this.queue.highPriorityQueue.push( newEntry );
} else
{
// Insert before the first "Get" entry
this.queue.highPriorityQueue.splice( getIndex, 0, newEntry );
}
} else
{
this.queue.highPriorityQueue[ index ] = newEntry;
}
}
this.queue.processQueueFunc( HIGH_PRIORITY_SET, this.queue );
}
// This function is called by homebridge to *PUT AN ENTRY INTO THE HIGHEST PRIORITY GET QUEUE*.
// We immediately return with success and the last known value if the device is accessible, otherwise
// the last failure error code.
// The Get is attempted no matter the devices availability. This is done after every Set
// request and at the bottom of the hightPrioritySetValue queue, but above any polling.
priorityGetValue( accTypeEnumIndex, characteristicString, timeout, homebridgeCallback )
{
// this is Accessory
// if ( settings.cmd5Dbg ) this.log.debug(`priorityGetValue for ${ this.displayName }, asked to Get: ${ characteristicString }`);
if ( this.errorValue != 0 )
{
// if ( settings.cmd5Dbg ) this.log.debug(`priorityGetValue for ${ this.displayName }, homebridgeCallback returning error ${ this.errorValue } ${ this.errorString}`);
homebridgeCallback( this.errorValue );
} else
{
// return the cached value
let storedValue = this.cmd5Storage.getStoredValueForIndex( accTypeEnumIndex );
// if ( settings.cmd5Dbg ) this.log.debug(`priorityGetValue for ${ this.displayName }, homebridgeCallback returning storedValue: ${ storedValue }`);
homebridgeCallback( 0, storedValue );
}
if ( this.queue.queueType != constants.QUEUETYPE_WORM2 )
{
// When the value is returned, it will update homebridge
this.queue.highPriorityQueue.push( { [ constants.IS_SET_lv ]: false, [ constants.QUEUE_GET_IS_UPDATE_lv ]: true, [ constants.ACCESSORY_lv ]: this, [ constants.ACC_TYPE_ENUM_INDEX_lv ]: accTypeEnumIndex, [ constants.CHARACTERISTIC_STRING_lv ]: characteristicString, [ constants.TIMEOUT_lv ]: timeout, [ constants.STATE_CHANGE_RESPONSE_TIME_lv ]: null, [ constants.VALUE_lv ]: null, [ constants.CALLBACK_lv ]: homebridgeCallback } );
this.queue.processQueueFunc( HIGH_PRIORITY_GET, this.queue );
}
}
// This function is called by polling to *PUT AN ENTRY INTO THE LOW PRIORITY POLLING QUEUE*.
addLowPriorityGetPolledQueueEntry( accessory, accTypeEnumIndex, characteristicString, interval, timeout )
{
// These are all gets from polling
accessory.queue.lowPriorityQueue.push( { [ constants.ACCESSORY_lv ]: accessory, [ constants.ACC_TYPE_ENUM_INDEX_lv ]: accTypeEnumIndex, [ constants.CHARACTERISTIC_STRING_lv ]: characteristicString, [ constants.INTERVAL_lv ]: interval, [ constants.TIMEOUT_lv ]: timeout } );
}
processHighPrioritySetQueue( entry )
{
if ( settings.cmd5Dbg ) this.log.debug( `Processing high priority queue "Set" entry: ${ entry.accTypeEnumIndex } length: ${ this.highPriorityQueue.length }` );
this.inProgressSets ++;
this.qSetValue( entry.accessory, entry.accTypeEnumIndex, entry.characteristicString, entry.timeout, entry.value, function ( error )
{
let queue = entry.accessory.queue;
// Save the error code - Pass or fail
entry.accessory.errorValue = error;
if ( error == 0 )
{
// Now that the set was successful, store the value
entry.accessory.cmd5Storage.setStoredValueForIndex( entry.accTypeEnumIndex, entry.value );
// Since the "Set" passed, do the stateChangeResponseTime
setTimeout( ( ) =>
{
// A set with no error means the queue is sane to do reading
queue.lastGoodTransactionTime = Date.now( );
queue.errorCountSinceLastGoodTransaction = 0;
// After the stateChangeResponseTime, do the related characteristic ( if any )
let relatedCurrentAccTypeEnumIndex = entry.accessory.getDevicesRelatedCurrentAccTypeEnumIndex( entry.accTypeEnumIndex );
if ( relatedCurrentAccTypeEnumIndex != null )
{
let relatedCurrentCharacteristicString = CMD5_ACC_TYPE_ENUM.properties[ relatedCurrentAccTypeEnumIndex ].type;
// Change the entry to a get and set queueGetIsUpdate to true
// Use unshift to make it next in line
entry.isSet = false;
entry.accTypeEnumIndex = relatedCurrentAccTypeEnumIndex;
entry.characteristicString = relatedCurrentCharacteristicString;
entry.queueGetIsUpdate = true;
queue.highPriorityQueue.unshift( entry );
}
// The "Set" is now complete after its stateChangeResponseTime.
queue.inProgressSets --;
setTimeout( ( ) => { queue.processQueueFunc( HIGH_PRIORITY_GET, queue ); }, 0 );
return;
}, entry.stateChangeResponseTime );
} else // setValue failed
{
// The "Set" is complete, even if it failed.
queue.inProgressSets --;
// retrieved and reset to the storedValue
let storedValue = entry.accessory.cmd5Storage.getStoredValueForIndex( entry.accTypeEnumIndex );
entry.accessory.service.getCharacteristic( CMD5_ACC_TYPE_ENUM.properties[ entry.accTypeEnumIndex ].characteristic ).updateValue( storedValue );
let currentRetryCount = queue.errorCountSinceLastGoodTransaction;
if ( currentRetryCount >= queue.queueRetryCount )
{
if ( queue.echoRetryErrors( currentRetryCount ) )
{
// Counting starts from zero, i.e queueRetries = 0, so add 1
queue.log.warn( `*${ currentRetryCount + 1 }* error(s) were encountered for "${ entry.accessory.displayName }" getValue. Last error found Getting: "${ entry.characteristicString}". Perhaps you should run in debug mode to find out what the problem might be.` );
}
// Convert the errorValue into an errorString
entry.accessory.errorString = new Error( constants.errorString( error ) );
// This does not work - Nothing happens to HomeKit !
// queue.log.warn( `START processHighPrioritySetQueue calling updateCharacteristic errorValue: ${ entry.accessory.errorValue} errorString: ${ entry.accessory.errorString }`);
// entry.accessory.service.getCharacteristic( CMD5_ACC_TYPE_ENUM.properties[ entry.accTypeEnumIndex ].characteristic ).updateValue( entry.accessory.errorString );
// queue.log.warn( `END processHighPrioritySetQueue calling updateCharacteristic errorValue: ${ entry.accessory.errorValue} errorString: ${ entry.accessory.errorString }`);
} else
{
// Increment the errorCount/currentRetryCount
queue.errorCountSinceLastGoodTransaction++;
// Set failed. We need to keep trying
queue.highPriorityQueue.push( entry );
}
entry.accessory.queue.pauseQueue( entry.accessory.queue );
}
// Note 1.
// Do not call the callback as it was done when the "Set" entry was
// created.
// Note 2.
// We cannot release the queue for further processing as the
// statechangeResponseTime has not completed. This must be
// done first or any next "Get" or "Set" would interfere
// with the device
});
}
processHighPriorityGetQueue( entry )
{
if ( settings.cmd5Dbg ) this.log.debug( `Processing high priority queue "Get" entry: ${ entry.accTypeEnumIndex } isUpdate: ${ entry.queueGetIsUpdate } length: ${ this.highPriorityQueue.length }` );
this.inProgressGets ++;
this.qGetValue( entry.accessory, entry.accTypeEnumIndex, entry.characteristicString, entry.timeout, function ( error, properValue )
{
let queue = entry.accessory.queue;
// Save the error code - Pass or fail
entry.accessory.errorValue = error;
// Nothing special was done for casing on errors, so omit it.
if ( error == 0 )
{
// Save the new returned value
entry.accessory.cmd5Storage.setStoredValueForIndex( entry.accTypeEnumIndex, properValue );
// hmmm if ( entry.queueGetIsUpdate == true )
entry.accessory.service.getCharacteristic( CMD5_ACC_TYPE_ENUM.properties[ entry.accTypeEnumIndex ].characteristic ).updateValue( properValue );
// A good anything, updates the lastGoodTransactionTime
queue.lastGoodTransactionTime = Date.now( );
queue.errorCountSinceLastGoodTransaction = 0;
} else // highPriority getValue failed
{
let currentRetryCount = queue.errorCountSinceLastGoodTransaction;
if ( currentRetryCount >= queue.queueRetryCount )
{
if ( queue.echoRetryErrors( currentRetryCount ) )
queue.log.warn( `*${ currentRetryCount + 1}* error(s) were encountered for "${ entry.accessory.displayName }" getValue. Last error found Getting: "${ entry.characteristicString}". Perhaps you should run in debug mode to find out what the problem might be.` );
// Convert the errorValue into an errorString
entry.accessory.errorString = new Error( constants.errorString( error ) );
// This does not work - Nothing happens to HomeKit !
// queue.log.warn( `START processHighPriorityGetQueue calling updateCharacteristic errorValue: ${ entry.accessory.errorValue} errorString: ${ entry.accessory.errorString }`);
// entry.accessory.service.getCharacteristic( CMD5_ACC_TYPE_ENUM.properties[ entry.accTypeEnumIndex ].characteristic ).updateValue( entry.accessory.errorString );
// queue.log.warn( `END processHighPriorityGetQueue calling updateCharacteristic errorValue: ${ entry.accessory.errorValue} errorString: ${ entry.accessory.errorString }`);
} else
{
// Increment the errorCount/currentRetryCount
queue.errorCountSinceLastGoodTransaction++;
// High Priority Get failed. We keep retrying until the mod of
// queueRetryCount reaches zero, which is WoRm only as it has more than 1
// default retry count.
queue.highPriorityQueue.push( entry );
}
entry.accessory.queue.pauseQueue( entry.accessory.queue );
}
queue.inProgressGets --;
setTimeout( ( ) => { queue.processQueueFunc( HIGH_PRIORITY_GET, queue ); }, 0 );
});
}
// This is called from polling
processEntryFromLowPriorityQueue( entry )
{
if ( settings.cmd5Dbg ) this.log.debug( `Processing low priority queue entry: ${ entry.accTypeEnumIndex }` );
let queue = entry.accessory.queue;
queue.inProgressGets ++;
// isLowPriority is set to true,
queue.qGetValue( entry.accessory, entry.accTypeEnumIndex, entry.characteristicString, entry.timeout, function ( error, properValue )
{
// For the next one
queue.inProgressGets --;
// Save the error code - Pass or fail
entry.accessory.errorValue = error;
// Nothing special was done for casing on errors, so omit it.
if ( error == 0 )
{
// Save the new value
entry.accessory.cmd5Storage.setStoredValueForIndex( entry.accTypeEnumIndex, properValue );
if ( settings.cmd5Dbg ) entry.accessory.log.debug( `processEntryFromLowPriorityQueue calling updateValue properValue: ${ properValue }`);
// Update the new value in homebridge
entry.accessory.service.getCharacteristic( CMD5_ACC_TYPE_ENUM.properties[ entry.accTypeEnumIndex ].characteristic ).updateValue( properValue );
// A good anything, updates the lastGoodTransactionTime
queue.lastGoodTransactionTime = Date.now( );
queue.errorCountSinceLastGoodTransaction = 0;
} else { // LowPriority getValue failed
queue.errorCountSinceLastGoodTransaction++;
// Convert the errorValue into an errorString
entry.accessory.errorString = new Error( constants.errorString( error ));
// This does not work - Nothing happens to HomeKit !
// Call updateValue with new Error so device will become unavailable
// queue.log.warn( `START processEntryFromLowPriorityQueue calling updateCharacteristic errorValue: ${ entry.accessory.errorValue } errorString: ${ entry.accessory.errorString }`);
// entry.accessory.service.getCharacteristic( CMD5_ACC_TYPE_ENUM.properties[ entry.accTypeEnumIndex ].characteristic ).updateValue( entry.accessory.errorString );
// queue.log.warn( `END processEntryFromLowPriorityQueue calling updateCharacteristic errorValue: ${ entry.accessory.errorValue } errorString: ${ entry.accessory.errorString }`);
queue.pauseQueue( entry.accessory.queue );
}
// Now that this one has been processed, schedule it again for next time
queue.scheduleLowPriorityEntry( entry )
});
}
// ***********************************************
//
// qGetValue: Method to call an external script
// that returns an accessories status
// for a given characteristic.
//
// The script will be passed:
// Get <Device Name> <accTypeEnumIndex>
//
// Where:
// - Device name is the name in your
// config.json file.
// - accTypeEnumIndex represents
// the characteristic to get as in index into
// the CMD5_ACC_TYPE_ENUM.
//
// ***********************************************
qGetValue( accessory, accTypeEnumIndex, characteristicString, timeout, callback )
{
let self = accessory;
let queue = accessory.queue;
let cmd = self.state_cmd_prefix + self.state_cmd + ' Get "' + self.displayName + '" ' + "'" + characteristicString + "'" + self.state_cmd_suffix;
if ( settings.cmd5Dbg ) self.log.debug( `getValue: accTypeEnumIndex:( ${ accTypeEnumIndex } )-"${ characteristicString }" function for: ${ self.displayName } cmd: ${ cmd } timeout: ${ timeout }` );
let reply = "NxN";
// Execute command to Get a characteristics value for an accessory
// exec( cmd, { timeout: timeout }, function ( error, stdout, stderr )
//let child = spawn( cmd, { shell:true } );
let child = exec( cmd, { timeout: timeout }, function ( error, stdout, stderr )
{
if ( stderr )
if ( queue.echoE ) self.log.error( `getValue: ${ characteristicString } function for ${ self.displayName } streamed to stderr: ${ stderr }` );
// Handle errors when process closes
if ( error )
if ( queue.echoE ) self.log.error( chalk.red( `getValue ${ characteristicString } function failed for ${ self.displayName } cmd: ${ cmd } Failed. Generated Error: ${ error }` ) );
reply = stdout;
}).on('close', ( code ) =>
{
// Was the return code successful ?
if ( code != 0 )
{
// Commands that time out have "null" return codes. So get the real one.
if ( child.killed == true )
{
if ( queue.echoE ) self.log.error( chalk.red( `getValue ${ characteristicString } function timed out ${ timeout }ms for ${ self.displayName } cmd: ${ cmd } Failed` ) );
callback( constants.ERROR_TIMER_EXPIRED );
return;
}
if ( queue.echoE ) self.log.error( chalk.red( `getValue ${ characteristicString } function failed for ${ self.displayName } cmd: ${ cmd } Failed. Error: ${ code }. ${ constants.DBUSY }` ) );
callback( code );
return;
}
if ( reply == "NxN" )
{
if ( queue.echoE ) self.log.error( `getValue: nothing returned from stdout for ${ characteristicString } ${ self.displayName }. ${ constants.DBUSY }` );
callback( constants.ERROR_NO_DATA_REPLY );
return;
}
if ( reply == null )
{
if ( queue.echoE ) self.log.error( `getValue: null returned from stdout for ${ characteristicString } ${ self.displayName }. ${ constants.DBUSY }` );
// We can call our callback though ;-)
callback( constants.ERROR_NULL_REPLY );
return;
}
// Coerce to string for manipulation
reply += '';
// Remove trailing newline or carriage return, then
// Remove leading and trailing spaces, carriage returns ...
let trimmedReply = reply.replace(/\n|\r$/,"").trim( );
// Theoretically not needed as this is caught below, but I wanted
// to catch this before much string manipulation was done.
if ( trimmedReply.toUpperCase( ) == "NULL" )
{
if ( queue.echoE ) self.log.error( `getValue: "${ trimmedReply }" returned from stdout for ${ characteristicString } ${ self.displayName }. ${ constants.DBUSY }` );
callback( constants.ERROR_NULL_STRING_REPLY );
return;
}
// Handle beginning and ending matched single or double quotes. Previous version too heavy duty.
// - Remove matched double quotes at begining and end, then
// - Remove matched single quotes at beginning and end, then
// - remove leading and trailing spaces.
let unQuotedReply = trimmedReply.replace(/^"(.+)"$/,"$1").replace(/^'(.+)'$/,"$1").trim( );
if ( unQuotedReply == "" )
{
if ( queue.echoE ) self.log.error( `getValue: ${ characteristicString } function for: ${ self.displayName } returned an empty string "${ trimmedReply }". ${ constants.DBUSY }` );
callback( constants.ERROR_EMPTY_STRING_REPLY );
return;
}
// The above "null" checked could possibly have quotes around it.
// Now that the quotes are removed, I must check again. The
// things I must do for bad data ....
if ( unQuotedReply.toUpperCase( ) == "NULL" )
{
if ( queue.echoE ) self.log.error( `getValue: ${ characteristicString } function for ${ self.displayName } returned the string "${ trimmedReply }". ${ constants.DBUSY }` );
callback( constants.ERROR_2ND_NULL_STRING_REPLY );
return;
}
let words = unQuotedReply.split( " " ).length;
if ( words > 1 && CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ].props.allowedWordCount == 1 )
{
self.log.warn( `getValue: Warning, Retrieving ${ characteristicString }, expected only one word value for: ${ self.displayName } of: ${ trimmedReply }` );
}
if ( settings.cmd5Dbg ) self.log.debug( `getValue: ${ characteristicString } function for: ${ self.displayName } returned: ${ unQuotedReply }` );
var transposed = transposeConstantToValidValue( CMD5_ACC_TYPE_ENUM.properties, accTypeEnumIndex, unQuotedReply )
if ( settings.cmd5Dbg && transposed != unQuotedReply ) self.log.debug( `getValue: ${ characteristicString } for: ${ self.displayName } transposed: ${ transposed }` );
// Return the appropriate type, by seeing what it is
// defined as in Homebridge,
let properValue = CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ].stringConversionFunction( transposed );
if ( properValue == undefined )
{
self.log.warn( `${ self.displayName } ` + chalk.red( `Cannot convert value: ${ unQuotedReply } to ${ CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ].props.format } for ${ characteristicString }` ) );
callback( constants.ERROR_NON_CONVERTABLE_REPLY );
return;
}
if ( settings.cmd5Dbg && properValue != transposed ) self.log.debug( `getValue: ${ characteristicString } for: ${ self.displayName } properValue: ${ properValue }` );
// Success !!!!
callback( 0, properValue );
// Store history using fakegato if set up
self.updateAccessoryAttribute( accTypeEnumIndex, properValue );
});
}
// ***********************************************
//
// qSetValue: Method to call an external script
// that sets an accessories status
// for a given characteristic.
//
//
// The script will be passed:
// Set < Device Name > < accTypeEnumIndex > < Value >
//
//
// Where:
// - Device name is the name in your
// config.json file.
// - accTypeEnumIndex represents
// the characteristic to get as in index into
// the CMD5_ACC_TYPE_ENUM.
// - Characteristic is the accTypeEnumIndex
// in HAP form.
// - Value is new characteristic value.
//
// Notes:
// ( 1 ) In the special TARGET set characteristics, getValue
// is called to update HomeKit.
// Example: Set My_Door < TargetDoorState > 1
// calls: Get My_Door < CurrentDoorState >
//
// - Where he value in <> is an one of CMD5_ACC_TYPE_ENUM
// ***********************************************
qSetValue( accessory, accTypeEnumIndex, characteristicString, timeout, value, queueCallback )
{
let self = accessory;
let queue = accessory.queue;
if ( self.hV.outputConstants == true )
{
value = transposeValueToValidConstant( CMD5_ACC_TYPE_ENUM.properties, accTypeEnumIndex, value );
} else
{
value = transposeBoolToValue( value );
}
let cmd = accessory.state_cmd_prefix + accessory.state_cmd + ' Set "' + accessory.displayName + '" ' + "'" + characteristicString + "' '" + value + "'" + accessory.state_cmd_suffix;
if ( accessory.hV.statusMsg == "TRUE" )
self.log.info( chalk.blue( `Setting ${ self.displayName } ${ characteristicString }` ) + ` ${ value }` );
if ( settings.cmd5Dbg ) self.log.debug( `setValue: accTypeEnumIndex:( ${ accTypeEnumIndex } )-"${ characteristicString }" function for: ${ self.displayName } ${ value } cmd: ${ cmd } timeout: ${ timeout }` );
// Execute command to Set a characteristic value for an accessory
let child = exec( cmd, { timeout: timeout }, function ( error, stdout, stderr )
{
if ( stderr )
if ( queue.echoE ) self.log.error( `setValue: ${ characteristicString } function for ${ self.displayName } streamed to stderr: ${ stderr }` );
if ( error )
if ( queue.echoE ) self.log.error( chalk.red( `setValue ${ characteristicString } function failed for ${ self.displayName } cmd: ${ cmd } Failed. Error: ${ error.message }` ) );
}).on( "close", ( code ) =>
{
if ( code != 0 )
{
if ( child.killed == true )
{
if ( queue.echoE ) self.log.error( chalk.red( `setValue ${ characteristicString } function failed for ${ self.displayName } cmd: ${ cmd } Failed. Error: ${ code } ${ constants.DBUSY }` ) );
queueCallback( constants.ERROR_TIMER_EXPIRED );
return;
}
queueCallback( code );
return;
}
queueCallback( code );
});
}
// The queue is self maintaining, except for lowPriorityEntries
// which if passed in, must be rescheduled as they go by their own
// intervals and thus must handle the return code.
processWormQueue( lastTransactionType, queue, lowPriorityEntry = null )
{
// "WoRm", No matter what, only one "Set" allowed
if ( queue.inProgressSets > 0 )
{
// if ( settings.cmd5Dbg ) queue.log.debug(`processWormQueue queue.inProgressSets > 0 : ${queue.inProgressSets}`);
// We are *NOT* processing the low prioirity queue entry
return false;
}
// It is not a good time to do a anything, so skip it
if ( queue.lastGoodTransactionTime == 0 )
{
// if ( settings.cmd5Dbg ) queue.log.debug(`processWormQueue queue.lastGoodTransactionTime == 0`);
// We are *NOT* processing the low prioirity queue entry
return false;
}
if ( queue.highPriorityQueue.length > 0 )
{
let nextEntry = queue.highPriorityQueue[ 0 ];
if ( nextEntry.isSet == true )
{
// If already in progress, when they finish they will restart the queue
// Otherwise continuing will purge the next item from the queue as it
// cannot be run with an entry already in progress.
if ( nextEntry.accessory.queue.inProgressSets > 0 ||
nextEntry.accessory.queue.inProgressGets > 0 )
{
// Return as queue is busy.
// Return false as we are *NOT* processing the low prioirity queue entry
// if ( settings.cmd5Dbg ) queue.log.debug(`processWormQueue queue.inProgressSets> 0 ${ nextEntry.accessory.queue.inProgressSets } ${ nextEntry.accessory.queue.inProgressGets }`);
return false;
}
queue.processHighPrioritySetQueue( queue.highPriorityQueue.shift( ) );
// Return false as we are *NOT* processing the low prioirity queue entry
return false;
}
// This must be a "Get". Process them all.
let max = queue.highPriorityQueue.length;
while( queue.highPriorityQueue.length > 0 &&
nextEntry.isSet == false &&
max >= 1 )
{
queue.processHighPriorityGetQueue( queue.highPriorityQueue.shift( ) );
nextEntry = queue.highPriorityQueue[ 0 ];
max--;
}
// Return false as we are *NOT* processing the low prioirity queue entry
return false;
} else if ( lastTransactionType == HIGH_PRIORITY_SET ||
lastTransactionType == HIGH_PRIORITY_GET )
{
// Return false as we are *NOT* processing the low prioirity queue entry
return false;
}
// This is self evident, until their are other types of Prioritys
if ( lastTransactionType == LOW_PRIORITY_GET &&
lowPriorityEntry != null &&
queue.queueStarted == true )
{
queue.processEntryFromLowPriorityQueue( lowPriorityEntry );
// We are processing the low priority queue entry.
return true;
} else {
if ( lastTransactionType == LOW_PRIORITY_GET &&
queue.queueStarted == false )
{
// Return false as we are *NOT* processing the low prioirity queue entry
return false;
} if ( queue.inProgressGets == 0 &&
queue.inProgressSets == 0 )
{
// Return false as we are *NOT* processing the low prioirity queue entry
return false;
} else {
if ( settings.cmd5Dbg ) this.log.debug( `Unhandled lastTransactionType: ${ lastTransactionType } inProgressSets: ${ queue.inProgressSets } inProgressGets: ${ queue.inProgressGets } queueStarted: ${ queue.queueStarted } lowQueueLen: ${ queue.lowPriorityQueue.length } hiQueueLen: ${ queue.highPriorityQueue.length }` );
}
}
}
// The queue is self maintaining, except for lowPriorityEntries
// which if passed in, must be rescheduled as they go by their own
// intervals and thus must handle the return code.
processSequentialQueue( lastTransactionType, queue, lowPriorityEntry = null )
{
// Sequential, No matter what, only one transaction allowed
if ( queue.inProgressSets > 0 ||
queue.inProgressGets > 0 )
// Return false as we are *NOT* processing the low prioirity queue entry
return false;
// It is not a good time to do a anything, so skip it
if ( queue.lastGoodTransactionTime == 0 )
// Return false as we are *NOT* processing the low prioirity queue entry
return false;
if ( queue.highPriorityQueue.length > 0 )
{
let nextEntry = queue.highPriorityQueue[ 0 ];
if ( nextEntry.isSet == true )
{
queue.processHighPrioritySetQueue( queue.highPriorityQueue.shift( ) );
// Return false as we are *NOT* processing the low prioirity queue entry
return false;
}
// Has to be a High Priority "Get" entry. Process just this one.
queue.processHighPriorityGetQueue( queue.highPriorityQueue.shift( ) );
// Return false as we are *NOT* processing the low prioirity queue entry
return false;
} else if ( lastTransactionType == HIGH_PRIORITY_SET ||
lastTransactionType == HIGH_PRIORITY_GET )
{
// Return false as we are *NOT* processing the low prioirity queue entry
return false;
}
// This is self evident, until their are other types of Prioritys
if ( lastTransactionType == LOW_PRIORITY_GET &&
lowPriorityEntry != null &&
queue.queueStarted == true )
{
queue.processEntryFromLowPriorityQueue( lowPriorityEntry );
// We are processing the low priority queue entry.
return true;
} else {
if ( lastTransactionType == LOW_PRIORITY_GET &&
queue.queueStarted == false )
{
// Return false as we are *NOT* processing the low prioirity queue entry
return false;
} if ( queue.inProgressGets == 0 &&
queue.inProgressSets == 0 )
{
// Return false as we are *NOT* processing the low prioirity queue entry
return false;
} else {
if ( settings.cmd5Dbg ) this.log.debug( `Unhandled lastTransactionType: ${ lastTransactionType } inProgressSets: ${ queue.inProgressSets } inProgressGets: ${ queue.inProgressGets } queueStarted: ${ queue.queueStarted } lowQueueLen: ${ queue.lowPriorityQueue.length } hiQueueLen: ${ queue.highPriorityQueue.length }` );
}
}
}
// The standard queue is just free running, except if the queue has not
// been started yet.
processPassThruQueue( lastTransactionType, queue, lowPriorityEntry = null )
{
if ( lastTransactionType == LOW_PRIORITY_GET &&
lowPriorityEntry != null &&
queue.queueStarted == true )
{
queue.processEntryFromLowPriorityQueue( lowPriorityEntry );
}
if ( queue.highPriorityQueue.length > 0 )
{
let nextEntry = queue.highPriorityQueue[ 0 ];
if ( nextEntry.isSet == true )
{
queue.processHighPrioritySetQueue( queue.highPriorityQueue.shift( ) );
}
else
{
queue.processHighPriorityGetQueue( queue.highPriorityQueue.shift( ) );
}
}
}
scheduleLowPriorityEntry( entry )
{
let accessory = entry.accessory;
let queue = entry.accessory.queue;
if ( settings.cmd5Dbg ) accessory.log.debug( `Scheduling Poll of index: ${ entry.accTypeEnumIndex } characteristic: ${ entry.characteristicString } for: ${ accessory.displayName } timeout: ${ entry.timeout } interval: ${ entry.interval }` );
// Clear polling
if ( queue.listOfRunningPolls &&
queue.listOfRunningPolls[ accessory.displayName + entry.accTypeEnumIndex ] == undefined )
clearTimeout( queue.listOfRunningPolls[ accessory.displayName + entry.accTypeEnumIndex ] );
queue.listOfRunningPolls[ accessory.displayName + entry.accTypeEnumIndex ] = setTimeout( ( ) =>
{
// If the queue was busy/not available, schedule the entry at a later time
if ( queue.processQueueFunc( LOW_PRIORITY_GET, queue, entry ) == false )
{
if ( settings.cmd5Dbg ) accessory.log.debug( `processsQueue returned false` );
queue.scheduleLowPriorityEntry( entry );
}
}, entry.interval);
}
pauseQueue( queue )
{
if ( queue.queueType == constants.QUEUETYPE_STANDARD )
return;
queue.lastGoodTransactionTime = 0;
if ( queue.pauseTimer == null )
{
queue.pauseTimer = setTimeout( ( ) =>
{
// So we do not trip over this again immediately
queue.lastGoodTransactionTime = Date.now( );
queue.pauseTimer = null;
queue.processQueueFunc( HIGH_PRIORITY_GET, queue );
}, queue.pauseTimerTimeout );
}
}
printQueueStats( queue )
{
let line = `QUEUE "${ queue.queueName }" stats`;
this.log.info( line );
this.log.info( `${ "=".repeat( line.length ) }` );
this.log.info( "No longer applicable" );
}
dumpQueue( queue )
{
let line = `Low Priority Queue "${ queue.queueName }"`;
this.log.info( line );
this.log.info( `${ "=".repeat( line.length ) }` );
queue.lowPriorityQueue.forEach( ( entry, entryIndex ) =>
{
this.log.info( `${ entryIndex } ${ entry.accessory.displayName } characteristic: ${ entry.characteristicString } accTypeEnumIndex: ${ entry.accTypeEnumIndex } interval: ${ entry.interval } timeout: ${ entry.timeout }` );
} );
}
startQueue( queue, allDoneCallback )
{
queue.lowPriorityQueueIndex = 0 ;
let delay = 0;
let staggeredDelays = [ 3000, 6000, 9000, 12000 ];
let staggeredDelaysLength = staggeredDelays.length;
let staggeredDelayIndex = 0;
let lastAccessoryUUID = ""
let allDoneCount = 0;
if ( settings.cmd5Dbg ) this.log.debug( `enablePolling for the first time` );
// If there is nothing in the lowPriorityQueue, we are dome.
// Demo mode or Unit testing.
if ( queue.lowPriorityQueue.length == 0 )
{
allDoneCallback( allDoneCount );
setTimeout( ( ) => { queue.processQueueFunc( HIGH_PRIORITY_GET, queue ); }, 0 );
} else
{
queue.lowPriorityQueue.forEach( ( entry, entryIndex ) =>
{
allDoneCount ++;
setTimeout( ( ) =>
{
if ( entryIndex == 0 && settings.cmd5Dbg )
{
if ( queue.queueType == constants.QUEUETYPE_WORM ||
queue.queueType == constants.QUEUETYPE_WORM2
)
{
entry.accessory.log.debug( `Started staggered kick off of ${ queue.lowPriorityQueue.length } polled characteristics for queue: "${ entry.accessory.queue.queueName }"` );
} else
{
entry.accessory.log.debug( `Started staggered kick off of ${ queue.lowPriorityQueue.length } polled characteristics for "${ entry.accessory.displayName }"` );
}
}
if ( settings.cmd5Dbg ) entry.accessory.log.debug( `Kicking off polling for: ${ entry.accessory.displayName } ${ entry.characteristicString } interval:${ entry.interval }, staggered:${ staggeredDelays[ staggeredDelayIndex ] }` );
queue.scheduleLowPriorityEntry( entry );
if ( entryIndex == queue.lowPriorityQueue.length -1 )
{
if ( settings.cmd5Dbg )
{
if ( queue.queueType == constants.QUEUETYPE_WORM ||
queue.queueType == constants.QUEUETYPE_WORM2
)
{
entry.accessory.log.debug( `All characteristics are now being polled for queue: "${ queue.queueName }"` );
}
else
{
entry.accessory.log.debug( `All characteristics are now being polled for "${ entry.accessory.displayName }"` );
}
}
allDoneCallback( allDoneCount );
}
}, delay );
if ( staggeredDelayIndex++ >= staggeredDelaysLength )
staggeredDelayIndex = 0;
if ( lastAccessoryUUID != entry.accessory.uuid )
staggeredDelayIndex = 0;
lastAccessoryUUID = entry.accessory.uuid;
delay += staggeredDelays[ staggeredDelayIndex ];
});
}
queue.queueStarted = true;
}
changeQueueType( queue, queueType )
{
if ( queue.queueStarted )
throw new Error( `Cannot change queueType when queue is running` );
// The WoRm queue needs error messages to be silenced as
// they are inevitable, but are handled through retries
// By default non WoRm queus are allowed to echo errors
this.echoE = true;
// Default
this.processQueueFunc = this.processWormQueue;
switch ( queueType )
{
case constants.QUEUETYPE_SEQUENTIAL:
this.processQueueFunc = this.processSequentialQueue;
break;
case constants.QUEUETYPE_WORM:
case constants.QUEUETYPE_WORM2:
this.processQueueFunc = this.processWormQueue;
// When not in debug mode, do not echo errors for the WoRm queue
// as errors are handled through retries.
if ( ! settings.cmd5Dbg )
this.echoE = false;
break;
case constants.QUEUETYPE_STANDARD:
// only polled entries go straight through the queue
this.processQueueFunc = this.processPassThruQueue;
break;
case constants.QUEUETYPE_PASSTHRU:
// entries go straight through the queue
this.processQueueFunc = this.processPassThruQueue;
break;
default:
this.log.error( `Error: Invalid queue type: ${ queueType }` );
}
}
isCharacteristicPolled( accTypeEnumIndex, queue, accessory )
{
if ( queue.lowPriorityQueue.filter(
entry => entry.accessory.uuid == accessory.uuid &&
entry.accTypeEnumIndex == accTypeEnumIndex
).length == 0 )
{
return false;
}
return true;
}
}
var queueExists = function( queueName )
{
return settings.listOfCreatedPriorityQueues[ queueName ];
}
var addQueue = function( log, queueName, queueType = constants.DEFAULT_QUEUE_TYPE, queueRetryCount = constants.DEFAULT_WORM_QUEUE_RETRY_COUNT )
{
let queue = queueExists( queueName );
if ( queue != undefined )
return queue;
log.debug( `Creating new Priority Polled Queue "${ queueName }" with QueueType of: "${ queueType }" retryCount: ${queueRetryCount}` );
queue = new Cmd5PriorityPollingQueue( log, queueName, queueType, queueRetryCount );
settings.listOfCreatedPriorityQueues[ queueName ] = queue;
return queue;
}
var parseAddQueueTypes = function ( log, entrys )
{
if ( trueTypeOf( entrys ) != Array )
throw new Error( `${ constants.QUEUETYPES } is not an Array of { "Queue Name": "QueueType" }. found: ${ entrys }` );
entrys.forEach( ( entry, entryIndex ) =>
{
let queueName = null;
let queueType = constants.DEFAULT_QUEUE_TYPE;
let queueRetryCount = constants.DEFAULT_WORM_QUEUE_RETRY_COUNT;
for ( let key in entry )
{
let lcKey = lcFirst( key );
// warn now
if ( key.charAt( 0 ) === key.charAt( 0 ).toUpperCase( ) )
{
log.warn( `The config.json queueTypes key: ${ key } is Capitalized. All keys in the near future will ALWAYS start with a lower case character for homebridge-ui integration.\nTo remove this Warning, Please fix your config.json.` );
}
let value = entry[ key ];
switch( lcKey )
{
case constants.QUEUE:
if ( settings.listOfCreatedPriorityQueues[ entry.queue ] )
throw new Error( `QueueName: ${ entry.queue } was added twice` );
queueName = value;
break;
case constants.QUEUETYPE:
// Set the default Queue Retry Count based on QueueType
switch ( value )
{
case constants.QUEUETYPE_WORM:
queueRetryCount = constants.DEFAULT_WORM_QUEUE_RETRY_COUNT;
queueType = value;
break;
case constants.QUEUETYPE_WORM2:
queueRetryCount = constants.DEFAULT_WORM_QUEUE_RETRY_COUNT;
queueType = value;
break;
case constants.QUEUETYPE_SEQUENTIAL:
queueRetryCount = constants.DEFAULT_STANDARD_QUEUE_RETRY_COUNT;
queueType = value;
break;
case constants.QUEUETYPE_STANDARD:
queueRetryCount = constants.DEFAULT_STANDARD_QUEUE_RETRY_COUNT;
queueType = value;
break;
default:
throw new Error( `QueueType: ${ entry.queueType } is not valid at index: ${ entryIndex }. Expected: ${ constants.QUEUETYPE_WORM }, ${