diff --git a/README.md b/README.md index fbd96f1..f86b548 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,19 @@ When properly setup, you should see something like this in your Homebridge start *

debug Optional
Default to false
Enables debugging of HTTP calls to MakerAPI to troubleshoot issues

+ + *

dedupe_change_events_ms Optional
+ If this is set to a value greater then 0, change events that are recieved will be deduped within the specified time period. That last change event of the type recieved within the time period will be sent. All intermediary change events will be squashed. This is useful for some lights (inovelli) that transition colors.

+ + *

use_set_color Optional
+ This turns on setColor processing and will combine setHue and setSaturation commands that are received within dedupe_command_delay_ms of each other. This is useful for RGB lights that may have race conditions when receiving mulitple commands for a single color change.

+ + *

dedupe_command_delay_ms Optional
+ The amount of time to wait for a second command. This should be set to less then 1000. 50 or less is a good value + to use. This is ONLY used for squash_on_commands or if use_set_color is set.

+ + *

squash_on_commands Optional
+ This is a list of commands that receive an 'on' command before the command to force it on before the command is set, this will squash the on command and just send the value command. i.e. 'on' is sent before 'setLevel' when setting the brightness of the light. use ['setLevel']

*

logFile Optional
Settings to enable logging to file. Uses winston logging facility diff --git a/lib/api-homebridge-hubitat-makerapi.js b/lib/api-homebridge-hubitat-makerapi.js index 37cd69a..2620e6d 100644 --- a/lib/api-homebridge-hubitat-makerapi.js +++ b/lib/api-homebridge-hubitat-makerapi.js @@ -61,6 +61,19 @@ function transformAllDeviceData(inData, detailQuery = false, debug=false) } var he_maker_api = { + // setHue, setSaturation, and setValue commands can all be sent as a setColor command, we delay sending + // any of these commands for a short period to see if there is a complementary command, if there is, we + // group it and send a setColor command with all commands at once + deviceCommandMap: { + + }, + + // Keep track of when a comamnd an on command is recieved, if we recieve a command in the list of squash on commands + // we wont send the 'on' command to the device, just let the set level control it. + squashOnCommandMap: { + + }, + init: function(...args) { platform = args[4]; util_http.init(args); @@ -100,15 +113,180 @@ var he_maker_api = { }); }, runCommand: function(deviceid, command, secondaryValue = null) { + + + const convertCommandToKey = { + setHue: 'hue', + setSaturation: 'saturation' + }; + + // Some commands will always send an 'On' before sending the actual command. + const squashOnCommands = platform.config.squash_on_commands; + + const me = this; return new Promise(function(resolve, reject) { - util_http.GET({ - //debug: false, - path: '/devices/' + deviceid + '/' + command + (secondaryValue ? '/' + secondaryValue.value1 : '') - }).then(function(resp) { - resolve(resp); - }).catch(function(err) { - reject(err); - }); + function sendSingleCommand(id, cmd, val, resolveList, rejectList) { + if (!Array.isArray(resolveList)) { + resolveList = [resolveList]; + } + if (!Array.isArray(rejectList)) { + rejectList = [rejectList]; + } + // This is a simple command, execute immediatly + const path = '/devices/' + id + '/' + cmd + (val ? '/' + val : ''); + util_http.GET({ + //debug: false, + path: path + }).then(function(resp) { + for (const resolver of resolveList) { + resolver(resp); + } + }).catch(function(err) { + platform.log.error(err); + for (const rejector of rejectList) { + platform.log.error('Error with hubitat runCommand ' + err.message || err); + rejector(err); + } + }); + } + + const dedupeCommandDelay = platform.config.dedupe_command_delay_ms || 50; + + if (platform.config.use_set_color && convertCommandToKey[command]) { + platform.log('Delaying ' + command + ' because it is a setColor property'); + // This is a combined command, register it + if (!me.deviceCommandMap[deviceid]) { + me.deviceCommandMap[deviceid] = { + resolveList: [], + rejectList: [] + }; + } + + if (!me.deviceCommandMap[deviceid].command) { + me.deviceCommandMap[deviceid].command = {}; + } + + me.deviceCommandMap[deviceid].command[convertCommandToKey[command]] = + secondaryValue ? secondaryValue.value1 : undefined; + + me.deviceCommandMap[deviceid].resolveList.push(resolve); + me.deviceCommandMap[deviceid].rejectList.push(reject); + if (me.deviceCommandMap[deviceid].timerId) { + clearTimeout(me.deviceCommandMap[deviceid].timerId); + me.deviceCommandMap[deviceid].timerId = undefined; + } + + me.deviceCommandMap[deviceid].timerId = setTimeout(function () { + // Switch back to a single command if we don't have mulitple properties as using setColor will not + // work + if (!me.deviceCommandMap[deviceid].command['hue']) { + platform.log('Sending setSaturation(' + deviceid + ', ' + + me.deviceCommandMap[deviceid].command['saturation'] + + ') since we did not get a hue command in time'); + sendSingleCommand(deviceid, 'setSaturation', me.deviceCommandMap[deviceid].command['saturation'], + me.deviceCommandMap[deviceid].resolveList, me.deviceCommandMap[deviceid].rejectList); + } else if (!me.deviceCommandMap[deviceid].command['saturation']) { + platform.log('Sending setHue(' + deviceid + ', ' + + me.deviceCommandMap[deviceid].command['hue'] + + ') since we did not get a saturation command in time'); + sendSingleCommand(deviceid, 'setHue', me.deviceCommandMap[deviceid].command['hue'], + me.deviceCommandMap[deviceid].resolveList, me.deviceCommandMap[deviceid].rejectList); + } else { + let commandValueString = ''; + + for (const prop in me.deviceCommandMap[deviceid].command) { + const value = me.deviceCommandMap[deviceid].command[prop]; + if (commandValueString.length > 0) { + commandValueString += ','; + } + commandValueString += '"' + prop + '":' + value; + } + const path = '/devices/' + deviceid + '/setColor/{' + commandValueString + '}'; + platform.log('Sending multicommand ' + path); + + // Timeout expired, send everythign as a query string + // This is a simple command, execute immediatly + const resolveList = me.deviceCommandMap[deviceid].resolveList; + const rejectList = me.deviceCommandMap[deviceid].rejectList; + util_http.GET({ + //debug: false, + path: path + }).then(function (resp) { + platform.log('Resolve (' + resolveList.length + ') runCommand'); + for (const resolver of resolveList) { + resolver(resp); + } + }).catch(function (err) { + for (const rejector of rejectList) { + platform.log('reject (' + resolveList.length + ') runCommand'); + rejector(err); + } + }); + } + + me.deviceCommandMap[deviceid] = undefined; + }, dedupeCommandDelay); + + + + + + + + } else if (squashOnCommands && squashOnCommands.length > 0 && (squashOnCommands.indexOf(command) >= 0 || + command === 'on')) { + if (command === 'on') { + platform.log('Recieved on command, squashinng for now'); + // This is a combined command, register it + if (!me.squashOnCommandMap[deviceid]) { + me.squashOnCommandMap[deviceid] = { + resolveList: [], + rejectList: [] + }; + } + + me.squashOnCommandMap[deviceid].resolveList.push(resolve); + me.squashOnCommandMap[deviceid].rejectList.push(reject); + + if (me.squashOnCommandMap[deviceid].timerId) { + clearTimeout(me.squashOnCommandMap[deviceid].timerId); + me.squashOnCommandMap[deviceid].timerId = undefined; + } + + me.squashOnCommandMap[deviceid].timerId = setTimeout(function () { + platform.log('Sending on command as timeout has elapsed'); + sendSingleCommand(deviceid, command, undefined, me.squashOnCommandMap[deviceid].resolveList, + me.squashOnCommandMap[deviceid].rejectList); + + me.deviceCommandMap[deviceid] = undefined; + }, dedupeCommandDelay); + } else { + // Squash the on command + if (me.squashOnCommandMap[deviceid] && me.squashOnCommandMap[deviceid].timerId) { + platform.log('Squashing on command'); + clearTimeout(me.squashOnCommandMap[deviceid].timerId); + me.squashOnCommandMap[deviceid].timerId = undefined; + } + + me.squashOnCommandMap[deviceid].resolveList.push(resolve); + me.squashOnCommandMap[deviceid].rejectList.push(reject); + + // Send the command + platform.log('Sending squashed on command ' + command + ' to ' + deviceid); + sendSingleCommand(deviceid, command, secondaryValue ? secondaryValue.value1 : undefined, + me.squashOnCommandMap[deviceid].resolveList, me.squashOnCommandMap[deviceid].rejectList); + + me.squashOnCommandMap[deviceid] = undefined; + } + } else { + platform.log('Sending single command ' + command + ' to ' + deviceid); + // This is a simple command, execute immediatly + sendSingleCommand(deviceid, command, secondaryValue ? secondaryValue.value1 : undefined, resolve, reject); + + } + + + }); }, getDevicesSummary: function () { diff --git a/lib/receiver-homebridge-hubitat-makerapi.js b/lib/receiver-homebridge-hubitat-makerapi.js index cca9831..db0e46a 100644 --- a/lib/receiver-homebridge-hubitat-makerapi.js +++ b/lib/receiver-homebridge-hubitat-makerapi.js @@ -19,10 +19,46 @@ var fs = require('fs'); app.use(bodyParser.json()); +var dedupe_change_event_map = {}; + var receiver_makerapi = { start: function(platform) { return new Promise(function(resolve, reject) { var that = this; + + + function processChangeEvent(element, changeEventType) { + if (platform.config.ignore_change_events && platform.config.ignore_change_events.indexOf && + platform.config.ignore_change_events.indexOf(element['attribute']) >= 0) { + // Ignore this change event + platform.log('IGNORING Change Event (' + changeEventType + '):', '(' + element['displayName'] + ':' + element['device'] + ') [' + (element['attribute'] ? element['attribute'] : 'unknown') + '] is ' + element['value']); + } + + // Some devices send several change events very quickly as it transitions between colors. This delays processing of them + // so we don't show erratic values in home kit and just sets the last value recieved, tossing out + // any intermediary values recieved bfore it finishes. + else if (platform.config.dedupe_change_events_ms) { + const key = element['device'] + '_' + element['attribute']; + if (!dedupe_change_event_map[key]) { + dedupe_change_event_map[key] = {}; + } + if (dedupe_change_event_map[key].timer) { + clearTimeout(dedupe_change_event_map[key].timer); + dedupe_change_event_map[key].timer = undefined; + } + dedupe_change_event_map[key].element = element; + dedupe_change_event_map[key].timer = setTimeout(function() { + element = dedupe_change_event_map[key].element; + dedupe_change_event_map[key] = undefined; + platform.log('Deduped Change Event (' + changeEventType + '):', '(' + element['displayName'] + ':' + element['device'] + ') [' + (element['attribute'] ? element['attribute'] : 'unknown') + '] is ' + element['value']); + platform.processFieldUpdate(element, platform); + }, platform.config.dedupe_change_events_ms); + } else { + platform.log('Change Event (' + changeEventType + '):', '(' + element['displayName'] + ':' + element['device'] + ') [' + (element['attribute'] ? element['attribute'].toUpperCase() : 'unknown') + '] is ' + element['value']); + platform.processFieldUpdate(element, platform); + } + } + platform.log('Starting receiver'); app.get('/action/:action', function(req, res) { switch(req.params.action) @@ -334,12 +370,7 @@ $(function () { } } newChange.forEach(function(element) { - platform.log('Change Event (http):', '(' + - element['displayName'] + ':' + - element['device'] + ') [' + - (element['attribute'] ? element['attribute'].toUpperCase() : 'unknown') + - '] is ' + element['value']); - platform.processFieldUpdate(element, platform); + processChangeEvent(element, 'http'); }); // return res.json({status: "success"}); }); @@ -555,8 +586,7 @@ $(function () { } } newChange.forEach(function(element) { - platform.log('Change Event (Socket):', '(' + element['displayName'] + ':' + element['device'] + ') [' + (element['attribute'] ? element['attribute'].toUpperCase() : 'unknown') + '] is ' + element['value']); - platform.processFieldUpdate(element, platform); + processChangeEvent(element, 'Socket'); }); }; diff --git a/package.json b/package.json index 53dd400..947896e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "homebridge-hubitat-makerapi", + "name": "homebridge-hubitat-makerapi-godofcpu", "description": "Hubitat plugin for HomeBridge with MakerAPI", - "version": "0.4.11", + "version": "0.4.12", "license": "Apache 2.0", "preferGlobal": true, "keywords": [ @@ -24,7 +24,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/danTapps/homebridge-hubitat-makerapi.git" + "url": "https://github.com/godofcpu/homebridge-hubitat-makerapi.git" }, "engines": { "node": ">=8.6.0",