From 465a4b7a9907aeba899971aada005da568ac9e0d Mon Sep 17 00:00:00 2001 From: Chuck Holbrook Date: Sat, 10 Oct 2020 18:00:39 -0400 Subject: [PATCH 1/8] Added 3 optional settings to the config that help tremendously with my inovelli smartlights to stop sending mulitple commands to set the color or level. By default when either the level or color is changed, homekit will send an on and setLevel command or a setHue and setSaturation command. This can cause the light to flicker as it cycles between both settings. In the worse case scenario, it appears that homekit will get confused and switch the hue and saturation back and forth and not set the combined command values when sending both as invidual comamnds. This changes the plugin to look to see if two compatible commands are sent in very close proximity of each other. It delays sending the first one until the second is recieved, then sends a setColor command, if a second command isn't recieved within 50ms (configurable) or so, it sends just the first command by itself. If none of the config values are specified, the default behavior isn't changed. New config values are below: "use_set_color": true, // This turns on setColor processing and will combine setHue and setSaturation commands that are recieved within dedupe_command_delay_ms of each other "dedupe_command_delay_ms": 10, // The amount of time to wait for a second command "squash_on_commands": ["setLevel"] // This is a list of commands that receive an 'on' command before a setLevel, this will squash the on command and just send a setLevel command --- lib/HomeKitTypes.js | 2 +- lib/api-homebridge-hubitat-makerapi.js | 194 ++++++++++++++++++++++++- package.json | 6 +- 3 files changed, 190 insertions(+), 12 deletions(-) diff --git a/lib/HomeKitTypes.js b/lib/HomeKitTypes.js index 3385f86..6d4fac8 100644 --- a/lib/HomeKitTypes.js +++ b/lib/HomeKitTypes.js @@ -3140,7 +3140,7 @@ Service.Lightbulb = function(displayName, subtype) { this.addOptionalCharacteristic(Characteristic.Hue); this.addOptionalCharacteristic(Characteristic.Saturation); this.addOptionalCharacteristic(Characteristic.Name); - this.addOptionalCharacteristic(Characteristic.ColorTemperature); //Manual fix to add temperature + // this.addOptionalCharacteristic(Characteristic.ColorTemperature); //Manual fix to add temperature }; inherits(Service.Lightbulb, Service); 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/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", From 2ee7cf8c02f10353de2dfc1eeac807ab780d5482 Mon Sep 17 00:00:00 2001 From: Chuck Holbrook Date: Sat, 10 Oct 2020 18:02:21 -0400 Subject: [PATCH 2/8] Removed debugging code --- lib/HomeKitTypes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/HomeKitTypes.js b/lib/HomeKitTypes.js index 6d4fac8..3385f86 100644 --- a/lib/HomeKitTypes.js +++ b/lib/HomeKitTypes.js @@ -3140,7 +3140,7 @@ Service.Lightbulb = function(displayName, subtype) { this.addOptionalCharacteristic(Characteristic.Hue); this.addOptionalCharacteristic(Characteristic.Saturation); this.addOptionalCharacteristic(Characteristic.Name); - // this.addOptionalCharacteristic(Characteristic.ColorTemperature); //Manual fix to add temperature + this.addOptionalCharacteristic(Characteristic.ColorTemperature); //Manual fix to add temperature }; inherits(Service.Lightbulb, Service); From e925212ef0d2a2d79082d3da23f8a2768ed3314e Mon Sep 17 00:00:00 2001 From: Chuck Holbrook Date: Sat, 10 Oct 2020 18:03:21 -0400 Subject: [PATCH 3/8] Undoing package.json changes --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 947896e..53dd400 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "homebridge-hubitat-makerapi-godofcpu", + "name": "homebridge-hubitat-makerapi", "description": "Hubitat plugin for HomeBridge with MakerAPI", - "version": "0.4.12", + "version": "0.4.11", "license": "Apache 2.0", "preferGlobal": true, "keywords": [ @@ -24,7 +24,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/godofcpu/homebridge-hubitat-makerapi.git" + "url": "https://github.com/danTapps/homebridge-hubitat-makerapi.git" }, "engines": { "node": ">=8.6.0", From e99354705d2d60d145e83c00f0b757eac55d4888 Mon Sep 17 00:00:00 2001 From: Chuck Holbrook Date: Sat, 10 Oct 2020 18:06:15 -0400 Subject: [PATCH 4/8] Updating package.json version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 53dd400..a019572 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "homebridge-hubitat-makerapi", "description": "Hubitat plugin for HomeBridge with MakerAPI", - "version": "0.4.11", + "version": "0.4.12", "license": "Apache 2.0", "preferGlobal": true, "keywords": [ From 5b6c0529d1b162ed68d0f59a707a0e97305d7a01 Mon Sep 17 00:00:00 2001 From: Chuck Holbrook Date: Sat, 10 Oct 2020 18:10:53 -0400 Subject: [PATCH 5/8] Undoing package.json changes --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index a019572..947896e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "homebridge-hubitat-makerapi", + "name": "homebridge-hubitat-makerapi-godofcpu", "description": "Hubitat plugin for HomeBridge with MakerAPI", "version": "0.4.12", "license": "Apache 2.0", @@ -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", From a468b77a7da1601ae7dbfe7b2fd079ed7f808cf5 Mon Sep 17 00:00:00 2001 From: Chuck Holbrook Date: Sat, 10 Oct 2020 19:00:33 -0400 Subject: [PATCH 6/8] Added dedupe_change_events_ms config value, updated readme.md --- README.md | 13 ++++++++++ lib/receiver-homebridge-hubitat-makerapi.js | 28 +++++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) 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/receiver-homebridge-hubitat-makerapi.js b/lib/receiver-homebridge-hubitat-makerapi.js index cca9831..9032ae0 100644 --- a/lib/receiver-homebridge-hubitat-makerapi.js +++ b/lib/receiver-homebridge-hubitat-makerapi.js @@ -19,6 +19,8 @@ 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) { @@ -555,8 +557,30 @@ $(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); + + // 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. + 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 (Socket):', '(' + element['displayName'] + ':' + element['device'] + ') [' + (element['attribute'] ? element['attribute'].toUpperCase() : 'unknown') + '] is ' + element['value']); + platform.processFieldUpdate(element, platform); + }, platform.config.dedupe_change_events_ms); + } else { + platform.log('Change Event (Socket):', '(' + element['displayName'] + ':' + element['device'] + ') [' + (element['attribute'] ? element['attribute'].toUpperCase() : 'unknown') + '] is ' + element['value']); + platform.processFieldUpdate(element, platform); + } }); }; From a592801cb8d3114c09b3085f2efb4e682f59097a Mon Sep 17 00:00:00 2001 From: Chuck Holbrook Date: Sat, 10 Oct 2020 19:09:35 -0400 Subject: [PATCH 7/8] Fix to update change events for http calls too --- lib/receiver-homebridge-hubitat-makerapi.js | 60 ++++++++++----------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/lib/receiver-homebridge-hubitat-makerapi.js b/lib/receiver-homebridge-hubitat-makerapi.js index 9032ae0..a84f8fb 100644 --- a/lib/receiver-homebridge-hubitat-makerapi.js +++ b/lib/receiver-homebridge-hubitat-makerapi.js @@ -25,6 +25,34 @@ var receiver_makerapi = { start: function(platform) { return new Promise(function(resolve, reject) { var that = this; + + + function processChangeEvent(element, changeEventType) { + // 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. + 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'].toUpperCase() : '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) @@ -336,12 +364,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"}); }); @@ -557,30 +580,7 @@ $(function () { } } newChange.forEach(function(element) { - - // 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. - 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 (Socket):', '(' + element['displayName'] + ':' + element['device'] + ') [' + (element['attribute'] ? element['attribute'].toUpperCase() : 'unknown') + '] is ' + element['value']); - platform.processFieldUpdate(element, platform); - }, platform.config.dedupe_change_events_ms); - } else { - 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'); }); }; From caa405fc902e583d995a2267060f404ace21029e Mon Sep 17 00:00:00 2001 From: Chuck Holbrook Date: Sat, 10 Oct 2020 19:41:19 -0400 Subject: [PATCH 8/8] Undoing package.json changes --- lib/receiver-homebridge-hubitat-makerapi.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/receiver-homebridge-hubitat-makerapi.js b/lib/receiver-homebridge-hubitat-makerapi.js index a84f8fb..db0e46a 100644 --- a/lib/receiver-homebridge-hubitat-makerapi.js +++ b/lib/receiver-homebridge-hubitat-makerapi.js @@ -28,10 +28,16 @@ var receiver_makerapi = { 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. - if (platform.config.dedupe_change_events_ms) { + 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] = {}; @@ -44,7 +50,7 @@ var receiver_makerapi = { 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'].toUpperCase() : 'unknown') + '] is ' + element['value']); + 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 {