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. 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); } diff --git a/config-sample.json b/config-sample.json index 3f8b136..afb2893 100644 --- a/config-sample.json +++ b/config-sample.json @@ -79,7 +79,17 @@ "password": "zwayuserpassword", "poll_interval": 2, "split_services": false - } + }, + { + "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": [ @@ -160,16 +170,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", @@ -180,7 +180,7 @@ { "accessory": "Hyperion", "name": "TV Backlight", - "description": "Control the Hyperion TV backlight server. https://github.com/tvdzwan/hyperion" + "description": "Control the Hyperion TV backlight server. https://github.com/tvdzwan/hyperion", "host": "localhost", "port": "19444" } diff --git a/package.json b/package.json index 546aa22..05db017 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,13 @@ "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", + "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 new file mode 100644 index 0000000..79988eb --- /dev/null +++ b/platforms/LIFx.js @@ -0,0 +1,302 @@ +'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 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"]; + + lifx_remote = new lifxRemoteObj(this.access_token); + + // use remote or lan api ? + use_lan = config["use_lan"] || false; + + if (use_lan != false) { + lifxLanObj = require('lifx'); + lifx_lan = lifxLanObj.init(); + } + + this.log = log; +} + +LIFxPlatform.prototype = { + accessories: function(callback) { + this.log("Fetching LIFx devices."); + + var that = this; + var foundAccessories = []; + + lifx_remote.listLights("all", function(body) { + var bulbs = JSON.parse(body); + + for(var i = 0; i < bulbs.length; i ++) { + var accessory = new LIFxBulbAccessory(that.log, bulbs[i]); + foundAccessories.push(accessory); + } + callback(foundAccessories) + }); + } +} + +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; +} + +LIFxBulbAccessory.prototype = { + getLan: function(type, callback){ + var that = this; + + 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) { + callback(new Error("Device not found"), false); + return; + } + + 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; + } + }); + }, + identify: function(callback) { + lifx_remote.breatheEffect("id:"+ this.deviceId, 'green', null, 1, 3, false, true, 0.5, function (body) { + callback(); + }); + }, + 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:" + (value / 100); + break; + case "hue": + color = "hue:" + value; + break; + case "saturation": + color = "saturation:" + (value / 100); + break; + } + + lifx_remote.setColor("id:"+ this.deviceId, color, 0, null, function (body) { + callback(); + }); + }, + setRemotePower: function(state, callback){ + var that = this; + + 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); + + switch(use_lan) { + case true: + case "true": + // gets and sets over the lan api + service + .getCharacteristic(Characteristic.On) + .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.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.getLan("hue", callback);}) + .on('set', function(value, callback) { that.setLanColor("hue", 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('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('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); + + service = new Service.AccessoryInformation(); + + service + .setCharacteristic(Characteristic.Manufacturer, "LIFX") + .setCharacteristic(Characteristic.Model, this.model) + .setCharacteristic(Characteristic.SerialNumber, this.serial); + + services.push(service); + + return services; + } +} + +module.exports.accessory = LIFxBulbAccessory; +module.exports.platform = LIFxPlatform; diff --git a/accessories/MiLight.js b/platforms/MiLight.js similarity index 68% rename from accessories/MiLight.js rename to platforms/MiLight.js index c5ce67c..3869e74 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 - *delay (optional): Delay between commands sent over UDP. Default 30ms + *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. 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. 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,63 @@ 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 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,14 +129,14 @@ function MiLight(log, config) { }); } -MiLight.prototype = { +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(); @@ -93,11 +145,11 @@ MiLight.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 @@ -105,7 +157,7 @@ MiLight.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)); @@ -132,7 +184,7 @@ MiLight.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); @@ -150,16 +202,16 @@ MiLight.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(); }, identify: function(callback) { - this.log("Identify requested!"); + this.log("["+this.name+"] Identify requested!"); callback(); // success },