homebridge-myplace
Version:
Exec Plugin bringing Advanatge Air MyPlace system to Homekit
1,072 lines (890 loc) • 94.7 kB
JavaScript
'use strict';
const moment = require( "moment" );
// Settings, Globals and Constants
let settings = require( "./cmd5Settings" );
const constants = require( "./cmd5Constants" );
let Logger = require( "./utils/Logger" );
const { getAccessoryName, getAccessoryDisplayName
} = require( "./utils/getAccessoryNameFunctions" );
let getAccessoryUUID = require( "./utils/getAccessoryUUID" );
const { addQueue, queueExists } = require( "./Cmd5PriorityPollingQueue" );
// Hierarchy variables
let HV = require( "./utils/HV" );
let createAccessorysInformationService = require( "./utils/createAccessorysInformationService" );
let lcFirst = require( "./utils/lcFirst" );
let trueTypeOf = require( "./utils/trueTypeOf" );
// The sObject.defineProperty is to resolve a lint issue.
// See utils/Cmd5indexOfEnumLintTest.js for further information.
let Cmd5indexOfEnum = require( "./utils/Cmd5indexOfEnum" );
Object.defineProperty( exports, "Cmd5indexOfEnum", { enumerable: true, get: function ( ){ return Cmd5indexOfEnum.Cmd5indexOfEnum; } });
// For changing validValue Constants to Values and back again
var { transposeConstantToValidValue,
} = require( "./utils/transposeCMD5Props" );
let isJSON = require( "./utils/isJSON" );
let isCmd5Directive = require( "./utils/isCmd5Directive" );
let isAccDirective = require( "./utils/isAccDirective" );
let isDevDirective = require( "./utils/isDevDirective" );
// Pretty Colors
var chalk = require( "chalk" );
// These would already be initialized by index.js
let CMD5_ACC_TYPE_ENUM = require( "./lib/CMD5_ACC_TYPE_ENUM" ).CMD5_ACC_TYPE_ENUM;
let CMD5_DEVICE_TYPE_ENUM = require( "./lib/CMD5_DEVICE_TYPE_ENUM" ).CMD5_DEVICE_TYPE_ENUM;
const Cmd5Storage = require( "./utils/Cmd5Storage" );
let FakeGatoHistoryService = null;
// Only one TV is allowed per bridge. Circumvented by
// publishing the TV externally.
let numberOfTVsPerBridge = 0;
// Array Remove
function removeFromArray( arr, val )
{
for (let i = arr.length - 1; i >= 0; i--)
{
if (arr[i] === val)
{
// console.log("Removing %s", val );
arr.splice(i, 1);
}
}
return arr;
}
// Accessory definitions - THE GOOD STUFF STARTs HERE
//
// An Homebridge accessory by default is passed the following params
//
// @params:
// log - Logging functionality.
// config - The JSON description of the accessory.
// api - Homebridge API.
//
// @Optional params
// parentInfo - Optionally passed from a parent as if this is a linked accessory,
// or from a CMD5 Platform.
//
//
class Cmd5Accessory
{
constructor( log, config, api, STORED_DATA_ARRAY, parentInfo )
{
// Non Platform accessories get called with homebridges Logger
// replace with ours
if ( typeof log.setOutputEnabled === "function" )
{
this.log = log;
// Carry the debug flag from the platform
settings.cmd5Dbg = log.debugEnabled;
}
else
{
this.log = new Logger( );
if ( config[ constants.DEBUG ] == true ||
config[ "Debug" ] == true ||
process.env.DEBUG == settings.PLATFORM_NAME )
{
settings.cmd5Dbg = true;
}
}
this.log.setDebugEnabled( settings.cmd5Dbg );
this.config = config;
this.api = api;
// keep a copy because traversing it for format checking can be slow.
this.Characteristic = api.hap.Characteristic;
this.parentInfo = parentInfo;
// Use parent values ( if any ) or these defaults.
// LEVEL is a number, possibly 0 which must be handled more precisely.
this.CMD5 = ( parentInfo && parentInfo.CMD5 ) ? parentInfo.CMD5 : constants.STANDALONE;
this.LEVEL = ( parentInfo && parentInfo.LEVEL !== undefined ) ? parentInfo.LEVEL + 1 : 0;
this.createdCmd5Accessories = ( parentInfo && parentInfo.createdCmd5Accessories ) ? parentInfo.createdCmd5Accessories : [ ];
let typeMsg = [ "", "Linked ", "Added " ][ this.LEVEL ] || "";
if ( settings.cmd5Dbg ) log.debug( chalk.blue ( `Creating ${ typeMsg }${ this.CMD5 } Accessory type for : ${ config.displayName } LEVEL: ${ this.LEVEL }` ) );
this.services = [ ];
this.linkedAccessories = [ ];
this.listOfVariables = { };
this.listOfConstants = { };
// Determines if the accessory is communicable
this.errorValue = 0;
this.errorString = "init";
// Used to determine missing related characteristics and
// to determine if the related characteristic is also polled.
this.listOfPollingCharacteristics = { };
// An extra flag
this.ServiceCreated = false;
// DisplayName and/or Name must be defined.
// No need to update config anymore as it is no longer cached, only the Characteristic values are.
this.name = getAccessoryName( this.config );
this.displayName = getAccessoryDisplayName( this.config );
// Everything that needs to talk to the device now goes through the queue
this.queue = null;
// Use the Hierarhy variables from the parent, if not create it.
this.hV = new HV( );
if ( parentInfo && parentInfo.hV )
{
this.hV.update( parentInfo.hV );
}
// In case it is not passed in.
if ( STORED_DATA_ARRAY == undefined || STORED_DATA_ARRAY == null )
this.STORED_DATA_ARRAY = [ ];
else
this.STORED_DATA_ARRAY = STORED_DATA_ARRAY;
let parseConfigShouldUseCharacteristicValues = true;
if ( ! Array.isArray( this.STORED_DATA_ARRAY ) )
{
this.log.warn( "STORED_DATA_ARRAY passed in is not an array and should be reported." );
this.STORED_DATA_ARRAY = [ ];
}
// generate a unique id for the accessory this should be generated from
// something globally unique, but constant, for example, the device serial
// number or MAC address.
let uuid = getAccessoryUUID( config, this.api.hap.uuid );
// Handle case change
let existingDataU = this.STORED_DATA_ARRAY.find( data => data[ "UUID" ] === uuid );
if ( existingDataU )
{
//Z this.log.info( chalk.blue ( `THIS MSG TO BE REMOVED. RENAMING UUID for: ${ config.displayName } LEVEL: ${ this.LEVEL }` ) );
existingDataU[ "uuid" ] = existingDataU[ "UUID" ];
delete existingDataU[ "UUID" ];
}
// NOTE: We saved the data via lower case uuid.
let existingData = this.STORED_DATA_ARRAY.find( data => data[ constants.UUID ] === uuid );
if ( existingData )
{
//Z this.log.info( chalk.blue ( `THIS MSG TO BE REMOVED. Found existing data for: ${ this.displayName }` ) );
if ( settings.cmd5Dbg ) this.log.debug(`Cmd5Accessory: found existingData for ${ this.displayName }` );
if ( existingData.storedValuesPerCharacteristic )
{
//Z this.log.info( chalk.blue ( `THIS MSG TO BE REMOVED. Found old storedValuesPerCharacteristic for: ${ this.displayName }` ) );
if ( settings.cmd5Dbg ) this.log.debug( `Upgrading to cmd5Storage` );
this.cmd5Storage = new Cmd5Storage( this.log, existingData.storedValuesPerCharacteristic );
this.STORED_DATA_ARRAY.push( { [ constants.UUID ]: uuid,
[ constants.CMD5_STORAGE_lv ]: this.cmd5Storage
}
);
//this.STORED_DATA_ARRAY.remove( existingData );
removeFromArray( this.STORED_DATA_ARRAY, existingData );
} else if ( existingData.cmd5Storage )
{
//Z this.log.info( chalk.blue ( `THIS MSG TO BE REMOVED. Using existing cmd5Storage for: ${ this.displayName }` ) );
if ( settings.cmd5Dbg ) this.log.debug( `Using existing cmd5Storage` );
this.cmd5Storage = new Cmd5Storage( this.log, existingData.cmd5Storage );
this.STORED_DATA_ARRAY.push( { [ constants.UUID ]: uuid,
[ constants.CMD5_STORAGE_lv ]: this.cmd5Storage
}
);
//this.STORED_DATA_ARRAY.remove( existingData );
removeFromArray( this.STORED_DATA_ARRAY, existingData );
} else {
//Z log.info( chalk.blue ( `THIS MSG TO BE REMOVED. Unexpected empty cmd5Storage for: ${ this.displayName }` ) );
this.log.warn( `Unexpected empty cmd5Storage` );
this.cmd5Storage = new Cmd5Storage( this.log );
this.STORED_DATA_ARRAY.push( { [ constants.UUID ]: uuid,
[ constants.CMD5_STORAGE_lv ]: this.cmd5Storage
}
);
//this.STORED_DATA_ARRAY.remove( existingData );
removeFromArray( this.STORED_DATA_ARRAY, existingData );
}
// Do not read stored values from config.json
parseConfigShouldUseCharacteristicValues = false;
} else
{
//Z log.info( chalk.blue ( `THIS MSG TO BE REMOVED. Creating new cmd5Storage for: ${ this.displayName }` ) );
if ( settings.cmd5Dbg ) this.log.debug(`Cmd5Accessory: creating new cmd5Storage for ${ this.displayName }` );
// Instead of local variables for every characteristic, create an array to
// hold values for all characteristics based on the size of all possible
// characteristics. Placing them in .config will make them be cached over
// restarts.
this.cmd5Storage = new Cmd5Storage( this.log );
this.STORED_DATA_ARRAY.push( { [ constants.UUID ]: uuid,
[ constants.CMD5_STORAGE_lv ]: this.cmd5Storage
}
);
}
// Add the global constants to the listOfConstants
if ( this.parentInfo && this.parentInfo.globalConstants != null )
{
this.processConstants( this.parentInfo.globalConstants );
// Since linked accessories get processed first, The parentInfo they
// get is actually "this" and we need for the linked accessory to
// process the constants first in order to see them. i.e. ${IP}
this.globalConstants = this.parentInfo.globalConstants;
}
// Direct if polling should be set or false.
// You cannot copy polling from the parent because you would be copying the array
// of polled characteristics that the child does not have, or turning on polling
// for linked accessories too.
//this.polling = false;
// Init the Global Fakegato service once !
if ( FakeGatoHistoryService == null )
FakeGatoHistoryService = require( "fakegato-history" )( api );
// Get the supplied values from the accessory config.
this.parseConfig( this.config, parseConfigShouldUseCharacteristicValues );
// Update the accessories namespace for stored variables
// like timeout, stateChangeResponseTime ... As it may require
// changes from parseConfig.
this.hV.update( this );
// Add any required characteristics of a device that are missing from
// a users config.json file.
this.addRequiredCharacteristicStoredValues( );
// The accessory cannot have the same UUID as any other
checkAccessoryForDuplicateUUID( this, this.uuid );
// The default response time is in seconds
if ( ! this.stateChangeResponseTime )
this.stateChangeResponseTime = CMD5_DEVICE_TYPE_ENUM.properties[ this.typeIndex ].devicesStateChangeDefaultTime;
// Check the polling config for characteristics that may be set there
// and not in the config.json.
this.checkPollingConfigForUnsetCharacteristics( this.polling );
// Convert the accessoriesConfig ( if any ) to an array of Cmd5Accessory
if ( this.accessoriesConfig && this.CMD5 == constants.PLATFORM && this.LEVEL == 0 )
{
log.info( `Creating accessories for: ${ this.displayName }` );
// Let me explain.
// Level 0 are standalone or platform.
// Level 1 is linked.
// Added accessories are on the same level as linked,
// but they are not linkedTypes, just added to the platform.
// For Example: TelevisionSpeaker.
let savedLevel = this.LEVEL;
this.LEVEL = 1; // will be incremented to 2.
this.accessories = this.accessoryTypeConfigToCmd5Accessories( this.accessoriesConfig, this );
this.LEVEL = savedLevel;
}
// Convert the linkedTypes ( if any ) to an array of Cmd5Accessory
// Linked Accessories can be on Standalone or Platform Accessories.
if ( this.linkedAccessoriesConfig && this.LEVEL == 0 )
{
log.info( `Creating linked accessories for: ${ this.displayName }` );
this.linkedAccessories = this.accessoryTypeConfigToCmd5Accessories( this.linkedAccessoriesConfig, this );
}
// This sets up which characteristics, if any, will be polled
// This can be done for only LEVEL 0 accessories and itself
if ( this.LEVEL == 0 )
{
// if ( settings.cmd5Dbg ) log.debug( "CMD5=%s LEVEL=%s for %s", accessory.CMD5, accessory.LEVEL, accessory.displayName );
// The linked accessory children are at different levels of recursion, so only
// allow what is posssible.
if ( this.linkedAccessories && this.linkedAccessories.length > 0 )
{
if ( settings.cmd5Dbg ) this.log.debug( `Setting up which characteristics will be polled for Linked Accessories of ${ this.displayName }` );
this.linkedAccessories.forEach( ( linkedAccessory ) =>
{
if ( linkedAccessory.polling != false )
{
linkedAccessory.determineCharacteristicsToPollForAccessory( linkedAccessory );
}
});
}
// The Television Speaker Platform Example
if ( this.accessories && this.accessories.length > 0 )
{
if ( settings.cmd5Dbg ) this.log.debug( `Setting up which characteristics will be polled for Added Accessories of ${ this.displayName }` );
this.accessories.forEach( ( addedAccessory ) =>
{
if ( addedAccessory.polling )
{
addedAccessory.determineCharacteristicsToPollForAccessory( addedAccessory );
}
});
}
if ( settings.cmd5Dbg ) this.log.debug( `Setting up which characteristics will be polled for ${ this.displayName }` );
this.determineCharacteristicsToPollForAccessory( this );
}
// Create all the services for the accessory, including fakegato and polling
// Only true Standalone accessories can have their services created and
// polling started. Otherwise the platform will have to do this.
if ( this.CMD5 == constants.STANDALONE && this.LEVEL == 0 )
{
if ( settings.cmd5Dbg ) log.debug( `Creating Standalone service for: ${ this.displayName }` );
this.createServicesForStandaloneAccessoryAndItsChildren( this )
}
} // Cmd5Accessory ( log, config, api, STORED_DATA_ARRAY, parentInfo )
identify( callback )
{
callback( );
}
getServices( )
{
//if ( this.services )
//{
// if ( settings.cmd5Dbg ) this.log.debug( Fg.Red + "ZZZZ Returning:%s number of services for:%s" + Fg.Rm, this.services.length, this.displayName );
//} else {
// if ( settings.cmd5Dbg ) this.log.debug( Fg.Red + "ZZZZ Returning this.services:%s for:%s" + Fg.Rm, this.services, this.displayName );
//}
return this.services;
}
// Any required characteristic of an accessory that is not in the accessories
// config will be added later by the existance of its stored value, so
// find the missing characteristics and add their value s here.
addRequiredCharacteristicStoredValues ( )
{
// Get the properties for this accessories device type
let properties = CMD5_DEVICE_TYPE_ENUM.properties[ this.typeIndex ];
// Check if required characteristics should be added, or TLV8 removed.
for ( let accTypeEnumIndex = 0 ; accTypeEnumIndex < CMD5_ACC_TYPE_ENUM.EOL; accTypeEnumIndex++ )
{
// Get the properties for this accessories device type
let devProperties = CMD5_DEVICE_TYPE_ENUM.properties[ this.typeIndex ];
// See if the characteristic index is in the required characteristics of the device
let requiredIndex = devProperties.requiredCharacteristics.Cmd5indexOfEnum( i => i.type === accTypeEnumIndex );
let format = CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ].props.format;
// No matter what, remove it
if ( format == this.api.hap.Formats.TLV8 && this.hV.allowTLV8 == false )
{
if ( this.cmd5Storage.getStoredValueForIndex( accTypeEnumIndex ) != null )
{
this.cmd5Storage.setStoredValueForIndex( accTypeEnumIndex, null );
this.log.warn( `****** Removing TLV8 required characteristic: ${ CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ].type }` );
}
continue;
}
// if it is required and not stored, add it
if ( requiredIndex != -1 && this.cmd5Storage.getStoredValueForIndex( accTypeEnumIndex ) == null )
{
this.log.warn( `**** Adding required characteristic ${ CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ].type } for ${ this.displayName }` );
this.log.warn( `Not defining a required characteristic can be problematic` );
// Get the default value to store
let defaultValue = properties.requiredCharacteristics[ requiredIndex ].defaultValue;
// If ConfiguredName was not defined, then use the Accessories Name
if ( accTypeEnumIndex == CMD5_ACC_TYPE_ENUM.ConfiguredName )
defaultValue = getAccessoryName( this.config );
if ( settings.cmd5Dbg ) this.log.debug( `*****Adding default value ${ defaultValue } for: ${ this.displayName }` );
this.cmd5Storage.setStoredValueForIndex( accTypeEnumIndex, defaultValue );
}
}
}
checkPollingConfigForUnsetCharacteristics( pollingConfig )
{
if ( trueTypeOf( pollingConfig ) != Array )
return;
if ( settings.cmd5Dbg ) this.log.debug( `Checking ${ this.displayName } for polling of unset characteristics.` );
pollingConfig.forEach( ( jsonPollingConfig ) =>
{
let value;
let valueToStore = null;
let accTypeEnumIndex = -1;
let key;
for ( key in jsonPollingConfig )
{
value = jsonPollingConfig[ key ];
let rcDirective = isCmd5Directive( key );
if ( rcDirective == null )
{
rcDirective = isCmd5Directive( key, true );
if ( rcDirective != null )
{
// warn now
this.log.warn( `The config.json Cmd5 Polling Directive: ${ key } is Capitalized. It should be: ${ rcDirective.key }. In the near future this will be an error for homebridge-ui integration.\nTo remove this Warning, Please fix your config.json.` );
// create the proper lower case value
jsonPollingConfig[ rcDirective.key ] = value;
// delete the upper case value
delete jsonPollingConfig[ key ];
//set the key
key = rcDirective.key;
}
}
// Not finding the key is not an error as it could be a Characteristic
switch ( key )
{
case constants.TIMEOUT:
case constants.INTERVAL:
// break omitted
case constants.QUEUE:
{
break;
}
case constants.QUEUETYPES:
{
// This whole record is not a characteristic polling entry
// continue to next ( via return )
return;
}
case constants.CHARACTERISTIC:
{
//2 checkPollingOfUnsetCharacteristics
valueToStore = null;
let rcDirective = isAccDirective( value, false );
if ( rcDirective.accTypeEnumIndex == null )
{
rcDirective = isAccDirective( value, true );
if ( rcDirective.accTypeEnumIndex == null )
throw new Error( `No such polling characteristic: "${ value }" for: "${ this.displayName }".` );
this.log.warn( `The config.json Polling characteristic: ${ value } is Capitalized it should be: ${ rcDirective.type }. In the near future this will be an Error so that Cmd5 can use homebridge-ui.\nTo remove this Warning, Please fix your config.json.` );
}
accTypeEnumIndex = rcDirective.accTypeEnumIndex;
// We can do this as this is a new way to do things.
let storedValue = this.cmd5Storage.getStoredValueForIndex( accTypeEnumIndex );
if ( storedValue == undefined )
throw new Error( `Polling for: "${ value }" requested, but characteristic is not in your config.json file for: "${ this.displayName }".` );
// This makes thinks nice down below.
valueToStore = storedValue;
break;
}
default:
{
// Is this still useful?
accTypeEnumIndex = CMD5_ACC_TYPE_ENUM.Cmd5indexOfEnum( key );
if ( accTypeEnumIndex < 0 )
// throw new Error( `OOPS: "${ key }" not found while parsing for characteristic polling. There something wrong with your config.json file?` );
throw new Error( `OOPS: "${ key }" not found while parsing for characteristic polling of "${ this.displayName }". There something wrong with your config.json file?` );
valueToStore = value;
}
}
}
if ( accTypeEnumIndex == -1 )
throw new Error( `No characteristic found while parsing for characteristic polling of: "${ this.displayName }". There something wrong with your config.json file?` );
if ( this.cmd5Storage.getStoredValueForIndex( accTypeEnumIndex ) == undefined )
{
this.log.warn( `Polling for: "${ key }" requested, but characteristic` );
this.log.warn( `is not in your config.json file for: ${ this.displayName }` );
this.log.warn( `This will be an error in the future.` );
}
this.cmd5Storage.setStoredValueForIndex( accTypeEnumIndex, valueToStore );
});
}
createServicesForStandaloneAccessoryAndItsChildren( accessory )
{
if ( settings.cmd5Dbg ) accessory.log.debug( chalk.blue( `createServicesFor${ this.CMD5 }AccessoryAndItsChildren` ) );
if ( accessory.ServiceCreated == true )
{
if ( settings.cmd5Dbg ) accessory.log.debug( chalk.red( `SERVICES ALREADY CREATED FOR ${ this.displayName } ${ this.CMD5 } ${ this.LEVEL }` ) );
return;
} else {
accessory.ServiceCreated = true;
}
let properties = CMD5_DEVICE_TYPE_ENUM.properties[ accessory.typeIndex ];
//
// Standalone Accessory
//
// Create the accessory's service
accessory.service = new properties.service( accessory.name, accessory.subType )
if ( settings.cmd5Dbg ) accessory.log.debug( `Creating information service for standalone accessory: ${ accessory.displayName }` );
// Create the Standalone accessory's information service.
createAccessorysInformationService( accessory );
// Create the Standalone accessory's services for all its linked children
if ( accessory.linkedAccessories )
{
accessory.linkedAccessories.forEach( ( linkedAccessory ) =>
{
let properties = CMD5_DEVICE_TYPE_ENUM.properties[ linkedAccessory.typeIndex ];
// Standalone Step 4.
// const hdmi1InputService = this.tvAccessory.addService( this.Service.InputSource, `hdmi1', 'HDMI 1' );
if ( settings.cmd5Dbg ) accessory.log.debug( `Standalone Step 4. linkedAccessory( ${ accessory.displayName } ).service = new Service( ${ linkedAccessory.name }, ${ linkedAccessory.subType } )` );
linkedAccessory.service = new properties.service( linkedAccessory.name, linkedAccessory.subType )
accessory.services.push( linkedAccessory.service );
// Hmmm Double Check this !!
// Create Information Service
//if ( settings.cmd5Dbg ) linkedAccessory.log.debug( "Creating information service for linkedAccessory:%s", linkedAccessory.displayName );
//createAccessorysInformationService( linkedAccessory );
if ( settings.cmd5Dbg ) accessory.log.debug( `Standalone Step 5. ${ accessory.displayName }.service.addLinkedService( ${ linkedAccessory.displayName }.service` );
// Standalone Step 5.
// tvService.addLinkedService( hdmi1InputService ); // link to tv service
accessory.service.addLinkedService( linkedAccessory.service );
linkedAccessory.addAllServiceCharacteristicsForAccessory( linkedAccessory );
// Setup the fakegato service if defined in the config.json file
linkedAccessory.setupAccessoryFakeGatoService( linkedAccessory.fakegatoConfig );
// Move the information service to the top of the list
linkedAccessory.services.unshift( linkedAccessory.informationService );
});
}
accessory.addAllServiceCharacteristicsForAccessory( accessory );
// Setup the fakegato service if defined in the config.json file
accessory.setupAccessoryFakeGatoService( accessory.fakegatoConfig );
accessory.services.push( accessory.service );
// Move the information service to the top of the list
accessory.services.unshift( accessory.informationService );
}
// ***********************************************
//
// setCachedValue:
// This methos will update the cached value of a
// characteristic of a accessory.
//
// ***********************************************
setCachedValue( accTypeEnumIndex, characteristicString, value, callback )
{
let self = this;
if ( self.hV.statusMsg == "TRUE" )
self.log.info( chalk.blue( `Setting (Cached) ${ self.displayName } ${ characteristicString }` ) + ` ${ value }` );
else
if ( settings.cmd5Dbg ) self.log.debug( `setCachedvalue accTypeEnumIndex:( ${ accTypeEnumIndex } )-"${ characteristicString }" function for: ${ self.displayName } value: ${ value }` );
// Save the cached value.
// Fakegato does not need to be updated as that is done on a "Get".
self.cmd5Storage.setStoredValueForIndex( accTypeEnumIndex, value );
let relatedCurrentAccTypeEnumIndex = this.getDevicesRelatedCurrentAccTypeEnumIndex( accTypeEnumIndex );
// We are currently tring to set a cached characteristics
// like "Target*".
// There is no way for its relatedCurrentAccTypeEnumIndex characteristic like "Current*"
// to be set if cached or Polled (with the exception below).
if ( relatedCurrentAccTypeEnumIndex != null )
{
// We are in a "Set" but this applies to the "Get" for why we would need to
// set the relatedCurrentAccTypeEnumIndex Characteristic as well.
if ( self.listOfPollingCharacteristics[ relatedCurrentAccTypeEnumIndex ])
{
let relatedCharacteristicString = CMD5_ACC_TYPE_ENUM.properties[ relatedCurrentAccTypeEnumIndex ].type;
self.log.info( chalk.blue( `Also Setting (Cached) ${ self.displayName } ${ relatedCharacteristicString }` ) + ` ${ value }` );
self.cmd5Storage.setStoredValueForIndex( relatedCurrentAccTypeEnumIndex, value );
}
}
callback( null );
}
// ***********************************************
//
// GetCachedValue:
// This methos will return an accessories cached
// characteristic value.
//
// ***********************************************
getCachedValue( accTypeEnumIndex, characteristicString, callback )
{
let self = this;
let storedValue = self.cmd5Storage.getStoredValueForIndex( accTypeEnumIndex );
if ( storedValue == null || storedValue == undefined )
{
self.log.warn( `getCachedValue ${ characteristicString } for: ${ self.displayName } has no cached value` );
callback( 10, null );
}
if ( settings.cmd5Dbg ) self.log.debug( `getCachedValue ${ characteristicString } for: ${ self.displayName } returned (CACHED) value: ${ storedValue }` );
callback( 0, storedValue );
// Store history using fakegato if set up
self.updateAccessoryAttribute( accTypeEnumIndex, storedValue );
}
// Check props to see if any characteristic properties
// are to be changed. For example, currentTemperature
// minValue to be below zero.
configHasCharacteristicProps( accTypeEnumIndex )
{
if ( this.props == undefined )
return undefined;
if ( ! isJSON( this.props ) )
return undefined;
let characteristicProps = CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ].props;
let type = CMD5_ACC_TYPE_ENUM.accEnumIndexToLC( accTypeEnumIndex );
let ucType = CMD5_ACC_TYPE_ENUM.accEnumIndexToUC( accTypeEnumIndex );
let definitions;
if ( this.props[ type ] )
definitions = this.props[ type ];
if ( this.props[ ucType ] )
definitions = this.props[ ucType ];
if ( ! definitions )
return undefined;
let rc = definitions;
for ( let key in definitions )
{
// warn now
if ( key.charAt( 0 ) === key.charAt( 0 ).toUpperCase() )
{
this.log.warn( `The property definition key: ${ key } is Capitalized. In the near future all characteristics will start with a lower case character for homebridge-ui integration.\nTo remove this Warning, Please fix your config.json.` );
}
let lcKey = lcFirst ( key );
if ( characteristicProps[ lcKey ] == undefined )
throw new Error( `props for key "${ key }" not in definition of "${ type }"` );
if ( typeof characteristicProps[ lcKey ] != typeof definitions[ lcKey ] )
throw new Error( `props for key "${ key }" type "${ typeof definitions[ key ] }" Not equal to definition of "${ typeof characteristicProps[ key ] }"` );
}
return rc;
}
checkCharacteristicNeedsFixing( accessory, accTypeEnumIndex )
{
// Hap keeps changing this where Current and Target don't match.
// We fix this here.
if ( accTypeEnumIndex == CMD5_ACC_TYPE_ENUM.CurrentHeatingCoolingState )
{
if ( settings.cmd5Dbg ) this.log.debug( "fixing heatingCoolingState" );
accessory.service.getCharacteristic(
CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ].
characteristic ).setProps(
{
maxValue: 3,
validValues: [ 0, 1, 2, 3 ]
});
}
return;
}
// ***********************************************
//
// addAllServiceCharacteristicsForAccessory:
// Method to set up all services for those characteristics in the
// config.json file.
//
//
// Explanation:
// If you are wondering why this is done this way as compared to
// other plugins that do the switch and a bind in their getServices
// section; It took a week to figure out why the security
// system was not getting updated after setting the target state.
// The get currentState needs to be called after the set targetState,
// but that was not enough. Something is different with their
// getServices bind implementation. While everything works, for
// some reason the IOS HomeKit app and even the Eve app never gets
// the result of the get currentState.
// I could delve further into their implementation, but this works.
// It was one of many methods I tried after examining and trying
// many plugins.
// This method was taken from homebridge-real-fake-garage-doors by
// plasticrake.
// P.S - This is probably more documentation of code anywhere
// in Homebridge :-) If you find it useful, send
// me a like ;-)
//
//
// Note: This code wipes out 5K of duplicate code.
// by using a bound function. It appears
// to work on my iMac.
//
// ***********************************************
addAllServiceCharacteristicsForAccessory( accessory )
{
if ( settings.cmd5Dbg ) accessory.log.debug( `Adding All Service Characteristics for: ${ accessory.displayName }` );
let perms = "";
// Check every possible characteristic
for ( let accTypeEnumIndex = 0; accTypeEnumIndex < CMD5_ACC_TYPE_ENUM.EOL; accTypeEnumIndex++ )
{
// For "Get" or "Set" commands, we send uppercase
let uCCharacteristicString = CMD5_ACC_TYPE_ENUM.accEnumIndexToUC( accTypeEnumIndex );
// If there is a stored value for this characteristic ( defined by the config file )
// Then we need to add the characteristic too
let storedValue = accessory.cmd5Storage.getStoredValueForIndex( accTypeEnumIndex );
if ( storedValue != undefined )
{
if ( settings.cmd5Dbg ) accessory.log.debug( "Found characteristic:%s value:%s for:%s",
uCCharacteristicString,
storedValue,
this.displayName );
// Find out if the characteristic is not part of the service
// and needs to be added.
if ( ! accessory.service.testCharacteristic(
CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ].characteristic ) )
{
//if ( settings.cmd5Dbg ) accessory.log.debug( "Adding optional characteristic:%s for: %s", CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ].type, this.displayName );
if ( settings.cmd5Dbg ) accessory.log.debug( "Adding optional characteristic:%s for: %s", uCCharacteristicString, this.displayName );
accessory.service.addCharacteristic( CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ].characteristic );
}
this.checkCharacteristicNeedsFixing( accessory, accTypeEnumIndex );
let props = accessory.configHasCharacteristicProps( accTypeEnumIndex );
if ( props )
{
if ( settings.cmd5Dbg ) accessory.log.debug( "Overriding characteristic %s props for: %s ", CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ].type, this.displayName );
accessory.service.getCharacteristic( CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ].
characteristic )
.setProps(
// props is an object of name value pairs (characteristics)
props
);
}
// Get the permissions of characteristic ( Read/Write ... )
// Both are 100% the same.
// perms = CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ].props.perms
perms = accessory.service.getCharacteristic(
CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ]
.characteristic ).props.perms;
// Comment before change
// "Read and or write, we need to set the value once.
// If the characteristic was optional and read only, this will add
// it with the correct value. You cannot add and set a read characteristic."
//
// What was happening was at startup all writeable characteristics were calling
// setValue and the MyAir was getting hammered.
// We need to check if the characteristic is readable but not writeable.
// Things this will set are like:
// - Name
// - CurrentTemperature
// - CurrentHeatingCoolingState
// - StatusFault
// Homebridge V2 removes Perms.READ && Perms.WRITE
if ( //perms.indexOf( this.api.hap.Perms.READ ) >= 0 &&
//perms.indexOf( this.api.hap.Perms.WRITE ) == -1 ||
perms.indexOf( this.api.hap.Perms.PAIRED_READ ) >= 0 &&
perms.indexOf( this.api.hap.Perms.PAIRED_WRITE ) == -1 )
{
accessory.service.setCharacteristic(
CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ].characteristic,
this.cmd5Storage.getStoredValueForIndex( accTypeEnumIndex ) );
}
// Add getValue via getCachedValue funtion to service
if ( accessory.service.getCharacteristic(
CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ]
.characteristic ).listeners( "get" ).length == 0 )
{
// Add Read services for characterisitcs, if possible
// Homebridge v2 removed Perms.READ
if ( // perms.indexOf( this.api.hap.Perms.READ ) != -1 ||
perms.indexOf( this.api.hap.Perms.PAIRED_READ ) != -1 )
{
// getCachedValue or getValue
if ( ! accessory.polling ||
accessory.listOfPollingCharacteristics[ accTypeEnumIndex ] == undefined
)
{
if ( settings.cmd5Dbg ) this.log.debug( chalk.yellow( `Adding getCachedValue for ${ accessory.displayName } characteristic: ${ uCCharacteristicString } ` ) );
//Get cachedValue
accessory.service.getCharacteristic(
CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ]
.characteristic )
.on( "get", accessory.getCachedValue.bind( accessory, accTypeEnumIndex, uCCharacteristicString ) );
} else
{
if ( settings.cmd5Dbg ) this.log.debug( chalk.yellow( `Adding priorityGetValue for ${ accessory.displayName } characteristic: ${ uCCharacteristicString }` ) );
let details = accessory.lookupAccessoryHVForPollingCharacteristic( accessory, accTypeEnumIndex );
// Set parms are accTypeEnumIndex, value, callback
// Get parms are accTypeEnumIndex, callback
accessory.service.getCharacteristic(
CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ]
.characteristic )
.on( "get", accessory.queue.priorityGetValue.bind( accessory, accTypeEnumIndex, uCCharacteristicString, details.timeout ) );
}
}
}
// Add setValue function to service
if ( accessory.service.getCharacteristic(
CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ]
.characteristic ).listeners( "set" ).length == 0 )
{
// Add Write services for characterisitcs, if possible
// Homebridge V2 removes Perms.WRITE
if ( // perms.indexOf( this.api.hap.Perms.WRITE ) != -1 ||
perms.indexOf( this.api.hap.Perms.PAIRED_WRITE ) != -1 )
{
// setCachedValue or setValue
if ( ! accessory.polling ||
accessory.listOfPollingCharacteristics[ accTypeEnumIndex ] == undefined)
{
if ( settings.cmd5Dbg ) this.log.debug( chalk.yellow( `Adding setCachedValue for ${ accessory.displayName } characteristic: ${ uCCharacteristicString } ` ) );
// setCachedValue has parameters:
// accTypeEnumIndex, value, callback
// The first bound value though is "this"
let boundSetCachedValue = accessory.setCachedValue.bind( this, accTypeEnumIndex, uCCharacteristicString );
accessory.service.getCharacteristic(
CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ]
.characteristic ).on( "set", ( value, callback ) => {
boundSetCachedValue( value, callback );
});
} else {
if ( settings.cmd5Dbg ) this.log.debug( chalk.yellow( `Adding prioritySetValue for ${ accessory.displayName } characteristic: ${ uCCharacteristicString } ` ) );
let details = accessory.lookupAccessoryHVForPollingCharacteristic( accessory, accTypeEnumIndex );
// Set parms are accTypeEnumIndex, value, callback
// Get parms are accTypeEnumIndex, callback
let boundSetValue = accessory.queue.prioritySetValue.bind( this, accTypeEnumIndex, uCCharacteristicString, details.timeout, details.stateChangeResponseTime );
accessory.service.getCharacteristic(
CMD5_ACC_TYPE_ENUM.properties[ accTypeEnumIndex ]
.characteristic ).on( "set", ( value, callback ) => {
boundSetValue( value, callback );
});
}
}
}
}
}
}
updateAccessoryAttribute( accTypeEnumIndex, value )
{
if ( accTypeEnumIndex < 0 || accTypeEnumIndex > CMD5_ACC_TYPE_ENUM.EOL )
{
this.log.error( `Internal error: updateAccessoryAttribute - accTypeEnumIndex: ${ accTypeEnumIndex } for: ${ this.displayName } not found` );
return;
}
this.cmd5Storage.setStoredValueForIndex( accTypeEnumIndex, value );
if ( this.loggingService )
{
let firstParm, secondParm, thirdParm;
let firstParmValue, secondParmValue, thirdParmValue = 0;
let firstParmDirective, secondParmDirective, thirdParmDirective;
switch ( this.eve )
{
case constants.FAKEGATO_TYPE_ENERGY:
{
firstParm = this.fakegatoConfig[ constants.POWER ] || "0";
firstParmDirective = isAccDirective( firstParm, true );
firstParmValue = ( this.cmd5Storage.testStoredValueForIndex( firstParmDirective.accTypeEnumIndex ) == undefined ) ?
firstParmValue : this.cmd5Storage.getStoredValueForIndex( firstParmDirective.accTypeEnumIndex );
if ( settings.cmd5Dbg ) this.log.debug( `Logging ${ constants.POWER }: ${ firstParmValue }` );
// Eve Energy ( Outlet service )
this.loggingService.addEntry(
{ [ constants.TIME ] : moment( ).unix( ),
[ constants.POWER ] : firstParmValue
});
break;
}
case constants.FAKEGATO_TYPE_ROOM:
{
firstParm = this.fakegatoConfig[ constants.TEMP ] || "0";
firstParmDirective = isAccDirective( firstParm, true );
secondParm = this.fakegatoConfig[ constants.HUMIDITY ] || "0";
secondParmDirective = isAccDirective( secondParm, true );
thirdParm = this.fakegatoConfig[ constants.PPM ] || "0";
thirdParmDirective = isAccDirective( thirdParm, true );
firstParmValue = ( this.cmd5Storage.testStoredValueForIndex( firstParmDirective.accTypeEnumIndex ) == undefined ) ?
firstParmValue : this.cmd5Storage.getStoredValueForIndex( firstParmDirective.accTypeEnumIndex );
secondParmValue = ( this.cmd5Storage.testStoredValueForIndex( secondParmDirective.accTypeEnumIndex ) == undefined ) ?
secondParmValue : this.cmd5Storage.getStoredValueForIndex( secondParmDirective.accTypeEnumIndex );
thirdParmValue = ( this.cmd5Storage.testStoredValueForIndex( thirdParmDirective.accTypeEnumIndex ) == undefined ) ?
thirdParmValue : this.cmd5Storage.getStoredValueForIndex( thirdParmDirective.accTypeEnumIndex );
if ( settings.cmd5Dbg ) this.log.debug( `Logging ${ constants.TEMP }:${ firstParmValue } ${constants.HUMIDITY }:${ secondParmValue } ${ constants.PPM }:${ thirdParmValue }` );
// Eve Room ( TempSensor, HumiditySensor and AirQuality Services )
this.loggingService.addEntry(
{ [ constants.TIME ] : moment( ).unix( ),
[ constants.TEMP ] : firstParmValue,
[ constants.HUMIDITY ] : secondParmValue,
[ constants.PPM ] : thirdParmValue
});
break;
}
case constants.FAKEGATO_TYPE_WEATHER:
{
firstParm = this.fakegatoConfig[ constants.TEMP ] || "0";
firstParmDirective = isAccDirective( firstParm, true );
secondParm = this.fakegatoConfig[ constants.PRESSURE ] || "0";
secondParmDirective = isAccDirective( secondParm, true );
thirdParm = this.fakegatoConfig[ constants.HUMIDITY ] || "0";
thirdParmDirective = isAccDirective( thirdParm, true );
firstParmValue = ( this.cmd5Storage.testStoredValueForIndex( firstParmDirective.accTypeEnumIndex ) == undefined ) ?
firstParmValue : this.cmd5Storage.getStoredValueForIndex( firstParmDirective.accTypeEnumIndex );
secondParmValue = ( this.cmd5Storage.testStoredValueForIndex( secondParmDirective.accTypeEnumIndex ) == undefined ) ?
secondParmValue : this.cmd5Storage.getStoredValueForIndex( secondParmDirective.accTypeEnumIndex );
thirdParmValue = ( this.cmd5Storage.testStoredValueForIndex( thirdParmDirective.accTypeEnumIndex ) == undefined ) ?
thirdParmValue : this.cmd5Storage.getStoredValueForIndex( thirdParmDirective.accTypeEnumIndex );
if ( settings.cmd5Dbg ) this.log.debug( `Logging ${ constants.TEMP }: ${ firstParmValue } ${ constants.PRESSURE }: ${ secondParmValue } ${ constants.HUMIDITY }: ${ thirdParmValue }` );
// Eve Weather ( TempSensor Service )
this.loggingService.addEntry(
{ [ constants.TIME ] : moment( ).unix( ),
[ constants.TEMP ] : firstParmValue,
[ constants.PRESSURE ] : secondParmValue,
[ constants.HUMIDITY ] : thirdParmValue
});
break;
}
case constants.FAKEGATO_TYPE_DOOR:
{
firstParm = this.fakegatoConfig[ constants.STATUS ] || "0";
firstParmDirective = isAccDirective( firstParm, true );
firstParmValue = ( this.cmd5Storage.testStoredValueForIndex( firstParmDirective.accTypeEnumIndex ) == undefined ) ?
firstParmValue : this.cmd5Storage.getStoredValueForIndex( firstParmDirective.accTypeEnumIndex );
if ( settings.cmd5Dbg ) this.log.debug( `Logging ${ constants.STATUS } status: ${ firstParmValue }` );
this.loggingService.addEntry(
{ [ constants.TIME ] : moment( ).unix( ),
[ constants.STATUS ] : firstParmValue
});
break;
}
case constants.FAKEGATO_TYPE_MOTION:
{
firstParm = this.fakegatoConfig[ constants.STATUS ] || "0";
firstParmDirective = isAccDirective( firstParm, true );
firstParmValue = ( this.cmd5Storage.testStoredValueForIndex( firstParmDirective.accTypeEnumIndex ) == undefined ) ?
firstParmValue : this.cmd5Storage.getStoredValueForIndex( firstParmDirective.accTypeEnumIndex );
if ( settings.cmd5Dbg ) this.log.debug( `Logging ${ constants.STATUS }: ${ firstParmValue }` );
this.loggingService.addEntry(
{ [ constants.TIME ] : moment( ).unix( ),
[ constants.STATUS ] : firstParmValue
});
break;
}
case constants.FAKEGATO_TYPE_THERMO:
{
firstParm = this.fakegatoConfig[ constants.CURRENTTEMP ] || "0";
firstParmDirective = isAccDirective( firstParm, true );
secondParm = this.fakegatoConfig[ constants.SETTEMP ] || "0";
secondParmDirective = isAccDirective( secondParm, true );
thirdParm = this.fakegatoConfig[ constants.VALVEPOSITION ] || "0";
thirdParmDirective = isAccDirective( thirdParm, true );
firstParmValue = ( this.cmd5Storage.testStoredValueForIndex( firstParmDirective.accTypeEnumIndex ) == undefined ) ?
firstParmValue : this.cmd5Storage.getStoredValueForIndex( firstParmDirective.accTypeEnumIndex );
secondParmValue = ( this.cmd5Storage.t