From abe88b75020c73551747228355ccdcf03701f6d0 Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Tue, 8 Sep 2015 10:41:03 -0700 Subject: [PATCH 01/11] [MiLight] Converted accessory to platform. Not fully tested yet --- {accessories => platforms}/MiLight.js | 81 ++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 14 deletions(-) rename {accessories => platforms}/MiLight.js (73%) diff --git a/accessories/MiLight.js b/platforms/MiLight.js similarity index 73% rename from accessories/MiLight.js rename to platforms/MiLight.js index c5ce67c..d38cb3f 100644 --- a/accessories/MiLight.js +++ b/platforms/MiLight.js @@ -1,6 +1,6 @@ /* -MiLight accessory shim for Homebridge +MiLight platform shim for Homebridge Written by Sam Edwards (https://samedwards.ca/) Uses the node-milight-promise library (https://github.com/mwittig/node-milight-promise) which features some code from @@ -8,28 +8,28 @@ applamp.nl (http://www.applamp.nl/service/applamp-api/) and uses other details f Configure in config.json as follows: -"accessories": [ +"platforms": [ { - "accessory":"MiLight", - "name": "Lamp", + "platform":"MiLight", + "name":"MiLight", "ip_address": "255.255.255.255", "port": 8899, - "zone": 1, "type": "rgbw", "delay": 30, - "repeat": 3 + "repeat": 3, + "zones":["Kitchen Lamp","Bedroom Lamp","Living Room Lamp","Hallway Lamp"] } ] Where the parameters are: - *accessory (required): This must be "MiLight", and refers to the name of the accessory as exported from this file - *name (required): The name for this light/zone, as passed on to Homebridge and HomeKit + *platform (required): This must be "MiLight", and refers to the name of the accessory as exported from this file + *name (optional): The display name used for logging output by Homebridge. Best to set to "MiLight" *ip_address (optional): The IP address of the WiFi Bridge. Default to the broadcast address of 255.255.255.255 if not specified *port (optional): Port of the WiFi bridge. Defaults to 8899 if not specified - *zone (required): The zone to target with this accessory. "0" for all zones on the bridge, otherwise 1-4 for a specific zone - *type (required): One of either "rgbw", "rgb", or "white", depending on the type of bulb being controlled + *type (optional): One of either "rgbw", "rgb", or "white", depending on the type of bulb being controlled. This applies to all zones. Defaults to rgbw. *delay (optional): Delay between commands sent over UDP. Default 30ms *repeat (optional): Number of times to repeat the UDP command for better reliability. Default 3 + *zones (required): An array of the names of the zones, in order, 1-4. Use null if a zone is skipped. RGB lamps can only have a single zone. Tips and Tricks: *Setting the brightness of an rgbw or a white bulb will set it to "night mode", which is dimmer than the lowest brightness setting @@ -43,7 +43,6 @@ Troubleshooting: The node-milight-promise library provides additional debugging output when the MILIGHT_DEBUG environmental variable is set TODO: - *Probably convert this module to a platform that can configure an entire bridge at once, just passing a name for each zone *Possibly build in some sort of state logging and persistance so that we can answswer HomeKit status queries to the best of our ability */ @@ -54,10 +53,64 @@ var Milight = require('node-milight-promise').MilightController; var commands = require('node-milight-promise').commands; module.exports = { - accessory: MiLight + accessory: MiLightAccessory, + platform: MiLightPlatform } -function MiLight(log, config) { +function MiLightPlatform(log, config) { + this.log = log; + + this.config = config; +} + +MiLightPlatform.prototype = { + accessories: function(callback) { + var that = this; + var zones = []; + + // Various error checking + if (this.config.zones) { + var zoneLength = this.config.zones.length; + } else { + this.log("ERROR: Could not read zones from configuration."); + return; + } + + if (!this.config["type"]) { + this.log("INFO: Type not specified, defaulting to rgbw"); + this.config["type"] = "rgbw"; + } + + if (zoneLength == 0) { + this.log("ERROR: No zones found in configuration."); + return; + } else if (this.config["type"] == "rgb" && zoneLength > 1) { + this.log("WARNING: RGB lamps only have a single zone. Only the first defined zone will be used."); + zoneLength = 1; + } else if (zoneLength > 4) { + this.log("WARNING: Only a maximum of 4 zones are supported per bridge. Only recognizing the first 4 zones."); + zoneLength = 4; + } + + // Create lamp accessories for all of the defined zones + for (var i=0; i < zoneLength; i++) { + if (!!this.config.zones[i]) { + this.config["name"] = this.config.zones[i]; + this.config["zone"] = i+1; + lamp = new MiLightAccessory(this.log, this.config); + zones.push(lamp); + } + } + if (zones.length > 0) { + callback(zones); + } else { + this.log("ERROR: Unable to find any valid zones"); + return; + } + } +} + +function MiLightAccessory(log, config) { this.log = log; // config info @@ -77,7 +130,7 @@ function MiLight(log, config) { }); } -MiLight.prototype = { +MiLightAccessory.prototype = { setPowerState: function(powerOn, callback) { if (powerOn) { From 5cccd3f916d93b0dd576a3b31d6f72d4bfb63b89 Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Tue, 8 Sep 2015 11:01:30 -0700 Subject: [PATCH 02/11] [MiLight] Modify logging to show the zone name when used as a platform accessory --- platforms/MiLight.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/platforms/MiLight.js b/platforms/MiLight.js index d38cb3f..7cbf1c9 100644 --- a/platforms/MiLight.js +++ b/platforms/MiLight.js @@ -134,10 +134,10 @@ MiLightAccessory.prototype = { setPowerState: function(powerOn, callback) { if (powerOn) { - this.log("Setting power state to on"); + this.log("["+this.name+"] Setting power state to on"); this.light.sendCommands(commands[this.type].on(this.zone)); } else { - this.log("Setting power state to off"); + this.log("["+this.name+"] Setting power state to off"); this.light.sendCommands(commands[this.type].off(this.zone)); } callback(); @@ -146,11 +146,11 @@ MiLightAccessory.prototype = { setBrightness: function(level, callback) { if (level == 0) { // If brightness is set to 0, turn off the lamp - this.log("Setting brightness to 0 (off)"); + this.log("["+this.name+"] Setting brightness to 0 (off)"); this.light.sendCommands(commands[this.type].off(this.zone)); } else if (level <= 2 && (this.type == "rgbw" || this.type == "white")) { // If setting brightness to 2 or lower, instead set night mode for lamps that support it - this.log("Setting night mode", level); + this.log("["+this.name+"] Setting night mode", level); this.light.sendCommands(commands[this.type].off(this.zone)); // Ensure we're pausing for 100ms between these commands as per the spec @@ -158,7 +158,7 @@ MiLightAccessory.prototype = { this.light.sendCommands(commands[this.type].nightMode(this.zone)); } else { - this.log("Setting brightness to %s", level); + this.log("["+this.name+"] Setting brightness to %s", level); // Send on command to ensure we're addressing the right bulb this.light.sendCommands(commands[this.type].on(this.zone)); @@ -185,7 +185,7 @@ MiLightAccessory.prototype = { }, setHue: function(value, callback) { - this.log("Setting hue to %s", value); + this.log("["+this.name+"] Setting hue to %s", value); var hue = Array(value, 0, 0); @@ -212,7 +212,7 @@ MiLightAccessory.prototype = { }, identify: function(callback) { - this.log("Identify requested!"); + this.log("["+this.name+"] Identify requested!"); callback(); // success }, From 18333242ff3866fb66750123caf001bfe9f74760 Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Tue, 8 Sep 2015 11:20:36 -0700 Subject: [PATCH 03/11] [MiLight] Add missing callback from hue function --- platforms/MiLight.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/platforms/MiLight.js b/platforms/MiLight.js index 7cbf1c9..4382dea 100644 --- a/platforms/MiLight.js +++ b/platforms/MiLight.js @@ -65,7 +65,6 @@ function MiLightPlatform(log, config) { MiLightPlatform.prototype = { accessories: function(callback) { - var that = this; var zones = []; // Various error checking @@ -208,7 +207,7 @@ MiLightAccessory.prototype = { this.light.sendCommands(commands.white.cooler()); } } - + callback(); }, identify: function(callback) { From 7dc168e9dc51178df4e84793488c3ad06192d76f Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Tue, 8 Sep 2015 11:33:17 -0700 Subject: [PATCH 04/11] [MiLight] Update config-sample.json to replace MiLight accessory with MiLight platform --- config-sample.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/config-sample.json b/config-sample.json index 4245bd5..d684c9d 100644 --- a/config-sample.json +++ b/config-sample.json @@ -71,7 +71,17 @@ "platform": "YamahaAVR", "play_volume": -35, "setMainInputTo": "AirPlay" - } + }, + { + "platform": "MiLight", + "name": "MiLight", + "ip_address": "255.255.255.255", + "port": 8899, + "type": "rgbw", + "delay": 30, + "repeat": 3, + "zones":["Kitchen Lamp","Bedroom Lamp","Living Room Lamp","Hallway Lamp"] + } ], "accessories": [ @@ -152,16 +162,6 @@ "port" : 4999, // Port the SER2SOCK process is running on "pin": "1234" // PIN used for arming / disarming }, - { - "accessory":"MiLight", - "name": "Lamp", - "ip_address": "255.255.255.255", - "port": 8899, - "zone": 1, - "type": "rgbw", - "delay": 35, - "repeat": 3 - }, { "accessory": "Tesla", "name": "Tesla", From d6e31b4aa71d388e6f2905893acfa4273c423b4a Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Wed, 9 Sep 2015 08:13:22 -0700 Subject: [PATCH 05/11] [MiLight] Swap cooler/warmer direction for white bulbs, and add note about delay --- platforms/MiLight.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/platforms/MiLight.js b/platforms/MiLight.js index 4382dea..3869e74 100644 --- a/platforms/MiLight.js +++ b/platforms/MiLight.js @@ -27,7 +27,7 @@ Where the parameters are: *ip_address (optional): The IP address of the WiFi Bridge. Default to the broadcast address of 255.255.255.255 if not specified *port (optional): Port of the WiFi bridge. Defaults to 8899 if not specified *type (optional): One of either "rgbw", "rgb", or "white", depending on the type of bulb being controlled. This applies to all zones. Defaults to rgbw. - *delay (optional): Delay between commands sent over UDP. Default 30ms + *delay (optional): Delay between commands sent over UDP. Default 30ms. May cause delays when sending a lot of commands. Try decreasing to improve. *repeat (optional): Number of times to repeat the UDP command for better reliability. Default 3 *zones (required): An array of the names of the zones, in order, 1-4. Use null if a zone is skipped. RGB lamps can only have a single zone. @@ -202,9 +202,9 @@ MiLightAccessory.prototype = { } else if (this.type == "white") { // Again, white lamps don't support setting an absolue colour temp, so trying to do warmer/cooler step at a time based on colour if (value >= 180) { - this.light.sendCommands(commands.white.warmer()); - } else { this.light.sendCommands(commands.white.cooler()); + } else { + this.light.sendCommands(commands.white.warmer()); } } callback(); From c73e22984d06d2668e96667eb096de91ec73c34d Mon Sep 17 00:00:00 2001 From: Nelson Melo Date: Wed, 9 Sep 2015 15:26:21 -0400 Subject: [PATCH 06/11] Implemented LIFx bulb platform --- package.json | 3 +- platforms/LIFx.js | 174 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 platforms/LIFx.js diff --git a/package.json b/package.json index 9fa4cb5..8427666 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,12 @@ "ad2usb": "git+https://github.com/alistairg/node-ad2usb.git#local", "carwingsjs": "0.0.x", "color": "0.10.x", - "elkington": "kevinohara80/elkington", "eibd": "^0.3.1", + "elkington": "kevinohara80/elkington", "hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#6bf0f9eaaa2d87db8d1768114c61f4acbb095c41", "harmonyhubjs-client": "^1.1.4", "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", + "lifx-api": "^1.0.1", "mdns": "^2.2.4", "node-hue-api": "^1.0.5", "node-icontrol": "^0.1.4", diff --git a/platforms/LIFx.js b/platforms/LIFx.js new file mode 100644 index 0000000..55d66f2 --- /dev/null +++ b/platforms/LIFx.js @@ -0,0 +1,174 @@ +var types = require("HAP-NodeJS/accessories/types.js"); +var lifxObj = require('lifx-api'); +var lifx; + +function LIFxPlatform(log, config){ + + // auth info + this.access_token = config["access_token"]; + + lifx = new lifxObj(this.access_token); + + this.log = log; +} + +LIFxPlatform.prototype = { + accessories: function(callback) { + this.log("Fetching LIFx devices."); + + var that = this; + var foundAccessories = []; + + lifx.listLights("all", function(body) { + var bulbs = JSON.parse(body); + + for(var i = 0; i < bulbs.length; i ++) { + var bulb = bulbs[i]; + var accessory = new LIFxBulbAccessory( + that.log, + bulb.label, + bulb.uuid, + bulb.model, + bulb.id + ); + foundAccessories.push(accessory); + } + callback(foundAccessories) + }); + } +} + +function LIFxBulbAccessory(log, label, serial, model, deviceId) { + // device info + this.name = label; + this.model = model; + this.deviceId = deviceId; + this.serial = serial; + this.log = log; +} + +LIFxBulbAccessory.prototype = { + getPower: function(callback){ + var that = this; + + lifx.listLights("all", function(body) { + var bulbs = JSON.parse(body); + + for(var i = 0; i < bulbs.length; i ++) { + var bulb = bulbs[i]; + + if(bulb.deviceId == that.deviceId) { + return bulb.state; + } + } + return "off"; + }); + + nest.fetchStatus(function (data) { + var device = data.shared[that.deviceId]; + that.log("Target temperature for " + this.name + " is: " + device.target_temperature); + callback(device.target_temperature); + }); + }, + setPower: function(state){ + var that = this; + this.log("Setting power state for heating cooling for " + this.name + " to: " + targetTemperatureType); + lifx.setPower("all", state, 1, function (body) { + this.log("body"); + }); + }, + + getServices: function() { + var that = this; + var chars= [{ + sType: types.ACCESSORY_INFORMATION_STYPE, + characteristics: [{ + cType: types.NAME_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.name, + supportEvents: false, + supportBonjour: false, + manfDescription: "Name of the accessory", + designedMaxLength: 255 + },{ + cType: types.MANUFACTURER_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "LIFx", + supportEvents: false, + supportBonjour: false, + manfDescription: "Manufacturer", + designedMaxLength: 255 + },{ + cType: types.MODEL_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.model, + supportEvents: false, + supportBonjour: false, + manfDescription: "Model", + designedMaxLength: 255 + },{ + cType: types.SERIAL_NUMBER_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.serial, + supportEvents: false, + supportBonjour: false, + manfDescription: "SN", + designedMaxLength: 255 + },{ + cType: types.IDENTIFY_CTYPE, + onUpdate: null, + perms: ["pw"], + format: "bool", + initialValue: true, + supportEvents: false, + supportBonjour: false, + manfDescription: "Identify Accessory", + designedMaxLength: 1 + }] + }, { + sType: types.LIGHTBULB_STYPE, + characteristics: [{ + cType: types.NAME_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.name, + supportEvents: false, + supportBonjour: false, + manfDescription: "Name of LIFx bulb", + designedMaxLength: 255 + }, { + cType: types.POWER_STATE_CTYPE, + onUpdate: function (value) { + that.setPower(value); + }, + onRead: function (callback) { + that.getPower(function (state) { + callback(state); + }); + }, + perms: ["pw", "pr", "ev"], + format: "int", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Power state", + designedMinValue: 0, + designedMaxValue: 1, + designedMinStep: 1 + }] + }]; + return chars; + } +} + +module.exports.accessory = LIFxBulbAccessory; +module.exports.platform = LIFxPlatform; From 4b1637152219a27aadbad5a3cbed01918ab7206d Mon Sep 17 00:00:00 2001 From: Nelson Melo Date: Wed, 9 Sep 2015 15:31:11 -0400 Subject: [PATCH 07/11] Added LIFx on Readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4fae78b..f11dd75 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Since Siri supports devices added through HomeKit, this means that with Homebrid * _Siri, turn off the Speakers._ ([Sonos](http://www.sonos.com)) * _Siri, turn on the Dehumidifier._ ([WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/)) * _Siri, turn on Away Mode._ ([Xfinity Home](http://www.comcast.com/home-security.html)) - * _Siri, turn on the living room lights._ ([Wink](http://www.wink.com), [SmartThings](http://www.smartthings.com), [X10](http://github.com/edc1591/rest-mochad), [Philips Hue](http://meethue.com), [LimitlessLED/MiLight/Easybulb](http://www.limitlessled.com/)) + * _Siri, turn on the living room lights._ ([Wink](http://www.wink.com), [SmartThings](http://www.smartthings.com), [X10](http://github.com/edc1591/rest-mochad), [Philips Hue](http://meethue.com), [LimitlessLED/MiLight/Easybulb](http://www.limitlessled.com/), [LIFx](http://www.lifx.com/)) * _Siri, set the movie scene._ ([Logitech Harmony](http://myharmony.com/)) If you would like to support any other devices, please write a shim and create a pull request and I'd be happy to add it to this official list. From 09f5e2bed0274e885b9c0198186f4492d23345fa Mon Sep 17 00:00:00 2001 From: David Parry Date: Thu, 10 Sep 2015 12:50:55 +1000 Subject: [PATCH 08/11] add support for WeMo motion sensor --- accessories/WeMo.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/accessories/WeMo.js b/accessories/WeMo.js index 26ef822..df16f56 100644 --- a/accessories/WeMo.js +++ b/accessories/WeMo.js @@ -30,6 +30,30 @@ WeMoAccessory.prototype.search = function() { }.bind(this)); } +WeMoAccessory.prototype.getMotion = function(callback) { + + if (!this.device) { + this.log("No '%s' device found (yet?)", this.wemoName); + callback(new Error("Device not found"), false); + return; + } + + this.log("Getting motion state on the '%s'...", this.wemoName); + + this.device.getBinaryState(function(err, result) { + if (!err) { + var binaryState = parseInt(result); + var powerOn = binaryState > 0; + this.log("Motion state for the '%s' is %s", this.wemoName, binaryState); + callback(null, powerOn); + } + else { + this.log("Error getting motion state on the '%s': %s", this.wemoName, err.message); + callback(err); + } + }.bind(this)); +} + WeMoAccessory.prototype.getPowerOn = function(callback) { if (!this.device) { @@ -122,6 +146,15 @@ WeMoAccessory.prototype.getServices = function() { return [garageDoorService]; } + else if (this.service == "MotionSensor") { + var motionSensorService = new Service.MotionSensor(this.name); + + motionSensorService + .getCharacteristic(Characteristic.MotionDetected) + .on('get', this.getMotion.bind(this)); + + return [motionSensorService]; + } else { throw new Error("Unknown service type '%s'", this.service); } From 7e6df6191e923d44b8b829a89524ba20520d36e5 Mon Sep 17 00:00:00 2001 From: David Parry Date: Thu, 10 Sep 2015 22:19:41 +1000 Subject: [PATCH 09/11] [LiFX] fix/enhance the LiFX platform --- platforms/LIFx.js | 218 +++++++++++++++++++++------------------------- 1 file changed, 98 insertions(+), 120 deletions(-) diff --git a/platforms/LIFx.js b/platforms/LIFx.js index 55d66f2..8f4ef0d 100644 --- a/platforms/LIFx.js +++ b/platforms/LIFx.js @@ -1,4 +1,5 @@ -var types = require("HAP-NodeJS/accessories/types.js"); +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; var lifxObj = require('lifx-api'); var lifx; @@ -23,14 +24,7 @@ LIFxPlatform.prototype = { var bulbs = JSON.parse(body); for(var i = 0; i < bulbs.length; i ++) { - var bulb = bulbs[i]; - var accessory = new LIFxBulbAccessory( - that.log, - bulb.label, - bulb.uuid, - bulb.model, - bulb.id - ); + var accessory = new LIFxBulbAccessory(that.log, bulbs[i]); foundAccessories.push(accessory); } callback(foundAccessories) @@ -38,135 +32,119 @@ LIFxPlatform.prototype = { } } -function LIFxBulbAccessory(log, label, serial, model, deviceId) { +function LIFxBulbAccessory(log, bulb) { // device info - this.name = label; - this.model = model; - this.deviceId = deviceId; - this.serial = serial; + this.name = bulb.label; + this.model = bulb.product_name; + this.deviceId = bulb.id; + this.serial = bulb.uuid; + this.capabilities = bulb.capabilities; this.log = log; } LIFxBulbAccessory.prototype = { - getPower: function(callback){ + get: function(type, callback){ var that = this; - lifx.listLights("all", function(body) { - var bulbs = JSON.parse(body); + lifx.listLights("id:"+ that.deviceId, function(body) { + var bulb = JSON.parse(body); - for(var i = 0; i < bulbs.length; i ++) { - var bulb = bulbs[i]; - - if(bulb.deviceId == that.deviceId) { - return bulb.state; - } + if (bulb.connected != true) { + callback(new Error("Device not found"), false); + return; } - return "off"; - }); - nest.fetchStatus(function (data) { - var device = data.shared[that.deviceId]; - that.log("Target temperature for " + this.name + " is: " + device.target_temperature); - callback(device.target_temperature); + switch(type) { + case "power": + callback(null, bulb.power == "on" ? 1 : 0); + break; + case "brightness": + callback(null, Math.round(bulb.brightness * 100)); + break; + case "hue": + callback(null, bulb.color.hue); + break; + case "saturation": + callback(null, Math.round(bulb.color.saturation * 100)); + break; + } }); }, - setPower: function(state){ + identify: function(callback) { var that = this; - this.log("Setting power state for heating cooling for " + this.name + " to: " + targetTemperatureType); - lifx.setPower("all", state, 1, function (body) { - this.log("body"); + + lifx.breatheEffect("id:"+ that.deviceId, 'green', null, 1, 3, false, true, 0.5, function (body) { + callback(); + }); + }, + setColor: function(type, state, callback){ + var that = this; + var color; + + switch(type) { + case "brightness": + color = "brightness:" + (state / 100); + break; + case "hue": + color = "hue:" + state; + break; + case "saturation": + color = "saturation:" + (state / 100); + break; + } + + lifx.setColor("id:"+ that.deviceId, color, 0, null, function (body) { + callback(); + }); + }, + setPower: function(state, callback){ + var that = this; + + lifx.setPower("id:"+ that.deviceId, (state == 1 ? "on" : "off"), 0, function (body) { + callback(); }); }, getServices: function() { var that = this; - var chars= [{ - sType: types.ACCESSORY_INFORMATION_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.name, - supportEvents: false, - supportBonjour: false, - manfDescription: "Name of the accessory", - designedMaxLength: 255 - },{ - cType: types.MANUFACTURER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "LIFx", - supportEvents: false, - supportBonjour: false, - manfDescription: "Manufacturer", - designedMaxLength: 255 - },{ - cType: types.MODEL_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.model, - supportEvents: false, - supportBonjour: false, - manfDescription: "Model", - designedMaxLength: 255 - },{ - cType: types.SERIAL_NUMBER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.serial, - supportEvents: false, - supportBonjour: false, - manfDescription: "SN", - designedMaxLength: 255 - },{ - cType: types.IDENTIFY_CTYPE, - onUpdate: null, - perms: ["pw"], - format: "bool", - initialValue: true, - supportEvents: false, - supportBonjour: false, - manfDescription: "Identify Accessory", - designedMaxLength: 1 - }] - }, { - sType: types.LIGHTBULB_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.name, - supportEvents: false, - supportBonjour: false, - manfDescription: "Name of LIFx bulb", - designedMaxLength: 255 - }, { - cType: types.POWER_STATE_CTYPE, - onUpdate: function (value) { - that.setPower(value); - }, - onRead: function (callback) { - that.getPower(function (state) { - callback(state); - }); - }, - perms: ["pw", "pr", "ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Power state", - designedMinValue: 0, - designedMaxValue: 1, - designedMinStep: 1 - }] - }]; - return chars; + var services = [] + var service = new Service.Lightbulb(this.name); + + service + .getCharacteristic(Characteristic.On) + .on('identify', function(callback) {}) + .on('get', function(callback) { that.get("power", callback);}) + .on('set', function(value, callback) {that.setPower(value, callback);}); + + service + .addCharacteristic(Characteristic.Brightness) + .on('get', function(callback) { that.get("brightness", callback);}) + .on('set', function(value, callback) { that.setColor("brightness", value, callback);}); + + if (this.capabilities.has_color == true) { + service + .addCharacteristic(Characteristic.Hue) + .on('get', function(callback) { that.get("hue", callback);}) + .on('set', function(value, callback) { that.setColor("hue", value, callback);}); + + service + .addCharacteristic(Characteristic.Saturation) + .on('get', function(callback) { that.get("saturation", callback);}) + .on('set', function(value, callback) { that.setColor("saturation", value, callback);}); + } + + services.push(service); + + service = new Service.AccessoryInformation(); + + service + .setCharacteristic(Characteristic.Manufacturer, "LiFX") + .setCharacteristic(Characteristic.Model, this.model) + .setCharacteristic(Characteristic.SerialNumber, this.serial); + + services.push(service); + + return services; } } From 17fc8f1829ee884c097cda3127122858220c509a Mon Sep 17 00:00:00 2001 From: David Parry Date: Fri, 11 Sep 2015 13:04:09 +1000 Subject: [PATCH 10/11] implement the LiFX LAN API as a configurable option for higher lantency connections --- package.json | 1 + platforms/LIFx.js | 245 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 200 insertions(+), 46 deletions(-) diff --git a/package.json b/package.json index 8427666..3732b9d 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "harmonyhubjs-client": "^1.1.4", "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", "lifx-api": "^1.0.1", + "lifx": "https://github.com/magicmonkey/lifxjs.git", "mdns": "^2.2.4", "node-hue-api": "^1.0.5", "node-icontrol": "^0.1.4", diff --git a/platforms/LIFx.js b/platforms/LIFx.js index 8f4ef0d..62167f6 100644 --- a/platforms/LIFx.js +++ b/platforms/LIFx.js @@ -1,16 +1,45 @@ +'use strict'; + +// LiFX Platform Shim for HomeBridge +// +// Remember to add platform to config.json. Example: +// "platforms": [ +// { +// "platform": "LIFx", // required +// "name": "LIFx", // required +// "access_token": "access token", // required +// "use_lan": "true" // optional set to "true" (gets and sets over the lan) or "get" (gets only over the lan) +// } +// ], +// +// When you attempt to add a device, it will ask for a "PIN code". +// The default code for all HomeBridge accessories is 031-45-154. +// + var Service = require("HAP-NodeJS").Service; var Characteristic = require("HAP-NodeJS").Characteristic; -var lifxObj = require('lifx-api'); -var lifx; +var lifxRemoteObj = require('lifx-api'); +var lifx_remote; + +var lifxLanObj; +var lifx_lan; +var use_lan; function LIFxPlatform(log, config){ + // auth info + this.access_token = config["access_token"]; - // auth info - this.access_token = config["access_token"]; + lifx_remote = new lifxRemoteObj(this.access_token); - lifx = new lifxObj(this.access_token); + // use remote or lan api ? + use_lan = config["use_lan"] || false; - this.log = log; + if (use_lan != false) { + lifxLanObj = require('lifx'); + lifx_lan = lifxLanObj.init(); + } + + this.log = log; } LIFxPlatform.prototype = { @@ -20,7 +49,7 @@ LIFxPlatform.prototype = { var that = this; var foundAccessories = []; - lifx.listLights("all", function(body) { + lifx_remote.listLights("all", function(body) { var bulbs = JSON.parse(body); for(var i = 0; i < bulbs.length; i ++) { @@ -33,20 +62,54 @@ LIFxPlatform.prototype = { } function LIFxBulbAccessory(log, bulb) { - // device info - this.name = bulb.label; - this.model = bulb.product_name; - this.deviceId = bulb.id; - this.serial = bulb.uuid; - this.capabilities = bulb.capabilities; - this.log = log; + // device info + this.name = bulb.label; + this.model = bulb.product_name; + this.deviceId = bulb.id; + this.serial = bulb.uuid; + this.capabilities = bulb.capabilities; + this.log = log; } LIFxBulbAccessory.prototype = { - get: function(type, callback){ + getLan: function(type, callback){ var that = this; - lifx.listLights("id:"+ that.deviceId, function(body) { + if (!lifx_lan.bulbs[this.deviceId]) { + callback(new Error("Device not found"), false); + return; + } + + lifx_lan.requestStatus(); + lifx_lan.on('bulbstate', function(bulb) { + if (callback == null) { + return; + } + + if (bulb.addr.toString('hex') == that.deviceId) { + switch(type) { + case "power": + callback(null, bulb.state.power > 0); + break; + case "brightness": + callback(null, Math.round(bulb.state.brightness * 100 / 65535)); + break; + case "hue": + callback(null, Math.round(bulb.state.hue * 360 / 65535)); + break; + case "saturation": + callback(null, Math.round(bulb.state.saturation * 100 / 65535)); + break; + } + + callback = null + } + }); + }, + getRemote: function(type, callback){ + var that = this; + + lifx_remote.listLights("id:"+ that.deviceId, function(body) { var bulb = JSON.parse(body); if (bulb.connected != true) { @@ -71,66 +134,156 @@ LIFxBulbAccessory.prototype = { }); }, identify: function(callback) { - var that = this; - - lifx.breatheEffect("id:"+ that.deviceId, 'green', null, 1, 3, false, true, 0.5, function (body) { + lifx_remote.breatheEffect("id:"+ this.deviceId, 'green', null, 1, 3, false, true, 0.5, function (body) { callback(); }); }, - setColor: function(type, state, callback){ - var that = this; + setLanColor: function(type, value, callback){ + var bulb = lifx_lan.bulbs[this.deviceId]; + + if (!bulb) { + callback(new Error("Device not found"), false); + return; + } + + var state = { + hue: bulb.state.hue, + saturation: bulb.state.saturation, + brightness: bulb.state.brightness, + kelvin: bulb.state.kelvin + }; + + var scale = type == "hue" ? 360 : 100; + + state[type] = Math.round(value * 65535 / scale) & 0xffff; + lifx_lan.lightsColour(state.hue, state.saturation, state.brightness, state.kelvin, 0, bulb); + + callback(null); + }, + setLanPower: function(state, callback){ + var bulb = lifx_lan.bulbs[this.deviceId]; + + if (!bulb) { + callback(new Error("Device not found"), false); + return; + } + + if (state) { + lifx_lan.lightsOn(bulb); + } + else { + lifx_lan.lightsOff(bulb); + } + + callback(null); + }, + setRemoteColor: function(type, value, callback){ var color; switch(type) { case "brightness": - color = "brightness:" + (state / 100); + color = "brightness:" + (value / 100); break; case "hue": - color = "hue:" + state; + color = "hue:" + value; break; case "saturation": - color = "saturation:" + (state / 100); + color = "saturation:" + (value / 100); break; } - lifx.setColor("id:"+ that.deviceId, color, 0, null, function (body) { + lifx_remote.setColor("id:"+ this.deviceId, color, 0, null, function (body) { callback(); }); }, - setPower: function(state, callback){ + setRemotePower: function(state, callback){ var that = this; - lifx.setPower("id:"+ that.deviceId, (state == 1 ? "on" : "off"), 0, function (body) { + lifx_remote.setPower("id:"+ that.deviceId, (state == 1 ? "on" : "off"), 0, function (body) { callback(); }); }, - getServices: function() { var that = this; var services = [] var service = new Service.Lightbulb(this.name); - service - .getCharacteristic(Characteristic.On) - .on('identify', function(callback) {}) - .on('get', function(callback) { that.get("power", callback);}) - .on('set', function(value, callback) {that.setPower(value, callback);}); + switch(use_lan) { + case true: + case "true": + // gets and sets over the lan api + service + .getCharacteristic(Characteristic.On) + .on('identify', function(callback) {}) + .on('get', function(callback) { that.getLan("power", callback);}) + .on('set', function(value, callback) {that.setLanPower(value, callback);}); - service - .addCharacteristic(Characteristic.Brightness) - .on('get', function(callback) { that.get("brightness", callback);}) - .on('set', function(value, callback) { that.setColor("brightness", value, callback);}); + service + .addCharacteristic(Characteristic.Brightness) + .on('get', function(callback) { that.getLan("brightness", callback);}) + .on('set', function(value, callback) { that.setLanColor("brightness", value, callback);}); - if (this.capabilities.has_color == true) { - service - .addCharacteristic(Characteristic.Hue) - .on('get', function(callback) { that.get("hue", callback);}) - .on('set', function(value, callback) { that.setColor("hue", value, callback);}); + if (this.capabilities.has_color == true) { + service + .addCharacteristic(Characteristic.Hue) + .on('get', function(callback) { that.getLan("hue", callback);}) + .on('set', function(value, callback) { that.setLanColor("hue", value, callback);}); - service - .addCharacteristic(Characteristic.Saturation) - .on('get', function(callback) { that.get("saturation", callback);}) - .on('set', function(value, callback) { that.setColor("saturation", value, callback);}); + service + .addCharacteristic(Characteristic.Saturation) + .on('get', function(callback) { that.getLan("saturation", callback);}) + .on('set', function(value, callback) { that.setLanColor("saturation", value, callback);}); + } + break; + case "get": + // gets over the lan api, sets over the remote api + service + .getCharacteristic(Characteristic.On) + .on('identify', function(callback) {}) + .on('get', function(callback) { that.getLan("power", callback);}) + .on('set', function(value, callback) {that.setRemotePower(value, callback);}); + + service + .addCharacteristic(Characteristic.Brightness) + .on('get', function(callback) { that.getLan("brightness", callback);}) + .on('set', function(value, callback) { that.setRemoteColor("brightness", value, callback);}); + + if (this.capabilities.has_color == true) { + service + .addCharacteristic(Characteristic.Hue) + .on('get', function(callback) { that.getLan("hue", callback);}) + .on('set', function(value, callback) { that.setRemoteColor("hue", value, callback);}); + + service + .addCharacteristic(Characteristic.Saturation) + .on('get', function(callback) { that.getLan("saturation", callback);}) + .on('set', function(value, callback) { that.setRemoteColor("saturation", value, callback);}); + } + break; + default: + // gets and sets over the remote api + service + .getCharacteristic(Characteristic.On) + .on('identify', function(callback) {}) + .on('get', function(callback) { that.getRemote("power", callback);}) + .on('set', function(value, callback) {that.setRemotePower(value, callback);}); + + service + .addCharacteristic(Characteristic.Brightness) + .on('get', function(callback) { that.getRemote("brightness", callback);}) + .on('set', function(value, callback) { that.setRemoteColor("brightness", value, callback);}); + + if (this.capabilities.has_color == true) { + service + .addCharacteristic(Characteristic.Hue) + .on('get', function(callback) { that.getRemote("hue", callback);}) + .on('set', function(value, callback) { that.setRemoteColor("hue", value, callback);}); + + service + .addCharacteristic(Characteristic.Saturation) + .on('get', function(callback) { that.getRemote("saturation", callback);}) + .on('set', function(value, callback) { that.setRemoteColor("saturation", value, callback);}); + } } services.push(service); From 7d5a992c98173d8486df8ae4ee68a15db346119f Mon Sep 17 00:00:00 2001 From: David Parry Date: Fri, 11 Sep 2015 17:30:25 +1000 Subject: [PATCH 11/11] minor cleanup --- platforms/LIFx.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/platforms/LIFx.js b/platforms/LIFx.js index 62167f6..79988eb 100644 --- a/platforms/LIFx.js +++ b/platforms/LIFx.js @@ -214,7 +214,6 @@ LIFxBulbAccessory.prototype = { // gets and sets over the lan api service .getCharacteristic(Characteristic.On) - .on('identify', function(callback) {}) .on('get', function(callback) { that.getLan("power", callback);}) .on('set', function(value, callback) {that.setLanPower(value, callback);}); @@ -239,7 +238,6 @@ LIFxBulbAccessory.prototype = { // gets over the lan api, sets over the remote api service .getCharacteristic(Characteristic.On) - .on('identify', function(callback) {}) .on('get', function(callback) { that.getLan("power", callback);}) .on('set', function(value, callback) {that.setRemotePower(value, callback);}); @@ -264,7 +262,6 @@ LIFxBulbAccessory.prototype = { // gets and sets over the remote api service .getCharacteristic(Characteristic.On) - .on('identify', function(callback) {}) .on('get', function(callback) { that.getRemote("power", callback);}) .on('set', function(value, callback) {that.setRemotePower(value, callback);}); @@ -291,7 +288,7 @@ LIFxBulbAccessory.prototype = { service = new Service.AccessoryInformation(); service - .setCharacteristic(Characteristic.Manufacturer, "LiFX") + .setCharacteristic(Characteristic.Manufacturer, "LIFX") .setCharacteristic(Characteristic.Model, this.model) .setCharacteristic(Characteristic.SerialNumber, this.serial);