UNPKG

homebridge-bondbridge

Version:

Catered shell script to integrate BondBridge units by Bond

965 lines (833 loc) 31.3 kB
const { HomebridgePluginUiServer } = require('@homebridge/plugin-ui-utils'); const { RequestError } = require('@homebridge/plugin-ui-utils'); const fs = require('fs') const chalk = require('chalk') const which = require('which'); const path = require( "path" ); const commandExistsSync = require( "command-exists" ).sync; class UiServer extends HomebridgePluginUiServer { constructor () { super(); this.BONDBRIDGE_SH = "/homebridge-bondbridge/BondBridge.sh"; this.CONFIGCREATOR_SH = "/homebridge-bondbridge/ConfigCreator.sh"; this.listOfConstants = { }; this.debug = false; this.config = { }; this.updateConfigFirstTime( true ); // handle request this.onRequest('/configcreator', this.ConfigCreator.bind(this)); this.onRequest('/checkInstallationButtonPressed', this.checkInstallationButtonPressed.bind(this)); this.onRequest('/consoleLog', this.consoleLog.bind(this)); // console.log("HomebridgePluginUIServer ready"); this.ready(); } async ConfigCreator(payload) { if (payload.ip !== "") { console.log(`Processing BondBridge device: ${payload.ip}, token: ${payload.token}, debug: ${payload.debug}`); console.log(`Setup instruction: ${payload.CFsetupOption}`); console.log(`Timer setup instruction: ${payload.CFtimerSetup}`); } if (payload.ip2 !== "") { console.log(`Processing BondBridge device: ${payload.ip2}, token: ${payload.token2}, debug: ${payload.debug2}`); console.log(`Setup instruction: ${payload.CFsetupOption2}`); console.log(`Timer setup instruction: ${payload.CFtimerSetup2}`); } if (payload.ip3 !== "") { console.log(`Processing BondBridge device: ${payload.ip3}, token: ${payload.token3}, debug: ${payload.debug3}`); console.log(`Setup instruction: ${payload.CFsetupOption3}`); console.log('Timer setup instruction:', payload.CFtimerSetup3); } try { const BondBridge_shPath = this.getGlobalNodeModulesPathForFile( this.BONDBRIDGE_SH ); const ConfigCreator_shPath = this.getGlobalNodeModulesPathForFile( this.CONFIGCREATOR_SH ); //This spawns a child process which runs a bash script const spawnSync = require('child_process').spawnSync; let FeedBack = spawnSync(ConfigCreator_shPath, [payload.ip,payload.token,payload.CFsetupOption,payload.CFtimerSetup,payload.debug,payload.ip2,payload.token2,payload.CFsetupOption2,payload.CFtimerSetup2,payload.debug2,payload.ip3,payload.token3,payload.CFsetupOption3,payload.CFtimerSetup3,payload.debug3,BondBridge_shPath], {encoding: 'utf8'}); let feedback = `${ FeedBack.stdout.replace(/\n*$/, "")}` // return data to the ui return { feedback: feedback } } catch (e) { throw new RequestError('Failed to run ConfigCreator.sh', { message: e.message }); } } async consoleLog( msg ) { if ( this.debug ) console.log( msg ); } // Have the server send an error to the listening HTML page. // We could return the same structure, but this would be synchronously. // which is okay in most instances. The other reason is that toast // error messages close within a few seconds. Not giving time for // complicated messages like the proper state_cmd to use. advError( data ) { this.pushEvent('advErrorEvent', data ); } checkQueueTypesForQueue( queueTypes, queue ) { for ( let queueTypesIndex = 0; queueTypesIndex < queueTypes.length; queueTypesIndex++ ) { let entry = queueTypes[ queueTypesIndex ]; if ( entry.queue == queue ) { if ( entry.queueType == "WoRm2" ) { return( { rc: true, message: `passed` }); } return( { rc: false, message: `queue ${ queue } queueType is not WoRm2. Please change to Worm2.` }); } } return( { rc: false, message: `No matching queue: "${ queue }" in queueTypes` }); } // MyPlace platform has the ability to allow constants which could be used for the IP // processConstants( constantsArgArray ) { // // Check #8A // Constants must be an Array // if ( ! Array.isArray ( constantsArgArray ) ) { this.advError( { "rc": false, "message": `Constants must be an array of { "key": "\${SomeKey}", "value": "some replacement string" }` }); return false; } // Iterate over the groups of key/value constants in the array. // Note: DO NOT USE: forEach as javascript continues after a return! for ( let argIndex = 0; argIndex < constantsArgArray.length; argIndex++ ) { let argEntry = constantsArgArray[ argIndex ]; if ( argEntry.key == undefined ) { // // Check #8B // key must be defined // this.advError( { "rc": false, "message": `Constant definition at index: "${ argIndex }" has no "key":` }); return false; } if ( argEntry.value == undefined ) { // // Check #8c // value must be defined // this.advError( { "rc": false, "message": `Constant definition at index: "${ argIndex }" has no "value":` }); return false; } let keyToAdd = argEntry.key; let valueToAdd = argEntry.value; if ( ! keyToAdd.startsWith( "${" ) ) { if ( this.debug ) console.log( `Constant definition for: "${ keyToAdd }" must start with "\${" for clarity.` ); // // Check #8D // key must start with ${ // this.advError( { "rc": false, "message": `Constant definition for: "${ keyToAdd }" must start with "\${" for clarity.` }); return false; } if ( ! keyToAdd.endsWith( "}" ) ) { // // Check #8E // key must end with } // if ( this.debug ) console.log( `Constant definition for: "${ keyToAdd }" must end with "}" for clarity.` ); this.advError( { "rc": false, "message": `Constant definition for: "${ keyToAdd }" must end with "}" for clarity.` }); return false; } // remove any leading and trailing single quotes // so that using it for replacement will be easier. valueToAdd.replace(/^'/, "") valueToAdd.replace(/'$/, "") if ( this.debug ) console.log( "Server.js keyToAa=%s valueToAdd:%s", keyToAdd, valueToAdd ); this.listOfConstants[ keyToAdd ] = valueToAdd; } return true; } replaceConstantsInString( orig ) { let finalAns = orig; for ( let key in this.listOfConstants ) { let replacementConstant = this.listOfConstants[ key ]; if ( this.debug ) console.log(`replacing key: ${ key } with: ${ replacementConstant }` ); finalAns = finalAns.replace( key, replacementConstant ); } return finalAns; } updateConfigFirstTime( firstTime ) { // // Check #1 // See if the config.json file exists // let configFile = this.homebridgeConfigPath; if ( configFile == undefined ) { if ( this.debug ) console.log( `Server.js returning false configFile is undefined` ); this.advError( { "rc": false, "message": `No config.json yet` }); return false; } if ( ! fs.existsSync( configFile ) ) { if ( ! firstTime ) { if ( this.debug ) console.log( `Server.js returning false configFile ${ configFile }` ); this.advError( { "rc": false, "message": `No ${ configFile } yet` }); } return false; } // Open the config.json file for reading let config_in = fs.readFileSync( configFile, 'utf8' ); // // Check #2 // Convert the config.json into a json type // This can throw an Error so catch it. try { this.config = JSON.parse( config_in ); } catch ( e ) { if ( ! firstTime ) { if ( this.debug ) console.log( `Server.js returning false parse failed ${ e }` ); this.advError( { "rc": false, "message": `Parse config.json failed: ${ e }` }); } return false; } let BondBridgeConfig = this.config.platforms.find( platform => platform[ "BondBridge" ] !== null ); if ( BondBridgeConfig && BondBridgeConfig.debug ) { console.log( `Setting debug for platform BondBridge` ); this.debug = BondBridgeConfig.debug; } if ( this.debug ) console.log( `main.js After JSONPARSE` ); return true; } // There is nothing really to differentiate a regular Accessory for that of // a Bond Bridge device // isAccessoryAbondbridge( accessory ) { if ( accessory.manufacturer && accessory.manufacturer.match( /Bond/ ) ) return true; // Trigger off of the state_cmd, if it exists if ( accessory.state_cmd != undefined ) { // The new BondBridge if ( accessory.state_cmd.match( /BondBridge.sh/ ) ) return true; } return false; } // This method is called by the html page (main.js) to check the users config.json // for a valid configuration of the BondBridge accessory. checkInstallationButtonPressed( ) { // The read in config.json in JSON format let fileToFind = ""; if ( this.debug ) console.log( "Server.js in checkInstallationButtonPressed(" ); // Update the config, this is not the first time // return if it fails. As this is not the First time, it will // error if need be. if ( this.updateConfigFirstTime( false ) == false ) return; // // Check #3 // Check that jq is installed. if ( ! commandExistsSync( "jq" ) ) { if ( this.debug ) console.log( `Server.js returning false jq not installed` ); this.advError( { "rc": false, "message": `jq is required globally and not installed.` }); return; } // // Check #4 // Check that jq is installed. if ( ! commandExistsSync( "curl" ) ) { if ( this.debug ) console.log( `Server.js returning false curl not installed` ); this.advError( { "rc": false, "message": `curl is required globally and not installed.` }); return; } // // Check #5A // Find Node modules // let node_modules = this.getGlobalNodeModulesPathForFile( "" ); if ( node_modules == null ) { if ( this.debug ) console.log( `Server.js Could not determine where node_modules is.` ); this.advError( { "rc": false, "message": `Could not determine where node_modules is installed globally.` }); return; } // // Check #5B // See if MyPlace is installed from node_modules // fileToFind = "/homebridge-myplace/index.js"; let myPlaceIndex = this.getGlobalNodeModulesPathForFile( fileToFind ) if ( myPlaceIndex == null ) { if ( this.debug ) console.log( `Server.js returning false myPlaceIndex <Your Global node_modules Path>${ fileToFind }` ); this.advError( { "rc": false, "message": `MyPlace Plugin not installed` }); return; } // // Check #6 // See if our BondBridge.sh script is present // // Create the path to the BondBridge.sh from node_modules let ourScript = this.getGlobalNodeModulesPathForFile( this.BONDBRIDGE_SH ) if ( ourScript == null ) { if ( this.debug ) console.log( `Server.js returning false. No BondBridge.sh present. Looking for: <Your Global node_modules Path>${ this.BONDBRIDGE_SH }` ); this.advError( { "rc": false, "message": `No BondBridge.sh script present. Looking for: <Your Global node_modules Path>${ this.BONDBRIDGE_SH }` }); return; } let myPlaceAccessoriesFound = false; let bondBridgeAccessoriesFound = []; let myPlaceQueueTypesFound = []; let retVal = { }; // Iterate over the elements in the array. // Note: DO NOT USE: forEach as javascript continues after a return! for ( let entryIndex = 0; entryIndex < this.config.platforms.length; entryIndex++ ) { let entry = this.config.platforms[ entryIndex]; if ( this.debug ) console.log( `Server.js Checking Platform entry ${ entry.platform }` ); // // Check #7 // See if any MyPlace accessories are defined in config.json // if ( entry.platform != "MyPlace" ) continue; myPlaceAccessoriesFound = true; // // Check #18 // See if there are any accessory queues defined // if ( entry.queueTypes != undefined ) { // // Check #19 // queueTypes must be an array // if ( ! Array.isArray( entry.queueTypes ) ) { if ( this.debug ) console.log( `Server.js returning false queueTypes is not an Array` ); this.advError( { "rc": false, "message": `queueTypes is not an Array` }); return; } // Iterate over the elements in the array. // Note: DO NOT USE: forEach as javascript continues after a return! for ( let queueTypesIndex = 0; queueTypesIndex < entry.queueTypes.length; queueTypesIndex++ ) { let queueTypeEntry = entry.queueTypes[ queueTypesIndex ]; // Need to append each one retVal = this.checkQueueTypesForQueue( myPlaceQueueTypesFound, queueTypeEntry.queue ); if ( retVal.rc == true ) // if ( myPlaceQueueTypesFound.find( queueTypeEntry ) ) { // // Check #20 // Duplicate queue // this.advError( { "rc": false, "message": `Duplicate queue found: ${ queueTypeEntry.queue }` }); return; } myPlaceQueueTypesFound.push( queueTypeEntry ); } } // // Check #8 // Process Constants // if ( entry.constants != undefined ) if ( this.processConstants( entry.constants ) == false ) return; // Iterate over the elements in the array. // Note: DO NOT USE: forEach as javascript continues after a return! for ( let accessoryIndex = 0; accessoryIndex < entry.accessories.length; accessoryIndex++ ) { let accessory = entry.accessories[ accessoryIndex ]; if ( this.debug ) console.log( `Server.js Checking accessory ${ accessory.name }` ); // // Check #9 // See if any BondBridge accessories are defined in config.json // if ( ! this.isAccessoryAbondbridge( accessory ) ) continue; // // Check #10 // See if any BondBridge accessory has a defined name // if ( this.debug ) console.log( `Server.js Checking accessory ${ accessory.name }` ); if ( accessory.name == undefined ) { this.advError( { "rc": false, "message": `Accessory at index: ${ entryIndex } accessory.name is undefined` }); return; } // // Check #11 // See if any BondBridge accessory has a defined displayName // if ( this.debug ) console.log( `Server.js Checking accessory ${ accessory.name } for displayName` ); if ( accessory.displayName == undefined ) { this.advError( { "rc": false, "message": `Accessory at index: ${ entryIndex } "${ accessory.name }" has no displayName` }); return; } // // Check #12 // Polling is done by displayName, It cannot already exist. // if ( this.debug ) console.log( `Server.js Checking accessory ${ accessory.displayName } for duplicate displayName` ); if ( bondBridgeAccessoriesFound.find( ( displayName ) => displayName == accessory.displayName ) ) { this.advError( { "rc": false, "message": `Accessory: "${ accessory.displayName }"'s displayName is defined twice` }); return; } // Add it to the Array bondBridgeAccessoriesFound.push( accessory.displayName ); if ( this.debug ) console.log( `Server.js Checking BondBridge accessory ${ accessory.displayName }` ); // // Check #13 // The state_cmd must be defined for the BondBridge accessory // if ( accessory.state_cmd == undefined ) { this.advError( { "rc": false, "message": `No state_cmd for: "${ accessory.displayName }"` }); return; } // // Check #14 // See if the state_cmd does not match the BondBridge.sh // if ( ! accessory.state_cmd.match( ourScript ) ) { if ( this.debug ) console.log( `Server.js returning false accessory.displayName ${ accessory.displayName } invalid state_cmd` ); this.advError( { "rc": false, "message": `Invalid state_cmd for: "${ accessory.displayName }". It should be:\n${ ourScript }` }); return; } // // Check #15 // See if the state_cmd_suffix is defined for the BondBridge accessory // It must have at least an IP if ( accessory.state_cmd_suffix == undefined ) { this.advError( { "rc": false, "message": `No state_cmd_suffix for: "${ accessory.displayName }". It must at least contain an IP.` }); return; } if ( this.debug ) console.log(`Calling replaceConstantsInString`); let state_cmd_suffix = this.replaceConstantsInString( accessory.state_cmd_suffix ); if ( this.debug ) console.log(`after replaceConstantsInString state_cmd_suffix=${ state_cmd_suffix }`); // // Check #16A // The state_cmd_suffix must have an IP for the BondBridge accessory // if ( ! state_cmd_suffix.match( /[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*/ ) ) { this.advError( { "rc": false, "message": `state_cmd_suffix has no IP for: "${ accessory.displayName }" state_cmd_suffix: ${ state_cmd_suffix }` }); return; } // // Check #16B // The state_cmd_suffix must have a 'token: for the BondBridge accessory // if ( ! state_cmd_suffix.match( /'token:[a-f0-9]*'/ ) ) { this.advError( { "rc": false, "message": `state_cmd_suffix has no 'token' defined for: "${ accessory.displayName }" state_cmd_suffix: ${ state_cmd_suffix }` }); return; } // // Check #16C // The state_cmd_suffix must have a 'device: for the BondBridge accessory // if ( ! state_cmd_suffix.match( /'device:[a-f0-9]*'/ ) ) { this.advError( { "rc": false, "message": `state_cmd_suffix has no 'device' defined for: "${ accessory.displayName }" state_cmd_suffix: ${ state_cmd_suffix }` }); return; } // // Check #17A // The state_cmd_suffix must have a fanSwitch or a lightSwitch for a BondBridge Switch accessory // if ( accessory.type.match( /^Switch/ ) ) { if ( ! ( state_cmd_suffix.match( /fanSwitch/ ) || state_cmd_suffix.match( /lightSwitch/ ) ) ) { this.advError( { "rc": false, "message": `The state_cmd_suffix for: "${ accessory.displayName }" requires a 'fanSwitch' or 'lightSwitch'.` }); return; } } // // Check #17B // The state_cmd_suffix must have a 'fan ' for a Fan accessory // if ( accessory.type.match( /^Fan/ ) ) { if ( ! state_cmd_suffix.match( /fan / ) ) { this.advError( { "rc": false, "message": `The state_cmd_suffix for: "${ accessory.displayName }" requires a key word 'fan'.` }); return; } } // // Check #17C // The state_cmd_suffix must have a 'light ', 'dimmer', 'lightTimer', 'lightDevice', 'fanDevice' or 'fanTimer' for a Lightbulb accessory // if ( accessory.type.match( /^Lightbulb/ ) ) { if ( ! ( state_cmd_suffix.match( /light / ) || state_cmd_suffix.match( /dimmer / ) || state_cmd_suffix.match( /lightTimer/ ) || state_cmd_suffix.match( /fanTimer/ ) ) ) { this.advError( { "rc": false, "message": `The state_cmd_suffix for: "${ accessory.displayName }" requires a keyword 'light ' or 'lightTimer' or 'fanTimer'.` }); return; } // // Check #17C1 // if state_cmd_suffix has lightTimer, then it must also has lightDecice // if ( state_cmd_suffix.match( /lightTimer/ ) ) { if ( ! state_cmd_suffix.match( /'lightDevice:[a-f0-9]*'/ ) ) { this.advError( { "rc": false, "message": `The state_cmd_suffix for: "${ accessory.displayName }" requires 'lightDevice' properly defined.` }); return; } } // // Check #17C2 // if state_cmd_suffix has fanTimer, then it must also has fanDecive // if ( state_cmd_suffix.match( /fanTimer/ ) ) { if ( ! state_cmd_suffix.match( /'fanDevice:[a-f0-9]*'/ ) ) { this.advError( { "rc": false, "message": `The state_cmd_suffix for: "${ accessory.displayName }" requires 'fanDevice' properly defined.` }); return; } } } // // Check #21 // See if there is a queue defined // if ( accessory.queue == undefined ) { if ( this.debug ) console.log( `Server.js returning false accessory.displayName ${ accessory.displayName } queue is not a string` ); this.advError( { "rc": false, "message": `No queue defined for: "${ accessory.displayName }"` }); return; } // // Check #22 // queue name must be an string // if ( typeof accessory.queue != "string" ) { if ( this.debug ) console.log( `Server.js returning false accessory.displayName ${ accessory.displayName } queue is not a string` ); this.advError( { "rc": false, "message": `queue for: "${ accessory.displayName }" is not a string` }); return; } retVal = this.checkQueueTypesForQueue( myPlaceQueueTypesFound, accessory.queue ); // Check #23 // queue must be defined in queueTypes if ( retVal.rc == false ) { if ( this.debug ) console.log( `Server.js returning false accessory.displayName ${ accessory.displayName } no queue defined in queueTypes` ); this.advError( { "rc": false, "message": `For: "${ accessory.displayName }" ${ retVal.message }` }); return; } // Check #24 Polling must be defined for BondBridge accessories if ( ! accessory.polling || ( typeof accessory.polling == "boolean" && accessory.polling != true && ! Array.isArray( accessory.polling) ) ) { if ( this.debug ) console.log( `Server.js returning false accessory.displayName ${ accessory.displayName } polling not defined correctly` ); this.advError( { "rc": false, "message": `Polling for: "${ accessory.displayName }" is not an Array or Boolean` }); return; } } } // // Check #32 // See if any MyPlace accessories are defined in config.json // if ( myPlaceAccessoriesFound == false ) { if ( this.debug ) console.log( `Server.js returning false no MyPlace Accessories` ); this.advError( { "rc": false, "message": `No MyPlace Accessories found` }); return; } // // Check #33 // See if any BondBridge accessories are defined in config.json // if ( bondBridgeAccessoriesFound.length == 0 ) { if ( this.debug ) console.log( `Server.js returning false noBondBridgeAccessories` ); this.advError( { "rc": false, "message": `No BondBridge Accessories found` }); return; } // // Check #34 // See if any queueTypes were defined // ( Most likely an earlier failure will succeed this one ) // if ( myPlaceQueueTypesFound == null ) { if ( this.debug ) console.log( `Server.js returning false no MyPlace Queue types defined` ); this.advError( { "rc": false, "message": `No MyPlace Queue Types were defined for BondBridge Accessories` }); return; } if ( this.debug ) { console.log( chalk.red( `Remember to remove the "BondBridge" debug entry from your config.json when done.` ) ); } // PASS ! this.advError( { "rc": true, "message": `Passed` }); } getGlobalNodeModulesPathForFile( file ) { let fullPath = null; for ( let tryIndex = 1; tryIndex <= 5; tryIndex ++ ) { switch ( tryIndex ) { case 1: { if ( commandExistsSync( "npm" ) ) { // Use spawnSync as execSync does not allow capture of // stdio, even when using try/catch const spawnSync = require('child_process').spawnSync; let foundPath = spawnSync("npm", ["root", "-g"], {encoding: 'utf8'}); if ( foundPath.stderr ) { console.log( "Error: %s", foundPath.stderr ); console.log( "This error is a Debian packaging issue. See: https://github.com/nodejs/node-v0.x-archive/issues/3911#issuecomment-8956154" ); break; } if ( ! foundPath.stdout ) break; // Remove any trailing carriage returns and combine // with file. let fullPath = `${ foundPath.stdout.replace(/\n*$/, "")}${ file }`; if ( fs.existsSync( fullPath ) ) return fullPath; } break; } case 2: { if ( commandExistsSync( "homebridge" ) ) { const homebridgePath = which.sync( 'homebridge', { nothrow: true } ) if ( homebridgePath ) { let dirname = path.dirname( homebridgePath ); dirname = dirname.replace(/\/[^/]+$/, ''); fullPath = `${dirname}${ file }`; if ( fs.existsSync( fullPath ) ) return fullPath; } } break; } case 3: { fullPath = `/usr/local/lib/node_modules${ file }`; if ( fs.existsSync( fullPath ) ) return fullPath; break; } case 4: { fullPath = `/usr/lib/node_modules${ file }`; if ( fs.existsSync( fullPath ) ) return fullPath; break; } case 5: { fullPath = `/opt/homebrew/lib/node_modules${ file }`; if ( fs.existsSync( fullPath ) ) return fullPath; break; } } } return null; } } module.exports = UiServer; //(() => { // return new UiServer(); //})(); (function() { return new UiServer; })();