Merge branch 'nfarina/master' into z-way-server-2

Conflicts:
	config-sample.json
This commit is contained in:
S'pht'Kr
2015-09-12 14:26:27 +02:00
6 changed files with 428 additions and 39 deletions

View File

@@ -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.

View File

@@ -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);
}

View File

@@ -79,6 +79,16 @@
"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"]
}
],
@@ -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"
}

View File

@@ -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",

302
platforms/LIFx.js Normal file
View File

@@ -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;

View File

@@ -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
},