mirror of
https://github.com/mtan93/homebridge.git
synced 2026-05-25 22:08:34 +01:00
Merge remote-tracking branch 'nfarina/master'
This commit is contained in:
@@ -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 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 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 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/))
|
* _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.
|
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.
|
||||||
|
|||||||
@@ -1,132 +0,0 @@
|
|||||||
var Service = require("HAP-NodeJS").Service;
|
|
||||||
var Characteristic = require("HAP-NodeJS").Characteristic;
|
|
||||||
var Milight = require('node-milight-promise').MilightController;
|
|
||||||
var commands = require('node-milight-promise').commands;
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
accessory: MiLight
|
|
||||||
}
|
|
||||||
|
|
||||||
function MiLight(log, config) {
|
|
||||||
this.log = log;
|
|
||||||
|
|
||||||
// config info
|
|
||||||
this.ip_address = config["ip_address"];
|
|
||||||
this.port = config["port"];
|
|
||||||
this.name = config["name"];
|
|
||||||
this.zone = config["zone"];
|
|
||||||
this.type = config["type"];
|
|
||||||
this.delay = config["delay"];
|
|
||||||
this.repeat = config["repeat"];
|
|
||||||
}
|
|
||||||
|
|
||||||
var light = new Milight({
|
|
||||||
ip: this.ip_address,
|
|
||||||
port: this.port,
|
|
||||||
delayBetweenCommands: this.delay,
|
|
||||||
commandRepeat: this.repeat
|
|
||||||
});
|
|
||||||
|
|
||||||
MiLight.prototype = {
|
|
||||||
|
|
||||||
setPowerState: function(powerOn, callback) {
|
|
||||||
if (powerOn) {
|
|
||||||
light.sendCommands(commands[this.type].on(this.zone));
|
|
||||||
this.log("Setting power state to on");
|
|
||||||
} else {
|
|
||||||
light.sendCommands(commands[this.type].off(this.zone));
|
|
||||||
this.log("Setting power state to off");
|
|
||||||
}
|
|
||||||
callback();
|
|
||||||
},
|
|
||||||
|
|
||||||
setBrightness: function(level, callback) {
|
|
||||||
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);
|
|
||||||
|
|
||||||
light.sendCommands(commands[this.type].off(this.zone));
|
|
||||||
// Not sure if this timing is going to work or not? It's supposed to be 100ms after the off command
|
|
||||||
light.sendCommands(commands[this.type].nightMode(this.zone));
|
|
||||||
} else {
|
|
||||||
this.log("Setting brightness to %s", level);
|
|
||||||
|
|
||||||
// Send on command to ensure we're addressing the right bulb
|
|
||||||
light.sendCommands(commands[this.type].on(this.zone));
|
|
||||||
|
|
||||||
// If this is an rgbw lamp, set the absolute brightness specified
|
|
||||||
if (this.type == "rgbw") {
|
|
||||||
light.sendCommands(commands.rgbw.brightness(level));
|
|
||||||
} else {
|
|
||||||
// If this is an rgb or a white lamp, they only support brightness up and down.
|
|
||||||
// Set brightness up when value is >50 and down otherwise. Not sure how well this works real-world.
|
|
||||||
if (level >= 50) {
|
|
||||||
if (this.type == "white" && level == 100) {
|
|
||||||
// But the white lamps do have a "maximum brightness" command
|
|
||||||
light.sendCommands(commands.white.maxBright(this.zone));
|
|
||||||
} else {
|
|
||||||
light.sendCommands(commands[this.type].brightUp());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
light.sendCommands(commands[this.type].brightDown());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
callback();
|
|
||||||
},
|
|
||||||
|
|
||||||
setHue: function(value, callback) {
|
|
||||||
this.log("Setting hue to %s", value);
|
|
||||||
|
|
||||||
// Send on command to ensure we're addressing the right bulb
|
|
||||||
light.sendCommands(commands[this.type].on(this.zone));
|
|
||||||
|
|
||||||
if (this.type == "rgbw") {
|
|
||||||
if (value == 0) {
|
|
||||||
light.sendCommands(commands.rgbw.whiteMode(this.zone));
|
|
||||||
} else {
|
|
||||||
light.sendCommands(commands.rgbw.hue(commands.rgbw.hsvToMilightColor(Array(value, 0, 0))));
|
|
||||||
}
|
|
||||||
} else if (this.type == "rgb") {
|
|
||||||
light.sendCommands(commands.rgb.hue(commands.rgbw.hsvToMilightColor(Array(value, 0, 0))));
|
|
||||||
} 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) {
|
|
||||||
light.sendCommands(commands.white.warmer());
|
|
||||||
} else {
|
|
||||||
light.sendCommands(commands.white.cooler());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
identify: function(callback) {
|
|
||||||
this.log("Identify requested!");
|
|
||||||
callback(); // success
|
|
||||||
},
|
|
||||||
|
|
||||||
getServices: function() {
|
|
||||||
var informationService = new Service.AccessoryInformation();
|
|
||||||
|
|
||||||
informationService
|
|
||||||
.setCharacteristic(Characteristic.Manufacturer, "MiLight")
|
|
||||||
.setCharacteristic(Characteristic.Model, this.type)
|
|
||||||
.setCharacteristic(Characteristic.SerialNumber, "MILIGHT12345");
|
|
||||||
|
|
||||||
var lightbulbService = new Service.Lightbulb();
|
|
||||||
|
|
||||||
lightbulbService
|
|
||||||
.getCharacteristic(Characteristic.On)
|
|
||||||
.on('set', this.setPowerState.bind(this));
|
|
||||||
|
|
||||||
lightbulbService
|
|
||||||
.addCharacteristic(new Characteristic.Brightness())
|
|
||||||
.on('set', this.setBrightness.bind(this));
|
|
||||||
|
|
||||||
lightbulbService
|
|
||||||
.addCharacteristic(new Characteristic.Hue())
|
|
||||||
.on('set', this.setHue.bind(this));
|
|
||||||
|
|
||||||
return [informationService, lightbulbService];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -30,6 +30,30 @@ WeMoAccessory.prototype.search = function() {
|
|||||||
}.bind(this));
|
}.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) {
|
WeMoAccessory.prototype.getPowerOn = function(callback) {
|
||||||
|
|
||||||
if (!this.device) {
|
if (!this.device) {
|
||||||
@@ -122,6 +146,15 @@ WeMoAccessory.prototype.getServices = function() {
|
|||||||
|
|
||||||
return [garageDoorService];
|
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 {
|
else {
|
||||||
throw new Error("Unknown service type '%s'", this.service);
|
throw new Error("Unknown service type '%s'", this.service);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"accessory_type": "knxdevice",
|
"accessory_type": "knxdevice",
|
||||||
"description":"sample device with multiple services. Multiple services of different types are widely supported"
|
"description":"sample device with multiple services. Multiple services of different types are widely supported",
|
||||||
"name": "Office",
|
"name": "Office",
|
||||||
"services": [
|
"services": [
|
||||||
{
|
{
|
||||||
|
|||||||
+11
-11
@@ -71,7 +71,17 @@
|
|||||||
"platform": "YamahaAVR",
|
"platform": "YamahaAVR",
|
||||||
"play_volume": -35,
|
"play_volume": -35,
|
||||||
"setMainInputTo": "AirPlay"
|
"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": [
|
"accessories": [
|
||||||
@@ -152,16 +162,6 @@
|
|||||||
"port" : 4999, // Port the SER2SOCK process is running on
|
"port" : 4999, // Port the SER2SOCK process is running on
|
||||||
"pin": "1234" // PIN used for arming / disarming
|
"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",
|
"accessory": "Tesla",
|
||||||
"name": "Tesla",
|
"name": "Tesla",
|
||||||
|
|||||||
+3
-1
@@ -14,11 +14,13 @@
|
|||||||
"ad2usb": "git+https://github.com/alistairg/node-ad2usb.git#local",
|
"ad2usb": "git+https://github.com/alistairg/node-ad2usb.git#local",
|
||||||
"carwingsjs": "0.0.x",
|
"carwingsjs": "0.0.x",
|
||||||
"color": "0.10.x",
|
"color": "0.10.x",
|
||||||
"elkington": "kevinohara80/elkington",
|
|
||||||
"eibd": "^0.3.1",
|
"eibd": "^0.3.1",
|
||||||
|
"elkington": "kevinohara80/elkington",
|
||||||
"hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#6bf0f9eaaa2d87db8d1768114c61f4acbb095c41",
|
"hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#6bf0f9eaaa2d87db8d1768114c61f4acbb095c41",
|
||||||
"harmonyhubjs-client": "^1.1.4",
|
"harmonyhubjs-client": "^1.1.4",
|
||||||
"harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git",
|
"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",
|
"mdns": "^2.2.4",
|
||||||
"node-hue-api": "^1.0.5",
|
"node-hue-api": "^1.0.5",
|
||||||
"node-icontrol": "^0.1.4",
|
"node-icontrol": "^0.1.4",
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
/*
|
||||||
|
|
||||||
|
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
|
||||||
|
applamp.nl (http://www.applamp.nl/service/applamp-api/) and uses other details from (http://www.limitlessled.com/dev/)
|
||||||
|
|
||||||
|
Configure in config.json as follows:
|
||||||
|
|
||||||
|
"platforms": [
|
||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
Where the parameters are:
|
||||||
|
*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
|
||||||
|
*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
|
||||||
|
*White and rgb bulbs don't support absolute brightness setting, so we just send a brightness up/brightness down command depending
|
||||||
|
if we got a percentage above/below 50% respectively
|
||||||
|
*The only exception to the above is that white bulbs support a "maximum brightness" command, so we send that when we get 100%
|
||||||
|
*Implemented warmer/cooler for white lamps in a similar way to brightnes, except this time above/below 180 degrees on the colour wheel
|
||||||
|
*I welcome feedback on a better way to work the brightness/hue for white and rgb bulbs
|
||||||
|
|
||||||
|
Troubleshooting:
|
||||||
|
The node-milight-promise library provides additional debugging output when the MILIGHT_DEBUG environmental variable is set
|
||||||
|
|
||||||
|
TODO:
|
||||||
|
*Possibly build in some sort of state logging and persistance so that we can answswer HomeKit status queries to the best of our ability
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
var Service = require("HAP-NodeJS").Service;
|
||||||
|
var Characteristic = require("HAP-NodeJS").Characteristic;
|
||||||
|
var Milight = require('node-milight-promise').MilightController;
|
||||||
|
var commands = require('node-milight-promise').commands;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
accessory: MiLightAccessory,
|
||||||
|
platform: MiLightPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
this.ip_address = config["ip_address"];
|
||||||
|
this.port = config["port"];
|
||||||
|
this.name = config["name"];
|
||||||
|
this.zone = config["zone"];
|
||||||
|
this.type = config["type"];
|
||||||
|
this.delay = config["delay"];
|
||||||
|
this.repeat = config["repeat"];
|
||||||
|
|
||||||
|
this.light = new Milight({
|
||||||
|
ip: this.ip_address,
|
||||||
|
port: this.port,
|
||||||
|
delayBetweenCommands: this.delay,
|
||||||
|
commandRepeat: this.repeat
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
MiLightAccessory.prototype = {
|
||||||
|
|
||||||
|
setPowerState: function(powerOn, callback) {
|
||||||
|
if (powerOn) {
|
||||||
|
this.log("["+this.name+"] Setting power state to on");
|
||||||
|
this.light.sendCommands(commands[this.type].on(this.zone));
|
||||||
|
} else {
|
||||||
|
this.log("["+this.name+"] Setting power state to off");
|
||||||
|
this.light.sendCommands(commands[this.type].off(this.zone));
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
|
||||||
|
setBrightness: function(level, callback) {
|
||||||
|
if (level == 0) {
|
||||||
|
// If brightness is set to 0, turn off the lamp
|
||||||
|
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("["+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
|
||||||
|
this.light.pause(100);
|
||||||
|
this.light.sendCommands(commands[this.type].nightMode(this.zone));
|
||||||
|
|
||||||
|
} else {
|
||||||
|
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));
|
||||||
|
|
||||||
|
// If this is an rgbw lamp, set the absolute brightness specified
|
||||||
|
if (this.type == "rgbw") {
|
||||||
|
this.light.sendCommands(commands.rgbw.brightness(level));
|
||||||
|
} else {
|
||||||
|
// If this is an rgb or a white lamp, they only support brightness up and down.
|
||||||
|
// Set brightness up when value is >50 and down otherwise. Not sure how well this works real-world.
|
||||||
|
if (level >= 50) {
|
||||||
|
if (this.type == "white" && level == 100) {
|
||||||
|
// But the white lamps do have a "maximum brightness" command
|
||||||
|
this.light.sendCommands(commands.white.maxBright(this.zone));
|
||||||
|
} else {
|
||||||
|
this.light.sendCommands(commands[this.type].brightUp());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.light.sendCommands(commands[this.type].brightDown());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
|
||||||
|
setHue: function(value, callback) {
|
||||||
|
this.log("["+this.name+"] Setting hue to %s", value);
|
||||||
|
|
||||||
|
var hue = Array(value, 0, 0);
|
||||||
|
|
||||||
|
// Send on command to ensure we're addressing the right bulb
|
||||||
|
this.light.sendCommands(commands[this.type].on(this.zone));
|
||||||
|
|
||||||
|
if (this.type == "rgbw") {
|
||||||
|
if (value == 0) {
|
||||||
|
this.light.sendCommands(commands.rgbw.whiteMode(this.zone));
|
||||||
|
} else {
|
||||||
|
this.light.sendCommands(commands.rgbw.hue(commands.rgbw.hsvToMilightColor(hue)));
|
||||||
|
}
|
||||||
|
} else if (this.type == "rgb") {
|
||||||
|
this.light.sendCommands(commands.rgb.hue(commands.rgbw.hsvToMilightColor(hue)));
|
||||||
|
} 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.cooler());
|
||||||
|
} else {
|
||||||
|
this.light.sendCommands(commands.white.warmer());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
|
||||||
|
identify: function(callback) {
|
||||||
|
this.log("["+this.name+"] Identify requested!");
|
||||||
|
callback(); // success
|
||||||
|
},
|
||||||
|
|
||||||
|
getServices: function() {
|
||||||
|
var informationService = new Service.AccessoryInformation();
|
||||||
|
|
||||||
|
informationService
|
||||||
|
.setCharacteristic(Characteristic.Manufacturer, "MiLight")
|
||||||
|
.setCharacteristic(Characteristic.Model, this.type)
|
||||||
|
.setCharacteristic(Characteristic.SerialNumber, "MILIGHT12345");
|
||||||
|
|
||||||
|
var lightbulbService = new Service.Lightbulb();
|
||||||
|
|
||||||
|
lightbulbService
|
||||||
|
.getCharacteristic(Characteristic.On)
|
||||||
|
.on('set', this.setPowerState.bind(this));
|
||||||
|
|
||||||
|
lightbulbService
|
||||||
|
.addCharacteristic(new Characteristic.Brightness())
|
||||||
|
.on('set', this.setBrightness.bind(this));
|
||||||
|
|
||||||
|
lightbulbService
|
||||||
|
.addCharacteristic(new Characteristic.Hue())
|
||||||
|
.on('set', this.setHue.bind(this));
|
||||||
|
|
||||||
|
return [informationService, lightbulbService];
|
||||||
|
}
|
||||||
|
};
|
||||||
+19
-1
@@ -6,6 +6,8 @@ function SonosPlatform(log, config){
|
|||||||
this.config = config;
|
this.config = config;
|
||||||
this.name = config["name"];
|
this.name = config["name"];
|
||||||
this.playVolume = config["play_volume"];
|
this.playVolume = config["play_volume"];
|
||||||
|
// timeout for device discovery
|
||||||
|
this.discoveryTimeout = (config.deviceDiscoveryTimeout || 10)*1000; // assume 10sec as a default
|
||||||
}
|
}
|
||||||
|
|
||||||
SonosPlatform.prototype = {
|
SonosPlatform.prototype = {
|
||||||
@@ -16,6 +18,18 @@ SonosPlatform.prototype = {
|
|||||||
// track found devices so we don't add duplicates
|
// track found devices so we don't add duplicates
|
||||||
var roomNamesFound = {};
|
var roomNamesFound = {};
|
||||||
|
|
||||||
|
// collector array for the devices from callbacks
|
||||||
|
var devicesFound = [];
|
||||||
|
// tell the sonos callbacks if timeout already occured
|
||||||
|
var timeout = false;
|
||||||
|
|
||||||
|
// the timeout event will push the accessories back
|
||||||
|
setTimeout(function(){
|
||||||
|
timeout=true;
|
||||||
|
callback(devicesFound);
|
||||||
|
}, this.discoveryTimeout);
|
||||||
|
|
||||||
|
|
||||||
sonos.search(function (device) {
|
sonos.search(function (device) {
|
||||||
that.log("Found device at " + device.host);
|
that.log("Found device at " + device.host);
|
||||||
|
|
||||||
@@ -26,9 +40,13 @@ SonosPlatform.prototype = {
|
|||||||
if (!roomNamesFound[roomName]) {
|
if (!roomNamesFound[roomName]) {
|
||||||
roomNamesFound[roomName] = true;
|
roomNamesFound[roomName] = true;
|
||||||
that.log("Found playable device - " + roomName);
|
that.log("Found playable device - " + roomName);
|
||||||
|
if (timeout) {
|
||||||
|
that.log("Ignored: Discovered after timeout (Set deviceDiscoveryTimeout parameter in Sonos section of config.json)");
|
||||||
|
}
|
||||||
// device is an instance of sonos.Sonos
|
// device is an instance of sonos.Sonos
|
||||||
var accessory = new SonosAccessory(that.log, that.config, device, description);
|
var accessory = new SonosAccessory(that.log, that.config, device, description);
|
||||||
callback([accessory]);
|
// add it to the collector array
|
||||||
|
devicesFound.push(accessory);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
that.log("Ignoring playable device with duplicate room name - " + roomName);
|
that.log("Ignoring playable device with duplicate room name - " + roomName);
|
||||||
|
|||||||
Reference in New Issue
Block a user