Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Andre Schröter
2015-09-12 20:11:31 +02:00
22 changed files with 3067 additions and 938 deletions

View File

@@ -1,9 +1,9 @@
# HomeBridge
# Homebridge
HomeBridge is a lightweight NodeJS server you can run on your home network that emulates the iOS HomeKit API. It includes a set of "shims" (found in the [accessories](accessories/) and [platforms](platforms/) folders) that provide a basic bridge from HomeKit to various 3rd-party APIs provided by manufacturers of "smart home" devices.
Homebridge is a lightweight NodeJS server you can run on your home network that emulates the iOS HomeKit API. It includes a set of "shims" (found in the [accessories](accessories/) and [platforms](platforms/) folders) that provide a basic bridge from HomeKit to various 3rd-party APIs provided by manufacturers of "smart home" devices.
Since Siri supports devices added through HomeKit, this means that with HomeBridge you can ask Siri to control devices that don't have any support for HomeKit at all. For instance, using the included shims, you can say things like:
Since Siri supports devices added through HomeKit, this means that with Homebridge you can ask Siri to control devices that don't have any support for HomeKit at all. For instance, using the included shims, you can say things like:
* _Siri, unlock the front door._ ([Lockitron](https://lockitron.com))
* _Siri, open the garage door._ ([LiftMaster MyQ](https://www.myliftmaster.com))
@@ -11,13 +11,13 @@ 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))
* _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.
# Shim types
There are 2 types of shims supported in HomeBridge.
There are 2 types of shims supported in Homebridge.
* Accessory - Individual device
* Platform - A full bridge to another system
@@ -28,9 +28,9 @@ Accessories are individual devices you would like to bridge to HomeKit. You set
## Platforms
Platforms bridge entire systems to HomeKit. Platforms can be things like Wink or SmartThings or Vera. By adding a platform to your `config.json`, HomeBridge will automatically detect all of your devices for you.
Platforms bridge entire systems to HomeKit. Platforms can be things like Wink or SmartThings or Vera. By adding a platform to your `config.json`, Homebridge will automatically detect all of your devices for you.
All you have to do is add the right config options so HomeBridge can authenticate and communicate with your other system, and voila, your devices will be available to HomeKit via HomeBridge.
All you have to do is add the right config options so Homebridge can authenticate and communicate with your other system, and voila, your devices will be available to HomeKit via Homebridge.
# Why?
@@ -38,7 +38,7 @@ Technically, the device manufacturers should be the ones implementing the HomeKi
# Credit
HomeBridge itself is basically just a set of shims and a README. The actual HomeKit API work was done by [KhaosT](http://twitter.com/khaost) in his [HAP-NodeJS](https://github.com/KhaosT/HAP-NodeJS) project. Additionally, many of the shims benefit from amazing NodeJS projects out there like `sonos` and `wemo` that implement all the interesting functionality.
Homebridge itself is basically just a set of shims and a README. The actual HomeKit API work was done by [KhaosT](http://twitter.com/khaost) in his [HAP-NodeJS](https://github.com/KhaosT/HAP-NodeJS) project. Additionally, many of the shims benefit from amazing NodeJS projects out there like `sonos` and `wemo` that implement all the interesting functionality.
# Before you Begin
@@ -50,8 +50,6 @@ I would call this project a "novelty" in its current form, and is for **intrepid
You'll also need some patience, as Siri can be very strict about sentence structure, and occasionally she will forget about HomeKit altogether. But it's not surprising that HomeKit isn't rock solid, since almost no one can actually use it today besides developers who are creating hardware accessories for it. There are, to my knowledge, exactly zero licensed HomeKit devices on the market right now, so Apple can easily get away with this all being a work in progress.
Additionally, the shims I've created implement the bare minimum of HomeKit needed to provide basic functionality like turning things off and on. I haven't written any kind of good feedback or error handling, and although they support changing state, they don't support reading the current state, so if you ask questions like "Is my door unlocked?" Siri will respond with the default of "Nope!" no matter what.
# Getting Started
OK, if you're still excited enough about ordering Siri to make your coffee (which, who wouldn't be!) then here's how to set things up. First, clone this repo:
@@ -66,7 +64,7 @@ Now you should be able to run the homebridge server:
$ cd homebridge
$ npm run start
Starting HomeBridge server...
Starting Homebridge server...
Couldn't find a config.json file [snip]
The server won't do anything until you've created a `config.json` file containing your home devices (or _accessories_ in HomeKit parlance) or platforms you wish to make available to iOS. You can start by copying and modifying the included `config-sample.json` file which includes declarations for all supported accessories and platforms.
@@ -74,7 +72,7 @@ The server won't do anything until you've created a `config.json` file containin
Once you've added your devices and/or platforms, you should be able to run the server again and see them initialize:
$ npm run start
Starting HomeBridge server...
Starting Homebridge server...
Loading 6 accessories...
[Speakers] Initializing 'Sonos' accessory...
[Coffee Maker] Initializing 'WeMo' accessory...
@@ -94,18 +92,18 @@ Fortunately, there are now a few apps in the App Store that can manage your Home
There are also some free apps that work OK. Try [Insteon+](https://itunes.apple.com/US/app/id919270334?mt=8) or [Lutron](https://itunes.apple.com/us/app/lutron-app-for-caseta-wireless/id886753021?mt=8) or a number of others.
If you are a member of the iOS developer program, I highly recommend Apple's [HomeKit Catalog](https://developer.apple.com/library/ios/samplecode/HomeKitCatalog/Introduction/Intro.html) app, as it is reliable and comprehensive and free (and open source).
## Adding HomeKit Accessories
Once you've gotten a HomeKit app running on your iOS device, you can begin adding accessories. The app should "discover" the accessories defined in your `config.json` file, assuming that you're still running the HomeBridge server and you're on the same Wifi network.
Once you've gotten a HomeKit app running on your iOS device, you can use it to add your Homebridge devices. The app should "discover" the single accessory "Homebridge", assuming that you're still running the Homebridge server and you're on the same Wifi network. Adding this accessory will automatically add all accessories and platforms defined in `config.json`.
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`. Adding the device should create some files in the `persist` directory of the HomeBridge server, which stores the pairing relationship.
When you attempt to add Homebridge, it will ask for a "PIN code". The default code is `031-45-154` (but this can be changed, see `config-sample.json`). This process will create some files in the `persist` directory of the Homebridge server, which stores the pairing relationship.
# Interacting with your Devices
Once your device has been added to HomeKit, you should be able to tell Siri to control your devices. However, realize that Siri is a cloud service, and iOS may need some time to synchronize your device information with iCloud.
Also, keep in mind HomeKit is not very robust yet, and it is common for it to fail intermittently ("Sorry, I wasn't able to control your devices" etc.) then start working again for no reason. Also I've noticed that it will get cranky and stop working altogether sometimes. The usual voodoo applies here: reboot your device, restart the homebridge server, run your HomeKit iOS app and poke around, etc.
One final thing to remember is that Siri will almost always prefer its default phrase handling over HomeKit devices. For instance, if you name your Sonos device "Radio" and try saying "Siri, turn on the Radio" then Siri will probably start playing an iTunes Radio station on your phone. Even if you name it "Sonos" and say "Siri, turn on the Sonos", Siri will probably just launch the Sonos app instead. This is why, for instance, the suggested `name` for the Sonos shim in `config-samples.json` is "Speakers".
# Final Notes

0
accessories/HomeMatic.js Executable file → Normal file
View File

View File

@@ -1,6 +1,11 @@
var types = require("HAP-NodeJS/accessories/types.js");
var Service = require("HAP-NodeJS").Service;
var Characteristic = require("HAP-NodeJS").Characteristic;
var request = require("request");
module.exports = {
accessory: HttpAccessory
}
function HttpAccessory(log, config) {
this.log = log;
@@ -9,9 +14,6 @@ function HttpAccessory(log, config) {
this.off_url = config["off_url"];
this.brightness_url = config["brightness_url"];
this.http_method = config["http_method"];
// device info
this.name = config["name"];
}
HttpAccessory.prototype = {
@@ -26,135 +28,73 @@ HttpAccessory.prototype = {
})
},
setPowerState: function(powerOn) {
setPowerState: function(powerOn, callback) {
var url;
if (powerOn) {
url = this.on_url
this.log("Setting power state on the '"+this.name+"' to on");
}else{
url = this.off_url
this.log("Setting power state on the '"+this.name+"' to off");
url = this.on_url;
this.log("Setting power state to on");
}
else {
url = this.off_url;
this.log("Setting power state to off");
}
this.httpRequest(url, this.http_method, function(error, response, body){
this.httpRequest(url, this.http_method, function(error, response, body) {
if (error) {
return console.error('http power function failed:', error);
}else{
return console.log('http power function succeeded!');
this.log('HTTP power function failed: %s', error.message);
callback(error);
}
});
else {
this.log('HTTP power function succeeded!');
callback();
}
}.bind(this));
},
setBrightness: function(level) {
setBrightness: function(level, callback) {
var url = this.brightness_url.replace("%b", level)
this.log("Setting brightness on the '"+this.name+"' to " + level);
this.log("Setting brightness to %s", level);
this.httpRequest(url, this.http_method, function(error, response, body){
this.httpRequest(url, this.http_method, function(error, response, body) {
if (error) {
return console.error('http brightness function failed:', error);
}else{
return console.log('http brightness function succeeded!');
this.log('HTTP brightness function failed: %s', error);
callback(error);
}
});
else {
this.log('HTTP brightness function succeeded!');
callback();
}
}.bind(this));
},
identify: function(callback) {
this.log("Identify requested!");
callback(); // success
},
getServices: function() {
var that = this;
return [{
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: "Http",
supportEvents: false,
supportBonjour: false,
manfDescription: "Manufacturer",
designedMaxLength: 255
},{
cType: types.MODEL_CTYPE,
onUpdate: null,
perms: ["pr"],
format: "string",
initialValue: "Rev-1",
supportEvents: false,
supportBonjour: false,
manfDescription: "Model",
designedMaxLength: 255
},{
cType: types.SERIAL_NUMBER_CTYPE,
onUpdate: null,
perms: ["pr"],
format: "string",
initialValue: "A1S2NASF88EW",
supportEvents: false,
supportBonjour: false,
manfDescription: "SN",
designedMaxLength: 255
},{
cType: types.IDENTIFY_CTYPE,
onUpdate: null,
perms: ["pw"],
format: "bool",
initialValue: false,
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: true,
supportBonjour: false,
manfDescription: "Name of service",
designedMaxLength: 255
},{
cType: types.POWER_STATE_CTYPE,
onUpdate: function(value) { that.setPowerState(value); },
perms: ["pw","pr","ev"],
format: "bool",
initialValue: 0,
supportEvents: true,
supportBonjour: false,
manfDescription: "Change the power state",
designedMaxLength: 1
},{
cType: types.BRIGHTNESS_CTYPE,
onUpdate: function(value) { that.setBrightness(value); },
perms: ["pw","pr","ev"],
format: "int",
initialValue: 0,
supportEvents: true,
supportBonjour: false,
manfDescription: "Adjust Brightness",
designedMinValue: 0,
designedMaxValue: 100,
designedMinStep: 1,
unit: "%"
}]
}];
// you can OPTIONALLY create an information service if you wish to override
// the default values for things like serial number, model, etc.
var informationService = new Service.AccessoryInformation();
informationService
.setCharacteristic(Characteristic.Manufacturer, "HTTP Manufacturer")
.setCharacteristic(Characteristic.Model, "HTTP Model")
.setCharacteristic(Characteristic.SerialNumber, "HTTP Serial Number");
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));
return [informationService, lightbulbService];
}
};
module.exports.accessory = HttpAccessory;

221
accessories/Hyperion.js Normal file
View File

@@ -0,0 +1,221 @@
var types = require("HAP-NodeJS/accessories/types.js");
var net = require('net');
var Color = require('color');
function HyperionAccessory(log, config) {
this.log = log;
this.host = config["host"];
this.port = config["port"];
this.name = config["name"];
this.color = Color().hsv([0, 0, 0]);
this.prevColor = Color().hsv([0,0,100]);
}
HyperionAccessory.prototype = {
sendHyperionCommand: function(command, cmdParams, priority) {
var that = this;
var client = new net.Socket();
var data = {};
if (typeof priority === 'undefined') { priority = 100; }
switch (command) {
case 'color':
data = {"command":"color", "priority":priority,"color":cmdParams};
break;
case 'blacklevel':
data = {"command":"transform","transform":{"blacklevel":cmdParams}}
break;
default:
that.log("Hyperion command not found");
return;
}
//that.log(JSON.stringify(data));
client.connect(that.port, that.host, function() {
client.write(JSON.stringify(data) + "\n");
});
client.on('data', function(data){
that.log("Response: " + data.toString().trim());
that.log("***** Color HSV:" + that.color.hsvArray() + "*****");
that.log("***** Color RGB:" + that.color.rgbArray() + "*****");
client.end();
});
},
setPowerState: function(powerOn) {
var that = this;
if (powerOn) {
that.log("Setting power state on the '"+that.name+"' to on");
that.color.rgb(that.prevColor.rgb());
that.sendHyperionCommand('color', that.color.rgbArray());
} else {
that.log("Setting power state on the '"+that.name+"' to off");
that.prevColor.rgb(that.color.rgb());
that.color.value(0);
that.sendHyperionCommand('color', that.color.rgbArray());
that.sendHyperionCommand('blacklevel', [0,0,0]);
}
},
setBrightness: function(level) {
var that = this;
that.color.value(level);
that.log("Setting brightness on the '"+that.name+"' to '" + level + "'");
that.sendHyperionCommand('color', that.color.rgbArray());
},
setHue: function(level) {
var that = this;
that.color.hue(level);
that.prevColor.hue(level);
that.log("Setting hue on the '"+that.name+"' to '" + level + "'");
that.sendHyperionCommand('color', that.color.rgbArray());
},
setSaturation: function(level) {
var that = this;
that.color.saturationv(level);
that.prevColor.saturationv(level);
that.log("Setting saturation on the '"+that.name+"' to '" + level + "'");
that.sendHyperionCommand('color', that.color.rgbArray());
},
getServices: function() {
var that = this;
return [{
sType: types.ACCESSORY_INFORMATION_STYPE,
characteristics: [{
cType: types.NAME_CTYPE,
onUpdate: null,
perms: ["pr"],
format: "string",
initialValue: that.name,
supportEvents: false,
supportBonjour: false,
manfDescription: "Name of the accessory",
designedMaxLength: 255
},{
cType: types.MANUFACTURER_CTYPE,
onUpdate: null,
perms: ["pr"],
format: "string",
initialValue: "Hyperion",
supportEvents: false,
supportBonjour: false,
manfDescription: "Manufacturer",
designedMaxLength: 255
},{
cType: types.MODEL_CTYPE,
onUpdate: null,
perms: ["pr"],
format: "string",
initialValue: "Rev-1",
supportEvents: false,
supportBonjour: false,
manfDescription: "Model",
designedMaxLength: 255
},{
cType: types.SERIAL_NUMBER_CTYPE,
onUpdate: null,
perms: ["pr"],
format: "string",
initialValue: "DEADBEEF",
supportEvents: false,
supportBonjour: false,
manfDescription: "SN",
designedMaxLength: 255
},{
cType: types.IDENTIFY_CTYPE,
onUpdate: null,
perms: ["pw"],
format: "bool",
initialValue: false,
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: that.name,
supportEvents: true,
supportBonjour: false,
manfDescription: "Name of service",
designedMaxLength: 255
},{
cType: types.POWER_STATE_CTYPE,
onUpdate: function(value) { that.setPowerState(value); },
onRead: ((that.color.value() > 0) ? true : false),
perms: ["pw","pr","ev"],
format: "bool",
initialValue: 0,
supportEvents: true,
supportBonjour: false,
manfDescription: "Turn on the light",
designedMaxLength: 1
},{
cType: types.BRIGHTNESS_CTYPE,
onUpdate: function(value) { that.setBrightness(value); },
onRead: that.color.value(),
perms: ["pw","pr","ev"],
format: "int",
initialValue: that.color.value(),
supportEvents: false,
supportBonjour: false,
manfDescription: "Adjust Brightness",
designedMinValue: 0,
designedMaxValue: 100,
designedMinStep: 1,
unit: "%"
},{
cType: types.HUE_CTYPE,
onUpdate: function(value) { that.setHue(value) },
onRead: that.color.hue(),
perms: ["pw","pr","ev"],
format: "int",
initialValue: that.color.hue(),
supportEvents: false,
supportBonjour: false,
manfDescription: "Adjust Hue",
designedMinValue: 0,
designedMaxValue: 360,
designedMinStep: 1,
unit: "arcdegrees"
},{
cType: types.SATURATION_CTYPE,
onUpdate: function(value) { that.setSaturation(value) },
onRead: that.color.saturationv(),
perms: ["pw","pr","ev"],
format: "int",
initialValue: that.color.saturationv(),
supportEvents: false,
supportBonjour: false,
manfDescription: "Adjust Saturation",
designedMinValue: 0,
designedMaxValue: 100,
designedMinStep: 1,
unit: "%"
}]
}];
}
};
module.exports.accessory = HyperionAccessory;

View File

@@ -92,7 +92,7 @@ LiftMasterAccessory.prototype = {
if (device["MyQDeviceTypeName"] == "GarageDoorOpener") {
// If we haven't explicity specified a door ID, we'll loop to make sure we don't have multiple openers, which is confusing
if (that.requiredDeviceId == undefined) {
if (!that.requiredDeviceId) {
var thisDeviceId = device.MyQDeviceId;
var thisDoorName = "Unknown";
for (var j = 0; j < device.Attributes.length; j ++) {
@@ -103,6 +103,7 @@ LiftMasterAccessory.prototype = {
}
}
foundDoors.push(thisDeviceId + " - " + thisDoorName);
that.deviceId = thisDeviceId;
}
// We specified a door ID, sanity check to make sure it's the one we expected

View File

@@ -1,181 +0,0 @@
var types = require("HAP-NodeJS/accessories/types.js");
var Milight = require('node-milight-promise').MilightController;
var commands = require('node-milight-promise').commands;
function MiLight(log, config) {
this.log = log;
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) {
var binaryState = powerOn ? "on" : "off";
var that = this;
if (binaryState === "on") {
this.log("Setting power state of zone " + this.zone + " to " + powerOn);
light.sendCommands(commands[this.type].on(this.zone));
} else {
this.log("Setting power state of zone " + this.zone + " to " + powerOn);
light.sendCommands(commands[this.type].off(this.zone));
}
},
setBrightnessLevel: function(value) {
var that = this;
this.log("Setting brightness level of zone " + this.zone + " to " + value);
light.sendCommands(commands[this.type].brightness(value));
},
setHue: function(value) {
var that = this;
this.log("Setting hue of zone " + this.zone + " to " + value);
if (value == "0") {
light.sendCommands(commands.rgbw.whiteMode(this.zone));
} else {
light.sendCommands(commands.rgbw.hue(commands.rgbw.hsvToMilightColor(Array(value, 0, 0))));
}
},
getServices: function() {
var that = this;
var services = [{
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: "MiLight",
supportEvents: false,
supportBonjour: false,
manfDescription: "Manufacturer",
designedMaxLength: 255
}, {
cType: types.MODEL_CTYPE,
onUpdate: null,
perms: ["pr"],
format: "string",
initialValue: this.type,
supportEvents: false,
supportBonjour: false,
manfDescription: "Model",
designedMaxLength: 255
}, {
cType: types.SERIAL_NUMBER_CTYPE,
onUpdate: null,
perms: ["pr"],
format: "string",
initialValue: "MILIGHT1234",
supportEvents: false,
supportBonjour: false,
manfDescription: "SN",
designedMaxLength: 255
}, {
cType: types.IDENTIFY_CTYPE,
onUpdate: null,
perms: ["pw"],
format: "bool",
initialValue: false,
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 service",
designedMaxLength: 255
}, {
cType: types.POWER_STATE_CTYPE,
onUpdate: function(value) {
that.setPowerState(value);
},
perms: ["pw", "pr", "ev"],
format: "bool",
initialValue: false,
supportEvents: false,
supportBonjour: false,
manfDescription: "Turn on the light",
designedMaxLength: 1
}, {
cType: types.BRIGHTNESS_CTYPE,
onUpdate: function(value) {
that.setBrightnessLevel(value);
},
perms: ["pw", "pr", "ev"],
format: "bool",
initialValue: 100,
supportEvents: false,
supportBonjour: false,
manfDescription: "Adjust brightness of light",
designedMinValue: 0,
designedMaxValue: 100,
designedMinStep: 1,
unit: "%"
}]
}];
if (that.type == "rgbw" || that.type == "rgb") {
services[1].characteristics.push({
cType: types.HUE_CTYPE,
onUpdate: function(value) {
that.setHue(value);
},
perms: ["pw", "pr", "ev"],
format: "int",
initialValue: 0,
supportEvents: false,
supportBonjour: false,
manfDescription: "Adjust Hue of Light",
designedMinValue: 0,
designedMaxValue: 360,
designedMinStep: 1,
unit: "arcdegrees"
});
}
return services;
}
};
module.exports.accessory = MiLight;

View File

@@ -1,166 +1,161 @@
var types = require("HAP-NodeJS/accessories/types.js");
var Service = require("HAP-NodeJS").Service;
var Characteristic = require("HAP-NodeJS").Characteristic;
var wemo = require('wemo');
// extend our search timeout from 5 seconds to 60
wemo.SearchTimeout = 60000;
wemo.timeout = wemo.SearchTimeout // workaround for a bug in wemo.js v0.0.4
module.exports = {
accessory: WeMoAccessory
}
function WeMoAccessory(log, config) {
this.log = log;
this.name = config["name"];
this.wemoName = config["wemo_name"];
this.device = null;
this.service = config["service"] || "Switch";
this.wemoName = config["wemo_name"] || this.name; // fallback to "name" if you didn't specify an exact "wemo_name"
this.device = null; // instance of WeMo, for controlling the discovered device
this.log("Searching for WeMo device with exact name '" + this.wemoName + "'...");
this.search();
}
WeMoAccessory.prototype = {
search: function() {
var that = this;
wemo.Search(this.wemoName, function(err, device) {
if (!err && device) {
that.log("Found '"+that.wemoName+"' device at " + device.ip);
that.device = new wemo(device.ip, device.port);
}
else {
that.log("Error finding device '" + that.wemoName + "': " + err);
that.log("Continuing search for WeMo device with exact name '" + that.wemoName + "'...");
that.search();
}
});
},
setPowerState: function(powerOn) {
if (!this.device) {
this.log("No '"+this.wemoName+"' device found (yet?)");
return;
WeMoAccessory.prototype.search = function() {
wemo.Search(this.wemoName, function(err, device) {
if (!err && device) {
this.log("Found '"+this.wemoName+"' device at " + device.ip);
this.device = new wemo(device.ip, device.port);
}
var binaryState = powerOn ? 1 : 0;
var that = this;
this.log("Setting power state on the '"+this.wemoName+"' to " + binaryState);
this.device.setBinaryState(binaryState, function(err, result) {
if (!err) {
that.log("Successfully set power state on the '"+that.wemoName+"' to " + binaryState);
}
else {
that.log("Error setting power state on the '"+that.wemoName+"'")
}
});
},
getPowerState: function(callback) {
if (!this.device) {
this.log("No '"+this.wemoName+"' device found (yet?)");
return;
else {
this.log("Error finding device '" + this.wemoName + "': " + err);
this.log("Continuing search for WeMo device with exact name '" + this.wemoName + "'...");
this.search();
}
}.bind(this));
}
var that = this;
WeMoAccessory.prototype.getMotion = function(callback) {
this.log("checking power state for: " + this.wemoName);
this.device.getBinaryState(function(err, result) {
if (!err) {
var binaryState = parseInt(result)
that.log("power state for " + that.wemoName + " is: " + binaryState)
callback(binaryState)
}
else {
that.log(err)
}
});
},
getServices: function() {
var that = this;
return [{
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: "WeMo",
supportEvents: false,
supportBonjour: false,
manfDescription: "Manufacturer",
designedMaxLength: 255
},{
cType: types.MODEL_CTYPE,
onUpdate: null,
perms: ["pr"],
format: "string",
initialValue: "Rev-1",
supportEvents: false,
supportBonjour: false,
manfDescription: "Model",
designedMaxLength: 255
},{
cType: types.SERIAL_NUMBER_CTYPE,
onUpdate: null,
perms: ["pr"],
format: "string",
initialValue: "A1S2NASF88EW",
supportEvents: false,
supportBonjour: false,
manfDescription: "SN",
designedMaxLength: 255
},{
cType: types.IDENTIFY_CTYPE,
onUpdate: null,
perms: ["pw"],
format: "bool",
initialValue: false,
supportEvents: false,
supportBonjour: false,
manfDescription: "Identify Accessory",
designedMaxLength: 1
}]
},{
sType: types.SWITCH_STYPE,
characteristics: [{
cType: types.NAME_CTYPE,
onUpdate: null,
perms: ["pr"],
format: "string",
initialValue: this.name,
supportEvents: false,
supportBonjour: false,
manfDescription: "Name of service",
designedMaxLength: 255
},{
cType: types.POWER_STATE_CTYPE,
onUpdate: function(value) { that.setPowerState(value); },
onRead: function(callback) {
that.getPowerState(function(powerState){
callback(powerState);
});
},
perms: ["pw","pr","ev"],
format: "bool",
initialValue: false,
supportEvents: false,
supportBonjour: false,
manfDescription: "Change the power state of the WeMo",
designedMaxLength: 1
}]
}];
if (!this.device) {
this.log("No '%s' device found (yet?)", this.wemoName);
callback(new Error("Device not found"), false);
return;
}
};
module.exports.accessory = WeMoAccessory;
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) {
this.log("No '%s' device found (yet?)", this.wemoName);
callback(new Error("Device not found"), false);
return;
}
this.log("Getting power state on the '%s'...", this.wemoName);
this.device.getBinaryState(function(err, result) {
if (!err) {
var binaryState = parseInt(result);
var powerOn = binaryState > 0;
this.log("Power state for the '%s' is %s", this.wemoName, binaryState);
callback(null, powerOn);
}
else {
this.log("Error getting power state on the '%s': %s", this.wemoName, err.message);
callback(err);
}
}.bind(this));
}
WeMoAccessory.prototype.setPowerOn = function(powerOn, callback) {
if (!this.device) {
this.log("No '%s' device found (yet?)", this.wemoName);
callback(new Error("Device not found"));
return;
}
var binaryState = powerOn ? 1 : 0; // wemo langauge
this.log("Setting power state on the '%s' to %s", this.wemoName, binaryState);
this.device.setBinaryState(binaryState, function(err, result) {
if (!err) {
this.log("Successfully set power state on the '%s' to %s", this.wemoName, binaryState);
callback(null);
}
else {
this.log("Error setting power state to %s on the '%s'", binaryState, this.wemoName);
callback(err);
}
}.bind(this));
}
WeMoAccessory.prototype.setTargetDoorState = function(targetDoorState, callback) {
if (!this.device) {
this.log("No '%s' device found (yet?)", this.wemoName);
callback(new Error("Device not found"));
return;
}
this.log("Activating WeMo switch '%s'", this.wemoName);
this.device.setBinaryState(1, function(err, result) {
if (!err) {
this.log("Successfully activated WeMo switch '%s'", this.wemoName);
callback(null);
}
else {
this.log("Error activating WeMo switch '%s'", this.wemoName);
callback(err);
}
}.bind(this));
}
WeMoAccessory.prototype.getServices = function() {
if (this.service == "Switch") {
var switchService = new Service.Switch(this.name);
switchService
.getCharacteristic(Characteristic.On)
.on('get', this.getPowerOn.bind(this))
.on('set', this.setPowerOn.bind(this));
return [switchService];
}
else if (this.service == "GarageDoor") {
var garageDoorService = new Service.GarageDoorOpener("Garage Door Opener");
garageDoorService
.getCharacteristic(Characteristic.TargetDoorState)
.on('set', this.setTargetDoorState.bind(this))
.supportsEventNotification = false;
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

@@ -1,284 +0,0 @@
var types = require("HAP-NodeJS/accessories/types.js");
var request = require("request");
var xmldoc = require("xmldoc");
function XfinityHomeAccessory(log, config) {
this.log = log;
this.name = config["name"];
this.email = config["email"];
this.password = config["password"];
this.dsig = config["dsig"];
this.pinCode = config["pin"];
}
XfinityHomeAccessory.prototype = {
armWithType: function(armed, type) {
this.log("Arming with type " + type + " = " + armed + "...");
this.targetArmed = armed;
this.targetArmType = type;
this.getLoginToken();
},
getLoginToken: function() {
this.log("Retrieving login token...");
var that = this;
request.post({
url: "https://login.comcast.net/api/login",
form: {
appkey:"iControl",
dsig: this.dsig,
u: this.email,
p: this.password
}
}, function(err, response, body) {
if (!err && response.statusCode == 200) {
var doc = new xmldoc.XmlDocument(body);
that.loginToken = doc.valueWithPath("LoginToken");
that.refreshLoginCookie();
}
else {
that.log("Error '"+err+"' getting login token: " + body);
}
});
},
refreshLoginCookie: function() {
this.log("Refreshing login cookie...");
var that = this;
request.post({
url: "https://www.xfinityhomesecurity.com/rest/icontrol/login",
form: {
token: this.loginToken
}
}, function(err, response, body) {
if (!err && response.statusCode == 200) {
// extract our "site" from the login response
var json = JSON.parse(body);
that.siteHref = json["login"]["site"]["href"];
// manual cookie handling
that.loginCookie = response.headers["set-cookie"];
that.getInstances();
}
else {
that.log("Error '"+err+"' refreshing login cookie: " + body);
}
});
},
getInstances: function() {
this.log("Getting instances for site " + this.siteHref + "...");
this.panelHref = null;
var that = this;
request.get({
url: "https://www.xfinityhomesecurity.com/"+that.siteHref+"/network/instances",
headers: { Cookie: this.loginCookie },
json: true
}, function(err, response, json) {
if (!err && response.statusCode == 200) {
// extract our "instance" from the response. look for the first "panel"
var instances = json["instances"]["instance"];
for (var i=0; i<instances.length; i++) {
var instance = instances[i];
if (instance["mediaType"] == "instance/panel") {
that.panelHref = instance.href;
}
}
if (that.panelHref) {
that.log("Found panel " + that.panelHref + ". Ready to arm.");
that.finishArm();
}
else {
that.log("Couldn't find a panel.");
}
}
else {
that.log("Error '"+err+"' getting instances: " + JSON.stringify(json));
}
});
},
finishArm: function() {
this.log("Finish arming with type " + this.targetArmType + " = " + this.targetArmed + "...");
var path, form;
var that = this;
if (!this.targetArmed) {
path = this.panelHref + "/functions/disarm";
form = {code: this.pinCode};
}
else {
path = this.panelHref + "/functions/arm";
form = {code: this.pinCode, armType: this.targetArmType };
}
request.post({
url: "https://www.xfinityhomesecurity.com"+path,
headers: { Cookie: this.loginCookie },
form: form
}, function(err, response, body) {
if (!err && response.statusCode >= 200 && response.statusCode < 300) {
that.log("Arm response: " + response);
}
else {
that.log("Error '"+err+"' performing arm request: " + body);
}
});
},
getServices: function() {
var that = this;
return [{
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: "Comcast",
supportEvents: false,
supportBonjour: false,
manfDescription: "Manufacturer",
designedMaxLength: 255
},{
cType: types.MODEL_CTYPE,
onUpdate: null,
perms: ["pr"],
format: "string",
initialValue: "Rev-1",
supportEvents: false,
supportBonjour: false,
manfDescription: "Model",
designedMaxLength: 255
},{
cType: types.SERIAL_NUMBER_CTYPE,
onUpdate: null,
perms: ["pr"],
format: "string",
initialValue: "A1S2NASF88EW",
supportEvents: false,
supportBonjour: false,
manfDescription: "SN",
designedMaxLength: 255
},{
cType: types.IDENTIFY_CTYPE,
onUpdate: null,
perms: ["pw"],
format: "bool",
initialValue: false,
supportEvents: false,
supportBonjour: false,
manfDescription: "Identify Accessory",
designedMaxLength: 1
}]
},{
sType: types.SWITCH_STYPE,
characteristics: [{
cType: types.NAME_CTYPE,
onUpdate: null,
perms: ["pr"],
format: "string",
initialValue: "Away Mode",
supportEvents: false,
supportBonjour: false,
manfDescription: "Away Mode service",
designedMaxLength: 255
},{
cType: types.POWER_STATE_CTYPE,
onUpdate: function(value) { that.armWithType(value, "away"); },
perms: ["pw","pr","ev"],
format: "bool",
initialValue: false,
supportEvents: false,
supportBonjour: false,
manfDescription: "Turn on the Away alarm",
designedMaxLength: 1
}]
},{
sType: types.SWITCH_STYPE,
characteristics: [{
cType: types.NAME_CTYPE,
onUpdate: null,
perms: ["pr"],
format: "string",
initialValue: "Night Mode",
supportEvents: false,
supportBonjour: false,
manfDescription: "Night Mode service",
designedMaxLength: 255
},{
cType: types.POWER_STATE_CTYPE,
onUpdate: function(value) { that.armWithType(value, "night"); },
perms: ["pw","pr","ev"],
format: "bool",
initialValue: false,
supportEvents: false,
supportBonjour: false,
manfDescription: "Turn on the Night alarm",
designedMaxLength: 1
}]
},{
sType: types.SWITCH_STYPE,
characteristics: [{
cType: types.NAME_CTYPE,
onUpdate: null,
perms: ["pr"],
format: "string",
initialValue: "Stay Mode",
supportEvents: false,
supportBonjour: false,
manfDescription: "Stay Mode service",
designedMaxLength: 255
},{
cType: types.POWER_STATE_CTYPE,
onUpdate: function(value) { that.armWithType(value, "stay"); },
perms: ["pw","pr","ev"],
format: "bool",
initialValue: false,
supportEvents: false,
supportBonjour: false,
manfDescription: "Turn on the Stay alarm",
designedMaxLength: 1
}]
}];
}
};
// Enable cookie handling and append our expected headers
request = request.defaults({
headers: {
"X-appkey": "comcastTokenKey",
"X-ClientInfo": "5.2.51",
"X-format": "json"
}
});
module.exports.accessory = XfinityHomeAccessory;

130
accessories/iControl.js Normal file
View File

@@ -0,0 +1,130 @@
var iControl = require('node-icontrol').iControl;
var Service = require('HAP-NodeJS').Service;
var Characteristic = require('HAP-NodeJS').Characteristic;
module.exports = {
accessory: iControlAccessory
}
/**
* Provides a Security System accessory for an iControl-based security system like Xfinity Home.
*/
function iControlAccessory(log, config) {
this.log = log;
this.iControl = new iControl({
system: iControl.Systems[config.system],
email: config.email,
password: config.password,
pinCode: config.pin
});
this.iControl.on('change', this._handleChange.bind(this));
this.iControl.on('error', this._handleError.bind(this));
this.log("Logging into iControl...");
this.iControl.login();
this._securitySystem = new Service.SecuritySystem("Security System");
this._securitySystem
.getCharacteristic(Characteristic.SecuritySystemTargetState)
.on('get', this._getTargetState.bind(this))
.on('set', this._setTargetState.bind(this));
this._securitySystem
.getCharacteristic(Characteristic.SecuritySystemCurrentState)
.on('get', this._getCurrentState.bind(this));
}
iControlAccessory.prototype._getTargetState = function(callback) {
this.iControl.getArmState(function(err, armState) {
if (err) return callback(err);
var currentState = this._getHomeKitStateFromArmState(armState);
callback(null, currentState);
}.bind(this));
}
iControlAccessory.prototype._getCurrentState = function(callback) {
this.iControl.getArmState(function(err, armState) {
if (err) return callback(err);
var currentState = this._getHomeKitStateFromArmState(armState);
callback(null, currentState);
}.bind(this));
}
iControlAccessory.prototype._setTargetState = function(targetState, callback, context) {
if (context == "internal") return callback(null); // we set this state ourself, no need to react to it
var armState = this._getArmStateFromHomeKitState(targetState);
this.log("Setting target state to %s", armState);
this.iControl.setArmState(armState, function(err) {
if (err) return callback(err);
this.log("Successfully set target state to %s", armState);
// also update current state
this._securitySystem
.getCharacteristic(Characteristic.SecuritySystemCurrentState)
.setValue(targetState);
callback(null); // success!
}.bind(this));
}
iControlAccessory.prototype._handleChange = function(armState) {
this.log("Arm state changed to %s", armState);
var homeKitState = this._getHomeKitStateFromArmState(armState);
this._securitySystem
.getCharacteristic(Characteristic.SecuritySystemCurrentState)
.setValue(homeKitState);
this._securitySystem
.getCharacteristic(Characteristic.SecuritySystemTargetState)
.setValue(homeKitState, null, "internal"); // these characteristics happen to share underlying values
}
iControlAccessory.prototype._handleError = function(err) {
this.log(err.message);
}
iControlAccessory.prototype.getServices = function() {
return [this._securitySystem];
}
iControlAccessory.prototype._getHomeKitStateFromArmState = function(armState) {
switch (armState) {
case "disarmed": return Characteristic.SecuritySystemCurrentState.DISARMED;
case "away": return Characteristic.SecuritySystemCurrentState.AWAY_ARM;
case "night": return Characteristic.SecuritySystemCurrentState.NIGHT_ARM;
case "stay": return Characteristic.SecuritySystemCurrentState.STAY_ARM;
}
}
iControlAccessory.prototype._getArmStateFromHomeKitState = function(homeKitState) {
switch (homeKitState) {
case Characteristic.SecuritySystemCurrentState.DISARMED: return "disarmed";
case Characteristic.SecuritySystemCurrentState.AWAY_ARM: return "away";
case Characteristic.SecuritySystemCurrentState.NIGHT_ARM: return "night";
case Characteristic.SecuritySystemCurrentState.STAY_ARM: return "stay";
}
}
/**
* TESTING
*/
if (require.main === module) {
var config = JSON.parse(require('fs').readFileSync("config.json")).accessories[0];
var accessory = new iControlAccessory(console.log, config);
}

650
accessories/knxdevice.js Normal file
View File

@@ -0,0 +1,650 @@
/*
* This is a KNX universal accessory shim.
*
*
*/
var Service = require("HAP-NodeJS").Service;
var Characteristic = require("HAP-NodeJS").Characteristic;
var knxd = require("eibd");
var knxd_registerGA = require('../platforms/KNX.js').registerGA;
var knxd_startMonitor = require('../platforms/KNX.js').startMonitor;
var milliTimeout = 300; // used to block responses while swiping
function KNXDevice(log, config) {
this.log = log;
// everything in one object, do not copy individually
this.config = config;
log("Accessory constructor called");
if (config.name) {
this.name = config.name;
}
if (config.knxd_ip){
this.knxd_ip = config.knxd_ip;
} else {
throw new Error("MISSING KNXD IP");
}
if (config.knxd_port){
this.knxd_port = config.knxd_port;
} else {
throw new Error("MISSING KNXD PORT");
}
}
//debugging helper only
//inspects an object and prints its properties (also inherited properties)
var iterate = function nextIteration(myObject, path){
// this function iterates over all properties of an object and print them to the console
// when finding objects it goes one level deeper
var name;
if (!path){
console.log("---iterating--------------------")
}
for (name in myObject) {
if (typeof myObject[name] !== 'function') {
if (typeof myObject[name] !== 'object' ) {
console.log((path || "") + name + ': ' + myObject[name]);
} else {
nextIteration(myObject[name], path ? path + name + "." : name + ".");
}
} else {
console.log((path || "") + name + ': (function)' );
}
}
if (!path) {
console.log("================================");
}
};
module.exports = {
accessory: KNXDevice
};
KNXDevice.prototype = {
// all purpose / all types write function
knxwrite: function(callback, groupAddress, dpt, value) {
// this.log("DEBUG in knxwrite");
var knxdConnection = new knxd.Connection();
// this.log("DEBUG in knxwrite: created empty connection, trying to connect socket to "+this.knxd_ip+":"+this.knxd_port);
knxdConnection.socketRemote({ host: this.knxd_ip, port: this.knxd_port }, function() {
var dest = knxd.str2addr(groupAddress);
// this.log("DEBUG got dest="+dest);
knxdConnection.openTGroup(dest, 1, function(err) {
if (err) {
this.log("[ERROR] knxwrite:openTGroup: " + err);
callback(err);
} else {
// this.log("DEBUG opened TGroup ");
var msg = knxd.createMessage('write', dpt, parseFloat(value));
knxdConnection.sendAPDU(msg, function(err) {
if (err) {
this.log("[ERROR] knxwrite:sendAPDU: " + err);
callback(err);
} else {
// this.log("knx data sent");
callback();
}
}.bind(this));
}
}.bind(this));
}.bind(this));
},
// issues an all purpose read request on the knx bus
// DOES NOT WAIT for an answer. Please register the address with a callback using registerGA() function
knxread: function(groupAddress){
// this.log("DEBUG in knxread");
if (!groupAddress) {
return null;
}
var knxdConnection = new knxd.Connection();
// this.log("DEBUG in knxread: created empty connection, trying to connect socket to "+this.knxd_ip+":"+this.knxd_port);
knxdConnection.socketRemote({ host: this.knxd_ip, port: this.knxd_port }, function() {
var dest = knxd.str2addr(groupAddress);
// this.log("DEBUG got dest="+dest);
knxdConnection.openTGroup(dest, 1, function(err) {
if (err) {
this.log("[ERROR] knxread:openTGroup: " + err);
} else {
// this.log("DEBUG knxread: opened TGroup ");
var msg = knxd.createMessage('read', 'DPT1', 0);
knxdConnection.sendAPDU(msg, function(err) {
if (err) {
this.log("[ERROR] knxread:sendAPDU: " + err);
} else {
this.log("knx request sent for "+groupAddress);
}
}.bind(this));
}
}.bind(this));
}.bind(this));
},
// issuing multiple read requests at once
knxreadarray: function (groupAddresses) {
if (groupAddresses.constructor.toString().indexOf("Array") > -1) {
// handle multiple addresses
for (var i = 0; i < groupAddresses.length; i++) {
if (groupAddresses[i]) { // do not bind empty addresses
this.knxread (groupAddresses[i]);
}
}
} else {
// it's only one
this.knxread (groupAddresses);
}
},
// special types
knxwrite_percent: function(callback, groupAddress, value) {
var numericValue = 0;
if (value && value>=0 && value <= 100) {
numericValue = 255*value/100; // convert 1..100 to 1..255 for KNX bus
} else {
this.log("[ERROR] Percentage value ot of bounds ");
numericValue = 0;
}
this.knxwrite(callback, groupAddress,'DPT5',numericValue);
},
// need to spit registers into types
// boolean: get 0 or 1 from the bus, write boolean
knxregister_bool: function(addresses, characteristic) {
this.log("knx registering BOOLEAN " + addresses);
knxd_registerGA(addresses, function(val, src, dest, type){
this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type"+type + " for " + characteristic.displayName);
// iterate(characteristic);
characteristic.setValue(val ? 1 : 0, undefined, 'fromKNXBus');
}.bind(this));
},
knxregister_boolReverse: function(addresses, characteristic) {
this.log("knx registering BOOLEAN " + addresses);
knxd_registerGA(addresses, function(val, src, dest, type){
this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type"+type + " for " + characteristic.displayName);
// iterate(characteristic);
characteristic.setValue(val ? 0 : 1, undefined, 'fromKNXBus');
}.bind(this));
},
// percentage: get 0..255 from the bus, write 0..100 to characteristic
knxregister_percent: function(addresses, characteristic) {
this.log("knx registering PERCENT " + addresses);
knxd_registerGA(addresses, function(val, src, dest, type){
this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type"+type+ " for " + characteristic.displayName);
if (type !== "DPT5") {
this.log("[ERROR] Received value cannot be a percentage value");
} else {
if (!characteristic.timeout) {
if (characteristic.timeout < Date.now()) {
characteristic.setValue(Math.round(val/255*100), undefined, 'fromKNXBus');
} else {
this.log("Blackout time");
}
} else {
characteristic.setValue(Math.round(val/255*100), undefined, 'fromKNXBus');
} // todo get the boolean logic right into one OR expresssion
}
}.bind(this));
},
// float
knxregister_float: function(addresses, characteristic) {
this.log("knx registering FLOAT " + addresses);
knxd_registerGA(addresses, function(val, src, dest, type){
this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type"+type+ " for " + characteristic.displayName);
var hk_value = Math.round(val*10)/10;
if (hk_value>=characteristic.minimumValue && hk_value<=characteristic.maximumValue) {
characteristic.setValue(hk_value, undefined, 'fromKNXBus'); // 1 decoimal for HomeKit
} else {
this.log("Value %s out of bounds %s...%s ",hk_value, characteristic.minimumValue, characteristic.maximumValue);
}
}.bind(this));
},
// what about HVAC heating cooling types?
knxregister_HVAC: function(addresses, characteristic) {
this.log("knx registering HVAC " + addresses);
knxd_registerGA(addresses, function(val, src, dest, type){
this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type"+type+ " for " + characteristic.displayName);
var HAPvalue = 0;
switch (val){
case 0:
HAPvalue = 1;
break;
case 1:
HAPvalue = 1;
break;
case 2:
HAPvalue = 1;
break;
case 3:
HAPvalue = 1;
break;
case 4:
HAPvalue = 0;
break;
default:
HAPvalue = 0;
}
characteristic.setValue(HAPvalue, undefined, 'fromKNXBus');
}.bind(this));
},
// to do! KNX: DPT 20.102 = One Byte like DPT5
// 0 = Auto
// 1 = Comfort
// 2 = Standby
// 3 = Night
// 4 = Freezing/Heat Protection
// 5 255 = not allowed”
// The value property of TargetHeatingCoolingState must be one of the following:
// Characteristic.TargetHeatingCoolingState.OFF = 0;
// Characteristic.TargetHeatingCoolingState.HEAT = 1;
// Characteristic.TargetHeatingCoolingState.COOL = 2;
// Characteristic.TargetHeatingCoolingState.AUTO = 3;
// undefined, has to match!
knxregister: function(addresses, characteristic) {
this.log("knx registering " + addresses);
knxd_registerGA(addresses, function(val, src, dest, type){
this.log("Received value from bus:"+val+ " for " +dest+ " from "+src+" of type"+type+ " for " + characteristic.displayName);
characteristic.setValue(val, undefined, 'fromKNXBus');
}.bind(this));
},
/*
* set methods used for creating callbacks, such as
* var Characteristic = myService.addCharacteristic(new Characteristic.Brightness())
* .on('set', function(value, callback, context) {
* this.setPercentage(value, callback, context, this.config[index].Set)
* }.bind(this));
*
*/
setBooleanState: function(value, callback, context, gaddress) {
if (context === 'fromKNXBus') {
this.log(gaddress + " event ping pong, exit!");
if (callback) {
callback();
}
} else {
var numericValue = 0;
if (value) {
numericValue = 1; // need 0 or 1, not true or something
}
this.log("Setting "+gaddress+" Boolean to %s", numericValue);
this.knxwrite(callback, gaddress,'DPT1',numericValue);
}
},
setBooleanReverseState: function(value, callback, context, gaddress) {
if (context === 'fromKNXBus') {
this.log(gaddress + " event ping pong, exit!");
if (callback) {
callback();
}
} else {
var numericValue = 0;
if (!value) {
numericValue = 1; // need 0 or 1, not true or something
}
this.log("Setting "+gaddress+" Boolean to %s", numericValue);
this.knxwrite(callback, gaddress,'DPT1',numericValue);
}
},
setPercentage: function(value, callback, context, gaddress) {
if (context === 'fromKNXBus') {
this.log("event ping pong, exit!");
if (callback) {
callback();
}
} else {
var numericValue = 0;
if (value) {
numericValue = Math.round(255*value/100); // convert 1..100 to 1..255 for KNX bus
}
this.log("Setting "+gaddress+" percentage to %s (%s)", value, numericValue);
this.knxwrite(callback, gaddress,'DPT5',numericValue);
}
},
setFloat: function(value, callback, context, gaddress) {
if (context === 'fromKNXBus') {
this.log(gaddress + " event ping pong, exit!");
if (callback) {
callback();
}
} else {
var numericValue = 0;
if (value) {
numericValue = value; // homekit expects precision of 1 decimal
}
this.log("Setting "+gaddress+" Float to %s", numericValue);
this.knxwrite(callback, gaddress,'DPT9',numericValue);
}
},
setHVACState: function(value, callback, context, gaddress) {
if (context === 'fromKNXBus') {
this.log(gaddress + " event ping pong, exit!");
if (callback) {
callback();
}
} else {
var numericValue = 0;
switch (value){
case 0:
KNXvalue = 4;
break;
case 1:
KNXvalue = 1;
break;
case 2:
KNXvalue = 1;
break;
case 3:
KNXvalue = 1;
break;
default:
KNXvalue = 1;
}
this.log("Setting "+gaddress+" HVAC to %s", KNXvalue);
this.knxwrite(callback, gaddress,'DPT5',KNXvalue);
}
},
identify: function(callback) {
this.log("Identify requested!");
callback(); // success
},
/*
* function getXXXXXXXService(config)
*
* returns a configured service object to the caller (accessory/device)
*
*/
bindCharacteristic: function(myService, characteristicType, valueType, config) {
var myCharacteristic = myService.getCharacteristic(characteristicType);
if (myCharacteristic === undefined) {
throw new Error("unknown characteristics cannot be bound");
}
if (config.Set) {
// can write
switch (valueType) {
case "Bool":
myCharacteristic.on('set', function(value, callback, context) {
this.setBooleanState(value, callback, context, config.Set);
}.bind(this));
break;
case "BoolReverse":
myCharacteristic.on('set', function(value, callback, context) {
this.setBooleanReverseState(value, callback, context, config.Set);
}.bind(this));
break;
case "Percent":
myCharacteristic.on('set', function(value, callback, context) {
this.setPercentage(value, callback, context, config.Set);
myCharacteristic.timeout = Date.now()+milliTimeout;
}.bind(this));
break;
case "Float":
myCharacteristic.on('set', function(value, callback, context) {
this.setFloat(value, callback, context, config.Set);
}.bind(this));
break;
case "HVAC":
myCharacteristic.on('set', function(value, callback, context) {
this.setHVACState(value, callback, context, config.Set);
}.bind(this));
break;
default:
this.log("[ERROR] unknown type passed");
throw new Error("[ERROR] unknown type passed");
}
}
if ([config.Set].concat(config.Listen || []).length>0) {
//this.log("Binding LISTEN");
// can read
switch (valueType) {
case "Bool":
this.knxregister_bool([config.Set].concat(config.Listen || []), myCharacteristic);
break;
case "BoolReverse":
this.knxregister_boolReverse([config.Set].concat(config.Listen || []), myCharacteristic);
break;
case "Percent":
this.knxregister_percent([config.Set].concat(config.Listen || []), myCharacteristic);
break;
case "Float":
this.knxregister_float([config.Set].concat(config.Listen || []), myCharacteristic);
break;
case "HVAC":
this.knxregister_HVAC([config.Set].concat(config.Listen || []), myCharacteristic);
break;
default:
this.log("[ERROR] unknown type passed");
throw new Error("[ERROR] unknown type passed");
}
this.log("Issuing read requests on the KNX bus...");
this.knxreadarray([config.Set].concat(config.Listen || []));
}
return myCharacteristic; // for chaining or whatsoever
},
getLightbulbService: function(config) {
// some sanity checks
//this.config = config;
if (config.type !== "Lightbulb") {
this.log("[ERROR] Lightbulb Service for non 'Lightbulb' service called");
return undefined;
}
if (!config.name) {
this.log("[ERROR] Lightbulb Service without 'name' property called");
return undefined;
}
var myService = new Service.Lightbulb(config.name,config.name);
// On (and Off)
if (config.On) {
this.log("Lightbulb on/off characteristic enabled");
this.bindCharacteristic(myService, Characteristic.On, "Bool", config.On);
} // On characteristic
// Brightness if available
if (config.Brightness) {
this.log("Lightbulb Brightness characteristic enabled");
myService.addCharacteristic(Characteristic.Brightness); // it's an optional
this.bindCharacteristic(myService, Characteristic.Brightness, "Percent", config.Brightness);
}
// Hue and Saturation could be added here if available in KNX lamps
//iterate(myService);
return myService;
},
getLockMechanismService: function(config) {
// some sanity checks
//this.config = config;
// Characteristic.LockCurrentState.UNSECURED = 0;
// Characteristic.LockCurrentState.SECURED = 1;
if (config.type !== "LockMechanism") {
this.log("[ERROR] LockMechanism Service for non 'LockMechanism' service called");
return undefined;
}
if (!config.name) {
this.log("[ERROR] LockMechanism Service without 'name' property called");
return undefined;
}
var myService = new Service.LockMechanism(config.name,config.name);
// LockCurrentState
if (config.LockCurrentState) {
// for normal contacts: Secured = 1
this.log("LockMechanism LockCurrentState characteristic enabled");
this.bindCharacteristic(myService, Characteristic.LockCurrentState, "Bool", config.LockCurrentState);
} else if (config.LockCurrentStateSecured0) {
// for reverse contacts Secured = 0
this.log("LockMechanism LockCurrentState characteristic enabled");
this.bindCharacteristic(myService, Characteristic.LockCurrentState, "BoolReverse", config.LockCurrentStateSecured0);
}
// LockTargetState
if (config.LockTargetState) {
this.log("LockMechanism LockTargetState characteristic enabled");
this.bindCharacteristic(myService, Characteristic.LockTargetState, "Bool", config.LockTargetState);
} else if (config.LockTargetStateSecured0) {
this.log("LockMechanism LockTargetState characteristic enabled");
this.bindCharacteristic(myService, Characteristic.LockTargetState, "BoolReverse", config.LockTargetStateSecured0);
}
//iterate(myService);
return myService;
},
getThermostatService: function(config) {
// // Required Characteristics
// this.addCharacteristic(Characteristic.CurrentHeatingCoolingState);
// this.addCharacteristic(Characteristic.TargetHeatingCoolingState);
// this.addCharacteristic(Characteristic.CurrentTemperature); //check
// this.addCharacteristic(Characteristic.TargetTemperature); //
// this.addCharacteristic(Characteristic.TemperatureDisplayUnits);
//
// // Optional Characteristics
// this.addOptionalCharacteristic(Characteristic.CurrentRelativeHumidity);
// this.addOptionalCharacteristic(Characteristic.TargetRelativeHumidity);
// this.addOptionalCharacteristic(Characteristic.CoolingThresholdTemperature);
// this.addOptionalCharacteristic(Characteristic.HeatingThresholdTemperature);
// some sanity checks
if (config.type !== "Thermostat") {
this.log("[ERROR] Thermostat Service for non 'Thermostat' service called");
return undefined;
}
if (!config.name) {
this.log("[ERROR] Thermostat Service without 'name' property called");
return undefined;
}
var myService = new Service.Thermostat(config.name,config.name);
// CurrentTemperature)
if (config.CurrentTemperature) {
this.log("Thermostat CurrentTemperature characteristic enabled");
this.bindCharacteristic(myService, Characteristic.CurrentTemperature, "Float", config.CurrentTemperature);
}
// TargetTemperature if available
if (config.TargetTemperature) {
this.log("Thermostat TargetTemperature characteristic enabled");
// default boundary too narrow for thermostats
myService.getCharacteristic(Characteristic.TargetTemperature).minimumValue=0; // °C
myService.getCharacteristic(Characteristic.TargetTemperature).maximumValue=40; // °C
this.bindCharacteristic(myService, Characteristic.TargetTemperature, "Float", config.TargetTemperature);
}
// HVAC missing yet
if (config.CurrentHeatingCoolingState) {
this.log("Thermostat CurrentHeatingCoolingState characteristic enabled");
this.bindCharacteristic(myService, Characteristic.CurrentHeatingCoolingState, "HVAC", config.CurrentHeatingCoolingState);
}
return myService;
},
// temperature sensor type (iOS9 assumed)
getTemperatureSensorService: function(config) {
// some sanity checks
if (config.type !== "TemperatureSensor") {
this.log("[ERROR] TemperatureSensor Service for non 'TemperatureSensor' service called");
return undefined;
}
if (!config.name) {
this.log("[ERROR] TemperatureSensor Service without 'name' property called");
return undefined;
}
var myService = new Service.TemperatureSensor(config.name,config.name);
// CurrentTemperature)
if (config.CurrentTemperature) {
this.log("Thermostat CurrentTemperature characteristic enabled");
this.bindCharacteristic(myService, Characteristic.CurrentTemperature, "Float", config.CurrentTemperature);
}
return myService;
},
/* assemble the device ***************************************************************************************************/
getServices: function() {
// you can OPTIONALLY create an information service if you wish to override
// the default values for things like serial number, model, etc.
var accessoryServices = [];
var informationService = new Service.AccessoryInformation();
informationService
.setCharacteristic(Characteristic.Manufacturer, "Opensource Community")
.setCharacteristic(Characteristic.Model, "KNX Universal Device")
.setCharacteristic(Characteristic.SerialNumber, "Version 1.1");
accessoryServices.push(informationService);
iterate(this.config);
// throw new Error("STOP");
if (!this.config.services){
this.log("No services found in accessory?!")
}
var currServices = this.config.services;
this.log("Preparing Services: " + currServices.length)
// go through the config thing and look for services
for (var int = 0; int < currServices.length; int++) {
var configService = currServices[int];
// services need to have type and name properties
if (!configService.type && !configService.name) {
this.log("[ERROR] must specify 'type' and 'name' properties for each service in config.json. KNX platform section fault ");
throw new Error("Must specify 'type' and 'name' properties for each service in config.json");
}
switch (configService.type) {
case "Lightbulb":
accessoryServices.push(this.getLightbulbService(configService));
break;
case "LockMechanism":
accessoryServices.push(this.getLockMechanismService(configService));
break;
case "TemperatureSensor":
accessoryServices.push(this.getTemperatureSensorService(configService));
break;
case "Thermostat":
accessoryServices.push(this.getThermostatService(configService));
break;
default:
this.log("[ERROR] unknown 'type' property for service "+ configService.name + " in config.json. KNX platform section fault ");
//throw new Error("[ERROR] unknown 'type' property for service "+ configService.name + " in config.json. KNX platform section fault ");
}
}
// start listening for events on the bus (if not started yet - will prevent itself)
knxd_startMonitor({ host: this.knxd_ip, port: this.knxd_port });
return accessoryServices;
}
};

275
app.js
View File

@@ -1,10 +1,24 @@
var fs = require('fs');
var path = require('path');
var storage = require('node-persist');
var crypto = require('crypto');
var hap = require('HAP-NodeJS');
var uuid = require('HAP-NodeJS').uuid;
var Bridge = require('HAP-NodeJS').Bridge;
var Accessory = require('HAP-NodeJS').Accessory;
var Service = require('HAP-NodeJS').Service;
var Characteristic = require('HAP-NodeJS').Characteristic;
var accessoryLoader = require('HAP-NodeJS').AccessoryLoader;
var once = require('HAP-NodeJS/lib/util/once').once;
console.log("Starting HomeBridge server...");
console.log("_____________________________________________________________________");
console.log("IMPORTANT: Homebridge is in the middle of some big changes.");
console.log(" Read more about it here:");
console.log(" https://github.com/nfarina/homebridge/wiki/Migration-Guide");
console.log("_____________________________________________________________________");
console.log("");
// Look for the configuration file
var configPath = path.join(__dirname, "config.json");
@@ -14,176 +28,185 @@ if (!fs.existsSync(configPath)) {
process.exit(1);
}
// Initialize persistent storage
storage.initSync();
// Initialize HAP-NodeJS
hap.init();
// Load up the configuration file
var config = JSON.parse(fs.readFileSync(configPath));
var config;
try {
config = JSON.parse(fs.readFileSync(configPath));
}
catch (err) {
console.log("There was a problem reading your config.json file.");
console.log("Please try pasting your config.json file here to validate it: http://jsonlint.com");
console.log("");
throw err;
}
// Just to prevent them getting garbage collected
var accessories = [];
// pull out our custom Bridge settings from config.json, if any
var bridgeConfig = config.bridge || {};
// Start by creating our Bridge which will host all loaded Accessories
var bridge = new Bridge(bridgeConfig.name || 'Homebridge', uuid.generate("HomeBridge"));
// keep track of async calls we're waiting for callbacks on before we can start up
// this is hacky but this is all going away once we build proper plugin support
var asyncCalls = 0;
var asyncWait = false;
function startup() {
asyncWait = true;
if (config.platforms) loadPlatforms();
if (config.accessories) loadAccessories();
asyncWait = false;
// publish now unless we're waiting on anyone
if (asyncCalls == 0)
publish();
}
function loadAccessories() {
// Instantiate all accessories in the config
console.log("Loading " + config.accessories.length + " accessories...");
for (var i=0; i<config.accessories.length; i++) {
var accessoryConfig = config.accessories[i];
// Load up the class for this accessory
var accessoryName = accessoryConfig["accessory"]; // like "WeMo"
var accessoryModule = require('./accessories/' + accessoryName + ".js"); // like "./accessories/WeMo.js"
var accessoryType = accessoryConfig["accessory"]; // like "WeMo"
var accessoryModule = require('./accessories/' + accessoryType + ".js"); // like "./accessories/WeMo.js"
var accessoryConstructor = accessoryModule.accessory; // like "WeMoAccessory", a JavaScript constructor
// Create a custom logging function that prepends the device display name for debugging
var name = accessoryConfig["name"];
var log = function(name) { return function(s) { console.log("[" + name + "] " + s); }; }(name);
var accessoryName = accessoryConfig["name"];
var log = createLog(accessoryName);
log("Initializing " + accessoryName + " accessory...");
var accessory = new accessoryConstructor(log, accessoryConfig);
accessories.push(accessory);
// Extract the raw "services" for this accessory which is a big array of objects describing the various
// hooks in and out of HomeKit for the HAP-NodeJS server.
var services = accessory.getServices();
// Create the HAP server for this accessory
createHAPServer(name, services, accessory.transportCategory);
log("Initializing %s accessory...", accessoryType);
var accessoryInstance = new accessoryConstructor(log, accessoryConfig);
var accessory = createAccessory(accessoryInstance, accessoryName);
// add it to the bridge
bridge.addBridgedAccessory(accessory);
}
}
function loadPlatforms() {
console.log("Loading " + config.platforms.length + " platforms...");
for (var i=0; i<config.platforms.length; i++) {
var platformConfig = config.platforms[i];
// Load up the class for this accessory
var platformName = platformConfig["platform"]; // like "Wink"
var platformModule = require('./platforms/' + platformName + ".js"); // like "./platforms/Wink.js"
var platformType = platformConfig["platform"]; // like "Wink"
var platformName = platformConfig["name"];
var platformModule = require('./platforms/' + platformType + ".js"); // like "./platforms/Wink.js"
var platformConstructor = platformModule.platform; // like "WinkPlatform", a JavaScript constructor
// Create a custom logging function that prepends the platform display name for debugging
var name = platformConfig["name"];
var log = function(name) { return function(s) { console.log("[" + name + "] " + s); }; }(name);
// Create a custom logging function that prepends the platform name for debugging
var log = createLog(platformName);
log("Initializing " + platformName + " platform...");
log("Initializing %s platform...", platformType);
var platform = new platformConstructor(log, platformConfig);
// query for devices
platform.accessories(function(foundAccessories){
// loop through accessories adding them to the list and registering them
for (var i = 0; i < foundAccessories.length; i++) {
accessory = foundAccessories[i]
accessories.push(accessory);
log("Initializing device with name " + accessory.name + "...")
// Extract the raw "services" for this accessory which is a big array of objects describing the various
// hooks in and out of HomeKit for the HAP-NodeJS server.
var services = accessory.getServices();
// Create the HAP server for this accessory
createHAPServer(accessory.name, services, accessory.transportCategory);
}
accessories.push.apply(accessories, foundAccessories);
})
var platformInstance = new platformConstructor(log, platformConfig);
loadPlatformAccessories(platformInstance, log);
}
}
//
// Creates the actual HAP servers which listen on different sockets
//
function loadPlatformAccessories(platformInstance, log) {
asyncCalls++;
platformInstance.accessories(once(function(foundAccessories){
asyncCalls--;
// loop through accessories adding them to the list and registering them
for (var i = 0; i < foundAccessories.length; i++) {
var accessoryInstance = foundAccessories[i];
var accessoryName = accessoryInstance.name; // assume this property was set
log("Initializing platform accessory '%s'...", accessoryName);
var accessory = createAccessory(accessoryInstance, accessoryName);
// Pull in required HAP-NodeJS stuff
var accessory_Factor = new require("HAP-NodeJS/Accessory.js");
var accessoryController_Factor = new require("HAP-NodeJS/AccessoryController.js");
var service_Factor = new require("HAP-NodeJS/Service.js");
var characteristic_Factor = new require("HAP-NodeJS/Characteristic.js");
// Each accessory has its own little server. We'll need to allocate some ports for these servers
var nextPort = 51826;
var nextServer = 0;
var accessoryServers = [];
var accessoryControllers = [];
var usernames = {};
function createHAPServer(name, services, transportCategory) {
var accessoryController = new accessoryController_Factor.AccessoryController();
//loop through services
for (var j = 0; j < services.length; j++) {
var service = new service_Factor.Service(services[j].sType);
//loop through characteristics
for (var k = 0; k < services[j].characteristics.length; k++) {
var options = {
onRead: services[j].characteristics[k].onRead,
onRegister: services[j].characteristics[k].onRegister,
type: services[j].characteristics[k].cType,
perms: services[j].characteristics[k].perms,
format: services[j].characteristics[k].format,
initialValue: services[j].characteristics[k].initialValue,
supportEvents: services[j].characteristics[k].supportEvents,
supportBonjour: services[j].characteristics[k].supportBonjour,
manfDescription: services[j].characteristics[k].manfDescription,
designedMaxLength: services[j].characteristics[k].designedMaxLength,
designedMinValue: services[j].characteristics[k].designedMinValue,
designedMaxValue: services[j].characteristics[k].designedMaxValue,
designedMinStep: services[j].characteristics[k].designedMinStep,
unit: services[j].characteristics[k].unit
};
var characteristic = new characteristic_Factor.Characteristic(options, services[j].characteristics[k].onUpdate);
service.addCharacteristic(characteristic);
}
accessoryController.addService(service);
}
// create a unique "username" for this accessory based on the default display name
var username = createUsername(name);
if (usernames[username]) {
console.log("Cannot create another accessory with the same name '" + name + "'. The 'name' property must be unique for each accessory.");
return;
}
// remember that we used this name already
usernames[username] = name;
// increment ports for each accessory
nextPort = nextPort + (nextServer*2);
// hardcode the PIN to something random (same PIN as HAP-NodeJS sample accessories)
var pincode = "031-45-154";
var accessory = new accessory_Factor.Accessory(name, username, storage, parseInt(nextPort), pincode, accessoryController, transportCategory);
accessoryServers[nextServer] = accessory;
accessoryControllers[nextServer] = accessoryController;
accessory.publishAccessory();
nextServer++;
// add it to the bridge
bridge.addBridgedAccessory(accessory);
}
// were we the last callback?
if (asyncCalls === 0 && !asyncWait)
publish();
}));
}
// Creates a unique "username" for HomeKit from a hash of the given string
function createUsername(str) {
function createAccessory(accessoryInstance, displayName) {
var services = accessoryInstance.getServices();
if (!(services[0] instanceof Service)) {
// The returned "services" for this accessory is assumed to be the old style: a big array
// of JSON-style objects that will need to be parsed by HAP-NodeJS's AccessoryLoader.
// Hash str into something like "098F6BCD4621D373CADE4E832627B4F6"
var hash = crypto.createHash('md5').update(str).digest("hex").toUpperCase();
// Create the actual HAP-NodeJS "Accessory" instance
return accessoryLoader.parseAccessoryJSON({
displayName: displayName,
services: services
});
}
else {
// The returned "services" for this accessory are simply an array of new-API-style
// Service instances which we can add to a created HAP-NodeJS Accessory directly.
var accessoryUUID = uuid.generate(accessoryInstance.constructor.name + ":" + displayName);
var accessory = new Accessory(displayName, accessoryUUID);
// listen for the identify event if the accessory instance has defined an identify() method
if (accessoryInstance.identify)
accessory.on('identify', function(paired, callback) { accessoryInstance.identify(callback); });
services.forEach(function(service) {
// if you returned an AccessoryInformation service, merge its values with ours
if (service instanceof Service.AccessoryInformation) {
var existingService = accessory.getService(Service.AccessoryInformation);
// pull out any values you may have defined
var manufacturer = service.getCharacteristic(Characteristic.Manufacturer).value;
var model = service.getCharacteristic(Characteristic.Model).value;
var serialNumber = service.getCharacteristic(Characteristic.SerialNumber).value;
if (manufacturer) existingService.setCharacteristic(Characteristic.Manufacturer, manufacturer);
if (model) existingService.setCharacteristic(Characteristic.Model, model);
if (serialNumber) existingService.setCharacteristic(Characteristic.SerialNumber, serialNumber);
}
else {
accessory.addService(service);
}
});
return accessory;
}
}
// Turn it into a MAC-address-looking "username" for HomeKit
return hash[0] + hash[1] + ":" +
hash[2] + hash[3] + ":" +
hash[4] + hash[5] + ":" +
hash[6] + hash[7] + ":" +
hash[8] + hash[9] + ":" +
hash[10] + hash[11];
// Returns a logging function that prepends messages with the given name in [brackets].
function createLog(name) {
return function(message) {
var rest = Array.prototype.slice.call(arguments, 1 ); // any arguments after message
var args = ["[%s] " + message, name].concat(rest);
console.log.apply(console, args);
}
}
function publish() {
bridge.publish({
username: bridgeConfig.username || "CC:22:3D:E3:CE:30",
port: bridgeConfig.port || 51826,
pincode: bridgeConfig.pin || "031-45-154",
category: Accessory.Categories.OTHER
});
}
startup();

121
config-sample-knx.json Normal file
View File

@@ -0,0 +1,121 @@
{
"bridge": {
"name": "Homebridge",
"username": "CC:22:3D:E3:CE:30",
"port": 51826,
"pin": "031-45-154"
},
"description": "This is an example configuration file for KNX platform shim",
"hint": "Always paste into jsonlint.com validation page before starting your homebridge, saves a lot of frustration",
"platforms": [
{
"platform": "KNX",
"name": "KNX",
"knxd_ip": "192.168.178.205",
"knxd_port": 6720,
"accessories": [
{
"accessory_type": "knxdevice",
"description": "Only generic type knxdevice is supported, all previous knx type have been merged into that.",
"name": "Living Room North Lamp",
"services": [
{
"type": "Lightbulb",
"description": "iOS8 Lightbulb type, supports On (Switch) and Brightness",
"name": "Living Room North Lamp",
"On": {
"Set": "1/1/6",
"Listen": [
"1/1/63"
]
},
"Brightness": {
"Set": "1/1/62",
"Listen": [
"1/1/64"
]
}
}
],
"services-description": "Services is an array, you CAN have multiple service types in one accessory, though it is not fully supported in many iOS HK apps, such as EVE and myTouchHome"
},
{
"accessory_type": "knxdevice",
"name": "Office Temperature",
"description": "iOS8.4.1 TemperatureSensor type, supports CurrentTemperature",
"services": [
{
"type": "TemperatureSensor",
"name": "Raumtemperatur",
"CurrentTemperature": {
"Listen": "3/3/44"
}
}
]
},
{
"accessory_type": "knxdevice",
"name": "Office Window Lock",
"services": [
{
"type": "LockMechanism",
"description": "iOS8 Lock mechanism, Supports LockCurrentStateSecured0 OR LockCurrentState, LockTargetStateSecured0 OR LockTargetState, use depending if LOCKED is 0 or 1",
"name": "Office Window Lock",
"LockCurrentStateSecured0": {
"Listen": "5/3/15"
},
"LockTargetStateSecured0": {
"Listen": "5/3/15"
}
}
]
},
{
"accessory_type": "knxdevice",
"description":"sample device with multiple services. Multiple services of different types are widely supported",
"name": "Office",
"services": [
{
"type": "Lightbulb",
"name": "Office Lamp",
"On": {
"Set": "1/3/5"
}
},
{
"type": "Thermostat",
"description": "iOS8 Thermostat type, supports CurrentTemperature, TargetTemperature, CurrentHeatingCoolingState ",
"name": "Raumtemperatur",
"CurrentTemperature": {
"Listen": "3/3/44"
},
"TargetTemperature": {
"Set": "3/3/94"
},
"CurrentHeatingCoolingState": {
"Listen": "3/3/64"
}
},
{
"type": "WindowCovering",
"description": "iOS9 Window covering (blinds etc) type, still WIP",
"name": "Blinds",
"Target": {
"Set": "address",
"Listen": "adresses"
},
"Current": {
"Set": "address",
"Listen": "adresses"
},
"PositionState": {
"Listen": "adresses"
}
}
]
}
]
}
],
"accessories": []
}

View File

@@ -1,4 +1,11 @@
{
"bridge": {
"name": "Homebridge",
"username": "CC:22:3D:E3:CE:30",
"port": 51826,
"pin": "031-45-154"
},
"description": "This is an example configuration file with all supported devices. You can use this as a template for creating your own configuration file containing devices you actually own.",
"platforms": [
@@ -34,7 +41,9 @@
"platform": "Domoticz",
"name": "Domoticz",
"server": "127.0.0.1",
"port": "8005"
"port": "8080",
"roomid": 0,
"loadscenes": 1
},
{
"platform": "PhilipsHue",
@@ -62,7 +71,25 @@
"platform": "YamahaAVR",
"play_volume": -35,
"setMainInputTo": "AirPlay"
}
},
{
"platform": "ZWayServer",
"url": "http://192.168.1.10:8083/",
"login": "zwayusername",
"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": [
@@ -95,15 +122,15 @@
"password" : "your-carwings-password"
},
{
"accessory": "XfinityHome",
"accessory": "iControl",
"name": "Xfinity Home",
"description": "This shim supports the 'Xfinity Home' security system. Unfortunately I don't know how to generate the 'dsig' property, so you'll need to figure yours out by running the Xfinity Home app on your iOS device while connected to a proxy server like Charles. If you didn't understand any of that, sorry! I welcome any suggestions for how to figure out dsig automatically.",
"email": "your-comcast-email@example.com",
"description": "This shim supports iControl-based security systems like Xfinity Home.",
"system": "XFINITY_HOME",
"email": "your-comcast-email",
"password": "your-comcast-password",
"dsig": "your-digital-signature",
"pin": "your-security-system-pin-code"
},
{
{
"accessory": "HomeMatic",
"name": "Light",
"description": "Control HomeMatic devices (The XMP-API addon for the CCU is required)",
@@ -143,22 +170,19 @@
"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", // IP Address of the WiFi Bridge, or 255.255.255.255 to broadcast to all |
"port": 8899, // Default port 8899 (50000 for v1 or v2 bridge) |
"zone": 1, // Zone to address commands to (not used for rgb only bulbs) |
"type": "rgbw", // Bulb type (rgbw, rgb, white) |
"delay": 35, // Delay between commands sent to the WiFi bridge (default 35) |
"repeat": 3 // Number of times each command is repeated for reliability (default 3) |
},
{
"accessory": "Tesla",
"name": "Tesla",
"description": "This shim supports controlling climate control on the Tesla Model S.",
"username": "tesla_email",
"password" : "tesla_password"
},
{
"accessory": "Hyperion",
"name": "TV Backlight",
"description": "Control the Hyperion TV backlight server. https://github.com/tvdzwan/hyperion",
"host": "localhost",
"port": "19444"
}
]
}

View File

@@ -1,9 +1,9 @@
{
"name": "homebridge",
"description": "HomeKit support for the impatient",
"version": "0.0.0",
"version": "0.1.1",
"scripts": {
"start": "node app.js"
"start": "DEBUG=* node app.js || true"
},
"repository": {
"type": "git",
@@ -13,23 +13,31 @@
"dependencies": {
"ad2usb": "git+https://github.com/alistairg/node-ad2usb.git#local",
"carwingsjs": "0.0.x",
"color": "0.10.x",
"eibd": "^0.3.1",
"elkington": "kevinohara80/elkington",
"hap-nodejs": "git+https://github.com/khaost/HAP-NodeJS#2a1bc8d99a2009317ab5da93faebea34c89f197c",
"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-milight-promise": "0.0.2",
"node-icontrol": "^0.1.4",
"node-milight-promise": "0.0.x",
"node-persist": "0.0.x",
"q": "1.4.x",
"tough-cookie": "^2.0.0",
"request": "2.49.x",
"sonos": "0.8.x",
"telldus-live": "0.2.x",
"teslams": "1.0.1",
"unofficial-nest-api": "git+https://github.com/hachidorii/unofficial_nodejs_nest.git#d8d48edc952b049ff6320ef99afa7b2f04cdee98",
"wemo": "0.2.x",
"wink-js": "0.0.5",
"xml2js": "0.4.x",
"xmldoc": "0.1.x",
"yamaha-nodejs": "0.4.x",
"teslams": "1.0.1"
"debug": "^2.2.0"
}
}

View File

@@ -8,6 +8,15 @@
// - Added support for Scenes
// - Sorting device names
//
// 26 August 2015 [EddyK69]
// - Added parameter in config.json: 'loadscenes' for enabling/disabling loading scenes
// - Fixed issue with dimmer-range; was 0-100, should be 0-16
//
// 27 August 2015 [EddyK69]
// - Fixed issue that 'on/off'-type lights showed as dimmers in HomeKit. Checking now on SwitchType instead of HaveDimmer
// - Fixed issue that 'on-off'-type lights would not react on Siri 'Switch on/off light'; On/Off types are now handled as Lights instead of Switches
// (Cannot determine if 'on/off'-type device is a Light or a Switch :( )
//
// Domoticz JSON API required
// https://www.domoticz.com/wiki/Domoticz_API/JSON_URL's#Lights_and_switches
//
@@ -18,7 +27,8 @@
// "name": "Domoticz",
// "server": "127.0.0.1",
// "port": "8080",
// "roomid": 123 (0=no roomplan)
// "roomid": 123, (0=no roomplan)
// "loadscenes": 1 (0=disable scenes)
// }
// ],
//
@@ -47,6 +57,10 @@ function DomoticzPlatform(log, config){
if (typeof config["roomid"] != 'undefined') {
this.roomid = config["roomid"];
}
this.loadscenes = 1;
if (typeof config["loadscenes"] != 'undefined') {
this.loadscenes = config["loadscenes"];
}
}
function sortByKey(array, key) {
@@ -66,11 +80,17 @@ DomoticzPlatform.prototype = {
},
accessories: function(callback) {
this.log("Fetching Domoticz lights and switches...");
var that = this;
var foundAccessories = [];
if (this.roomid == 0) {
this.log("Fetching Domoticz lights and switches...");
var that = this;
var foundAccessories = [];
// mechanism to ensure callback is only executed once all requests complete
var asyncCalls = 0;
function callbackLater() { if (--asyncCalls == 0) callback(foundAccessories); }
if (this.roomid == 0) {
//Get Lights
asyncCalls++;
request.get({
url: this.urlForQuery("type=devices&filter=light&used=true&order=Name"),
json: true
@@ -79,18 +99,20 @@ DomoticzPlatform.prototype = {
if (json['result'] != undefined) {
var sArray=sortByKey(json['result'],"Name");
sArray.map(function(s) {
accessory = new DomoticzAccessory(that.log, that, false, s.idx, s.Name, s.HaveDimmer, s.MaxDimLevel, (s.SubType=="RGB")||(s.SubType=="RGBW"));
var havedimmer = (s.SwitchType == 'Dimmer')
accessory = new DomoticzAccessory(that.log, that, false, s.idx, s.Name, havedimmer, s.MaxDimLevel, (s.SubType=="RGB")||(s.SubType=="RGBW"));
foundAccessories.push(accessory);
})
}
callback(foundAccessories);
callbackLater();
} else {
that.log("There was a problem connecting to Domoticz. (" + err + ")");
}
});
}
else {
}
else {
//Get all devices specified in the room
asyncCalls++;
request.get({
url: this.urlForQuery("type=devices&plan=" + this.roomid),
json: true
@@ -101,36 +123,39 @@ DomoticzPlatform.prototype = {
sArray.map(function(s) {
//only accept switches for now
if (typeof s.SwitchType != 'undefined') {
accessory = new DomoticzAccessory(that.log, that, false, s.idx, s.Name, s.HaveDimmer, s.MaxDimLevel, (s.SubType=="RGB")||(s.SubType=="RGBW"));
var havedimmer = (s.SwitchType == 'Dimmer')
accessory = new DomoticzAccessory(that.log, that, false, s.idx, s.Name, havedimmer, s.MaxDimLevel, (s.SubType=="RGB")||(s.SubType=="RGBW"));
foundAccessories.push(accessory);
}
})
}
callback(foundAccessories);
callbackLater();
} else {
that.log("There was a problem connecting to Domoticz.");
}
});
}
}
//Get Scenes
foundAccessories = [];
request.get({
url: this.urlForQuery("type=scenes"),
json: true
}, function(err, response, json) {
if (!err && response.statusCode == 200) {
if (json['result'] != undefined) {
var sArray=sortByKey(json['result'],"Name");
sArray.map(function(s) {
accessory = new DomoticzAccessory(that.log, that, true, s.idx, s.Name, false, 0, false);
foundAccessories.push(accessory);
})
if (this.loadscenes == 1) {
asyncCalls++;
request.get({
url: this.urlForQuery("type=scenes"),
json: true
}, function(err, response, json) {
if (!err && response.statusCode == 200) {
if (json['result'] != undefined) {
var sArray=sortByKey(json['result'],"Name");
sArray.map(function(s) {
accessory = new DomoticzAccessory(that.log, that, true, s.idx, s.Name, false, 0, false);
foundAccessories.push(accessory);
})
}
callbackLater();
} else {
that.log("There was a problem connecting to Domoticz.");
}
callback(foundAccessories);
} else {
that.log("There was a problem connecting to Domoticz.");
}
});
});
}
}
}
@@ -158,6 +183,9 @@ DomoticzAccessory.prototype = {
url = this.platform.urlForQuery("type=command&param=setcolbrightnessvalue&idx=" + this.idx + "&hue=" + value + "&brightness=100" + "&iswhite=false");
}
else if (c == "setLevel") {
//Range should be 0-16 instead of 0-100
//See http://www.domoticz.com/wiki/Domoticz_API/JSON_URL%27s#Set_a_dimmable_light_to_a_certain_level
value = Math.round((value / 100) * 16)
url = this.platform.urlForQuery("type=command&param=switchlight&idx=" + this.idx + "&switchcmd=Set%20Level&level=" + value);
}
else if (value != undefined) {
@@ -311,11 +339,11 @@ DomoticzAccessory.prototype = {
},
sType: function() {
if (this.HaveDimmer == true) {
//if (this.HaveDimmer == true) {
return types.LIGHTBULB_STYPE
} else {
return types.SWITCH_STYPE
}
//} else {
// return types.SWITCH_STYPE
//}
},
getServices: function() {

203
platforms/KNX.js Normal file
View File

@@ -0,0 +1,203 @@
/** Sample platform outline
* based on Sonos platform
*/
'use strict';
var types = require("HAP-NodeJS/accessories/types.js");
//var hardware = require('myHardwareSupport'); //require any additional hardware packages
var knxd = require('eibd');
function KNXPlatform(log, config){
this.log = log;
this.config = config;
// this.property1 = config.property1;
// this.property2 = config.property2;
// initiate connection to bus for listening ==> done with first shim
};
KNXPlatform.prototype = {
accessories: function(callback) {
this.log("Fetching KNX devices.");
var that = this;
// iterate through all devices the platform my offer
// for each device, create an accessory
// read accessories from file !!!!!
var foundAccessories = this.config.accessories;
//create array of accessories
var myAccessories = [];
for (var int = 0; int < foundAccessories.length; int++) {
this.log("parsing acc " + int + " of " + foundAccessories.length);
// instantiate and push to array
switch (foundAccessories[int].accessory_type) {
case "knxdevice":
this.log("push new universal device "+foundAccessories[int].name);
// push knxd connection setting to each device from platform
foundAccessories[int].knxd_ip = this.config.knxd_ip;
foundAccessories[int].knxd_port = this.config.knxd_port;
var accConstructor = require('./../accessories/knxdevice.js');
var acc = new accConstructor.accessory(this.log,foundAccessories[int]);
this.log("created "+acc.name+" universal accessory");
myAccessories.push(acc);
break;
default:
// do something else
this.log("unkown accessory type found")
}
};
// if done, return the array to callback function
this.log("returning "+myAccessories.length+" accessories");
callback(myAccessories);
}
};
/**
* The buscallbacks module is to expose a simple function to listen on the bus and register callbacks for value changes
* of registered addresses.
*
* Usage:
* You can start the monitoring process at any time
startMonitor({host: name-ip, port: port-num });
* You can add addresses to the subscriptions using
registerGA(groupAddress, callback)
* groupAddress has to be an groupAddress in common knx notation string '1/2/3'
* the callback has to be a
* var f = function(value) { handle value update;}
* so you can do a
* registerGA('1/2/3', function(value){
* console.log('1/2/3 got a hit with '+value);
* });
* but of course it is meant to be used programmatically, not literally, otherwise it has no advantage
*
* You can also use arrays of addresses if your callback is supposed to listen to many addresses:
registerGA(groupAddresses[], callback)
* as in
* registerGA(['1/2/3','1/0/0'], function(value){
* console.log('1/2/3 or 1/0/0 got a hit with '+value);
* });
* if you are having central addresses like "all lights off" or additional response objects
*
*
* callbacks can have a signature of
* function(value, src, dest, type) but do not have to support these parameters (order matters)
* src = physical address such as '1.1.20'
* dest = groupAddress hit (you subscribed to that address, remember?), as '1/2/3'
* type = Data point type, as 'DPT1'
*
*
*/
//array of registered addresses and their callbacks
var subscriptions = [];
//check variable to avoid running two listeners
var running;
function groupsocketlisten(opts, callback) {
var conn = knxd.Connection();
conn.socketRemote(opts, function() {
conn.openGroupSocket(0, callback);
});
}
var registerSingleGA = function registerSingleGA (groupAddress, callback) {
subscriptions.push({address: groupAddress, callback: callback });
}
/*
* public busMonitor.startMonitor()
* starts listening for telegrams on KNX bus
*
*/
var startMonitor = function startMonitor(opts) { // using { host: name-ip, port: port-num } options object
if (!running) {
running = true;
} else {
console.log("<< knxd socket listener already running >>");
return null;
}
console.log(">>> knxd groupsocketlisten starting <<<");
groupsocketlisten(opts, function(parser) {
//console.log("knxfunctions.read: in callback parser");
parser.on('write', function(src, dest, type, val){
// search the registered group addresses
//console.log('recv: Write from '+src+' to '+dest+': '+val+' ['+type+'], listeners:' + subscriptions.length);
for (var i = 0; i < subscriptions.length; i++) {
// iterate through all registered addresses
if (subscriptions[i].address === dest) {
// found one, notify
console.log('HIT: Write from '+src+' to '+dest+': '+val+' ['+type+']');
subscriptions[i].callback(val, src, dest, type);
}
}
});
parser.on('response', function(src, dest, type, val) {
// search the registered group addresses
// console.log('recv: resp from '+src+' to '+dest+': '+val+' ['+type+']');
for (var i = 0; i < subscriptions.length; i++) {
// iterate through all registered addresses
if (subscriptions[i].address === dest) {
// found one, notify
// console.log('HIT: Response from '+src+' to '+dest+': '+val+' ['+type+']');
subscriptions[i].callback(val, src, dest, type);
}
}
});
//dont care about reads here
// parser.on('read', function(src, dest) {
// console.log('Read from '+src+' to '+dest);
// });
//console.log("knxfunctions.read: in callback parser at end");
}); // groupsocketlisten parser
}; //startMonitor
/*
* public registerGA(groupAdresses[], callback(value))
* parameters
* callback: function(value, src, dest, type) called when a value is sent on the bus
* groupAddresses: (Array of) string(s) for group addresses
*
*
*
*/
var registerGA = function (groupAddresses, callback) {
// check if the groupAddresses is an array
if (groupAddresses.constructor.toString().indexOf("Array") > -1) {
// handle multiple addresses
for (var i = 0; i < groupAddresses.length; i++) {
if (groupAddresses[i]) { // do not bind empty addresses
registerSingleGA (groupAddresses[i], callback);
}
}
} else {
// it's only one
registerSingleGA (groupAddresses, callback);
}
// console.log("listeners now: " + subscriptions.length);
};
module.exports.platform = KNXPlatform;
module.exports.registerGA = registerGA;
module.exports.startMonitor = startMonitor;

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;

242
platforms/MiLight.js Normal file
View File

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

View File

@@ -216,7 +216,13 @@ PhilipsHueAccessory.prototype = {
that.log(device.name + ", characteristic: " + characteristic + ", value: " + value + ".");
}
else {
that.log(err);
if (err.code == "ECONNRESET") {
setTimeout(function() {
that.executeChange(api, device, characteristic, value);
}, 300);
} else {
that.log(err);
}
}
});
},

View File

@@ -6,6 +6,8 @@ function SonosPlatform(log, config){
this.config = config;
this.name = config["name"];
this.playVolume = config["play_volume"];
// timeout for device discovery
this.discoveryTimeout = (config.deviceDiscoveryTimeout || 10)*1000; // assume 10sec as a default
}
SonosPlatform.prototype = {
@@ -13,15 +15,42 @@ SonosPlatform.prototype = {
this.log("Fetching Sonos devices.");
var that = this;
// track found devices so we don't add duplicates
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) {
that.log("Found device at " + device.host);
device.deviceDescription(function (err, description) {
if (description["zoneType"] != '11') {
that.log("Found playable device - " + description["roomName"]);
// device is an instance of sonos.Sonos
var accessory = new SonosAccessory(that.log, that.config, device, description);
callback([accessory]);
if (description["zoneType"] != '11' && description["zoneType"] != '8') { // 8 is the Sonos SUB
var roomName = description["roomName"];
if (!roomNamesFound[roomName]) {
roomNamesFound[roomName] = true;
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
var accessory = new SonosAccessory(that.log, that.config, device, description);
// add it to the collector array
devicesFound.push(accessory);
}
else {
that.log("Ignoring playable device with duplicate room name - " + roomName);
}
}
});
});

View File

@@ -1,13 +1,19 @@
var types = require("HAP-NodeJS/accessories/types.js");
var Yamaha = require('yamaha-nodejs');
var mdns = require('mdns');
//workaround for raspberry pi
var sequence = [
mdns.rst.DNSServiceResolve(),
'DNSServiceGetAddrInfo' in mdns.dns_sd ? mdns.rst.DNSServiceGetAddrInfo() : mdns.rst.getaddrinfo({families:[4]}),
mdns.rst.makeAddressesUnique()
];
function YamahaAVRPlatform(log, config){
this.log = log;
this.config = config;
this.playVolume = config["play_volume"];
this.setMainInputTo = config["setMainInputTo"];
this.browser = mdns.createBrowser(mdns.tcp('http'));
this.browser = mdns.createBrowser(mdns.tcp('http'), {resolverSequence: sequence});
}
YamahaAVRPlatform.prototype = {

667
platforms/ZWayServer.js Normal file
View File

@@ -0,0 +1,667 @@
var debug = require('debug')('ZWayServer');
var Service = require("HAP-NodeJS").Service;
var Characteristic = require("HAP-NodeJS").Characteristic;
var types = require("HAP-NodeJS/accessories/types.js");
var request = require("request");
var tough = require('tough-cookie');
var Q = require("q");
function ZWayServerPlatform(log, config){
this.log = log;
this.url = config["url"];
this.login = config["login"];
this.password = config["password"];
this.name_overrides = config["name_overrides"];
this.batteryLow = config["battery_low_level"] || 15;
this.pollInterval = config["poll_interval"] || 2;
this.splitServices= config["split_services"] || false;
this.lastUpdate = 0;
this.cxVDevMap = {};
this.vDevStore = {};
this.sessionId = "";
this.jar = request.jar(new tough.CookieJar());
}
ZWayServerPlatform.getVDevTypeKey = function(vdev){
return vdev.deviceType + (vdev.metrics && vdev.metrics.probeTitle ? "." + vdev.metrics.probeTitle : "")
}
ZWayServerPlatform.prototype = {
zwayRequest: function(opts){
var that = this;
var deferred = Q.defer();
opts.jar = true;//this.jar;
opts.json = true;
opts.headers = {
"Cookie": "ZWAYSession=" + this.sessionId
};
request(opts, function(error, response, body){
if(response.statusCode == 401){
debug("Authenticating...");
request({
method: "POST",
url: that.url + 'ZAutomation/api/v1/login',
body: { //JSON.stringify({
"form": true,
"login": that.login,
"password": that.password,
"keepme": false,
"default_ui": 1
},
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
json: true,
jar: true//that.jar
}, function(error, response, body){
if(response.statusCode == 200){
that.sessionId = body.data.sid;
opts.headers["Cookie"] = "ZWAYSession=" + that.sessionId;
debug("Authenticated. Resubmitting original request...");
request(opts, function(error, response, body){
if(response.statusCode == 200){
deferred.resolve(body);
} else {
deferred.reject(response);
}
});
} else {
deferred.reject(response);
}
});
} else if(response.statusCode == 200) {
deferred.resolve(body);
} else {
deferred.reject(response);
}
});
return deferred.promise;
}
,
accessories: function(callback) {
debug("Fetching Z-Way devices...");
//TODO: Unify this with getVDevServices, so there's only one place with mapping between service and vDev type.
//Note: Order matters!
var primaryDeviceClasses = [
"switchBinary",
"thermostat",
"sensorBinary.Door/Window",
"sensorMultilevel.Temperature",
"switchMultilevel"
];
var that = this;
var foundAccessories = [];
this.zwayRequest({
method: "GET",
url: this.url + 'ZAutomation/api/v1/devices'
}).then(function(result){
this.lastUpdate = result.data.updateTime;
var devices = result.data.devices;
var groupedDevices = {};
for(var i = 0; i < devices.length; i++){
var vdev = devices[i];
if(vdev.tags.indexOf("Homebridge:Skip") >= 0) { debug("Tag says skip!"); continue; }
var gdid = vdev.id.replace(/^(.*?)_zway_(\d+-\d+)-\d.*/, '$1_$2');
var gd = groupedDevices[gdid] || (groupedDevices[gdid] = {devices: [], types: {}, primary: undefined});
gd.devices.push(vdev);
gd.types[ZWayServerPlatform.getVDevTypeKey(vdev)] = gd.devices.length - 1;
gd.types[vdev.deviceType] = gd.devices.length - 1; // also include the deviceType only as a possibility
}
//TODO: Make a second pass, re-splitting any devices that don't make sense together
for(var gdid in groupedDevices) {
if(!groupedDevices.hasOwnProperty(gdid)) continue;
// Debug/log...
debug('Got grouped device ' + gdid + ' consiting of devices:');
var gd = groupedDevices[gdid];
for(var j = 0; j < gd.devices.length; j++){
debug(gd.devices[j].id + " - " + gd.devices[j].deviceType + (gd.devices[j].metrics && gd.devices[j].metrics.probeTitle ? "." + gd.devices[j].metrics.probeTitle : ""));
}
var accessory = null;
for(var ti = 0; ti < primaryDeviceClasses.length; ti++){
if(gd.types[primaryDeviceClasses[ti]] !== undefined){
gd.primary = gd.types[primaryDeviceClasses[ti]];
var pd = gd.devices[gd.primary];
var name = pd.metrics && pd.metrics.title ? pd.metrics.title : pd.id;
debug("Using primary device with type " + primaryDeviceClasses[ti] + ", " + name + " (" + pd.id + ") as primary.");
accessory = new ZWayServerAccessory(name, gd, that);
break;
}
}
if(!accessory)
debug("WARN: Didn't find suitable device class!");
else
foundAccessories.push(accessory);
}
//foundAccessories = foundAccessories.slice(0, 10); // Limit to a few devices for testing...
callback(foundAccessories);
// Start the polling process...
this.pollingTimer = setTimeout(this.pollUpdate.bind(this), this.pollInterval*1000);
}.bind(this));
}
,
pollUpdate: function(){
//debug("Polling for updates since " + this.lastUpdate + "...");
return this.zwayRequest({
method: "GET",
url: this.url + 'ZAutomation/api/v1/devices',
qs: {since: this.lastUpdate}
}).then(function(result){
this.lastUpdate = result.data.updateTime;
if(result.data && result.data.devices && result.data.devices.length){
var updates = result.data.devices;
debug("Got " + updates.length + " updates.");
for(var i = 0; i < updates.length; i++){
var upd = updates[i];
if(this.cxVDevMap[upd.id]){
var vdev = this.vDevStore[upd.id];
vdev.metrics.level = upd.metrics.level;
vdev.updateTime = upd.updateTime;
var cxs = this.cxVDevMap[upd.id];
for(var j = 0; j < cxs.length; j++){
var cx = cxs[j];
if(typeof cx.zway_getValueFromVDev !== "function") continue;
var oldValue = cx.value;
var newValue = cx.zway_getValueFromVDev(vdev);
if(oldValue !== newValue){
cx.value = newValue;
cx.emit('change', { oldValue:oldValue, newValue:cx.value, context:null });
debug("Updated characteristic " + cx.displayName + " on " + vdev.metrics.title);
}
}
}
}
}
// setup next poll...
this.pollingTimer = setTimeout(this.pollUpdate.bind(this), this.pollInterval*1000);
}.bind(this));
}
}
function ZWayServerAccessory(name, devDesc, platform) {
// device info
this.name = name;
this.devDesc = devDesc;
this.platform = platform;
this.log = platform.log;
}
ZWayServerAccessory.prototype = {
getVDev: function(vdev){
return this.platform.zwayRequest({
method: "GET",
url: this.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id
})//.then(function());
}
,
command: function(vdev, command, value) {
return this.platform.zwayRequest({
method: "GET",
url: this.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + '/command/' + command,
qs: (value === undefined ? undefined : value)
});
},
getVDevServices: function(vdev){
var typeKey = ZWayServerPlatform.getVDevTypeKey(vdev);
var services = [], service;
switch (typeKey) {
case "switchBinary":
services.push(new Service.Switch(vdev.metrics.title));
break;
case "switchMultilevel":
services.push(new Service.Lightbulb(vdev.metrics.title));
break;
case "thermostat":
services.push(new Service.Thermostat(vdev.metrics.title));
break;
case "sensorMultilevel.Temperature":
services.push(new Service.TemperatureSensor(vdev.metrics.title));
break;
case "sensorBinary.Door/Window":
services.push(new Service.GarageDoorOpener(vdev.metrics.title));
break;
case "battery.Battery":
services.push(new Service.BatteryService(vdev.metrics.title));
break;
case "sensorMultilevel.Luminiscence":
services.push(new Service.LightSensor(vdev.metrics.title));
break;
}
var validServices =[];
for(var i = 0; i < services.length; i++){
if(this.configureService(services[i], vdev))
validServices.push(services[i]);
}
return validServices;
}
,
uuidToTypeKeyMap: null
,
extraCharacteristicsMap: {
"battery.Battery": [Characteristic.BatteryLevel, Characteristic.StatusLowBattery],
"sensorMultilevel.Temperature": [Characteristic.CurrentTemperature, Characteristic.TemperatureDisplayUnits],
"sensorMultilevel.Luminiscence": [Characteristic.CurrentAmbientLightLevel]
}
,
getVDevForCharacteristic: function(cx, vdevPreferred){
var map = this.uuidToTypeKeyMap;
if(!map){
this.uuidToTypeKeyMap = map = {};
map[(new Characteristic.On).UUID] = ["switchBinary","switchMultilevel"];
map[(new Characteristic.Brightness).UUID] = ["switchMultilevel"];
map[(new Characteristic.CurrentTemperature).UUID] = ["sensorMultilevel.Temperature","thermostat"];
map[(new Characteristic.TargetTemperature).UUID] = ["thermostat"];
map[(new Characteristic.TemperatureDisplayUnits).UUID] = ["sensorMultilevel.Temperature","thermostat"]; //TODO: Always a fixed result
map[(new Characteristic.CurrentHeatingCoolingState).UUID] = ["thermostat"]; //TODO: Always a fixed result
map[(new Characteristic.TargetHeatingCoolingState).UUID] = ["thermostat"]; //TODO: Always a fixed result
map[(new Characteristic.CurrentDoorState).UUID] = ["sensorBinary.Door/Window","sensorBinary"];
map[(new Characteristic.TargetDoorState).UUID] = ["sensorBinary.Door/Window","sensorBinary"]; //TODO: Always a fixed result
map[(new Characteristic.ObstructionDetected).UUID] = ["sensorBinary.Door/Window","sensorBinary"]; //TODO: Always a fixed result
map[(new Characteristic.BatteryLevel).UUID] = ["battery.Battery"];
map[(new Characteristic.StatusLowBattery).UUID] = ["battery.Battery"];
map[(new Characteristic.ChargingState).UUID] = ["battery.Battery"]; //TODO: Always a fixed result
map[(new Characteristic.CurrentAmbientLightLevel).UUID] = ["sensorMultilevel.Luminiscence"];
}
if(cx instanceof Characteristic.Name) return vdevPreferred;
// Special case!: If cx is a CurrentTemperature, ignore the preferred device...we want the sensor if available!
if(cx instanceof Characteristic.CurrentTemperature) vdevPreferred = null;
//
var typekeys = map[cx.UUID];
if(typekeys === undefined) return null;
if(vdevPreferred && typekeys.indexOf(ZWayServerPlatform.getVDevTypeKey(vdevPreferred)) >= 0){
return vdevPreferred;
}
var candidates = this.devDesc.devices;
for(var i = 0; i < typekeys.length; i++){
for(var j = 0; j < candidates.length; j++){
if(ZWayServerPlatform.getVDevTypeKey(candidates[j]) === typekeys[i]) return candidates[j];
}
}
return null;
}
,
configureCharacteristic: function(cx, vdev){
var that = this;
// Add this combination to the maps...
if(!this.platform.cxVDevMap[vdev.id]) this.platform.cxVDevMap[vdev.id] = [];
this.platform.cxVDevMap[vdev.id].push(cx);
if(!this.platform.vDevStore[vdev.id]) this.platform.vDevStore[vdev.id] = vdev;
if(cx instanceof Characteristic.Name){
cx.zway_getValueFromVDev = function(vdev){
return vdev.metrics.title;
};
cx.value = cx.zway_getValueFromVDev(vdev);
cx.on('get', function(callback, context){
debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"...");
callback(false, that.name);
});
cx.writable = false;
return cx;
}
if(cx instanceof Characteristic.On){
cx.zway_getValueFromVDev = function(vdev){
var val = false;
if(vdev.metrics.level === "on"){
val = true;
} else if(vdev.metrics.level <= 5) {
val = false;
} else if (vdev.metrics.level > 5) {
val = true;
}
return val;
};
cx.value = cx.zway_getValueFromVDev(vdev);
cx.on('get', function(callback, context){
debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"...");
this.getVDev(vdev).then(function(result){
debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + ".");
callback(false, cx.zway_getValueFromVDev(result.data));
});
}.bind(this));
cx.on('set', function(powerOn, callback){
this.command(vdev, powerOn ? "on" : "off").then(function(result){
callback();
});
}.bind(this));
cx.on('change', function(ev){
debug("Device " + vdev.metrics.title + ", characteristic " + cx.displayName + " changed from " + ev.oldValue + " to " + ev.newValue);
});
return cx;
}
if(cx instanceof Characteristic.Brightness){
cx.zway_getValueFromVDev = function(vdev){
return vdev.metrics.level;
};
cx.value = cx.zway_getValueFromVDev(vdev);
cx.on('get', function(callback, context){
debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"...");
this.getVDev(vdev).then(function(result){
debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + ".");
callback(false, cx.zway_getValueFromVDev(result.data));
});
}.bind(this));
cx.on('set', function(level, callback){
this.command(vdev, "exact", {level: parseInt(level, 10)}).then(function(result){
callback();
});
}.bind(this));
return cx;
}
if(cx instanceof Characteristic.CurrentTemperature){
cx.zway_getValueFromVDev = function(vdev){
return vdev.metrics.level;
};
cx.value = cx.zway_getValueFromVDev(vdev);
cx.on('get', function(callback, context){
debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"...");
this.getVDev(vdev).then(function(result){
debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + ".");
callback(false, cx.zway_getValueFromVDev(result.data));
});
}.bind(this));
cx.minimumValue = vdev.metrics && vdev.metrics.min !== undefined ? vdev.metrics.min : -40;
cx.maximumValue = vdev.metrics && vdev.metrics.max !== undefined ? vdev.metrics.max : 999;
return cx;
}
if(cx instanceof Characteristic.TargetTemperature){
cx.zway_getValueFromVDev = function(vdev){
return vdev.metrics.level;
};
cx.value = cx.zway_getValueFromVDev(vdev);
cx.on('get', function(callback, context){
debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"...");
this.getVDev(vdev).then(function(result){
debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + ".");
callback(false, cx.zway_getValueFromVDev(result.data));
});
}.bind(this));
cx.on('set', function(level, callback){
this.command(vdev, "exact", {level: parseInt(level, 10)}).then(function(result){
//debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + ".");
callback();
});
}.bind(this));
cx.minimumValue = vdev.metrics && vdev.metrics.min !== undefined ? vdev.metrics.min : 5;
cx.maximumValue = vdev.metrics && vdev.metrics.max !== undefined ? vdev.metrics.max : 40;
return cx;
}
if(cx instanceof Characteristic.TemperatureDisplayUnits){
//TODO: Always in °C for now.
cx.zway_getValueFromVDev = function(vdev){
return Characteristic.TemperatureDisplayUnits.CELSIUS;
};
cx.value = cx.zway_getValueFromVDev(vdev);
cx.on('get', function(callback, context){
debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"...");
callback(false, Characteristic.TemperatureDisplayUnits.CELSIUS);
});
cx.writable = false;
return cx;
}
if(cx instanceof Characteristic.CurrentHeatingCoolingState){
//TODO: Always HEAT for now, we don't have an example to work with that supports another function.
cx.zway_getValueFromVDev = function(vdev){
return Characteristic.CurrentHeatingCoolingState.HEAT;
};
cx.value = cx.zway_getValueFromVDev(vdev);
cx.on('get', function(callback, context){
debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"...");
callback(false, Characteristic.CurrentHeatingCoolingState.HEAT);
});
return cx;
}
if(cx instanceof Characteristic.TargetHeatingCoolingState){
//TODO: Always HEAT for now, we don't have an example to work with that supports another function.
cx.zway_getValueFromVDev = function(vdev){
return Characteristic.TargetHeatingCoolingState.HEAT;
};
cx.value = cx.zway_getValueFromVDev(vdev);
cx.on('get', function(callback, context){
debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"...");
callback(false, Characteristic.TargetHeatingCoolingState.HEAT);
});
cx.writable = false;
return cx;
}
if(cx instanceof Characteristic.CurrentDoorState){
cx.zway_getValueFromVDev = function(vdev){
return vdev.metrics.level === "off" ? Characteristic.CurrentDoorState.CLOSED : Characteristic.CurrentDoorState.OPEN;
};
cx.value = cx.zway_getValueFromVDev(vdev);
cx.on('get', function(callback, context){
debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"...");
this.getVDev(vdev).then(function(result){
debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + ".");
callback(false, cx.zway_getValueFromVDev(result.data));
});
}.bind(this));
cx.on('change', function(ev){
debug("Device " + vdev.metrics.title + ", characteristic " + cx.displayName + " changed from " + ev.oldValue + " to " + ev.newValue);
});
}
if(cx instanceof Characteristic.TargetDoorState){
//TODO: We only support this for Door sensors now, so it's a fixed value.
cx.zway_getValueFromVDev = function(vdev){
return Characteristic.TargetDoorState.CLOSED;
};
cx.value = cx.zway_getValueFromVDev(vdev);
cx.on('get', function(callback, context){
debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"...");
callback(false, Characteristic.TargetDoorState.CLOSED);
});
//cx.readable = false;
cx.writable = false;
}
if(cx instanceof Characteristic.ObstructionDetected){
//TODO: We only support this for Door sensors now, so it's a fixed value.
cx.zway_getValueFromVDev = function(vdev){
return false;
};
cx.value = cx.zway_getValueFromVDev(vdev);
cx.on('get', function(callback, context){
debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"...");
callback(false, false);
});
//cx.readable = false;
cx.writable = false;
}
if(cx instanceof Characteristic.BatteryLevel){
cx.zway_getValueFromVDev = function(vdev){
return vdev.metrics.level;
};
cx.value = cx.zway_getValueFromVDev(vdev);
cx.on('get', function(callback, context){
debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"...");
this.getVDev(vdev).then(function(result){
debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + ".");
callback(false, cx.zway_getValueFromVDev(result.data));
});
}.bind(this));
}
if(cx instanceof Characteristic.StatusLowBattery){
cx.zway_getValueFromVDev = function(vdev){
return vdev.metrics.level <= that.platform.batteryLow ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL;
};
cx.value = cx.zway_getValueFromVDev(vdev);
cx.on('get', function(callback, context){
debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"...");
this.getVDev(vdev).then(function(result){
debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + ".");
callback(false, cx.zway_getValueFromVDev(result.data));
});
}.bind(this));
}
if(cx instanceof Characteristic.ChargingState){
//TODO: No known chargeable devices(?), so always return false.
cx.zway_getValueFromVDev = function(vdev){
return Characteristic.ChargingState.NOT_CHARGING;
};
cx.value = cx.zway_getValueFromVDev(vdev);
cx.on('get', function(callback, context){
debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"...");
callback(false, Characteristic.ChargingState.NOT_CHARGING);
});
//cx.readable = false;
cx.writable = false;
}
if(cx instanceof Characteristic.CurrentAmbientLightLevel){
cx.zway_getValueFromVDev = function(vdev){
if(vdev.metrics.scaleTitle === "%"){
// Completely unscientific guess, based on test-fit data and Wikipedia real-world lux values.
// This will probably change!
var lux = 0.0005 * (vdev.metrics.level^3.6);
if(lux < cx.minimumValue) return cx.minimumValue;
if(lux > cx.maximumValue) return cx.maximumValue;
return lux;
} else {
return vdev.metrics.level;
}
};
cx.value = cx.zway_getValueFromVDev(vdev);
cx.on('get', function(callback, context){
debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"...");
this.getVDev(vdev).then(function(result){
debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + ".");
callback(false, cx.zway_getValueFromVDev(result.data));
});
}.bind(this));
cx.on('change', function(ev){
debug("Device " + vdev.metrics.title + ", characteristic " + cx.displayName + " changed from " + ev.oldValue + " to " + ev.newValue);
});
return cx;
}
}
,
configureService: function(service, vdev){
var success = true;
for(var i = 0; i < service.characteristics.length; i++){
var cx = service.characteristics[i];
var vdev = this.getVDevForCharacteristic(cx, vdev);
if(!vdev){
success = false;
debug("ERROR! Failed to configure required characteristic \"" + service.characteristics[i].displayName + "\"!");
}
cx = this.configureCharacteristic(cx, vdev);
}
for(var i = 0; i < service.optionalCharacteristics.length; i++){
var cx = service.optionalCharacteristics[i];
var vdev = this.getVDevForCharacteristic(cx);
if(!vdev) continue;
cx = this.configureCharacteristic(cx, vdev);
if(cx) service.addCharacteristic(cx);
}
return success;
}
,
getServices: function() {
var that = this;
var informationService = new Service.AccessoryInformation();
informationService
.setCharacteristic(Characteristic.Name, this.name)
.setCharacteristic(Characteristic.Manufacturer, "Z-Wave.me")
.setCharacteristic(Characteristic.Model, "Virtual Device (VDev version 1)")
.setCharacteristic(Characteristic.SerialNumber, "VDev-" + this.devDesc.devices[this.devDesc.primary].h) //FIXME: Is this valid?);
var services = [informationService];
services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.primary]));
if(this.platform.splitServices){
if(this.devDesc.types["battery.Battery"]){
services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.types["battery.Battery"]]));
}
// Odds and ends...if there are sensors that haven't been used, add services for them...
var tempSensor = this.devDesc.types["sensorMultilevel.Temperature"] !== undefined ? this.devDesc.devices[this.devDesc.types["sensorMultilevel.Temperature"]] : false;
if(tempSensor && !this.platform.cxVDevMap[tempSensor.id]){
services = services.concat(this.getVDevServices(tempSensor));
}
var lightSensor = this.devDesc.types["sensorMultilevel.Luminiscence"] !== undefined ? this.devDesc.devices[this.devDesc.types["sensorMultilevel.Luminiscence"]] : false;
if(lightSensor && !this.platform.cxVDevMap[lightSensor.id]){
services = services.concat(this.getVDevServices(lightSensor));
}
} else {
// Everything outside the primary service gets added as optional characteristics...
var service = services[1];
var existingCxUUIDs = {};
for(var i = 0; i < service.characteristics.length; i++) existingCxUUIDs[service.characteristics[i].UUID] = true;
for(var i = 0; i < this.devDesc.devices.length; i++){
var vdev = this.devDesc.devices[i];
if(this.platform.cxVDevMap[vdev.id]) continue; // Don't double-use anything
var extraCxClasses = this.extraCharacteristicsMap[ZWayServerPlatform.getVDevTypeKey(vdev)];
var extraCxs = [];
if(!extraCxClasses || extraCxClasses.length === 0) continue;
for(var j = 0; j < extraCxClasses.length; j++){
var cx = new extraCxClasses[j]();
if(existingCxUUIDs[cx.UUID]) continue; // Don't have two of the same Characteristic type in one service!
var vdev2 = this.getVDevForCharacteristic(cx, vdev); // Just in case...will probably return vdev.
if(!vdev2){
// Uh oh... one of the extraCxClasses can't be configured! Abort all extras for this vdev!
extraCxs = []; // to wipe out any already setup cxs.
break;
}
this.configureCharacteristic(cx, vdev2);
extraCxs.push(cx);
}
for(var j = 0; j < extraCxs.length; j++)
service.addCharacteristic(extraCxs[j]);
}
}
debug("Loaded services for " + this.name);
return services;
}
};
module.exports.accessory = ZWayServerAccessory;
module.exports.platform = ZWayServerPlatform;