From 932ddf974ee68c94f93f8271d207c38a38516c80 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Mon, 4 Apr 2016 06:27:27 +0200 Subject: [PATCH 1/3] First attempt at interlock support, doesn't work as expected Attempt to build an interlock function that simply switches the perms on any writable characteristics to read-only. Fundamentally this works, but it does not transmit the permissions change to a client. This may not be supported by the spec, per khaost: https://homebridgeteam.slack.com/archives/plugins/p1458280632000080 . Will try another approach. --- index.js | 50 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 5474639..d6db05b 100644 --- a/index.js +++ b/index.js @@ -379,8 +379,12 @@ ZWayServerAccessory.prototype = { url: this.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + '/command/' + command, qs: (value === undefined ? undefined : value) }); - }, - + } + , + isInterlockOn: function(){ + return this.interlock.value; + } + , rgb2hsv: function(obj) { // RGB: 0-255; H: 0-360, S,V: 0-100 var r = obj.r/255, g = obj.g/255, b = obj.b/255; @@ -1245,12 +1249,54 @@ if(!vdev) debug("ERROR: vdev passed to getVDevServices is undefined!"); .setCharacteristic(Characteristic.SerialNumber, accId); var services = [informationService]; + services = services.concat(this.getVDevServices(vdevPrimary)); if(services.length === 1){ debug("WARN: Only the InformationService was successfully configured for " + vdevPrimary.id + "! No device services available!"); return services; } + // Interlock specified? Create an interlock control switch... + if(this.platform.getTagValue(vdevPrimary, "Interlock") && services.length > 1){ + var ilsvc = new Service.Switch("Interlock", vdevPrimary.id + "_interlock"); + ilsvc.setCharacteristic(Characteristic.Name, "Interlock"); + + var ilcx = ilsvc.getCharacteristic(Characteristic.On); + ilcx.value = false; // Going to set this true in a minute... + ilcx.on('change', function(ev){ + debug("Interlock for device " + vdevPrimary.metrics.title + " changed from " + ev.oldValue + " to " + ev.newValue + "!"); + for(var s = 1; s < services.length; s++){ + var service = services[s]; + if(service === ilsvc) continue; // Don't lock ourselves out! + var cxs = service.characteristics; + for(var c = 0; c < cxs.length; c++){ + var cx = cxs[c]; + if(cx instanceof Characteristic.Name) continue; +debug("Applying interlock to " + cx.displayName); + if(ev.newValue == true){ + var writePermIndex = cx.props.perms.indexOf(Characteristic.Perms.WRITE); + if(writePermIndex >= 0){ + cx.zway_nonInterlockedPerms = cx.props.perms; + var ptemp = cx.props.perms.slice(); + ptemp.splice(writePermIndex,1); + cx.setProps({perms: ptemp}); + } + } else { // ev.newValue === false + if(cx.zway_nonInterlockedPerms){ + cx.setProps({perms: cx.zway_nonInterlockedPerms}); + delete cx.zway_nonInterlockedPerms; + } + } + } + } + }.bind(this)); + + this.interlock = ilcx; + services.push(ilsvc); + + //ilcx.setValue(true); // Initializes the interlock as on, removing write perms and saving previous perms. + } + // Any extra switchMultilevels? Could be a RGBW+W bulb, add them as additional services... if(this.devDesc.extras["switchMultilevel"]) for(var i = 0; i < this.devDesc.extras["switchMultilevel"].length; i++){ var xvdev = this.devDesc.devices[this.devDesc.extras["switchMultilevel"][i]]; From 9b4bbbd7efa39a334fabb79d0824dd2e1421953f Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Tue, 5 Apr 2016 06:49:25 +0200 Subject: [PATCH 2/3] Second approach at interlock function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This one works. Don’t love the error it produces in the client app, but the effect is correct. --- index.js | 66 ++++++++++++++++++++++---------------------------------- 1 file changed, 26 insertions(+), 40 deletions(-) diff --git a/index.js b/index.js index d6db05b..d9fc6a7 100644 --- a/index.js +++ b/index.js @@ -382,7 +382,7 @@ ZWayServerAccessory.prototype = { } , isInterlockOn: function(){ - return this.interlock.value; + return !!this.interlock.value; } , rgb2hsv: function(obj) { @@ -598,6 +598,16 @@ if(!vdev) debug("ERROR: vdev passed to getVDevServices is undefined!"); this.platform.cxVDevMap[vdev.id].push(cx); if(!this.platform.vDevStore[vdev.id]) this.platform.vDevStore[vdev.id] = vdev; + var interlock = function(fnDownstream){ + return function(newval, callback){ + if(this.isInterlockOn()){ + callback(new Error("Interlock is on! Changes locked out!")); + } else { + fnDownstream(newval, callback); + } + }.bind(accessory); + }; + if(cx instanceof Characteristic.Name){ cx.zway_getValueFromVDev = function(vdev){ return vdev.metrics.title; @@ -636,11 +646,11 @@ if(!vdev) debug("ERROR: vdev passed to getVDevServices is undefined!"); callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); - cx.on('set', function(powerOn, callback){ + cx.on('set', interlock(function(powerOn, callback){ this.command(vdev, powerOn ? "on" : "off").then(function(result){ callback(); }); - }.bind(this)); + }.bind(this))); cx.on('change', function(ev){ debug("Device " + vdev.metrics.title + ", characteristic " + cx.displayName + " changed from " + ev.oldValue + " to " + ev.newValue); }); @@ -692,11 +702,11 @@ if(!vdev) debug("ERROR: vdev passed to getVDevServices is undefined!"); callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); - cx.on('set', function(level, callback){ + cx.on('set', interlock(function(level, callback){ this.command(vdev, "exact", {level: parseInt(level, 10)}).then(function(result){ callback(); }); - }.bind(this)); + }.bind(this))); return cx; } @@ -713,7 +723,7 @@ if(!vdev) debug("ERROR: vdev passed to getVDevServices is undefined!"); callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); - cx.on('set', function(hue, callback){ + cx.on('set', interlock(function(hue, callback){ var scx = service.getCharacteristic(Characteristic.Saturation); var vcx = service.getCharacteristic(Characteristic.Brightness); if(!scx || !vcx){ @@ -724,7 +734,7 @@ if(!vdev) debug("ERROR: vdev passed to getVDevServices is undefined!"); this.command(vdev, "exact", { red: rgb.r, green: rgb.g, blue: rgb.b }).then(function(result){ callback(); }); - }.bind(this)); + }.bind(this))); return cx; } @@ -742,7 +752,7 @@ if(!vdev) debug("ERROR: vdev passed to getVDevServices is undefined!"); callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); - cx.on('set', function(saturation, callback){ + cx.on('set', interlock(function(saturation, callback){ var hcx = service.getCharacteristic(Characteristic.Hue); var vcx = service.getCharacteristic(Characteristic.Brightness); if(!hcx || !vcx){ @@ -753,7 +763,7 @@ if(!vdev) debug("ERROR: vdev passed to getVDevServices is undefined!"); this.command(vdev, "exact", { red: rgb.r, green: rgb.g, blue: rgb.b }).then(function(result){ callback(); }); - }.bind(this)); + }.bind(this))); return cx; } @@ -808,12 +818,12 @@ if(!vdev) debug("ERROR: vdev passed to getVDevServices is undefined!"); callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); - cx.on('set', function(level, callback){ + cx.on('set', interlock(function(level, callback){ this.command(vdev, "exact", {level: parseInt(level, 10)}).then(function(result){ //debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); callback(); }); - }.bind(this)); + }.bind(this))); cx.setProps({ minValue: vdev.metrics && vdev.metrics.min !== undefined ? vdev.metrics.min : 5, maxValue: vdev.metrics && vdev.metrics.max !== undefined ? vdev.metrics.max : 40 @@ -861,10 +871,10 @@ if(!vdev) debug("ERROR: vdev passed to getVDevServices is undefined!"); callback(false, Characteristic.TargetHeatingCoolingState.HEAT); }); // Hmm... apparently if this is not setable, we can't add a thermostat change to a scene. So, make it writable but a no-op. - cx.on('set', function(newValue, callback){ + cx.on('set', interlock(function(newValue, callback){ debug("WARN: Set of TargetHeatingCoolingState not yet implemented, resetting to HEAT!") callback(undefined, Characteristic.TargetHeatingCoolingState.HEAT); - }.bind(this)); + }.bind(this))); return cx; } @@ -1072,12 +1082,12 @@ if(!vdev) debug("ERROR: vdev passed to getVDevServices is undefined!"); debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, cx.zway_getValueFromVDev(vdev)); }); - cx.on('set', function(level, callback){ + cx.on('set', interlock(function(level, callback){ this.command(vdev, "exact", {level: parseInt(level, 10)}).then(function(result){ //debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); callback(false, cx.zway_getValueFromVDev(result.data)); }); - }.bind(this)); + }.bind(this))); cx.setProps({ minValue: vdev.metrics && vdev.metrics.min !== undefined ? vdev.metrics.min : 0, maxValue: vdev.metrics && (vdev.metrics.max !== undefined || vdev.metrics.max != 99) ? vdev.metrics.max : 100 @@ -1265,36 +1275,12 @@ if(!vdev) debug("ERROR: vdev passed to getVDevServices is undefined!"); ilcx.value = false; // Going to set this true in a minute... ilcx.on('change', function(ev){ debug("Interlock for device " + vdevPrimary.metrics.title + " changed from " + ev.oldValue + " to " + ev.newValue + "!"); - for(var s = 1; s < services.length; s++){ - var service = services[s]; - if(service === ilsvc) continue; // Don't lock ourselves out! - var cxs = service.characteristics; - for(var c = 0; c < cxs.length; c++){ - var cx = cxs[c]; - if(cx instanceof Characteristic.Name) continue; -debug("Applying interlock to " + cx.displayName); - if(ev.newValue == true){ - var writePermIndex = cx.props.perms.indexOf(Characteristic.Perms.WRITE); - if(writePermIndex >= 0){ - cx.zway_nonInterlockedPerms = cx.props.perms; - var ptemp = cx.props.perms.slice(); - ptemp.splice(writePermIndex,1); - cx.setProps({perms: ptemp}); - } - } else { // ev.newValue === false - if(cx.zway_nonInterlockedPerms){ - cx.setProps({perms: cx.zway_nonInterlockedPerms}); - delete cx.zway_nonInterlockedPerms; - } - } - } - } }.bind(this)); this.interlock = ilcx; services.push(ilsvc); - //ilcx.setValue(true); // Initializes the interlock as on, removing write perms and saving previous perms. + ilcx.setValue(true); // Initializes the interlock as on } // Any extra switchMultilevels? Could be a RGBW+W bulb, add them as additional services... From 50197ca801380bb2682c1329e8833abdebddd63d Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Wed, 6 Apr 2016 06:15:49 +0200 Subject: [PATCH 3/3] Added docs and package version bump --- README.md | 6 +++++- package.json | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a6a996c..2341276 100644 --- a/README.md +++ b/README.md @@ -169,13 +169,17 @@ Like [`Homebridge.Service.Type`](#homebridgeservicetypevalue), this allows you t This tag is particularly useful for scenarios where the physical device is reported ambiguously by Z-Way. For instance, the Vision ZP 3012 motion sensor is presented by Z-Way merely as two `sensorBinary` devices (plus a temperature sensor), one of which is the actual motion sensor and the other is a tampering detector. The `sensorBinary` designation (with no accompanying `probeTitle`) is too ambiguous for the bridge to work with, so it will be ignored. To make this device work, you can tag the motion sensor device in Z-Way with `Homebridge.Characteristic.Type:MotionDetected` and (optionally) the tamper detector with `Homebridge.Characteristic.Type:StatusTampered`. (Note that for this device you will also need to tag the motion sensor with `Homebridge.Service.Type:MotionSensor` and `Homebridge.IsPrimary`, otherwise the more recognizable temperature sensor will take precedence.) +#### Homebridge.Interlock + +Adding the tag `Homebridge.Interlock` to the primary device will add an additional `Switch` service named "Interlock", defaulted to "on". When this switch is engaged, you will not be able to set the characteristics of any other devices in the accessory! You will be required to turn off the Interlock switch before changing/setting other values. This is a kind of a "safety" switch, so that you (or Siri) does not turn something on or off that you did not intend. A use case might be if you had your cable modem or router plugged into a power outlet switch so that you could power cycle it remotely: you would not want to turn this off accidentally, so add an Interlock switch. **Do NOT rely on this capability for health or life safety purposes--it is a convenience and is not designed or intended to be a robust safety feature.** + #### Homebridge.ContactSensorState.Invert If you have a `ContactSensor`, this will invert the state reported to HomeKit. This is useful if you are using the `ContactSensor` Service type for a `Door/Window` sensor, and you want it to show "Yes" when open and "No" when closed, which may be more intuitive. The default for a `ContactSensor` is to show "Yes" when there is contact (in the case of a door, when it's closed) and "No" when there is no contact (which for a door is when it's open). #### Homebridge.OutletInUse.Level:*value* -This can be used in conjunction with the `Homebridge.Service.Type:Outlet` tag and lets you change the threshold value that changes the `OutletInUse` value to true for a particular device. The main use case is if you have a USB charger or transformer that always consumes a given amount of power, but you want events to trigger when the consumption rises above that level (e.g. when a device is plugged into the USB charger and draws more power). You could also adjust this to trigger only when the higher settings on a 3-way lamp are used, when a fan is turned to high speed, or other creative purposes. +This can be used in conjunction with the `Homebridge.Service.Type:Outlet` tag and lets you change the threshold value that changes the `OutletInUse` value to true for a particular device. The main use case is if you have a USB charger or transformer that always consumes a given amount of power, but you want events to trigger when the consumption rises above that level (e.g. when a device is plugged into the USB charger and draws more power). You could also adjust this to trigger only when the higher settings on a 3-way lamp are used, when a fan is turned to high speed, or other creative purposes. # Technical Detail diff --git a/package.json b/package.json index e301c67..1c9e747 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homebridge-zway", - "version": "0.5.0-alpha0", + "version": "0.5.0-alpha1", "description": "homebridge-plugin for ZWay Server and RaZBerry", "main": "index.js", "scripts": {