Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for badly behaving RGB smart lights with HomeKit color picker #17

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,19 @@ When properly setup, you should see something like this in your Homebridge start

* <p><u>debug</u> <small style="color: orange; font-weight: 600;"><i>Optional</i></small><br>
Default to false<br>Enables debugging of HTTP calls to MakerAPI to troubleshoot issues</p>

* <p><u>dedupe_change_events_ms</u> <small style="color: orange; font-weight: 600;"><i>Optional</i></small><br>
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.</p>

* <p><u>use_set_color</u> <small style="color: orange; font-weight: 600;"><i>Optional</i></small><br>
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.</p>

* <p><u>dedupe_command_delay_ms</u> <small style="color: orange; font-weight: 600;"><i>Optional</i></small><br>
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.</p>

* <p><u>squash_on_commands</u> <small style="color: orange; font-weight: 600;"><i>Optional</i></small><br>
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'] </p>

* <p><u>logFile</u> <small style="color: orange; font-weight: 600;"><i>Optional</i></small><br>
Settings to enable logging to file. Uses winston logging facility
Expand Down
194 changes: 186 additions & 8 deletions lib/api-homebridge-hubitat-makerapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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 () {
Expand Down
46 changes: 38 additions & 8 deletions lib/receiver-homebridge-hubitat-makerapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"});
});
Expand Down Expand Up @@ -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');
});
};

Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": [
Expand All @@ -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",
Expand Down