UNPKG

homebridge-myplace

Version:

Exec Plugin bringing Advanatge Air MyPlace system to Homekit

1,072 lines (890 loc) 94.7 kB
'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