From 656a8057acbd177995f5a3dd9b63262082387d2b Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Sat, 19 Sep 2015 13:00:01 +0200 Subject: [PATCH 01/20] Initial work on reading, seems to work okay. --- platforms/ZWayServer.js | 71 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 5b79c8e..7c8b925 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -11,6 +11,7 @@ function ZWayServerPlatform(log, config){ this.url = config["url"]; this.login = config["login"]; this.password = config["password"]; + this.opt_in = config["opt_in"]; this.name_overrides = config["name_overrides"]; this.batteryLow = config["battery_low_level"] || 15; this.pollInterval = config["poll_interval"] || 2; @@ -110,6 +111,7 @@ ZWayServerPlatform.prototype = { for(var i = 0; i < devices.length; i++){ var vdev = devices[i]; if(vdev.tags.indexOf("Homebridge:Skip") >= 0) { debug("Tag says skip!"); continue; } + if(this.opt_in && vdev.tags.indexOf("Homebridge:Include") < 0) continue; var gdid = vdev.id.replace(/^(.*?)_zway_(\d+-\d+)-\d.*/, '$1_$2'); var gd = groupedDevices[gdid] || (groupedDevices[gdid] = {devices: [], types: {}, primary: undefined}); gd.devices.push(vdev); @@ -222,6 +224,27 @@ ZWayServerAccessory.prototype = { }); }, + rgb2hsv: function(obj) { + var r = obj.r/255, g = obj.g/255, b = obj.b/255; + var max, min, d, h, s, v; + + if (min === max) { + // shade of gray + return [0, 0, r]; + } + + min = Math.min(r, Math.min(g, b)); + max = Math.max(r, Math.max(g, b)); + + var d = (r === min) ? g - b : ((b === min) ? r - g : b - r); + h = (r === min) ? 3 : ((b === min) ? 1 : 5); + h = 60 * (h - d/(max - min)); + s = (max - min) / max; + v = max; + return {"h": h, "s": s * 100, "v": v}; + } + , + getVDevServices: function(vdev){ var typeKey = ZWayServerPlatform.getVDevTypeKey(vdev); var services = [], service; @@ -272,6 +295,8 @@ ZWayServerAccessory.prototype = { this.uuidToTypeKeyMap = map = {}; map[(new Characteristic.On).UUID] = ["switchBinary","switchMultilevel"]; map[(new Characteristic.Brightness).UUID] = ["switchMultilevel"]; + map[(new Characteristic.Hue).UUID] = ["switchRGBW"]; + map[(new Characteristic.Saturation).UUID] = ["switchRGBW"]; map[(new Characteristic.CurrentTemperature).UUID] = ["sensorMultilevel.Temperature","thermostat"]; map[(new Characteristic.TargetTemperature).UUID] = ["thermostat"]; map[(new Characteristic.TemperatureDisplayUnits).UUID] = ["sensorMultilevel.Temperature","thermostat"]; //TODO: Always a fixed result @@ -310,7 +335,7 @@ ZWayServerAccessory.prototype = { } , configureCharacteristic: function(cx, vdev){ - var that = this; + var that, accessory = this; // Add this combination to the maps... if(!this.platform.cxVDevMap[vdev.id]) this.platform.cxVDevMap[vdev.id] = []; @@ -381,6 +406,50 @@ ZWayServerAccessory.prototype = { return cx; } + if(cx instanceof Characteristic.Hue){ + cx.zway_getValueFromVDev = function(vdev){ + return accessory.rgb2hsv(vdev.metrics.color).h; + }; + cx.value = cx.zway_getValueFromVDev(vdev); + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); + }); + }.bind(this)); + + cx.writeable = false; + //cx.on('set', function(level, callback){ + // this.command(vdev, "exact", {level: "on", "color.r": 255, "color.g": 0, "color.b": 0}).then(function(result){ + // callback(); + // }); + //}.bind(this)); + return cx; + } + + if(cx instanceof Characteristic.Saturation){ + cx.zway_getValueFromVDev = function(vdev){ + return accessory.rgb2hsv(vdev.metrics.color).s; + }; + cx.value = cx.zway_getValueFromVDev(vdev); + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); + }); + }.bind(this)); + + cx.writeable = false; + //cx.on('set', function(level, callback){ + // this.command(vdev, "exact", {level: "on", "color.r": 255, "color.g": 0, "color.b": 0}).then(function(result){ + // callback(); + // }); + //}.bind(this)); + return cx; + } + if(cx instanceof Characteristic.CurrentTemperature){ cx.zway_getValueFromVDev = function(vdev){ return vdev.metrics.level; From 7aa758cb042bac0de651107a4e64eb28a82313cb Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Tue, 29 Sep 2015 06:53:22 +0200 Subject: [PATCH 02/20] Working towards getting the extra dimmers in one accessory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Having trouble with service subtypes…hmm… --- platforms/ZWayServer.js | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 7c8b925..4915f89 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -113,10 +113,16 @@ ZWayServerPlatform.prototype = { if(vdev.tags.indexOf("Homebridge:Skip") >= 0) { debug("Tag says skip!"); continue; } if(this.opt_in && vdev.tags.indexOf("Homebridge:Include") < 0) continue; var gdid = vdev.id.replace(/^(.*?)_zway_(\d+-\d+)-\d.*/, '$1_$2'); - var gd = groupedDevices[gdid] || (groupedDevices[gdid] = {devices: [], types: {}, primary: undefined}); + var gd = groupedDevices[gdid] || (groupedDevices[gdid] = {devices: [], types: {}, extras: {}, primary: undefined}); gd.devices.push(vdev); - gd.types[ZWayServerPlatform.getVDevTypeKey(vdev)] = gd.devices.length - 1; - gd.types[vdev.deviceType] = gd.devices.length - 1; // also include the deviceType only as a possibility + var tk = ZWayServerPlatform.getVDevTypeKey(vdev); + if(gd.types[tk] === undefined){ + gd.types[tk] = gd.devices.length - 1; + } else { + gd.extras[tk] = gd.extras[tk] || []; + gd.extras[tk].push(gd.devices.length - 1); + } + if(tk !== vdev.deviceType) gd.types[vdev.deviceType] = gd.devices.length - 1; // also include the deviceType only as a possibility } //TODO: Make a second pass, re-splitting any devices that don't make sense together for(var gdid in groupedDevices) { @@ -250,25 +256,25 @@ ZWayServerAccessory.prototype = { var services = [], service; switch (typeKey) { case "switchBinary": - services.push(new Service.Switch(vdev.metrics.title)); + services.push(new Service.Switch(vdev.metrics.title, vdev.id)); break; case "switchMultilevel": - services.push(new Service.Lightbulb(vdev.metrics.title)); + services.push(new Service.Lightbulb(vdev.metrics.title, vdev.id)); break; case "thermostat": - services.push(new Service.Thermostat(vdev.metrics.title)); + services.push(new Service.Thermostat(vdev.metrics.title, vdev.id)); break; case "sensorMultilevel.Temperature": - services.push(new Service.TemperatureSensor(vdev.metrics.title)); + services.push(new Service.TemperatureSensor(vdev.metrics.title, vdev.id)); break; case "sensorBinary.Door/Window": - services.push(new Service.GarageDoorOpener(vdev.metrics.title)); + services.push(new Service.GarageDoorOpener(vdev.metrics.title, vdev.id)); break; case "battery.Battery": - services.push(new Service.BatteryService(vdev.metrics.title)); + services.push(new Service.BatteryService(vdev.metrics.title, vdev.id)); break; case "sensorMultilevel.Luminiscence": - services.push(new Service.LightSensor(vdev.metrics.title)); + services.push(new Service.LightSensor(vdev.metrics.title, vdev.id)); break; } @@ -335,7 +341,7 @@ ZWayServerAccessory.prototype = { } , configureCharacteristic: function(cx, vdev){ - var that, accessory = this; + var accessory = this; // Add this combination to the maps... if(!this.platform.cxVDevMap[vdev.id]) this.platform.cxVDevMap[vdev.id] = []; @@ -349,7 +355,7 @@ ZWayServerAccessory.prototype = { cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); - callback(false, that.name); + callback(false, accessory.name); }); cx.writable = false; return cx; @@ -597,7 +603,7 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.StatusLowBattery){ cx.zway_getValueFromVDev = function(vdev){ - return vdev.metrics.level <= that.platform.batteryLow ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + return vdev.metrics.level <= accessory.platform.batteryLow ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; }; cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ @@ -686,6 +692,12 @@ ZWayServerAccessory.prototype = { var services = [informationService]; services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.primary])); + + // 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]]; + services = services.concat(this.getVDevServices(xvdev)); + } if(this.platform.splitServices){ if(this.devDesc.types["battery.Battery"]){ From 0b3930e458054fbbd93144dc7c8420b4120a39bd Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Tue, 29 Sep 2015 06:59:40 +0200 Subject: [PATCH 03/20] Erm...oops. --- platforms/ZWayServer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 33c5ac7..fcab891 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -93,8 +93,8 @@ ZWayServerPlatform.prototype = { "thermostat", "switchMultilevel", "switchBinary", - "sensorBinary.Door/Window" - "sensorMultilevel.Temperature", + "sensorBinary.Door/Window", + "sensorMultilevel.Temperature" ]; var that = this; From 99da61d30a1295b17e24bd8f1f8566d01eb29226 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Tue, 29 Sep 2015 07:06:05 +0200 Subject: [PATCH 04/20] Lost some subtype parameters in deconfliction --- platforms/ZWayServer.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index fcab891..c86d4ac 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -256,16 +256,16 @@ ZWayServerAccessory.prototype = { var services = [], service; switch (typeKey) { case "thermostat": - services.push(new Service.Thermostat(vdev.metrics.title)); + services.push(new Service.Thermostat(vdev.metrics.title, vdev.id)); break; case "switchBinary": - services.push(new Service.Switch(vdev.metrics.title)); + services.push(new Service.Switch(vdev.metrics.title, vdev.id)); break; case "switchMultilevel": - services.push(new Service.Lightbulb(vdev.metrics.title)); + services.push(new Service.Lightbulb(vdev.metrics.title, vdev.id)); break; case "sensorBinary.Door/Window": - services.push(new Service.GarageDoorOpener(vdev.metrics.title)); + services.push(new Service.GarageDoorOpener(vdev.metrics.title, vdev.id)); break; case "sensorMultilevel.Temperature": services.push(new Service.TemperatureSensor(vdev.metrics.title, vdev.id)); From d04d41734477256875717ab4d7a0c2a4457e5ca7 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Fri, 2 Oct 2015 06:19:59 +0200 Subject: [PATCH 05/20] Initial read/write support for RGB bulbs complete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Needs testing, but seems to work with my Aeon bulb…taking into account the wonkiness of that bulb with Z-Way, at least. --- platforms/ZWayServer.js | 108 +++++++++++++++++++++++++++++++++++----- 1 file changed, 95 insertions(+), 13 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index c86d4ac..730eb9b 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -180,6 +180,11 @@ ZWayServerPlatform.prototype = { if(this.cxVDevMap[upd.id]){ var vdev = this.vDevStore[upd.id]; vdev.metrics.level = upd.metrics.level; + if(upd.metrics.color){ + vdev.metrics.r = upd.metrics.r; + vdev.metrics.g = upd.metrics.g; + vdev.metrics.b = upd.metrics.b; + } vdev.updateTime = upd.updateTime; var cxs = this.cxVDevMap[upd.id]; for(var j = 0; j < cxs.length; j++){ @@ -231,26 +236,62 @@ ZWayServerAccessory.prototype = { }, 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; var max, min, d, h, s, v; - if (min === max) { - // shade of gray - return [0, 0, r]; - } - min = Math.min(r, Math.min(g, b)); max = Math.max(r, Math.max(g, b)); + if (min === max) { + // shade of gray + return {h: 0, s: 0, v: r * 100}; + } + var d = (r === min) ? g - b : ((b === min) ? r - g : b - r); h = (r === min) ? 3 : ((b === min) ? 1 : 5); h = 60 * (h - d/(max - min)); s = (max - min) / max; v = max; - return {"h": h, "s": s * 100, "v": v}; + return {"h": h, "s": s * 100, "v": v * 100}; + } + , + hsv2rgb: function(obj) { + // H: 0-360; S,V: 0-100; RGB: 0-255 + var r, g, b; + var sfrac = obj.s / 100; + var vfrac = obj.v / 100; + + if(sfrac === 0){ + var vbyte = Math.round(vfrac*255); + return { r: vbyte, g: vbyte, b: vbyte }; + } + + var hdb60 = (obj.h % 360) / 60; + var sector = Math.floor(hdb60); + var fpart = hdb60 - sector; + var c = vfrac * (1 - sfrac); + var x1 = vfrac * (1 - sfrac * fpart); + var x2 = vfrac * (1 - sfrac * (1 - fpart)); + switch(sector){ + case 0: + r = vfrac; g = x2; b = c; break; + case 1: + r = x1; g = vfrac; b = c; break; + case 2: + r = c; g = vfrac; b = x2; break; + case 3: + r = c; g = x1; b = vfrac; break; + case 4: + r = x2; g = c; b = vfrac; break; + case 5: + default: + r = vfrac; g = c; b = x1; break; + } + + return { "r": Math.round(255 * r), "g": Math.round(255 * g), "b": Math.round(255 * b) }; } , - getVDevServices: function(vdev){ var typeKey = ZWayServerPlatform.getVDevTypeKey(vdev); var services = [], service; @@ -340,7 +381,7 @@ ZWayServerAccessory.prototype = { return null; } , - configureCharacteristic: function(cx, vdev){ + configureCharacteristic: function(cx, vdev, service){ var accessory = this; // Add this combination to the maps... @@ -414,6 +455,7 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.Hue){ cx.zway_getValueFromVDev = function(vdev){ + debug("Derived value " + accessory.rgb2hsv(vdev.metrics.color).h + " for hue."); return accessory.rgb2hsv(vdev.metrics.color).h; }; cx.value = cx.zway_getValueFromVDev(vdev); @@ -424,6 +466,18 @@ ZWayServerAccessory.prototype = { callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); + cx.on('set', function(hue, callback){ + var scx = service.getCharacteristic(Characteristic.Saturation); + var vcx = service.getCharacteristic(Characteristic.Brightness); + if(!scx || !vcx){ + debug("Hue without Saturation and Brightness is not supported! Cannot set value!") + callback(true, cx.value); + } + var rgb = this.hsv2rgb({ h: hue, s: scx.value, v: vcx.value }); + this.command(vdev, "exact", { red: rgb.r, green: rgb.g, blue: rgb.b }).then(function(result){ + callback(); + }); + }.bind(this)); cx.writeable = false; //cx.on('set', function(level, callback){ @@ -436,6 +490,7 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.Saturation){ cx.zway_getValueFromVDev = function(vdev){ + debug("Derived value " + accessory.rgb2hsv(vdev.metrics.color).s + " for saturation."); return accessory.rgb2hsv(vdev.metrics.color).s; }; cx.value = cx.zway_getValueFromVDev(vdev); @@ -446,6 +501,18 @@ ZWayServerAccessory.prototype = { callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); + cx.on('set', function(saturation, callback){ + var hcx = service.getCharacteristic(Characteristic.Hue); + var vcx = service.getCharacteristic(Characteristic.Brightness); + if(!hcx || !vcx){ + debug("Saturation without Hue and Brightness is not supported! Cannot set value!") + callback(true, cx.value); + } + var rgb = this.hsv2rgb({ h: hcx.value, s: saturation, v: vcx.value }); + this.command(vdev, "exact", { red: rgb.r, green: rgb.g, blue: rgb.b }).then(function(result){ + callback(); + }); + }.bind(this)); cx.writeable = false; //cx.on('set', function(level, callback){ @@ -666,14 +733,29 @@ ZWayServerAccessory.prototype = { success = false; debug("ERROR! Failed to configure required characteristic \"" + service.characteristics[i].displayName + "\"!"); } - cx = this.configureCharacteristic(cx, vdev); + cx = this.configureCharacteristic(cx, vdev, service); } for(var i = 0; i < service.optionalCharacteristics.length; i++){ var cx = service.optionalCharacteristics[i]; - var vdev = this.getVDevForCharacteristic(cx); + var vdev = this.getVDevForCharacteristic(cx, vdev); if(!vdev) continue; - cx = this.configureCharacteristic(cx, vdev); - if(cx) service.addCharacteristic(cx); + + //NOTE: Questionable logic, but if the vdev has already been used for the same + // characteristic type elsewhere, lets not duplicate it just for the sake of an + // optional characteristic. This eliminates the problem with RGB+W+W bulbs + // having the HSV controls shown again, but might have unintended consequences... + var othercx, othercxs = this.platform.cxVDevMap[vdev.id]; + if(othercxs) for(var j = 0; j < othercxs.length; j++) if(othercxs[j].UUID === cx.UUID) othercx = othercxs[j]; + if(othercx) + continue; + + cx = this.configureCharacteristic(cx, vdev, service); + try { + if(cx) service.addCharacteristic(cx); + } + catch (ex) { + debug('Adding Characteristic "' + cx.displayName + '" failed with message "' + ex.message + '". This may be expected.'); + } } return success; } @@ -736,7 +818,7 @@ ZWayServerAccessory.prototype = { extraCxs = []; // to wipe out any already setup cxs. break; } - this.configureCharacteristic(cx, vdev2); + this.configureCharacteristic(cx, vdev2, service); extraCxs.push(cx); } for(var j = 0; j < extraCxs.length; j++) From e6afda55d645caeb960f65e4c094ba0cc4c37a61 Mon Sep 17 00:00:00 2001 From: Snowdd1 Date: Tue, 6 Oct 2015 20:47:07 +0200 Subject: [PATCH 06/20] New UUID generation addresses #203 # This is a breaking change for all configured homekit databases, as it will remove room/area assignments and other things configured in iOS apps ... unless your accessory type was called `Object` before Changes: - Without any changes to the configuration file all accessories UUIDs will be generated by `uuid.generate(accessoryType + ":"` instead of the constructor name which turned out to be unreliable in that context. So about all UUIDs will change. - new parameter uuid_base can be used for accessories in config.json to use a different base for UUID generation than the displayName, which might not be unique - platforms can add the same property to their accessories before returned, just as they have a name property right now. --- app.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app.js b/app.js index a8c2006..126b7e3 100644 --- a/app.js +++ b/app.js @@ -86,7 +86,7 @@ function loadAccessories() { log("Initializing %s accessory...", accessoryType); var accessoryInstance = new accessoryConstructor(log, accessoryConfig); - var accessory = createAccessory(accessoryInstance, accessoryName); + var accessory = createAccessory(accessoryInstance, accessoryName, accessoryType, accessoryConfig.uuid_base); //pass accessoryType for UUID generation, and optional parameter uuid_base which can be used instead of displayName for UUID generation // add it to the bridge bridge.addBridgedAccessory(accessory); @@ -113,11 +113,11 @@ function loadPlatforms() { log("Initializing %s platform...", platformType); var platformInstance = new platformConstructor(log, platformConfig); - loadPlatformAccessories(platformInstance, log); + loadPlatformAccessories(platformInstance, log, platformType); } } -function loadPlatformAccessories(platformInstance, log) { +function loadPlatformAccessories(platformInstance, log, platformType) { asyncCalls++; platformInstance.accessories(once(function(foundAccessories){ asyncCalls--; @@ -129,7 +129,7 @@ function loadPlatformAccessories(platformInstance, log) { log("Initializing platform accessory '%s'...", accessoryName); - var accessory = createAccessory(accessoryInstance, accessoryName); + var accessory = createAccessory(accessoryInstance, accessoryName, platformType, accessoryInstance.uuid_base); // add it to the bridge bridge.addBridgedAccessory(accessory); @@ -141,7 +141,7 @@ function loadPlatformAccessories(platformInstance, log) { })); } -function createAccessory(accessoryInstance, displayName) { +function createAccessory(accessoryInstance, displayName, accessoryType, uuid_base) { var services = accessoryInstance.getServices(); @@ -159,7 +159,7 @@ function createAccessory(accessoryInstance, displayName) { // The returned "services" for this accessory are simply an array of new-API-style // Service instances which we can add to a created HAP-NodeJS Accessory directly. - var accessoryUUID = uuid.generate(accessoryInstance.constructor.name + ":" + displayName); + var accessoryUUID = uuid.generate(accessoryType + ":" + (uuid_base || displayName)); var accessory = new Accessory(displayName, accessoryUUID); From af79ea4fbf5b172cdf0f076307da91e0b37fdd55 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Wed, 7 Oct 2015 05:38:24 +0200 Subject: [PATCH 07/20] Beginning tag support enhancements Generalized tag recognition, tags are now `Homebridge.*` instead of `Homebridge:*`, initial attempt at `IsPrimary` but probably not working right yet. --- platforms/ZWayServer.js | 49 +++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 730eb9b..f21f39b 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -83,7 +83,20 @@ ZWayServerPlatform.prototype = { return deferred.promise; } , - + getTagValue: function(vdev, tagStem){ + if(!(vdev.tags && vdev.tags.length > 0)) return false; + var tagStem = "Homebridge." + tagStem; + if(vdev.tags.indexOf(tagStem) >= 0) return true; + var tags = vdev.tags, l = tags.length, tag; + for(var i = 0; i < l; i++){ + tag = tags[i]; + if(tag.indexOf(tagStem + ":") === 0){ + return tag.substr(tagStem.length + 1); + } + } + return false; + } + , accessories: function(callback) { debug("Fetching Z-Way devices..."); @@ -110,12 +123,24 @@ ZWayServerPlatform.prototype = { var groupedDevices = {}; for(var i = 0; i < devices.length; i++){ var vdev = devices[i]; - if(vdev.tags.indexOf("Homebridge:Skip") >= 0) { debug("Tag says skip!"); continue; } - if(this.opt_in && vdev.tags.indexOf("Homebridge:Include") < 0) continue; + if(this.getTagValue("Skip")) { debug("Tag says skip!"); continue; } + if(this.opt_in && !this.getTagValue(vdev, "Include")) continue; var gdid = vdev.id.replace(/^(.*?)_zway_(\d+-\d+)-\d.*/, '$1_$2'); var gd = groupedDevices[gdid] || (groupedDevices[gdid] = {devices: [], types: {}, extras: {}, primary: undefined}); gd.devices.push(vdev); var tk = ZWayServerPlatform.getVDevTypeKey(vdev); + + // If this is explicitly set as primary, set it now... + if(this.getTagValue("IsPrimary")){ + gd.primary = gd.devices.length - 1; + if(gd.types[tk] !== undefined){ + // everybody out of the way! + gd.extras[tk] = gd.extras[tk] || []; + gd.extras[tk].push(gd.types[tk]); + } + gd.types[tk] = gd.primary; + } + if(gd.types[tk] === undefined){ gd.types[tk] = gd.devices.length - 1; } else { @@ -136,12 +161,17 @@ ZWayServerPlatform.prototype = { } var accessory = null; - for(var ti = 0; ti < primaryDeviceClasses.length; ti++){ + if(gd.primary !== undefined){ + var pd = gd.devices[gd.primary]; + var name = pd.metrics && pd.metrics.title ? pd.metrics.title : pd.id; + accessory = new ZWayServerAccessory(name, gd, that); + } + else for(var ti = 0; ti < primaryDeviceClasses.length; ti++){ if(gd.types[primaryDeviceClasses[ti]] !== undefined){ gd.primary = gd.types[primaryDeviceClasses[ti]]; var pd = gd.devices[gd.primary]; var name = pd.metrics && pd.metrics.title ? pd.metrics.title : pd.id; - debug("Using primary device with type " + primaryDeviceClasses[ti] + ", " + name + " (" + pd.id + ") as primary."); + //debug("Using primary device with type " + primaryDeviceClasses[ti] + ", " + name + " (" + pd.id + ") as primary."); accessory = new ZWayServerAccessory(name, gd, that); break; } @@ -303,7 +333,11 @@ ZWayServerAccessory.prototype = { services.push(new Service.Switch(vdev.metrics.title, vdev.id)); break; case "switchMultilevel": - services.push(new Service.Lightbulb(vdev.metrics.title, vdev.id)); + if(this.platform.getTagValue(vdev, "ServiceType") === "Switch"){ + services.push(new Service.Switch(vdev.metrics.title, vdev.id)); + } else { + services.push(new Service.Lightbulb(vdev.metrics.title, vdev.id)); + } break; case "sensorBinary.Door/Window": services.push(new Service.GarageDoorOpener(vdev.metrics.title, vdev.id)); @@ -778,7 +812,8 @@ ZWayServerAccessory.prototype = { // 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]]; - services = services.concat(this.getVDevServices(xvdev)); + var xservice = this.getVDevServices(xvdev); + services = services.concat(xservice); } if(this.platform.splitServices){ From a15c026f2f5503ca80ed47881c5bf102d5bf28b0 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Wed, 7 Oct 2015 06:59:04 +0200 Subject: [PATCH 08/20] Manual address specification working now --- platforms/YamahaAVR.js | 54 +++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/platforms/YamahaAVR.js b/platforms/YamahaAVR.js index 1659c0f..f08fa96 100644 --- a/platforms/YamahaAVR.js +++ b/platforms/YamahaAVR.js @@ -23,6 +23,7 @@ function YamahaAVRPlatform(log, config){ this.setMainInputTo = config["setMainInputTo"]; this.expectedDevices = config["expected_devices"] || 100; this.discoveryTimeout = config["discovery_timeout"] || 30; + this.manualAddresses = config["manual_addresses"] || {}; this.browser = mdns.createBrowser(mdns.tcp('http'), {resolverSequence: sequence}); } @@ -75,25 +76,44 @@ YamahaAVRPlatform.prototype = { var accessories = []; var timer, timeElapsed = 0, checkCyclePeriod = 5000; - browser.on('serviceUp', function(service){ + // Hmm... seems we need to prevent double-listing via manual and Bonjour... + var sysIds = {}; + + var setupFromService = function(service){ var name = service.name; //console.log('Found HTTP service "' + name + '"'); // We can't tell just from mdns if this is an AVR... if (service.port != 80) return; // yamaha-nodejs assumes this, so finding one on another port wouldn't do any good anyway. var yamaha = new Yamaha(service.host); - yamaha.getSystemConfig().then(function(sysConfig){ - var sysModel = sysConfig.YAMAHA_AV.System[0].Config[0].Model_Name[0]; - var sysId = sysConfig.YAMAHA_AV.System[0].Config[0].System_ID[0]; - that.log("Found Yamaha " + sysModel + " - " + sysId + ", \"" + name + "\""); - var accessory = new YamahaAVRAccessory(that.log, that.config, service, yamaha, sysConfig); - accessories.push(accessory); - if(accessories.length >= this.expectedDevices) - timeoutFunction(); // We're done, call the timeout function now. - //callback([accessory]); - }, function(err){ - return; + yamaha.getSystemConfig().then( + function(sysConfig){ + var sysModel = sysConfig.YAMAHA_AV.System[0].Config[0].Model_Name[0]; + var sysId = sysConfig.YAMAHA_AV.System[0].Config[0].System_ID[0]; + if(sysIds[sysId]){ + this.log("WARN: Got multiple systems with ID " + sysId + "! Omitting duplicate!"); + return; + } + sysIds[sysId] = true; + this.log("Found Yamaha " + sysModel + " - " + sysId + ", \"" + name + "\""); + var accessory = new YamahaAVRAccessory(this.log, this.config, name, yamaha, sysConfig); + accessories.push(accessory); + if(accessories.length >= this.expectedDevices) + timeoutFunction(); // We're done, call the timeout function now. + }.bind(this) + ); + }.bind(this); + + // process manually specified devices... + for(var key in this.manualAddresses){ + if(!this.manualAddresses.hasOwnProperty(key)) continue; + setupFromService({ + name: key, + host: this.manualAddresses[key], + port: 80 }); - }); + } + + browser.on('serviceUp', setupFromService); browser.start(); // The callback can only be called once...so we'll have to find as many as we can @@ -119,15 +139,15 @@ YamahaAVRPlatform.prototype = { } }; -function YamahaAVRAccessory(log, config, mdnsService, yamaha, sysConfig) { +function YamahaAVRAccessory(log, config, name, yamaha, sysConfig) { this.log = log; this.config = config; - this.mdnsService = mdnsService; this.yamaha = yamaha; this.sysConfig = sysConfig; - this.name = mdnsService.name; - this.serviceName = mdnsService.name + " Speakers"; + this.nameSuffix = config["name_suffix"] || " Speakers"; + this.name = name; + this.serviceName = name + this.nameSuffix; this.setMainInputTo = config["setMainInputTo"]; this.playVolume = this.config["play_volume"]; this.minVolume = config["min_volume"] || -50.0; From ed8a3c806231dc51fecbea64812c93a6cc4e42d0 Mon Sep 17 00:00:00 2001 From: Snowdd1 Date: Wed, 7 Oct 2015 10:39:00 +0200 Subject: [PATCH 09/20] uuid_base in constructor --- accessories/knxdevice.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/accessories/knxdevice.js b/accessories/knxdevice.js index 4656522..6aa4ffd 100644 --- a/accessories/knxdevice.js +++ b/accessories/knxdevice.js @@ -17,6 +17,8 @@ New 2015-09-19: New 2015-10-02: - Check for valid group addresses - new "R" flag allowed for Boolean addresses: 1/2/3R is the boolean not(1/2/3), i.e. 0 and 1 switched on read and write +New 2015-10-07: +- Accept uuid_base parameter from config.json to use as unique identifier in UUIDs instead of name (optional) * */ var Service = require("HAP-NodeJS").Service; @@ -38,6 +40,9 @@ function KNXDevice(log, config) { if (config.name) { this.name = config.name; } + if (config.uuid_base) { + this.uuid_base = config.uuid_base; + } if (config.knxd_ip){ this.knxd_ip = config.knxd_ip; } else { From 772f35efac270edde65cb6a4fe8af2cc02573bb2 Mon Sep 17 00:00:00 2001 From: stipus Date: Thu, 8 Oct 2015 09:07:33 +0200 Subject: [PATCH 10/20] Create HomeSeer.js --- platforms/HomeSeer.js | 370 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100644 platforms/HomeSeer.js diff --git a/platforms/HomeSeer.js b/platforms/HomeSeer.js new file mode 100644 index 0000000..4618efe --- /dev/null +++ b/platforms/HomeSeer.js @@ -0,0 +1,370 @@ +'use strict'; + +// +// HomeSeer Platform Shim for HomeBridge +// V0.1 - Jean-Michel Joudrier (stipus at stipus dot com) - 2015/10/07 - Initial version +// +// +// Remember to add platform to config.json. +// +// You can get HomeSeer Device References by clicking a HomeSeer device name, then +// choosing the Advanced Tab. +// +// Example: +// "platforms": [ +// { +// "platform": "HomeSeer", // required +// "name": "HomeSeer", // required +// "url": "http://192.168.3.4:81", // required +// "accessories":[ +// { +// "ref":8, // required - HomeSeer Device Reference (To get it, select the HS Device - then Advanced Tab) +// "type":"Lightbulb", // Optional - Lightbulb is the default +// "name":"My Light", // Optional - HomeSeer device name is the default +// "offValue":"0", // Optional - 0 is the default +// "onValue":"100", // Optional - 100 is the default +// "can_dim":true // Optional - true is the default - false for a non dimmable lightbulb +// }, +// { +// "ref":9 // This is a dimmable Lightbulb by default +// }, +// { +// "ref":58, // This is an controllable outlet +// "type":"Outlet" +// } +// ] +// } +// ], +// +// +// SUPORTED TYPES: +// - Lightbulb (can_dim, onValue, offValue options) +// - Fan (onValue, offValue options) +// - Switch (onValue, offValue options) +// - Outlet (onValue, offValue options) +// - TemperatureSensor +// - ContactSensor +// - MotionSensor +// - LeakSensor +// - LightSensor +// - OccupancySensor +// - SmokeSensor +// - Door + + +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; +var request = require("request"); + + +function httpRequest(url, method, callback) { + request({ + url: url, + method: method + }, + function (error, response, body) { + callback(error, response, body) + }) +} + + + +function HomeSeerPlatform(log, config){ + this.log = log; + this.config = config; +} + +HomeSeerPlatform.prototype = { + accessories: function(callback) { + this.log("Fetching HomeSeer devices."); + + var refList = ""; + for( var i=0; i Date: Thu, 8 Oct 2015 13:50:34 -0400 Subject: [PATCH 11/20] Simple fix for undefined Nest device names --- platforms/Nest.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/platforms/Nest.js b/platforms/Nest.js index 414fcef..c1ef3bd 100644 --- a/platforms/Nest.js +++ b/platforms/Nest.js @@ -44,7 +44,11 @@ NestPlatform.prototype = { function NestThermostatAccessory(log, name, device, deviceId) { // device info - this.name = name; + if (name) { + this.name = name; + } else { + this.name = "Nest"; + } this.model = device.model_version; this.serial = device.serial_number; this.deviceId = deviceId; @@ -390,4 +394,4 @@ NestThermostatAccessory.prototype = { } module.exports.accessory = NestThermostatAccessory; -module.exports.platform = NestPlatform; \ No newline at end of file +module.exports.platform = NestPlatform; From c0dfc9a8cdc95badc21c1956feadf1204786c2ad Mon Sep 17 00:00:00 2001 From: Theodor Tonum Date: Sat, 10 Oct 2015 01:14:10 +0200 Subject: [PATCH 12/20] Add support for local Telldus control --- config-sample.json | 4 + package.json | 1 + platforms/Telldus.js | 265 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 270 insertions(+) create mode 100644 platforms/Telldus.js diff --git a/config-sample.json b/config-sample.json index 7af74db..b837f61 100644 --- a/config-sample.json +++ b/config-sample.json @@ -23,6 +23,10 @@ "token" : "telldus token", "token_secret" : "telldus token secret" }, + { + "platform" : "Telldus", + "name" : "Telldus" + }, { "platform": "Wink", "name": "Wink", diff --git a/package.json b/package.json index 3a3ca74..8f88a35 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "tough-cookie": "^2.0.0", "request": "2.49.x", "sonos": "0.8.x", + "telldus": "0.0.9", "telldus-live": "0.2.x", "teslams": "1.0.1", "unofficial-nest-api": "git+https://github.com/hachidorii/unofficial_nodejs_nest.git#d8d48edc952b049ff6320ef99afa7b2f04cdee98", diff --git a/platforms/Telldus.js b/platforms/Telldus.js new file mode 100644 index 0000000..87d37f1 --- /dev/null +++ b/platforms/Telldus.js @@ -0,0 +1,265 @@ +var types = require("HAP-NodeJS/accessories/types.js"); +var telldus = require('telldus'); + +function TelldusPlatform(log, config) { + var that = this; + that.log = log; +} + +TelldusPlatform.prototype = { + + accessories: function(callback) { + var that = this; + + that.log("Fetching devices..."); + + var devices = telldus.getDevicesSync(); + + that.log("Found " + devices.length + " devices..."); + + var foundAccessories = []; + + // Clean non device + for (var i = 0; i < devices.length; i++) { + if (devices[i].type != 'DEVICE') { + devices.splice(i, 1); + } + } + + for (var i = 0; i < devices.length; i++) { + if (devices[i].type === 'DEVICE') { + TelldusAccessory.create(that.log, devices[i], function(err, accessory) { + if (!!err) that.log("Couldn't load device info"); + foundAccessories.push(accessory); + if (foundAccessories.length >= devices.length) { + callback(foundAccessories); + } + }); + } + } + } +}; + +var TelldusAccessory = function TelldusAccessory(log, device) { + + this.log = log; + + var m = device.model.split(':'); + + this.dimTimeout = false; + + // Set accessory info + this.device = device; + this.id = device.id; + this.name = device.name; + this.manufacturer = "Telldus"; // NOTE: Change this later + this.model = device.model; + this.status = device.status; + switch (device.status.name) { + case 'OFF': + this.state = 0; + this.stateValue = 0; + break; + case 'ON': + this.state = 2; + this.stateValue = 1; + break; + case 'DIM': + this.state = 16; + this.stateValue = device.status.level; + break; + } +}; + +TelldusAccessory.create = function (log, device, callback) { + + callback(null, new TelldusAccessory(log, device)); + +}; + +TelldusAccessory.prototype = { + + dimmerValue: function() { + + if (this.state === 1) { + return 100; + } + + if (this.state === 16 && this.stateValue != "unde") { + return parseInt(this.stateValue * 100 / 255); + } + + return 0; + }, + + informationCharacteristics: function() { + var that = this; + + informationCharacteristics = [ + { + cType: types.NAME_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: that.name, + supportEvents: false, + supportBonjour: false, + manfDescription: "Name of the accessory", + designedMaxLength: 255 + },{ + cType: types.MANUFACTURER_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: that.manufacturer, + supportEvents: false, + supportBonjour: false, + manfDescription: "Manufacturer", + designedMaxLength: 255 + },{ + cType: types.MODEL_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: that.model, + supportEvents: false, + supportBonjour: false, + manfDescription: "Model", + designedMaxLength: 255 + },{ + cType: types.SERIAL_NUMBER_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "A1S2NASF88EW", + supportEvents: false, + supportBonjour: false, + manfDescription: "SN", + designedMaxLength: 255 + },{ + cType: types.IDENTIFY_CTYPE, + onUpdate: function () { + telldus.turnOff(that.id, function(err){ + if (!!err) that.log("Error: " + err.message); + telldus.turnOn(that.id, function(err){ + if (!!err) that.log("Error: " + err.message); + telldus.turnOff(that.id, function(err){ + if (!!err) that.log("Error: " + err.message); + telldus.turnOn(that.id, function(err){ + if (!!err) that.log("Error: " + err.message); + }); + }); + }); + }); + }, + perms: ["pw"], + format: "bool", + initialValue: false, + supportEvents: false, + supportBonjour: false, + manfDescription: "Identify Accessory", + designedMaxLength: 1 + } + ]; + return informationCharacteristics; + }, + + controlCharacteristics: function() { + var that = this; + + cTypes = [{ + cType: types.NAME_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: that.name, + supportEvents: true, + supportBonjour: false, + manfDescription: "Name of service", + designedMaxLength: 255 + }] + + cTypes.push({ + cType: types.POWER_STATE_CTYPE, + onUpdate: function(value) { + if (value) { + telldus.turnOn(that.id, function(err){ + if (!!err) { + that.log("Error: " + err.message) + } else { + that.log(that.name + " - Updated power state: ON"); + } + }); + } else { + telldus.turnOff(that.id, function(err){ + if (!!err) { + that.log("Error: " + err.message) + } else { + that.log(that.name + " - Updated power state: OFF"); + } + }); + } + }, + perms: ["pw","pr","ev"], + format: "bool", + initialValue: (that.state != 2 && (that.state === 16 && that.stateValue != 0)) ? 1 : 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Change the power state", + designedMaxLength: 1 + }) + + if (that.model === "selflearning-dimmer") { + cTypes.push({ + cType: types.BRIGHTNESS_CTYPE, + onUpdate: function (value) { + if (that.dimTimeout) { + clearTimeout(that.dimTimeout); + } + + that.dimTimeout = setTimeout(function(){ + telldus.dim(that.id, (255 * (value / 100)), function(err, result){ + if (!!err) { + that.log("Error: " + err.message); + } else { + that.log(that.name + " - Updated brightness: " + value); + } + }); + that.dimTimeout = false; + }, 250); + }, + perms: ["pw", "pr", "ev"], + format: "int", + initialValue: that.dimmerValue(), + supportEvents: true, + supportBonjour: false, + manfDescription: "Adjust Brightness of Light", + designedMinValue: 0, + designedMaxValue: 100, + designedMinStep: 1, + unit: "%" + }) + } + + return cTypes + }, + + getServices: function() { + + var services = [ + { + sType: types.ACCESSORY_INFORMATION_STYPE, + characteristics: this.informationCharacteristics() + }, + { + sType: types.LIGHTBULB_STYPE, + characteristics: this.controlCharacteristics() + } + ]; + + return services; + } +}; + +module.exports.platform = TelldusPlatform; +module.exports.accessory = TelldusAccessory; From 4fbd7eb775a1137b8b88dfd674d4c6769c8ad54e Mon Sep 17 00:00:00 2001 From: stipus Date: Sat, 10 Oct 2015 12:38:46 +0200 Subject: [PATCH 13/20] Fix for HomeSeer occupancy sensor --- platforms/HomeSeer.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platforms/HomeSeer.js b/platforms/HomeSeer.js index 4618efe..1ecbd43 100644 --- a/platforms/HomeSeer.js +++ b/platforms/HomeSeer.js @@ -3,6 +3,7 @@ // // HomeSeer Platform Shim for HomeBridge // V0.1 - Jean-Michel Joudrier (stipus at stipus dot com) - 2015/10/07 - Initial version +// V0.2 - Jean-Michel Joudrier (stipus at stipus dot com) - 2015/10/10 - occupancy sensor fix // // // Remember to add platform to config.json. @@ -320,7 +321,7 @@ HomeSeerAccessory.prototype = { } case "OccupancySensor": { var occupancySensorService = new Service.OccupancySensor(); - motionSensorService + occupancySensorService .getCharacteristic(Characteristic.OccupancyDetected) .on('get', this.getPowerState.bind(this)); services.push( occupancySensorService ); From 4a831422eb3c565bb33c965992196f982f88a1fa Mon Sep 17 00:00:00 2001 From: "stevetrease@gmail.com" Date: Sat, 10 Oct 2015 14:41:00 +0100 Subject: [PATCH 14/20] Added two new accessories for a readonly thermometer and hygrometer (humidity meter) based on HttpAccessory. --- accessories/HttpHygrometer.js | 71 ++++++++++++++++++++++++++++++ accessories/HttpThermometer.js | 79 ++++++++++++++++++++++++++++++++++ config-sample.json | 15 ++++++- 3 files changed, 164 insertions(+), 1 deletion(-) create mode 100644 accessories/HttpHygrometer.js create mode 100644 accessories/HttpThermometer.js diff --git a/accessories/HttpHygrometer.js b/accessories/HttpHygrometer.js new file mode 100644 index 0000000..61ad3b9 --- /dev/null +++ b/accessories/HttpHygrometer.js @@ -0,0 +1,71 @@ +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; +var request = require("request"); + +module.exports = { + accessory: HygrometerAccessory +} + +function HygrometerAccessory(log, config) { + this.log = log; + + // url info + this.url = config["url"]; + this.http_method = config["http_method"]; +} + + +HygrometerAccessory.prototype = { + + httpRequest: function(url, method, callback) { + request({ + url: url, + method: method + }, + function (error, response, body) { + callback(error, response, body) + }) + }, + + + identify: function(callback) { + this.log("Identify requested!"); + callback(); // success + }, + + getCurrentRelativeHumidity: function (callback) { + var that = this; + that.log ("getting CurrentCurrentRelativeHumidity"); + + this.httpRequest(this.url, this.http_method, function(error, response, body) { + if (error) { + this.log('HTTP function failed: %s', error); + callback(error); + } + else { + this.log('HTTP function succeeded - %s', body); + callback(null, Number(body)); + } + }.bind(this)); + }, + + getServices: function() { + + // you can OPTIONALLY create an information service if you wish to override + // the default values for things like serial number, model, etc. + var informationService = new Service.AccessoryInformation(); + + informationService + .setCharacteristic(Characteristic.Manufacturer, "HTTP Manufacturer") + .setCharacteristic(Characteristic.Model, "HTTP Hygrometer") + .setCharacteristic(Characteristic.SerialNumber, "HTTP Serial Number"); + + var humidityService = new Service.HumiditySensor(); + + humidityService + .getCharacteristic(Characteristic.CurrentRelativeHumidity) + .on('get', this.getCurrentRelativeHumidity.bind(this)); + + return [informationService, humidityService]; + } +}; diff --git a/accessories/HttpThermometer.js b/accessories/HttpThermometer.js new file mode 100644 index 0000000..ac9bdc2 --- /dev/null +++ b/accessories/HttpThermometer.js @@ -0,0 +1,79 @@ +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; +var request = require("request"); + +module.exports = { + accessory: ThermometerAccessory +} + +function ThermometerAccessory(log, config) { + this.log = log; + + // url info + this.url = config["url"]; + this.http_method = config["http_method"]; +} + + +ThermometerAccessory.prototype = { + + httpRequest: function(url, method, callback) { + request({ + url: url, + method: method + }, + function (error, response, body) { + callback(error, response, body) + }) + }, + + + identify: function(callback) { + this.log("Identify requested!"); + callback(); // success + }, + + getCurrentTemperature: function (callback) { + var that = this; + that.log ("getting CurrentTemperature"); + + + this.httpRequest(this.url, this.http_method, function(error, response, body) { + if (error) { + this.log('HTTP function failed: %s', error); + callback(error); + } + else { + this.log('HTTP function succeeded - %s', body); + callback(null, Number(body)); + } + }.bind(this)); + }, + + getTemperatureUnits: function (callback) { + var that = this; + that.log ("getTemperature Units"); + // 1 = F and 0 = C + callback (null, 0); + }, + + getServices: function() { + + // you can OPTIONALLY create an information service if you wish to override + // the default values for things like serial number, model, etc. + var informationService = new Service.AccessoryInformation(); + + informationService + .setCharacteristic(Characteristic.Manufacturer, "HTTP Manufacturer") + .setCharacteristic(Characteristic.Model, "HTTP Thermometer") + .setCharacteristic(Characteristic.SerialNumber, "HTTP Serial Number"); + + var temperatureService = new Service.TemperatureSensor(); + + temperatureService + .getCharacteristic(Characteristic.CurrentTemperature) + .on('get', this.getCurrentTemperature.bind(this)); + + return [informationService, temperatureService]; + } +}; diff --git a/config-sample.json b/config-sample.json index 7af74db..85fd6e3 100644 --- a/config-sample.json +++ b/config-sample.json @@ -164,7 +164,20 @@ "off_url": "https://192.168.1.22:3030/devices/23222/off", "brightness_url": "https://192.168.1.22:3030/devices/23222/brightness/%b", "http_method": "POST" - },{ + }, + { + "accessory": "HttpHygrometer", + "name": "Kitchen", + "url": "http://host/URL", + "http_method": "GET" + }, + { + "accessory": "HttpThermometer", + "name": "Garage", + "url": "http://home/URL", + "http_method": "GET" + }, + { "accessory": "ELKM1", "name": "Security System", "description": "Allows basic control of Elk M1 security system. You can use 1 of 3 arm modes: Away, Stay, Night. If you need to access all 3, create 3 accessories with different names.", From 25299a7863455ed83b653e3a95419e609f48314a Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Sat, 10 Oct 2015 15:45:27 +0200 Subject: [PATCH 15/20] =?UTF-8?q?Last=20bits=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- platforms/ZWayServer.js | 38 ++++++++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index f21f39b..0b81e88 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -125,20 +125,26 @@ ZWayServerPlatform.prototype = { var vdev = devices[i]; if(this.getTagValue("Skip")) { debug("Tag says skip!"); continue; } if(this.opt_in && !this.getTagValue(vdev, "Include")) continue; - var gdid = vdev.id.replace(/^(.*?)_zway_(\d+-\d+)-\d.*/, '$1_$2'); + + var gdid = this.getTagValue(vdev, "Accessory.Id"); + if(!gdid){ + gdid = vdev.id.replace(/^(.*?)_zway_(\d+-\d+)-\d.*/, '$1_$2'); + } + var gd = groupedDevices[gdid] || (groupedDevices[gdid] = {devices: [], types: {}, extras: {}, primary: undefined}); gd.devices.push(vdev); var tk = ZWayServerPlatform.getVDevTypeKey(vdev); // If this is explicitly set as primary, set it now... - if(this.getTagValue("IsPrimary")){ - gd.primary = gd.devices.length - 1; + if(this.getTagValue(vdev, "IsPrimary")){ + // everybody out of the way! Can't be in "extras" if you're the primary... if(gd.types[tk] !== undefined){ - // everybody out of the way! gd.extras[tk] = gd.extras[tk] || []; gd.extras[tk].push(gd.types[tk]); + delete gd.types[tk]; // clear the way for this one to be set here below... } - gd.types[tk] = gd.primary; + gd.primary = gd.devices.length - 1; + //gd.types[tk] = gd.primary; } if(gd.types[tk] === undefined){ @@ -149,7 +155,7 @@ ZWayServerPlatform.prototype = { } if(tk !== vdev.deviceType) gd.types[vdev.deviceType] = gd.devices.length - 1; // also include the deviceType only as a possibility } - //TODO: Make a second pass, re-splitting any devices that don't make sense together + for(var gdid in groupedDevices) { if(!groupedDevices.hasOwnProperty(gdid)) continue; @@ -183,7 +189,6 @@ ZWayServerPlatform.prototype = { foundAccessories.push(accessory); } -//foundAccessories = foundAccessories.slice(0, 10); // Limit to a few devices for testing... callback(foundAccessories); // Start the polling process... @@ -332,8 +337,9 @@ ZWayServerAccessory.prototype = { case "switchBinary": services.push(new Service.Switch(vdev.metrics.title, vdev.id)); break; + case "switchRGBW": case "switchMultilevel": - if(this.platform.getTagValue(vdev, "ServiceType") === "Switch"){ + if(this.platform.getTagValue(vdev, "Service.Type") === "Switch"){ services.push(new Service.Switch(vdev.metrics.title, vdev.id)); } else { services.push(new Service.Lightbulb(vdev.metrics.title, vdev.id)); @@ -436,6 +442,12 @@ ZWayServerAccessory.prototype = { return cx; } + // We don't want to override "Name"'s name...so we just move this below that block. + var descOverride = this.platform.getTagValue(vdev, "Characteristic.Description"); + if(descOverride){ + cx.displayName = descOverride; + } + if(cx instanceof Characteristic.On){ cx.zway_getValueFromVDev = function(vdev){ var val = false; @@ -797,17 +809,23 @@ ZWayServerAccessory.prototype = { getServices: function() { var that = this; + var vdevPrimary = this.devDesc.devices[this.devDesc.primary]; + var accId = this.platform.getTagValue(vdevPrimary, "Accessory.Id"); + if(!accId){ + accId = "VDev-" + vdevPrimary.h; //FIXME: Is this valid? + } + var informationService = new Service.AccessoryInformation(); informationService .setCharacteristic(Characteristic.Name, this.name) .setCharacteristic(Characteristic.Manufacturer, "Z-Wave.me") .setCharacteristic(Characteristic.Model, "Virtual Device (VDev version 1)") - .setCharacteristic(Characteristic.SerialNumber, "VDev-" + this.devDesc.devices[this.devDesc.primary].h) //FIXME: Is this valid?); + .setCharacteristic(Characteristic.SerialNumber, accId); var services = [informationService]; - services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.primary])); + services = services.concat(this.getVDevServices(vdevPrimary)); // 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++){ From a274ae4edab182a22e08b1bab875a1a15fe37646 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Sun, 11 Oct 2015 05:44:44 +0200 Subject: [PATCH 16/20] Switches to the *new* new Characteristics API format for the two custom Characteristics. Fixes #247 --- platforms/YamahaAVR.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/platforms/YamahaAVR.js b/platforms/YamahaAVR.js index f08fa96..0dde24f 100644 --- a/platforms/YamahaAVR.js +++ b/platforms/YamahaAVR.js @@ -31,24 +31,24 @@ function YamahaAVRPlatform(log, config){ YamahaAVRPlatform.AudioVolume = function() { Characteristic.call(this, 'Audio Volume', '00001001-0000-1000-8000-135D67EC4377'); - this.format = 'uint8'; - this.unit = 'percentage'; - this.maximumValue = 100; - this.minimumValue = 0; - this.stepValue = 1; - this.readable = true; - this.writable = true; - this.supportsEventNotification = true; + this.setProps({ + format: Characteristic.Formats.UINT8, + unit: Characteristic.Units.PERCENTAGE, + maxValue: 100, + minValue: 0, + minStep: 1, + perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] + }); this.value = this.getDefaultValue(); }; inherits(YamahaAVRPlatform.AudioVolume, Characteristic); YamahaAVRPlatform.Muting = function() { Characteristic.call(this, 'Muting', '00001002-0000-1000-8000-135D67EC4377'); - this.format = 'bool'; - this.readable = true; - this.writable = true; - this.supportsEventNotification = true; + this.setProps({ + format: Characteristic.Formats.UINT8, + perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] + }); this.value = this.getDefaultValue(); }; inherits(YamahaAVRPlatform.Muting, Characteristic); From c506c44d6091b05b550b6fc901f4a1c69a7b2605 Mon Sep 17 00:00:00 2001 From: stipus Date: Mon, 12 Oct 2015 02:02:37 +0200 Subject: [PATCH 17/20] Update HomeSeer.js --- platforms/HomeSeer.js | 248 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 230 insertions(+), 18 deletions(-) diff --git a/platforms/HomeSeer.js b/platforms/HomeSeer.js index 1ecbd43..8cb371a 100644 --- a/platforms/HomeSeer.js +++ b/platforms/HomeSeer.js @@ -2,8 +2,15 @@ // // HomeSeer Platform Shim for HomeBridge -// V0.1 - Jean-Michel Joudrier (stipus at stipus dot com) - 2015/10/07 - Initial version -// V0.2 - Jean-Michel Joudrier (stipus at stipus dot com) - 2015/10/10 - occupancy sensor fix +// V0.1 - Jean-Michel Joudrier (stipus at stipus dot com) - 2015/10/07 +// - Initial version +// V0.2 - Jean-Michel Joudrier (stipus at stipus dot com) - 2015/10/10 +// - Occupancy sensor fix +// V0.3 - Jean-Michel Joudrier (stipus at stipus dot com) - 2015/10/11 +// - Added TemperatureUnit=F|C option to temperature sensors +// - Added negative temperature support to temperature sensors +// V0.4 - Jean-Michel Joudrier (stipus at stipus dot com) - 2015/10/11 +// - Added thermostat support // // // Remember to add platform to config.json. @@ -14,24 +21,50 @@ // Example: // "platforms": [ // { -// "platform": "HomeSeer", // required -// "name": "HomeSeer", // required -// "url": "http://192.168.3.4:81", // required +// "platform": "HomeSeer", // Required +// "name": "HomeSeer", // Required +// "host": "http://192.168.3.4:81", // Required - If you did setup HomeSeer authentication, use "http://user:password@ip_address:port" // "accessories":[ // { -// "ref":8, // required - HomeSeer Device Reference (To get it, select the HS Device - then Advanced Tab) -// "type":"Lightbulb", // Optional - Lightbulb is the default -// "name":"My Light", // Optional - HomeSeer device name is the default -// "offValue":"0", // Optional - 0 is the default -// "onValue":"100", // Optional - 100 is the default -// "can_dim":true // Optional - true is the default - false for a non dimmable lightbulb +// "ref":8, // Required - HomeSeer Device Reference (To get it, select the HS Device - then Advanced Tab) +// "type":"Lightbulb", // Optional - Lightbulb is the default +// "name":"My Light", // Optional - HomeSeer device name is the default +// "offValue":"0", // Optional - 0 is the default +// "onValue":"100", // Optional - 100 is the default +// "can_dim":true // Optional - true is the default - false for a non dimmable lightbulb // }, // { -// "ref":9 // This is a dimmable Lightbulb by default +// "ref":9 // This is a dimmable Lightbulb by default // }, // { -// "ref":58, // This is an controllable outlet +// "ref":58, // This is an controllable outlet // "type":"Outlet" +// }, +// { +// "ref":111, +// "type":"TemperatureSensor", // Required for a temperature sensor +// "temperatureUnit":"F", // Optional - C is the default +// "name":"Bedroom temp" // Optional - HomeSeer device name is the default +// }, +// { +// "ref":113, // Required - HomeSeer Device Reference of the Current Temperature Device +// "type":"Thermostat", // Required for a Thermostat +// "name":"Température Salon", // Optional - HomeSeer device name is the default +// "temperatureUnit":"C", // Optional - F for Fahrenheit, C for Celsius, C is the default +// "setPointRef":167, // Required - HomeSeer device reference for your thermostat Set Point. +// "setPointReadOnly":true, // Optional - Set to false if your SetPoint is read/write. true is the default +// "stateRef":166, // Required - HomeSeer device reference for your thermostat current state +// "stateOffValues":[0,4,5], // Required - List of the HomeSeer device values for a HomeKit state=OFF +// "stateHeatValues":[1], // Required - List of the HomeSeer device values for a HomeKit state=HEAT +// "stateCoolValues":[2], // Required - List of the HomeSeer device values for a HomeKit state=COOL +// "stateAutoValues":[3], // Required - List of the HomeSeer device values for a HomeKit state=AUTO +// "controlRef":168, // Required - HomeSeer device reference for your thermostat mode control (It can be the same as stateRef for some thermostats) +// "controlOffValue":0, // Required - Value for OFF +// "controlHeatValue":1, // Required - Value for HEAT +// "controlCoolValue":2, // Required - Value for COOL +// "controlAutoValue":3, // Required - Value for AUTO +// "coolingThresholdRef":169, // Optional - Not-implemented-yet - HomeSeer device reference for your thermostat cooling threshold +// "heatingThresholdRef":170 // Optional - Not-implemented-yet - HomeSeer device reference for your thermostat heating threshold // } // ] // } @@ -43,7 +76,8 @@ // - Fan (onValue, offValue options) // - Switch (onValue, offValue options) // - Outlet (onValue, offValue options) -// - TemperatureSensor +// - TemperatureSensor (temperatureUnit=C|F) +// - Thermostat (temperatureUnit, setPoint, state, control options) // - ContactSensor // - MotionSensor // - LeakSensor @@ -89,6 +123,7 @@ HomeSeerPlatform.prototype = { var that = this; var foundAccessories = []; var url = this.config["host"] + "/JSON?request=getstatus&ref=" + refList; + httpRequest( url, "GET", function(error, response, body) { if (error) { this.log('HomeSeer status function failed: %s', error.message); @@ -115,8 +150,9 @@ function HomeSeerAccessory(log, platformConfig, status ) { this.onValue = "100"; this.offValue = "0"; - this.control_url = platformConfig["host"] + "/JSON?request=controldevicebyvalue&ref=" + this.ref + "&value="; - this.status_url = platformConfig["host"] + "/JSON?request=getstatus&ref=" + this.ref; + this.access_url = platformConfig["host"] + "/JSON?"; + this.control_url = this.access_url + "request=controldevicebyvalue&ref=" + this.ref + "&value="; + this.status_url = this.access_url + "request=getstatus&ref=" + this.ref; for( var i=0; i Date: Mon, 12 Oct 2015 02:11:24 +0200 Subject: [PATCH 18/20] Update HomeSeer.js --- platforms/HomeSeer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platforms/HomeSeer.js b/platforms/HomeSeer.js index 8cb371a..e691b68 100644 --- a/platforms/HomeSeer.js +++ b/platforms/HomeSeer.js @@ -505,7 +505,7 @@ HomeSeerAccessory.prototype = { } case "OccupancySensor": { var occupancySensorService = new Service.OccupancySensor(); - motionSensorService + occupancySensorService .getCharacteristic(Characteristic.OccupancyDetected) .on('get', this.getPowerState.bind(this)); services.push( occupancySensorService ); From c59b87d17d3fb032ce087ca7cb7119e46c4b5ce0 Mon Sep 17 00:00:00 2001 From: stipus Date: Mon, 12 Oct 2015 02:15:41 +0200 Subject: [PATCH 19/20] Update HomeSeer.js --- platforms/HomeSeer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platforms/HomeSeer.js b/platforms/HomeSeer.js index e691b68..e3c289b 100644 --- a/platforms/HomeSeer.js +++ b/platforms/HomeSeer.js @@ -9,7 +9,7 @@ // V0.3 - Jean-Michel Joudrier (stipus at stipus dot com) - 2015/10/11 // - Added TemperatureUnit=F|C option to temperature sensors // - Added negative temperature support to temperature sensors -// V0.4 - Jean-Michel Joudrier (stipus at stipus dot com) - 2015/10/11 +// V0.4 - Jean-Michel Joudrier (stipus at stipus dot com) - 2015/10/12 // - Added thermostat support // // From 1f8db79324ad2935d143a43b9d4f23a786c89a9a Mon Sep 17 00:00:00 2001 From: stipus Date: Tue, 13 Oct 2015 00:53:19 +0200 Subject: [PATCH 20/20] Added support for humidity sensors, battery level sensors, low battery flag for all sensors, and ability to run HomeSeer events - Added Humidity sensor support - Added Battery support - Added low battery support for all sensors - Added HomeSeer event support (using HomeKit switches...) --- platforms/HomeSeer.js | 214 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 193 insertions(+), 21 deletions(-) diff --git a/platforms/HomeSeer.js b/platforms/HomeSeer.js index e3c289b..e0c5c79 100644 --- a/platforms/HomeSeer.js +++ b/platforms/HomeSeer.js @@ -11,6 +11,12 @@ // - Added negative temperature support to temperature sensors // V0.4 - Jean-Michel Joudrier (stipus at stipus dot com) - 2015/10/12 // - Added thermostat support +// V0.5 - Jean-Michel Joudrier (stipus at stipus dot com) - 2015/10/12 +// - Added Humidity sensor support +// V0.6 - Jean-Michel Joudrier (stipus at stipus dot com) - 2015/10/12 +// - Added Battery support +// - Added low battery support for all sensors +// - Added HomeSeer event support (using HomeKit switches...) // // // Remember to add platform to config.json. @@ -24,7 +30,16 @@ // "platform": "HomeSeer", // Required // "name": "HomeSeer", // Required // "host": "http://192.168.3.4:81", // Required - If you did setup HomeSeer authentication, use "http://user:password@ip_address:port" -// "accessories":[ +// +// "events":[ // Optional - List of Events - Currently they are imported into HomeKit as switches +// { +// "eventGroup":"My Group", // Required - The HomeSeer event group +// "eventName":"My Event", // Required - The HomeSeer event name +// "name":"Test" // Optional - HomeSeer event name is the default +// } +// ], +// +// "accessories":[ // Required - List of Accessories // { // "ref":8, // Required - HomeSeer Device Reference (To get it, select the HS Device - then Advanced Tab) // "type":"Lightbulb", // Optional - Lightbulb is the default @@ -37,14 +52,16 @@ // "ref":9 // This is a dimmable Lightbulb by default // }, // { -// "ref":58, // This is an controllable outlet +// "ref":58, // This is a controllable outlet // "type":"Outlet" // }, // { -// "ref":111, +// "ref":111, // Required - HomeSeer Device Reference for your sensor // "type":"TemperatureSensor", // Required for a temperature sensor // "temperatureUnit":"F", // Optional - C is the default -// "name":"Bedroom temp" // Optional - HomeSeer device name is the default +// "name":"Bedroom temp", // Optional - HomeSeer device name is the default +// "batteryRef":112, // Optional - HomeSeer device reference for the sensor battery level +// "batteryThreshold":15 // Optional - If sensor battery level is below this value, the HomeKit LowBattery characteristic is set to 1. Default is 10 // }, // { // "ref":113, // Required - HomeSeer Device Reference of the Current Temperature Device @@ -65,6 +82,12 @@ // "controlAutoValue":3, // Required - Value for AUTO // "coolingThresholdRef":169, // Optional - Not-implemented-yet - HomeSeer device reference for your thermostat cooling threshold // "heatingThresholdRef":170 // Optional - Not-implemented-yet - HomeSeer device reference for your thermostat heating threshold +// }, +// { +// "ref":115, // Required - HomeSeer Device Reference for a device holding battery level (0-100) +// "type":"Battery", // Required for a Battery +// "name":"Roomba battery", // Optional - HomeSeer device name is the default +// "batteryThreshold":15 // Optional - If the level is below this value, the HomeKit LowBattery characteristic is set to 1. Default is 10 // } // ] // } @@ -72,18 +95,20 @@ // // // SUPORTED TYPES: -// - Lightbulb (can_dim, onValue, offValue options) -// - Fan (onValue, offValue options) -// - Switch (onValue, offValue options) -// - Outlet (onValue, offValue options) +// - Lightbulb (can_dim, onValue, offValue options) +// - Fan (onValue, offValue options) +// - Switch (onValue, offValue options) +// - Outlet (onValue, offValue options) +// - Thermostat (temperatureUnit, setPoint, state, control options) // - TemperatureSensor (temperatureUnit=C|F) -// - Thermostat (temperatureUnit, setPoint, state, control options) -// - ContactSensor -// - MotionSensor -// - LeakSensor -// - LightSensor -// - OccupancySensor -// - SmokeSensor +// - ContactSensor (0=no contact, 1=contact - batteryRef, batteryThreshold option) +// - MotionSensor (0=no motion, 1=motion - batteryRef, batteryThreshold option) +// - LeakSensor (0=no leak, 1=leak - batteryRef, batteryThreshold option) +// - LightSensor (HomeSeer device value in Lux - batteryRef, batteryThreshold option) +// - HumiditySensor (HomeSeer device value in % - batteryRef, batteryThreshold option) +// - OccupancySensor (0=no occupancy, 1=occupancy - batteryRef, batteryThreshold option) +// - SmokeSensor (0=no smoke, 1=smoke - batteryRef, batteryThreshold option) +// - Battery (batteryThreshold option) // - Door @@ -111,19 +136,25 @@ function HomeSeerPlatform(log, config){ HomeSeerPlatform.prototype = { accessories: function(callback) { - this.log("Fetching HomeSeer devices."); + var that = this; + var foundAccessories = []; + if( this.config.events ) { + this.log("Creating HomeSeer events."); + for( var i=0; i