diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 70c2d6e..730eb9b 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; @@ -90,10 +91,10 @@ ZWayServerPlatform.prototype = { //Note: Order matters! var primaryDeviceClasses = [ "thermostat", - "sensorMultilevel.Temperature", "switchMultilevel", "switchBinary", - "sensorBinary.Door/Window" + "sensorBinary.Door/Window", + "sensorMultilevel.Temperature" ]; var that = this; @@ -110,11 +111,18 @@ 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}); + 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) { @@ -172,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++){ @@ -222,30 +235,87 @@ 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; + + 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 * 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; switch (typeKey) { - case "thermostat": - services.push(new Service.Thermostat(vdev.metrics.title)); - break; - case "sensorMultilevel.Temperature": - services.push(new Service.TemperatureSensor(vdev.metrics.title)); - break; - case "switchMultilevel": - services.push(new Service.Lightbulb(vdev.metrics.title)); - break; - case "battery.Battery": - services.push(new Service.BatteryService(vdev.metrics.title)); + case "thermostat": + 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, 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)); + break; + case "battery.Battery": + 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; } @@ -272,6 +342,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 @@ -309,8 +381,8 @@ ZWayServerAccessory.prototype = { return null; } , - configureCharacteristic: function(cx, vdev){ - var that = this; + configureCharacteristic: function(cx, vdev, service){ + var accessory = this; // Add this combination to the maps... if(!this.platform.cxVDevMap[vdev.id]) this.platform.cxVDevMap[vdev.id] = []; @@ -324,7 +396,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; @@ -381,6 +453,76 @@ ZWayServerAccessory.prototype = { return cx; } + 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); + 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.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){ + // 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){ + debug("Derived value " + accessory.rgb2hsv(vdev.metrics.color).s + " for saturation."); + 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.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){ + // 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; @@ -528,7 +670,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){ @@ -591,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; } @@ -617,6 +774,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"]){ @@ -655,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++)