UNPKG

homebridge-myplace

Version:

Exec Plugin bringing Advanatge Air MyPlace system to Homekit

606 lines (525 loc) 19.2 kB
const fs = require('fs') const chalk = require('chalk') const commandExistsSync = require( "command-exists" ).sync; // Parse the args var args = process.argv.slice(2); const MYPLACE_SH_PATH = args[0] || "/usr/local/lib/node_modules/homebridge-myplace/MyPlace.sh"; const homebridgeConfigPath = args[1] || "/var/lib/homebridge/config.json"; let listOfConstants = { }; var debug = false; consoleLog(`MYPLACE_path=${MYPLACE_SH_PATH}`) consoleLog(`configJsonPath=${homebridgeConfigPath}`) checkInstallationButtonPressed( true ) function consoleLog( msg ) { if ( debug ) { console.log( msg ); } } function message( data ) { console.log( data ); } function 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` }); } // Cmd5 has the ability to allow constants which could be used for the IP function processConstants( constantsArgArray ) { // // Check #8A // Constants must be an Array // consoleLog( `Check #8A` ); if ( ! Array.isArray ( constantsArgArray ) ) { message( chalk.red( `ERROR: 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 // consoleLog( `Check #8B` ); message( chalk.red( `ERROR: Constant definition at index: "${ argIndex }" has no "key":` ) ) return false; } if ( argEntry.value == undefined ) { // // Check #8c // value must be defined // consoleLog( `Check #8C` ); message( chalk.red( `ERROR: Constant definition at index: "${ argIndex }" has no "value":` ) ) return false; } let keyToAdd = argEntry.key; let valueToAdd = argEntry.value; if ( ! keyToAdd.startsWith( "${" ) ) { // // Check #8D // key must start with ${ // consoleLog( `Check #8D` ); message( chalk.red( `ERROR: Constant definition for: "${ keyToAdd }" must start with "\${" for clarity.` ) ) return false; } if ( ! keyToAdd.endsWith( "}" ) ) { // // Check #8E // key must end with } // consoleLog( `Check #8E` ); message( chalk.red( `ERROR: 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 ( debug ) console.log( chalk.cyan( `CheckConfig keyToAdd:${keyToAdd} valueToAdd:${valueToAdd}` ) ); listOfConstants[ keyToAdd ] = valueToAdd; } return true; } function replaceConstantsInString( orig ) { let finalAns = orig; for ( let key in listOfConstants ) { let replacementConstant = listOfConstants[ key ]; if ( debug ) console.log( chalk.cyan( `INFO: replacing key: ${ key } with: ${ replacementConstant }` ) ); finalAns = finalAns.replace( key, replacementConstant ); } return finalAns; } function updateConfigFirstTime( firstTime ) { // // Check #1 // See if the config.json file exists // consoleLog( `Check #1` ); let configFile = homebridgeConfigPath; if ( configFile == undefined ) { message( chalk.red( `ERROR: No config.json found or specified` ) ) return false; } if ( ! fs.existsSync( configFile ) ) { if ( ! firstTime ) { message( chalk.red( `ERROR: No ${ configFile } found or specified` ) ) } 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. consoleLog( `Check #2` ); try { this.config = JSON.parse( config_in ); } catch ( e ) { if ( ! firstTime ) { message( chalk.red( `ERROR: Parse config.json failed: ${ e }` ) ) } return false; } let myPlaceConfig = this.config.platforms.find( platform => platform[ "MyPlace" ] !== null ); if ( myPlaceConfig && myPlaceConfig.debug ) { console.log( `Setting debug for MyPlace` ); debug = myPlaceConfig.debug; } return true; } // There is nothing really to differentiate a regular Cmd5 Accessory for that of // an Advantage Air // function isAccessoryAnMyPlace( accessory ) { if ( accessory.manufacturer && accessory.manufacturer.match( /Advantage Air/ ) ) return true; // Trigger off of the state_cmd, if it exists if ( accessory.state_cmd != undefined ) { // The new MyPlace if ( accessory.state_cmd.match( /MyPlace.sh/ ) ) return true; } return false; } function checkInstallationButtonPressed( ) { // The read in config.json in JSON format if ( debug ) console.log( chalk.cyan( `INFO: CheckConfig is now in the process of checking the config.json` ) ); // 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 ( updateConfigFirstTime( false ) == false ) return; // // Check #3 // Check that jq is installed. consoleLog( `Check #3` ); if ( ! commandExistsSync( "jq" ) ) { message( chalk.red( `ERROR: jq is required globally and not installed.` ) ) return; } // // Check #4 // Check that curl is installed. consoleLog( `Check #4` ); if ( ! commandExistsSync( "curl" ) ) { message( chalk.red( `ERROR: curl is required globally and not installed.` ) ) return; } // // Check #6 // See if our MyPlace.sh script is present // // Create the path to the cmd5MyAir.sh from node_modules consoleLog( `Check #6` ); let ourScript = MYPLACE_SH_PATH if ( ourScript == null ) { message( chalk.red( `ERROR: No MyPlace.sh script present. Looking for: <Your Global node_modules Path>${ this.MYPLACE_SH }` ) ) return; } let cmd5AccessoriesFound = false; let advantageAirAccessoriesFound = []; let cmd5QueueTypesFound = []; 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 ( debug ) console.log( chalk.cyan( `INFO: CheckConfig is checking Platform entry ${ entry.platform }` ) ); // // Check #7 // See if any MyPlace accessories are defined in config.json // consoleLog( `Check #7` ); if ( entry.platform != "MyPlace" ) continue; cmd5AccessoriesFound = true; // // Check #18 // See if there are any accessory queues defined // consoleLog( `Check #18` ); if ( entry.queueTypes != undefined ) { // // Check #19 // queueTypes must be an array // consoleLog( `Check #19` ); if ( ! Array.isArray( entry.queueTypes ) ) { message( chalk.red( `ERROR: 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 = checkQueueTypesForQueue( cmd5QueueTypesFound, queueTypeEntry.queue ); if ( retVal.rc == true ) // if ( cmd5QueueTypesFound.find( queueTypeEntry ) ) { // // Check #20 // Duplicate queue // consoleLog( `Check #20` ); message( chalk.red( `ERROR: Duplicate queue found: ${ queueTypeEntry.queue }` ) ) return; } cmd5QueueTypesFound.push( queueTypeEntry ); } } // // Check #8 // Process Constants // consoleLog( `Check #8` ); if ( entry.constants != undefined ) if ( 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 ( debug ) console.log( chalk.cyan( `INFO: CheckConfig is checking accessory ${ accessory.name }` ) ); // // Check #9 // See if any Advantage Air accessories are defined in config.json // consoleLog( `Check #9` ); if ( ! isAccessoryAnMyPlace( accessory ) ) continue; // // Check #10 // See if any Advantage Air accessory has a defined name // consoleLog( `Check #10` ); if ( debug ) console.log( chalk.cyan( `INFO: CheckConfig is checking accessory ${ accessory.name }` ) ); if ( accessory.name == undefined ) { message( chalk.red( `ERROR: Accessory at index: ${ entryIndex } accessory.name is undefined` ) ) return; } // // Check #11 // See if any Advantage Air accessory has a defined displayName // consoleLog( `Check #11` ); if ( debug ) console.log( chalk.cyan( `INFO: CheckConfig is checking accessory ${ accessory.name } for displayName` ) ); if ( accessory.displayName == undefined ) { message( chalk.red( `ERROR: Accessory at index: ${ entryIndex } "${ accessory.name }" has no displayName` ) ) return; } // // Check #12 // Polling is done by displayName, It cannot already exist. // consoleLog( `Check #12` ); if ( debug ) console.log( chalk.cyan( `INFO: CheckConfig is Checking accessory ${ accessory.displayName } for duplicate displayName` ) ); if ( advantageAirAccessoriesFound.find( ( displayName ) => displayName == accessory.displayName ) ) { message( chalk.red( `ERROR: Accessory: "${ accessory.displayName }"'s displayName is defined twice` ) ) return; } // Add it to the Array advantageAirAccessoriesFound.push( accessory.displayName ); if ( debug ) console.log( chalk.cyan( `INFO: CheckConfig is Checking Advantage Air accessory ${ accessory.displayName }` ) ); // // Check #13 // The state_cmd must be defined for the Air accessory // consoleLog( `Check #13` ); if ( accessory.state_cmd == undefined ) { message( chalk.red( `ERROR: No state_cmd for: "${ accessory.displayName }"` ) ) return; } // // Check #14 // See if the state_cmd does not match the cmd5MyPlace.sh // consoleLog( `Check #14` ); if ( ! accessory.state_cmd.match( ourScript ) ) { message( chalk.red( `ERROR: Invalid state_cmd for: "${ accessory.displayName }". It should be:\n${ ourScript }` ) ) return; } // // Check #15 // See if the state_cmd_suffix is defined for the Air accessory // It must have at least an IP consoleLog( `Check #15` ); if ( accessory.state_cmd_suffix == undefined ) { message( chalk.red( `ERROR: No state_cmd_suffix for: "${ accessory.displayName }". It must at least contain an IP.` ) ) return; } if ( debug ) console.log( chalk.cyan( `INFO: Calling replaceConstantsInString` ) ); let state_cmd_suffix = replaceConstantsInString( accessory.state_cmd_suffix ); if ( debug ) console.log( chalk.cyan( `INFO: after replaceConstantsInString state_cmd_suffix=${ state_cmd_suffix }` ) ); // // Check #16 // The state_cmd_suffix must have an IP for the Air accessory // consoleLog( `Check #16` ); if ( ! state_cmd_suffix.match( /[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*/ ) ) { message( chalk.red( `ERROR: state_cmd_suffix has no IP for: "${ accessory.displayName }" state_cmd_suffix: ${ state_cmd_suffix }` ) ) return; } // // Check #17 // Checking for required linkedTypes accessory and required keywords in state_cmd_sufffix if ( accessory.type.match( /Switch/ ) || accessory.type.match( /Thermostat/ ) ) { // Check #17A // A Switch or a Thermostat must have a linkedTypes 'Fan' accessory for fanSpeed control if ( accessory.linkedTypes == undefined ) { message( chalk.red( `ERROR: "${ accessory.displayName }" requires a linkedTypes 'Fan' accessory for fan speed control.` ) ) return; // Check #17B // The state_cmd_suffix must have 'fanSpeed' linkedTypes Fan accessory associated with a Switch or a Thermostat } else if ( ! accessory.linkedTypes[0].state_cmd_suffix.match( /fanSpeed/ ) ) { message( chalk.red( `ERROR: The state_cmd_suffix for "${ accessory.linkedTypes[0].displayName }" requires the keyword 'fanSpeed' (without quotes).` ) ) return; } // Check #17C // The state_cmd_suffix must have 'timer' or 'Timer' or 'ligID' for a Lightbulb accessory } else if ( accessory.type.match( /Lightbulb/ ) ) { if ( ! ( state_cmd_suffix.match( /timer/ ) || state_cmd_suffix.match( /fanTimer/ ) || state_cmd_suffix.match( /coolTimer/ ) || state_cmd_suffix.match( /heatTimer/ ) || state_cmd_suffix.match( /ligID:/ ) ) ) { message( chalk.red( `ERROR: The state_cmd_suffix for "${ accessory.displayName }" requires 'timer' or 'fanTimer' or 'coolTimer' or 'heatTimer' (without quotes) if being used as timers or requires ligID:<light ID> if being used as a MyPlace Light.` ) ) return; } // Check #17D // The state_cmd_suffix must have 'thiID' for an Garage or WindowCovering accessory } else if ( accessory.type.match( /^Window/ ) || accessory.type.match( /^Garage/ ) ) { if ( ! state_cmd_suffix.match( /thiID:/ ) ) { message( chalk.red( `ERROR: The state_cmd_suffix has no 'thiID' for: "${ accessory.displayName }"` ) ) return; } // Check #17E // The state_cmd_suffix must have a zone for a Fan accessory with displayName ending with ' Zone' (without quotes) } else if ( accessory.type.match( /^Fan/ ) && accessory.displayName.match( / Zone$/ ) ) { if ( ! state_cmd_suffix.match( /z[0-9][0-9]/ ) ) { message( chalk.red( `ERROR: The state_cmd_suffix has no zone for: "${ accessory.displayName }"` ) ) return; } } // // Check #21 // See if there is a queue defined // consoleLog( `Check #21`); if ( accessory.queue == undefined ) { message( chalk.red( `ERROR: No queue defined for: "${ accessory.displayName }"` ) ) return; } // // Check #22 // queue name must be an string // consoleLog( `Check #22`); if ( typeof accessory.queue != "string" ) { message( chalk.red( `ERROR: queue for: "${ accessory.displayName }" is not a string` ) ) return; } retVal = checkQueueTypesForQueue( cmd5QueueTypesFound, accessory.queue ); // Check #23 // queue must be defined in queueTypes consoleLog( `Check #23`); if ( retVal.rc == false ) { message( chalk.red( `ERROR: For: "${ accessory.displayName }" ${ retVal.message }` ) ) return; } // Check #24 Polling must be defined for MyPlace accessories consoleLog( `Check #24`); if ( ! accessory.polling || ( typeof accessory.polling == "boolean" && accessory.polling != true && ! Array.isArray( accessory.polling) ) ) { message( chalk.red( `ERROR: Polling for: "${ accessory.displayName }" is not an Array or Boolean` ) ) return; } } } // // Check #32 // See if any Cmd5 accessories are defined in config.json // consoleLog( `Check #32`); if ( cmd5AccessoriesFound == false ) { message( chalk.red( `ERROR: No Cmd5 Accessories found` ) ) return; } // // Check #33 // See if any Advantage Air accessories are defined in config.json // consoleLog( `Check #33`); if ( advantageAirAccessoriesFound.length == 0 ) { message( chalk.red( `ERROR: No Advantage Air Accessories found` ) ) return; } // // Check #34 // See if any queueTypes were defined // ( Most likely an earlier failure will succeed this one ) // consoleLog( `Check #34`); if ( cmd5QueueTypesFound == null ) { message( chalk.red( `ERROR: No Cmd5 Queue Types were defined for Advantage Air Accessories` ) ) return; } // PASS ! message( chalk.green( chalk.bold ( `PASSED` ) ) ) }