mirror of
https://github.com/mtan93/homebridge.git
synced 2026-05-24 22:07:36 +01:00
Merge with upstream
This commit is contained in:
@@ -50,7 +50,7 @@
|
||||
// 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 types = require("HAP-NodeJS/accessories/types.js");
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var request = require("request");
|
||||
|
||||
function DomoticzPlatform(log, config){
|
||||
|
||||
+850
-261
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,253 @@
|
||||
// Fibaro Home Center 2 Platform Shim for HomeBridge
|
||||
//
|
||||
// Remember to add platform to config.json. Example:
|
||||
// "platforms": [
|
||||
// {
|
||||
// "platform": "FibaroHC2",
|
||||
// "name": "FibaroHC2",
|
||||
// "host": "PUT IP ADDRESS OF YOUR HC2 HERE",
|
||||
// "username": "PUT USERNAME OF YOUR HC2 HERE",
|
||||
// "password": "PUT PASSWORD OF YOUR HC2 HERE"
|
||||
// }
|
||||
// ],
|
||||
//
|
||||
// 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 types = require("hap-nodejs/accessories/types.js");
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var request = require("request");
|
||||
|
||||
function FibaroHC2Platform(log, config){
|
||||
this.log = log;
|
||||
this.host = config["host"];
|
||||
this.username = config["username"];
|
||||
this.password = config["password"];
|
||||
this.auth = "Basic " + new Buffer(this.username + ":" + this.password).toString("base64");
|
||||
this.url = "http://"+this.host+"/api/devices";
|
||||
|
||||
startPollingUpdate( this );
|
||||
}
|
||||
|
||||
FibaroHC2Platform.prototype = {
|
||||
accessories: function(callback) {
|
||||
this.log("Fetching Fibaro Home Center devices...");
|
||||
|
||||
var that = this;
|
||||
var foundAccessories = [];
|
||||
|
||||
request.get({
|
||||
url: this.url,
|
||||
headers : {
|
||||
"Authorization" : this.auth
|
||||
},
|
||||
json: true
|
||||
}, function(err, response, json) {
|
||||
if (!err && response.statusCode == 200) {
|
||||
if (json != undefined) {
|
||||
json.map(function(s) {
|
||||
that.log("Found: " + s.type);
|
||||
if (s.visible == true) {
|
||||
var accessory = null;
|
||||
if (s.type == "com.fibaro.multilevelSwitch")
|
||||
accessory = new FibaroAccessory(new Service.Lightbulb(s.name), [Characteristic.On, Characteristic.Brightness]);
|
||||
else if (s.type == "com.fibaro.FGRM222" || s.type == "com.fibaro.FGR221")
|
||||
accessory = new FibaroAccessory(new Service.WindowCovering(s.name), [Characteristic.CurrentPosition, Characteristic.TargetPosition, Characteristic.PositionState]);
|
||||
else if (s.type == "com.fibaro.binarySwitch" || s.type == "com.fibaro.developer.bxs.virtualBinarySwitch")
|
||||
accessory = new FibaroAccessory(new Service.Switch(s.name), [Characteristic.On]);
|
||||
else if (s.type == "com.fibaro.FGMS001" || s.type == "com.fibaro.motionSensor")
|
||||
accessory = new FibaroAccessory(new Service.MotionSensor(s.name), [Characteristic.MotionDetected]);
|
||||
else if (s.type == "com.fibaro.temperatureSensor")
|
||||
accessory = new FibaroAccessory(new Service.TemperatureSensor(s.name), [Characteristic.CurrentTemperature]);
|
||||
else if (s.type == "com.fibaro.doorSensor")
|
||||
accessory = new FibaroAccessory(new Service.ContactSensor(s.name), [Characteristic.ContactSensorState]);
|
||||
else if (s.type == "com.fibaro.lightSensor")
|
||||
accessory = new FibaroAccessory(new Service.LightSensor(s.name), [Characteristic.CurrentAmbientLightLevel]);
|
||||
else if (s.type == "com.fibaro.FGWP101")
|
||||
accessory = new FibaroAccessory(new Service.Outlet(s.name), [Characteristic.On, Characteristic.OutletInUse]);
|
||||
if (accessory != null) {
|
||||
accessory.getServices = function() {
|
||||
return that.getServices(accessory);
|
||||
};
|
||||
accessory.platform = that;
|
||||
accessory.remoteAccessory = s;
|
||||
accessory.id = s.id;
|
||||
accessory.name = s.name;
|
||||
accessory.model = s.type;
|
||||
accessory.manufacturer = "Fibaro";
|
||||
accessory.serialNumber = "<unknown>";
|
||||
foundAccessories.push(accessory);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
callback(foundAccessories);
|
||||
} else {
|
||||
that.log("There was a problem connecting with FibaroHC2.");
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
command: function(c,value, that) {
|
||||
var url = "http://"+this.host+"/api/devices/"+that.id+"/action/"+c;
|
||||
var body = value != undefined ? JSON.stringify({
|
||||
"args": [ value ]
|
||||
}) : null;
|
||||
var method = "post";
|
||||
request({
|
||||
url: url,
|
||||
body: body,
|
||||
method: method,
|
||||
headers: {
|
||||
"Authorization" : this.auth
|
||||
},
|
||||
}, function(err, response) {
|
||||
if (err) {
|
||||
that.platform.log("There was a problem sending command " + c + " to" + that.name);
|
||||
that.platform.log(url);
|
||||
} else {
|
||||
that.platform.log(that.name + " sent command " + c);
|
||||
that.platform.log(url);
|
||||
}
|
||||
});
|
||||
},
|
||||
getAccessoryValue: function(callback, returnBoolean, homebridgeAccessory, powerValue) {
|
||||
var url = "http://"+homebridgeAccessory.platform.host+"/api/devices/"+homebridgeAccessory.id+"/properties/";
|
||||
if (powerValue)
|
||||
url = url + "power";
|
||||
else
|
||||
url = url + "value";
|
||||
|
||||
request.get({
|
||||
headers : {
|
||||
"Authorization" : homebridgeAccessory.platform.auth
|
||||
},
|
||||
json: true,
|
||||
url: url
|
||||
}, function(err, response, json) {
|
||||
homebridgeAccessory.platform.log(url);
|
||||
if (!err && response.statusCode == 200) {
|
||||
if (powerValue) {
|
||||
callback(undefined, parseFloat(json.value) > 1.0 ? true : false);
|
||||
} else if (returnBoolean)
|
||||
callback(undefined, json.value == 0 ? 0 : 1);
|
||||
else
|
||||
callback(undefined, json.value);
|
||||
} else {
|
||||
homebridgeAccessory.platform.log("There was a problem getting value from" + homebridgeAccessory.id);
|
||||
}
|
||||
})
|
||||
},
|
||||
getInformationService: function(homebridgeAccessory) {
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Name, homebridgeAccessory.name)
|
||||
.setCharacteristic(Characteristic.Manufacturer, homebridgeAccessory.manufacturer)
|
||||
.setCharacteristic(Characteristic.Model, homebridgeAccessory.model)
|
||||
.setCharacteristic(Characteristic.SerialNumber, homebridgeAccessory.serialNumber);
|
||||
return informationService;
|
||||
},
|
||||
bindCharacteristicEvents: function(characteristic, homebridgeAccessory) {
|
||||
var onOff = characteristic.props.format == "bool" ? true : false;
|
||||
var readOnly = true;
|
||||
for (var i = 0; i < characteristic.props.perms.length; i++)
|
||||
if (characteristic.props.perms[i] == "pw")
|
||||
readOnly = false;
|
||||
var powerValue = (characteristic.UUID == "00000026-0000-1000-8000-0026BB765291") ? true : false;
|
||||
subscribeUpdate(characteristic, homebridgeAccessory, onOff);
|
||||
if (!readOnly) {
|
||||
characteristic
|
||||
.on('set', function(value, callback, context) {
|
||||
if( context !== 'fromFibaro' ) {
|
||||
if (onOff)
|
||||
homebridgeAccessory.platform.command(value == 0 ? "turnOff": "turnOn", null, homebridgeAccessory);
|
||||
else
|
||||
homebridgeAccessory.platform.command("setValue", value, homebridgeAccessory);
|
||||
}
|
||||
callback();
|
||||
}.bind(this) );
|
||||
}
|
||||
characteristic
|
||||
.on('get', function(callback) {
|
||||
homebridgeAccessory.platform.getAccessoryValue(callback, onOff, homebridgeAccessory, powerValue);
|
||||
}.bind(this) );
|
||||
},
|
||||
getServices: function(homebridgeAccessory) {
|
||||
var informationService = homebridgeAccessory.platform.getInformationService(homebridgeAccessory);
|
||||
for (var i=0; i < homebridgeAccessory.characteristics.length; i++) {
|
||||
var characteristic = homebridgeAccessory.controlService.getCharacteristic(homebridgeAccessory.characteristics[i]);
|
||||
if (characteristic == undefined)
|
||||
characteristic = homebridgeAccessory.controlService.addCharacteristic(homebridgeAccessory.characteristics[i]);
|
||||
homebridgeAccessory.platform.bindCharacteristicEvents(characteristic, homebridgeAccessory);
|
||||
}
|
||||
|
||||
return [informationService, homebridgeAccessory.controlService];
|
||||
}
|
||||
}
|
||||
|
||||
function FibaroAccessory(controlService, characteristics) {
|
||||
this.controlService = controlService;
|
||||
this.characteristics = characteristics;
|
||||
}
|
||||
|
||||
var lastPoll=0;
|
||||
var pollingUpdateRunning = false;
|
||||
|
||||
function startPollingUpdate( platform )
|
||||
{
|
||||
if( pollingUpdateRunning )
|
||||
return;
|
||||
pollingUpdateRunning = true;
|
||||
|
||||
var updateUrl = "http://"+platform.host+"/api/refreshStates?last="+lastPoll;
|
||||
|
||||
request.get({
|
||||
url: updateUrl,
|
||||
headers : {
|
||||
"Authorization" : platform.auth
|
||||
},
|
||||
json: true
|
||||
}, function(err, response, json) {
|
||||
if (!err && response.statusCode == 200) {
|
||||
if (json != undefined) {
|
||||
lastPoll = json.last;
|
||||
if (json.changes != undefined) {
|
||||
json.changes.map(function(s) {
|
||||
if (s.value != undefined) {
|
||||
|
||||
var value=parseInt(s.value);
|
||||
if (isNaN(value))
|
||||
value=(s.value === "true");
|
||||
for (i=0;i<updateSubscriptions.length; i++) {
|
||||
var subscription = updateSubscriptions[i];
|
||||
if (subscription.id == s.id) {
|
||||
if (s.power != undefined && subscription.characteristic.UUID == "00000026-0000-1000-8000-0026BB765291") {
|
||||
subscription.characteristic.setValue(parseFloat(s.power) > 1.0 ? true : false, undefined, 'fromFibaro');
|
||||
} else if ((subscription.onOff && typeof(value) == "boolean") || !subscription.onOff)
|
||||
subscription.characteristic.setValue(value, undefined, 'fromFibaro');
|
||||
else
|
||||
subscription.characteristic.setValue(value == 0 ? false : true, undefined, 'fromFibaro');
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
platform.log("There was a problem connecting with FibaroHC2.");
|
||||
}
|
||||
pollingUpdateRunning = false;
|
||||
setTimeout( function(){startPollingUpdate(platform)}, 2000 );
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
var updateSubscriptions = [];
|
||||
function subscribeUpdate(characteristic, accessory, onOff)
|
||||
{
|
||||
// TODO: optimized management of updateSubscription data structure (no array with sequential access)
|
||||
updateSubscriptions.push({ 'id': accessory.id, 'characteristic': characteristic, 'accessory': accessory, 'onOff': onOff });
|
||||
}
|
||||
|
||||
module.exports.platform = FibaroHC2Platform;
|
||||
@@ -67,8 +67,8 @@
|
||||
// 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 Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var url = require('url')
|
||||
var request = require("request");
|
||||
|
||||
@@ -152,15 +152,24 @@ HomeAssistantPlatform.prototype = {
|
||||
entity = data[i]
|
||||
entity_type = entity.entity_id.split('.')[0]
|
||||
|
||||
// ignore devices that are not in the list of supported types
|
||||
if (that.supportedTypes.indexOf(entity_type) == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// ignore hidden devices
|
||||
if (entity.attributes && entity.attributes.hidden) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var accessory = null
|
||||
|
||||
if (entity_type == 'light') {
|
||||
accessory = new HomeAssistantLight(that.log, entity, that)
|
||||
}else if (entity_type == 'switch'){
|
||||
console.log(JSON.stringify(entity))
|
||||
console.log("");
|
||||
console.log("");
|
||||
accessory = new HomeAssistantSwitch(that.log, entity, that)
|
||||
}else if (entity_type == 'scene'){
|
||||
accessory = new HomeAssistantSwitch(that.log, entity, that, 'scene')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,4 +1,4 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var xml2js = require('xml2js');
|
||||
var request = require('request');
|
||||
var util = require('util');
|
||||
|
||||
@@ -0,0 +1,552 @@
|
||||
// Indigo Platform Shim for HomeBridge
|
||||
// Written by Mike Riccio (https://github.com/webdeck)
|
||||
// Based on many of the other HomeBridge plartform modules
|
||||
// See http://www.indigodomo.com/ for more info on Indigo
|
||||
//
|
||||
// Remember to add platform to config.json. Example:
|
||||
// "platforms": [
|
||||
// {
|
||||
// "platform": "Indigo", // required
|
||||
// "name": "Indigo", // required
|
||||
// "host": "127.0.0.1", // required
|
||||
// "port": "8176", // required
|
||||
// "username": "username", // optional
|
||||
// "password": "password" // optional
|
||||
// }
|
||||
// ],
|
||||
//
|
||||
// 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 types = require("hap-nodejs/accessories/types.js");
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var request = require('request');
|
||||
var async = require('async');
|
||||
|
||||
|
||||
function IndigoPlatform(log, config) {
|
||||
this.log = log;
|
||||
|
||||
this.baseURL = "http://" + config["host"] + ":" + config["port"];
|
||||
|
||||
if (config["username"] && config["password"]) {
|
||||
this.auth = {
|
||||
'user': config["username"],
|
||||
'pass': config["password"],
|
||||
'sendImmediately': false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
IndigoPlatform.prototype = {
|
||||
accessories: function(callback) {
|
||||
var that = this;
|
||||
this.log("Discovering Indigo Devices.");
|
||||
|
||||
var options = {
|
||||
url: this.baseURL + "/devices.json/",
|
||||
method: 'GET'
|
||||
};
|
||||
if (this.auth) {
|
||||
options['auth'] = this.auth;
|
||||
}
|
||||
this.foundAccessories = [];
|
||||
this.callback = callback;
|
||||
|
||||
request(options, function(error, response, body) {
|
||||
if (error) {
|
||||
console.trace("Requesting Indigo devices.");
|
||||
that.log(error);
|
||||
return error;
|
||||
}
|
||||
|
||||
// Cheesy hack because response may have an extra comma at the start of the array, which is invalid
|
||||
var firstComma = body.indexOf(",");
|
||||
if (firstComma < 10) {
|
||||
body = "[" + body.substr(firstComma + 1);
|
||||
}
|
||||
|
||||
var json = JSON.parse(body);
|
||||
async.eachSeries(json, function(item, asyncCallback) {
|
||||
var deviceURL = that.baseURL + item.restURL;
|
||||
var deviceOptions = {
|
||||
url: deviceURL,
|
||||
method: 'GET'
|
||||
};
|
||||
if (that.auth) {
|
||||
deviceOptions['auth'] = that.auth;
|
||||
}
|
||||
|
||||
request(deviceOptions, function(deviceError, deviceResponse, deviceBody) {
|
||||
if (deviceError) {
|
||||
console.trace("Requesting Indigo device info: " + deviceURL + "\nError: " + deviceError + "\nResponse: " + deviceBody);
|
||||
}
|
||||
else {
|
||||
try {
|
||||
var deviceJson = JSON.parse(deviceBody);
|
||||
that.log("Discovered " + deviceJson.type + ": " + deviceJson.name);
|
||||
that.foundAccessories.push(
|
||||
new IndigoAccessory(that.log, that.auth, deviceURL, deviceJson));
|
||||
}
|
||||
catch (e) {
|
||||
that.log("Error parsing Indigo device info: " + deviceURL + "\nException: " + e + "\nResponse: " + deviceBody);
|
||||
}
|
||||
}
|
||||
asyncCallback();
|
||||
});
|
||||
}, function(asyncError) {
|
||||
// This will be called after all the requests complete
|
||||
if (asyncError) {
|
||||
console.trace("Requesting Indigo device info.");
|
||||
that.log(asyncError);
|
||||
}
|
||||
|
||||
that.callback(that.foundAccessories.sort(function (a,b) {
|
||||
return (a.name > b.name) - (a.name < b.name);
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function IndigoAccessory(log, auth, deviceURL, json) {
|
||||
this.log = log;
|
||||
this.auth = auth;
|
||||
this.deviceURL = deviceURL;
|
||||
|
||||
for (var prop in json) {
|
||||
if (json.hasOwnProperty(prop)) {
|
||||
this[prop] = json[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IndigoAccessory.prototype = {
|
||||
getStatus: function(callback) {
|
||||
var that = this;
|
||||
|
||||
var options = {
|
||||
url: this.deviceURL,
|
||||
method: 'GET'
|
||||
};
|
||||
if (this.auth) {
|
||||
options['auth'] = this.auth;
|
||||
}
|
||||
|
||||
request(options, function(error, response, body) {
|
||||
if (error) {
|
||||
console.trace("Requesting Device Status.");
|
||||
that.log(error);
|
||||
}
|
||||
else {
|
||||
that.log("getStatus of " + that.name + ": " + body);
|
||||
try {
|
||||
var json = JSON.parse(body);
|
||||
callback(json);
|
||||
}
|
||||
catch (e) {
|
||||
console.trace("Requesting Device Status.");
|
||||
that.log("Exception: " + e + "\nResponse: " + body);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
updateStatus: function(params) {
|
||||
var that = this;
|
||||
var options = {
|
||||
url: this.deviceURL + "?" + params,
|
||||
method: 'PUT'
|
||||
};
|
||||
if (this.auth) {
|
||||
options['auth'] = this.auth;
|
||||
}
|
||||
|
||||
this.log("updateStatus of " + that.name + ": " + params);
|
||||
request(options, function(error, response, body) {
|
||||
if (error) {
|
||||
console.trace("Updating Device Status.");
|
||||
that.log(error);
|
||||
return error;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
query: function(prop, callback) {
|
||||
this.getStatus(function(json) {
|
||||
callback(json[prop]);
|
||||
});
|
||||
},
|
||||
|
||||
turnOn: function() {
|
||||
if (this.typeSupportsOnOff) {
|
||||
this.updateStatus("isOn=1");
|
||||
}
|
||||
},
|
||||
|
||||
turnOff: function() {
|
||||
if (this.typeSupportsOnOff) {
|
||||
this.updateStatus("isOn=0");
|
||||
}
|
||||
},
|
||||
|
||||
setBrightness: function(brightness) {
|
||||
if (this.typeSupportsDim && brightness >= 0 && brightness <= 100) {
|
||||
this.updateStatus("brightness=" + brightness);
|
||||
}
|
||||
},
|
||||
|
||||
setSpeedIndex: function(speedIndex) {
|
||||
if (this.typeSupportsSpeedControl && speedIndex >= 0 && speedIndex <= 3) {
|
||||
this.updateStatus("speedIndex=" + speedIndex);
|
||||
}
|
||||
},
|
||||
|
||||
getCurrentHeatingCooling: function(callback) {
|
||||
this.getStatus(function(json) {
|
||||
var mode = 0;
|
||||
if (json["hvacOperatonModeIsHeat"]) {
|
||||
mode = 1;
|
||||
}
|
||||
else if (json["hvacOperationModeIsCool"]) {
|
||||
mode = 2;
|
||||
}
|
||||
else if (json["hvacOperationModeIsAuto"]) {
|
||||
mode = 3;
|
||||
}
|
||||
callback(mode);
|
||||
});
|
||||
},
|
||||
|
||||
setTargetHeatingCooling: function(mode) {
|
||||
if (mode == 0) {
|
||||
param = "Off";
|
||||
}
|
||||
else if (mode == 1) {
|
||||
param = "Heat";
|
||||
}
|
||||
else if (mode == 2) {
|
||||
param = "Cool";
|
||||
}
|
||||
else if (mode == 3) {
|
||||
param = "Auto";
|
||||
}
|
||||
|
||||
if (param) {
|
||||
this.updateStatus("hvacOperationModeIs" + param + "=true");
|
||||
}
|
||||
},
|
||||
|
||||
// Note: HomeKit wants all temperature values to be in celsius
|
||||
getCurrentTemperature: function(callback) {
|
||||
this.query("displayRawState", function(temperature) {
|
||||
callback((temperature - 32.0) * 5.0 / 9.0);
|
||||
});
|
||||
},
|
||||
|
||||
getTargetTemperature: function(callback) {
|
||||
this.getStatus(function(json) {
|
||||
var temperature;
|
||||
if (json["hvacOperatonModeIsHeat"]) {
|
||||
temperature = json["setpointHeat"];
|
||||
}
|
||||
else if (json["hvacOperationModeIsCool"]) {
|
||||
temperature = json["setpointCool"];
|
||||
}
|
||||
else {
|
||||
temperature = (json["setpointHeat"] + json["setpointCool"]) / 2.0;
|
||||
}
|
||||
callback((temperature - 32.0) * 5.0 / 9.0);
|
||||
});
|
||||
},
|
||||
|
||||
setTargetTemperature: function(temperature) {
|
||||
var that = this;
|
||||
var t = (temperature * 9.0 / 5.0) + 32.0;
|
||||
this.getStatus(function(json) {
|
||||
if (json["hvacOperatonModeIsHeat"]) {
|
||||
that.updateStatus("setpointHeat=" + t);
|
||||
}
|
||||
else if (json["hvacOperationModeIsCool"]) {
|
||||
that.updateStatus("setpointCool=" + t);
|
||||
}
|
||||
else {
|
||||
var cool = t + 5;
|
||||
var heat = t - 5;
|
||||
that.updateStatus("setpointCool=" + cool + "&setpointHeat=" + heat);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
informationCharacteristics: function() {
|
||||
return [
|
||||
{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: [Characteristic.Perms.READ],
|
||||
format: Characteristic.Formats.STRING,
|
||||
initialValue: this.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: [Characteristic.Perms.READ],
|
||||
format: Characteristic.Formats.STRING,
|
||||
initialValue: "Indigo",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: [Characteristic.Perms.READ],
|
||||
format: Characteristic.Formats.STRING,
|
||||
initialValue: this.type,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: [Characteristic.Perms.READ],
|
||||
format: Characteristic.Formats.STRING,
|
||||
initialValue: this.addressStr,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: [Characteristic.Perms.WRITE],
|
||||
format: Characteristic.Formats.BOOL,
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
controlCharacteristics: function(that) {
|
||||
var hasAType = false;
|
||||
|
||||
var cTypes = [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: [Characteristic.Perms.READ],
|
||||
format: Characteristic.Formats.STRING,
|
||||
initialValue: that.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
}];
|
||||
|
||||
if (that.typeSupportsDim) {
|
||||
hasAType = true;
|
||||
cTypes.push({
|
||||
cType: types.BRIGHTNESS_CTYPE,
|
||||
perms: [Characteristic.Perms.WRITE,Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||
format: Characteristic.Formats.INT,
|
||||
initialValue: that.brightness,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Adjust Brightness of Light",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 100,
|
||||
designedMinStep: 1,
|
||||
unit: Characteristic.Units.PERCENTAGE,
|
||||
onUpdate: function(value) {
|
||||
that.setBrightness(value);
|
||||
},
|
||||
onRead: function(callback) {
|
||||
that.query("brightness", callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (that.typeSupportsSpeedControl) {
|
||||
hasAType = true;
|
||||
cTypes.push({
|
||||
cType: types.ROTATION_SPEED_CTYPE,
|
||||
perms: [Characteristic.Perms.WRITE,Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||
format: Characteristic.Formats.INT,
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the speed of the fan",
|
||||
designedMaxLength: 1,
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 3,
|
||||
designedMinStep: 1,
|
||||
onUpdate: function(value) {
|
||||
that.setSpeedIndex(value);
|
||||
},
|
||||
onRead: function(callback) {
|
||||
that.query("speedIndex", callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (that.typeSupportsHVAC) {
|
||||
hasAType = true;
|
||||
cTypes.push({
|
||||
cType: types.CURRENTHEATINGCOOLING_CTYPE,
|
||||
perms: [Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||
format: Characteristic.Formats.INT,
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Current Mode",
|
||||
designedMaxLength: 1,
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 3,
|
||||
designedMinStep: 1,
|
||||
onUpdate: null,
|
||||
onRead: function(callback) {
|
||||
that.getCurrentHeatingCooling(callback);
|
||||
}
|
||||
});
|
||||
|
||||
cTypes.push({
|
||||
cType: types.TARGETHEATINGCOOLING_CTYPE,
|
||||
perms: [Characteristic.Perms.WRITE,Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||
format: Characteristic.Formats.INT,
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Target Mode",
|
||||
designedMaxLength: 1,
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 3,
|
||||
designedMinStep: 1,
|
||||
onUpdate: function(value) {
|
||||
that.setTargetHeatingCooling(value);
|
||||
},
|
||||
onRead: function(callback) {
|
||||
that.getCurrentHeatingCooling(callback);
|
||||
}
|
||||
});
|
||||
|
||||
cTypes.push({
|
||||
cType: types.CURRENT_TEMPERATURE_CTYPE,
|
||||
perms: [Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||
format: Characteristic.Formats.INT,
|
||||
designedMinValue: 16,
|
||||
designedMaxValue: 38,
|
||||
designedMinStep: 1,
|
||||
initialValue: 20,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Current Temperature",
|
||||
unit: Characteristic.Units.FAHRENHEIT,
|
||||
onUpdate: null,
|
||||
onRead: function(callback) {
|
||||
that.getCurrentTemperature(callback);
|
||||
}
|
||||
});
|
||||
|
||||
cTypes.push({
|
||||
cType: types.TARGET_TEMPERATURE_CTYPE,
|
||||
perms: [Characteristic.Perms.WRITE,Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||
format: Characteristic.Formats.INT,
|
||||
designedMinValue: 16,
|
||||
designedMaxValue: 38,
|
||||
designedMinStep: 1,
|
||||
initialValue: 20,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Target Temperature",
|
||||
unit: Characteristic.Units.FAHRENHEIT,
|
||||
onUpdate: function(value) {
|
||||
that.setTargetTemperature(value);
|
||||
},
|
||||
onRead: function(callback) {
|
||||
that.getTargetTemperature(callback);
|
||||
}
|
||||
});
|
||||
|
||||
cTypes.push({
|
||||
cType: types.TEMPERATURE_UNITS_CTYPE,
|
||||
perms: [Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||
format: Characteristic.Formats.INT,
|
||||
initialValue: 1,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Unit",
|
||||
onUpdate: null,
|
||||
onRead: function(callback) {
|
||||
callback(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (that.typeSupportsOnOff || !hasAType) {
|
||||
cTypes.push({
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
perms: [Characteristic.Perms.WRITE,Characteristic.Perms.READ,Characteristic.Perms.NOTIFY],
|
||||
format: Characteristic.Formats.BOOL,
|
||||
initialValue: (that.isOn) ? 1 : 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the power state",
|
||||
designedMaxLength: 1,
|
||||
onUpdate: function(value) {
|
||||
if (value == 0) {
|
||||
that.turnOff();
|
||||
} else {
|
||||
that.turnOn();
|
||||
}
|
||||
},
|
||||
onRead: function(callback) {
|
||||
that.query("isOn", function(isOn) {
|
||||
callback((isOn) ? 1 : 0);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return cTypes;
|
||||
},
|
||||
|
||||
sType: function() {
|
||||
if (this.typeSupportsHVAC) {
|
||||
return types.THERMOSTAT_STYPE;
|
||||
} else if (this.typeSupportsDim) {
|
||||
return types.LIGHTBULB_STYPE;
|
||||
} else if (this.typeSupportsSpeedControl) {
|
||||
return types.FAN_STYPE;
|
||||
} else if (this.typeSupportsOnOff) {
|
||||
return types.SWITCH_STYPE;
|
||||
}
|
||||
|
||||
return types.SWITCH_STYPE;
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
var services = [{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: that.informationCharacteristics(),
|
||||
},
|
||||
{
|
||||
sType: that.sType(),
|
||||
characteristics: that.controlCharacteristics(that)
|
||||
}];
|
||||
|
||||
that.log("Loaded services for " + that.name);
|
||||
return services;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = IndigoAccessory;
|
||||
module.exports.platform = IndigoPlatform;
|
||||
@@ -0,0 +1,156 @@
|
||||
{
|
||||
"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",
|
||||
"hint2": "Replace all group addresses by current addresses of your installation, these are arbitrary examples!",
|
||||
"hint3": "For valid services and their characteristics have a look at the KNX.md file in folder platforms!",
|
||||
"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 types 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 LockCurrentState, LockTargetState, append R to the addresses if LOCKED is 1",
|
||||
"name": "Office Window Lock",
|
||||
"LockCurrentState": {
|
||||
"Listen": "5/3/15R"
|
||||
},
|
||||
"LockTargetState": {
|
||||
"Listen": "5/3/16R"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"TargetPosition": {
|
||||
"Set": "1/2/3",
|
||||
"Listen": "1/2/4"
|
||||
},
|
||||
"CurrentPosition": {
|
||||
"Set": "1/3/1",
|
||||
"Listen": "1/3/2"
|
||||
},
|
||||
"PositionState": {
|
||||
"Listen": "2/7/1"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"accessory_type": "knxdevice",
|
||||
"description": "sample contact sensor device",
|
||||
"name": "Office Contact",
|
||||
"services": [
|
||||
{
|
||||
"type": "ContactSensor",
|
||||
"name": "Office Door",
|
||||
"ContactSensorState": {
|
||||
"Listen": "5/3/5"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"accessory_type": "knxdevice",
|
||||
"description": "sample garage door opener",
|
||||
"name": "Office Garage",
|
||||
"services": [
|
||||
{
|
||||
"type": "GarageDoorOpener",
|
||||
"name": "Office Garage Opener",
|
||||
"CurrentDoorState": {
|
||||
"Listen": "5/4/5"
|
||||
},
|
||||
"TargetDoorState": {
|
||||
"Listen": "5/4/6"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"accessories": [
|
||||
|
||||
]
|
||||
}
|
||||
+17
-15
@@ -2,20 +2,19 @@
|
||||
* based on Sonos platform
|
||||
*/
|
||||
'use strict';
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
//var hardware = require('myHardwareSupport'); //require any additional hardware packages
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
|
||||
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) {
|
||||
@@ -49,10 +48,10 @@ KNXPlatform.prototype = {
|
||||
break;
|
||||
default:
|
||||
// do something else
|
||||
this.log("unkown accessory type found")
|
||||
this.log("unkown accessory type found");
|
||||
}
|
||||
|
||||
};
|
||||
}
|
||||
// if done, return the array to callback function
|
||||
this.log("returning "+myAccessories.length+" accessories");
|
||||
callback(myAccessories);
|
||||
@@ -116,9 +115,9 @@ function groupsocketlisten(opts, callback) {
|
||||
}
|
||||
|
||||
|
||||
var registerSingleGA = function registerSingleGA (groupAddress, callback) {
|
||||
subscriptions.push({address: groupAddress, callback: callback });
|
||||
}
|
||||
var registerSingleGA = function registerSingleGA (groupAddress, callback, reverse) {
|
||||
subscriptions.push({address: groupAddress, callback: callback, reverse:reverse });
|
||||
};
|
||||
|
||||
/*
|
||||
* public busMonitor.startMonitor()
|
||||
@@ -143,7 +142,7 @@ var startMonitor = function startMonitor(opts) { // using { host: name-ip, port
|
||||
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);
|
||||
subscriptions[i].callback(val, src, dest, type, subscriptions[i].reverse);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -156,7 +155,7 @@ var startMonitor = function startMonitor(opts) { // using { host: name-ip, port
|
||||
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);
|
||||
subscriptions[i].callback(val, src, dest, type, subscriptions[i].reverse);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,13 +184,16 @@ var registerGA = function (groupAddresses, callback) {
|
||||
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);
|
||||
if (groupAddresses[i] && groupAddresses[i].match(/(\d*\/\d*\/\d*)/)) { // do not bind empty addresses or invalid addresses
|
||||
// clean the addresses
|
||||
registerSingleGA (groupAddresses[i].match(/(\d*\/\d*\/\d*)/)[0], callback,groupAddresses[i].match(/\d*\/\d*\/\d*(R)/) ? true:false );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// it's only one
|
||||
registerSingleGA (groupAddresses, callback);
|
||||
if (groupAddresses.match(/(\d*\/\d*\/\d*)/)) {
|
||||
registerSingleGA (groupAddresses.match(/(\d*\/\d*\/\d*)/)[0], callback, groupAddresses.match(/\d*\/\d*\/\d*(R)/) ? true:false);
|
||||
}
|
||||
}
|
||||
// console.log("listeners now: " + subscriptions.length);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
# Syntax of the config.json
|
||||
In the platforms section, you can insert a KNX type platform.
|
||||
You need to configure all devices directly in the config.json.
|
||||
````json
|
||||
"platforms": [
|
||||
{
|
||||
"platform": "KNX",
|
||||
"name": "KNX",
|
||||
"knxd_ip": "192.168.178.205",
|
||||
"knxd_port": 6720,
|
||||
"accessories": [
|
||||
{
|
||||
"accessory_type": "knxdevice",
|
||||
"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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
````
|
||||
In the accessories section (the array within the brackets [ ]) you can insert as many objects as you like in the following form
|
||||
````json
|
||||
{
|
||||
"accessory_type": "knxdevice",
|
||||
"name": "Here goes your display name, this will be shown in HomeKit apps",
|
||||
"services": [
|
||||
{
|
||||
}
|
||||
]
|
||||
}
|
||||
````
|
||||
You have to add services in the following syntax:
|
||||
````json
|
||||
{
|
||||
"type": "SERVICENAME",
|
||||
"description": "This is just for you to remember things",
|
||||
"name": "beer tap thermostat",
|
||||
"CHARACTERISTIC1": {
|
||||
"Set": "1/1/6",
|
||||
"Listen": [
|
||||
"1/1/63"
|
||||
]
|
||||
},
|
||||
"CHARACTERISTIC2": {
|
||||
"Set": "1/1/62",
|
||||
"Listen": [
|
||||
"1/1/64"
|
||||
]
|
||||
}
|
||||
}
|
||||
````
|
||||
`CHARACTERISTICx` are properties that are dependent on the service type, so they are listed below.
|
||||
|
||||
Two kinds of addresses are supported: `"Set":"1/2/3"` is a writable group address, to which changes are sent if the service supports changing values. Changes on the bus are listened to, too.
|
||||
`"Listen":["1/2/3","1/2/4","1/2/5"]` is an array of addresses that are listened to additionally. To these addresses never values get written, but the on startup the service will issue *KNX read requests* to ALL addresses listed in `Set:` and in `Listen:`
|
||||
|
||||
|
||||
For two characteristics there are additional minValue and maxValue attributes. These are CurrentTemperature and TargetTemperature, and are used in TemperatureSensor and Thermostat.
|
||||
|
||||
So the charcteristic section may look like:
|
||||
|
||||
````json
|
||||
{
|
||||
"type": "Thermostat",
|
||||
"description": "Sample thermostat",
|
||||
"name": "We need a name for each service, though it usually shows only if multiple services are present in one accessory",
|
||||
"CurrentTemperature": {
|
||||
"Set": "1/1/6",
|
||||
"Listen": [
|
||||
"1/1/63"
|
||||
],
|
||||
"minValue": -18,
|
||||
"maxValue": 30
|
||||
},
|
||||
"TargetTemperature": {
|
||||
"Set": "1/1/62",
|
||||
"Listen": [
|
||||
"1/1/64"
|
||||
],
|
||||
"minValue": -4,
|
||||
"maxValue": 12
|
||||
}
|
||||
}
|
||||
````
|
||||
|
||||
|
||||
## reversal of values for characteristics
|
||||
In general, all DPT1 types can be reversed. If you need a 1 for "contact" of a contact senser, you can append an "R" to the group address.
|
||||
Likewise, all percentages of DPT5 can be reversed, if you need a 100% (=255) for window closed, append an "R" to the group address. Do not forget the listening addresses!
|
||||
````json
|
||||
{
|
||||
"type": "ContactSensor",
|
||||
"description": "Sample ContactSensor with 1 as contact (0 is Apple's default)",
|
||||
"name": "WindowContact1",
|
||||
"ContactSensorState": {
|
||||
"Listen": [
|
||||
"1/1/100R"
|
||||
]
|
||||
}
|
||||
}
|
||||
````
|
||||
# Supported Services and their characteristics
|
||||
## ContactSensor
|
||||
- ContactSensorState: DPT 1.002, 0 as contact
|
||||
- ~~ContactSensorStateContact1: DPT 1.002, 1 as contact~~
|
||||
|
||||
- StatusActive: DPT 1.011, 1 as true
|
||||
- StatusFault: DPT 1.011, 1 as true
|
||||
- StatusTampered: DPT 1.011, 1 as true
|
||||
- StatusLowBattery: DPT 1.011, 1 as true
|
||||
|
||||
## GarageDoorOpener
|
||||
- CurrentDoorState: DPT5 integer value in range 0..4
|
||||
// Characteristic.CurrentDoorState.OPEN = 0;
|
||||
// Characteristic.CurrentDoorState.CLOSED = 1;
|
||||
// Characteristic.CurrentDoorState.OPENING = 2;
|
||||
// Characteristic.CurrentDoorState.CLOSING = 3;
|
||||
// Characteristic.CurrentDoorState.STOPPED = 4;
|
||||
|
||||
- TargetDoorState: DPT5 integer value in range 0..1
|
||||
// Characteristic.TargetDoorState.OPEN = 0;
|
||||
// Characteristic.TargetDoorState.CLOSED = 1;
|
||||
|
||||
- ObstructionDetected: DPT1, 1 as true
|
||||
|
||||
- LockCurrentState: DPT5 integer value in range 0..3
|
||||
// Characteristic.LockCurrentState.UNSECURED = 0;
|
||||
// Characteristic.LockCurrentState.SECURED = 1;
|
||||
// Characteristic.LockCurrentState.JAMMED = 2;
|
||||
// Characteristic.LockCurrentState.UNKNOWN = 3;
|
||||
|
||||
- LockTargetState: DPT5 integer value in range 0..1
|
||||
// Characteristic.LockTargetState.UNSECURED = 0;
|
||||
// Characteristic.LockTargetState.SECURED = 1;
|
||||
|
||||
|
||||
|
||||
## Lightbulb
|
||||
- On: DPT 1.001, 1 as on, 0 as off
|
||||
- Brightness: DPT5.001 percentage, 100% (=255) the brightest
|
||||
|
||||
## LightSensor
|
||||
- CurrentAmbientLightLevel: DPT 9.004, 0 to 100000 Lux
|
||||
|
||||
## LockMechanism (This is poorly mapped!)
|
||||
- LockCurrentState: DPT 1, 1 as secured
|
||||
- ~~LockCurrentStateSecured0: DPT 1, 0 as secured~~
|
||||
- LockTargetState: DPT 1, 1 as secured
|
||||
- ~~LockTargetStateSecured0: DPT 1, 0 as secured~~
|
||||
|
||||
*ToDo here: correction of mappings, HomeKit reqires lock states UNSECURED=0, SECURED=1, JAMMED = 2, UNKNOWN=3*
|
||||
|
||||
## MotionSensor
|
||||
- MotionDetected: DPT 1.002, 1 as motion detected
|
||||
|
||||
- StatusActive: DPT 1.011, 1 as true
|
||||
- StatusFault: DPT 1.011, 1 as true
|
||||
- StatusTampered: DPT 1.011, 1 as true
|
||||
- StatusLowBattery: DPT 1.011, 1 as true
|
||||
|
||||
## Outlet
|
||||
- On: DPT 1.001, 1 as on, 0 as off
|
||||
- OutletInUse: DPT 1.011, 1 as on, 0 as off
|
||||
|
||||
## Switch
|
||||
- On: DPT 1.001, 1 as on, 0 as off
|
||||
|
||||
## TemperatureSensor
|
||||
- CurrentTemperature: DPT9.001 in °C [listen only]
|
||||
|
||||
## Thermostat
|
||||
- CurrentTemperature: DPT9.001 in °C [listen only], -40 to 80°C if not overriden as shown above
|
||||
- TargetTemperature: DPT9.001, values 0..40°C only, all others are ignored
|
||||
- CurrentHeatingCoolingState: DPT20.102 HVAC, because of the incompatible mapping only off and heating (=auto) are shown, [listen only]
|
||||
- TargetHeatingCoolingState: DPT20.102 HVAC, as above
|
||||
|
||||
## Window
|
||||
- CurrentPosition: DPT5.001 percentage
|
||||
- TargetPosition: DPT5.001 percentage
|
||||
- PositionState: DPT5.005 value [listen only: 0 Increasing, 1 Decreasing, 2 Stopped]
|
||||
|
||||
## WindowCovering
|
||||
- CurrentPosition: DPT5 percentage
|
||||
- TargetPosition: DPT5 percentage
|
||||
- PositionState: DPT5 value [listen only: 0 Closing, 1 Opening, 2 Stopped]
|
||||
|
||||
### not yet supported
|
||||
- HoldPosition
|
||||
- TargetHorizontalTiltAngle
|
||||
- TargetVerticalTiltAngle
|
||||
- CurrentHorizontalTiltAngle
|
||||
- CurrentVerticalTiltAngle
|
||||
- ObstructionDetected
|
||||
|
||||
|
||||
|
||||
|
||||
# DISCLAIMER
|
||||
**This is work in progress!**
|
||||
|
||||
+2
-2
@@ -16,8 +16,8 @@
|
||||
// The default code for all HomeBridge accessories is 031-45-154.
|
||||
//
|
||||
|
||||
var Service = require("HAP-NodeJS").Service;
|
||||
var Characteristic = require("HAP-NodeJS").Characteristic;
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var lifxRemoteObj = require('lifx-api');
|
||||
var lifx_remote;
|
||||
|
||||
|
||||
+190
-228
@@ -17,13 +17,20 @@
|
||||
//
|
||||
|
||||
|
||||
var types = require('HAP-NodeJS/accessories/types.js');
|
||||
var types = require('hap-nodejs/accessories/types.js');
|
||||
|
||||
var harmonyDiscover = require('harmonyhubjs-discover');
|
||||
var harmony = require('harmonyhubjs-client');
|
||||
|
||||
var _harmonyHubPort = 61991;
|
||||
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var Accessory = require("hap-nodejs").Accessory;
|
||||
var uuid = require("hap-nodejs").uuid;
|
||||
var inherits = require('util').inherits;
|
||||
var queue = require('queue');
|
||||
|
||||
|
||||
function sortByKey (array, key) {
|
||||
return array.sort(function(a, b) {
|
||||
@@ -41,265 +48,220 @@ function LogitechHarmonyPlatform (log, config) {
|
||||
|
||||
LogitechHarmonyPlatform.prototype = {
|
||||
|
||||
// Find one Harmony remote hub (only support one for now)
|
||||
locateHub: function (callback) {
|
||||
var self = this;
|
||||
|
||||
// Connect to a Harmony hub
|
||||
var createClient = function (ipAddress) {
|
||||
self.log("Connecting to Logitech Harmony remote hub...");
|
||||
|
||||
harmony(ipAddress)
|
||||
.then(function (client) {
|
||||
self.log("Connected to Logitech Harmony remote hub");
|
||||
|
||||
callback(null, client);
|
||||
});
|
||||
};
|
||||
|
||||
// Use the ip address in configuration if available
|
||||
if (this.ip_address) {
|
||||
console.log("Using Logitech Harmony hub ip address from configuration");
|
||||
|
||||
return createClient(this.ip_address)
|
||||
}
|
||||
|
||||
this.log("Searching for Logitech Harmony remote hubs...");
|
||||
|
||||
// Discover the harmony hub with bonjour
|
||||
var discover = new harmonyDiscover(_harmonyHubPort);
|
||||
|
||||
// TODO: Support update event with some way to add accessories
|
||||
// TODO: Have some kind of timeout with an error message. Right now this searches forever until it finds one hub.
|
||||
discover.on('online', function (hubInfo) {
|
||||
self.log("Found Logitech Harmony remote hub: " + hubInfo.ip);
|
||||
|
||||
// Stop looking for hubs once we find the first one
|
||||
// TODO: Support multiple hubs
|
||||
discover.stop();
|
||||
|
||||
createClient(hubInfo.ip);
|
||||
});
|
||||
|
||||
// Start looking for hubs
|
||||
discover.start();
|
||||
},
|
||||
|
||||
accessories: function (callback) {
|
||||
var self = this;
|
||||
var plat = this;
|
||||
var foundAccessories = [];
|
||||
var activityAccessories = [];
|
||||
var hub = null;
|
||||
var hubIP = null;
|
||||
var hubQueue = queue();
|
||||
hubQueue.concurrency = 1;
|
||||
|
||||
// Get the first hub
|
||||
this.locateHub(function (err, hub) {
|
||||
locateHub(function (err, client, clientIP) {
|
||||
if (err) throw err;
|
||||
|
||||
self.log("Fetching Logitech Harmony devices and activites...");
|
||||
plat.log("Fetching Logitech Harmony devices and activites...");
|
||||
|
||||
hub = client;
|
||||
hubIP = clientIP;
|
||||
//getDevices(hub);
|
||||
getActivities(hub);
|
||||
getActivities();
|
||||
});
|
||||
|
||||
// Get Harmony Devices
|
||||
/*
|
||||
var getDevices = function(hub) {
|
||||
self.log("Fetching Logitech Harmony devices...");
|
||||
// Find one Harmony remote hub (only support one for now)
|
||||
function locateHub(callback) {
|
||||
// Use the ip address in configuration if available
|
||||
if (plat.ip_address) {
|
||||
console.log("Using Logitech Harmony hub ip address from configuration");
|
||||
|
||||
hub.getDevices()
|
||||
.then(function (devices) {
|
||||
self.log("Found devices: ", devices);
|
||||
return createClient(plat.ip_address, callback)
|
||||
}
|
||||
|
||||
var sArray = sortByKey(json['result'],"Name");
|
||||
plat.log("Searching for Logitech Harmony remote hubs...");
|
||||
|
||||
sArray.map(function(s) {
|
||||
accessory = new LogitechHarmonyAccessory(self.log, self.server, self.port, false, s.idx, s.Name, s.HaveDimmer, s.MaxDimLevel, (s.SubType=="RGB")||(s.SubType=="RGBW"));
|
||||
foundAccessories.push(accessory);
|
||||
// Discover the harmony hub with bonjour
|
||||
var discover = new harmonyDiscover(_harmonyHubPort);
|
||||
|
||||
// TODO: Support update event with some way to add accessories
|
||||
// TODO: Have some kind of timeout with an error message. Right now this searches forever until it finds one hub.
|
||||
discover.on('online', function (hubInfo) {
|
||||
plat.log("Found Logitech Harmony remote hub: " + hubInfo.ip);
|
||||
|
||||
// Stop looking for hubs once we find the first one
|
||||
// TODO: Support multiple hubs
|
||||
discover.stop();
|
||||
|
||||
createClient(hubInfo.ip, callback);
|
||||
});
|
||||
|
||||
// Start looking for hubs
|
||||
discover.start();
|
||||
}
|
||||
|
||||
// Connect to a Harmony hub
|
||||
function createClient(ipAddress, callback) {
|
||||
plat.log("Connecting to Logitech Harmony remote hub...");
|
||||
harmony(ipAddress)
|
||||
.then(function (client) {
|
||||
plat.log("Connected to Logitech Harmony remote hub");
|
||||
callback(null, client, ipAddress);
|
||||
});
|
||||
|
||||
callback(foundAccessories);
|
||||
});
|
||||
};
|
||||
*/
|
||||
}
|
||||
|
||||
// Get Harmony Activities
|
||||
var getActivities = function(hub) {
|
||||
self.log("Fetching Logitech Harmony activities...");
|
||||
function getActivities() {
|
||||
plat.log("Fetching Logitech Harmony activities...");
|
||||
|
||||
hub.getActivities()
|
||||
.then(function (activities) {
|
||||
self.log("Found activities: \n" + activities.map(function (a) { return "\t" + a.label; }).join("\n"));
|
||||
plat.log("Found activities: \n" + activities.map(function (a) { return "\t" + a.label; }).join("\n"));
|
||||
|
||||
var sArray = sortByKey(activities, "label");
|
||||
|
||||
sArray.map(function(s) {
|
||||
var accessory = new LogitechHarmonyAccessory(self.log, hub, s, true);
|
||||
// TODO: Update the initial power state
|
||||
foundAccessories.push(accessory);
|
||||
hub.getCurrentActivity().then(function (currentActivity) {
|
||||
var actAccessories = [];
|
||||
var sArray = sortByKey(activities, "label");
|
||||
sArray.map(function(s) {
|
||||
var accessory = createActivityAccessory(s);
|
||||
if (accessory.id > 0) {
|
||||
accessory.updateActivityState(currentActivity);
|
||||
actAccessories.push(accessory);
|
||||
foundAccessories.push(accessory);
|
||||
}
|
||||
});
|
||||
activityAccessories = actAccessories;
|
||||
keepAliveRefreshLoop();
|
||||
callback(foundAccessories);
|
||||
}).catch(function (err) {
|
||||
plat.log('Unable to get current activity with error', err);
|
||||
throw err;
|
||||
});
|
||||
|
||||
callback(foundAccessories);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function createActivityAccessory(activity) {
|
||||
var accessory = new LogitechHarmonyActivityAccessory(plat.log, activity, changeCurrentActivity.bind(plat), -1);
|
||||
return accessory;
|
||||
}
|
||||
|
||||
var isChangingActivity = false;
|
||||
function changeCurrentActivity(nextActivity, callback) {
|
||||
if (!nextActivity) {
|
||||
nextActivity = -1;
|
||||
}
|
||||
plat.log('Queue activity to ' + nextActivity);
|
||||
executeOnHub(function(h, cb) {
|
||||
plat.log('Set activity to ' + nextActivity);
|
||||
h.startActivity(nextActivity)
|
||||
.then(function () {
|
||||
cb();
|
||||
isChangingActivity = false;
|
||||
plat.log('Finished setting activity to ' + nextActivity);
|
||||
updateCurrentActivity(nextActivity);
|
||||
if (callback) callback(null, nextActivity);
|
||||
})
|
||||
.catch(function (err) {
|
||||
cb();
|
||||
isChangingActivity = false;
|
||||
plat.log('Failed setting activity to ' + nextActivity + ' with error ' + err);
|
||||
if (callback) callback(err);
|
||||
});
|
||||
}, function(){
|
||||
callback(Error("Set activity failed too many times"));
|
||||
});
|
||||
}
|
||||
|
||||
function updateCurrentActivity(currentActivity) {
|
||||
var actAccessories = activityAccessories;
|
||||
if (actAccessories instanceof Array) {
|
||||
actAccessories.map(function(a) { a.updateActivityState(currentActivity); });
|
||||
}
|
||||
}
|
||||
|
||||
// prevent connection from closing
|
||||
function keepAliveRefreshLoop() {
|
||||
setTimeout(function() {
|
||||
setInterval(function() {
|
||||
executeOnHub(function(h, cb) {
|
||||
plat.log("Refresh Status");
|
||||
h.getCurrentActivity()
|
||||
.then(function(currentActivity){
|
||||
cb();
|
||||
updateCurrentActivity(currentActivity);
|
||||
})
|
||||
.catch(cb);
|
||||
});
|
||||
}, 20000);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function executeOnHub(func, funcMaxTimeout)
|
||||
{
|
||||
if (!func) return;
|
||||
hubQueue.push(function(cb) {
|
||||
var tout = setTimeout(function(){
|
||||
plat.log("Reconnecting to Hub " + hubIP);
|
||||
createClient(hubIP, function(err, newHub){
|
||||
if (err) throw err;
|
||||
hub = newHub;
|
||||
if (funcMaxTimeout) {
|
||||
funcMaxTimeout();
|
||||
}
|
||||
cb();
|
||||
});
|
||||
}, 30000);
|
||||
func(hub, function(){
|
||||
clearTimeout(tout);
|
||||
cb();
|
||||
});
|
||||
});
|
||||
if (!hubQueue.running){
|
||||
hubQueue.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
function LogitechHarmonyAccessory (log, hub, details, isActivity) {
|
||||
function LogitechHarmonyActivityAccessory (log, details, changeCurrentActivity) {
|
||||
this.log = log;
|
||||
this.hub = hub;
|
||||
this.details = details;
|
||||
this.id = details.id;
|
||||
this.name = details.label;
|
||||
this.isActivity = isActivity;
|
||||
this.isActivityActive = false;
|
||||
this.isOn = false;
|
||||
this.changeCurrentActivity = changeCurrentActivity;
|
||||
Accessory.call(this, this.name, uuid.generate(this.id));
|
||||
var self = this;
|
||||
|
||||
this.getService(Service.AccessoryInformation)
|
||||
.setCharacteristic(Characteristic.Manufacturer, "Logitech")
|
||||
.setCharacteristic(Characteristic.Model, "Harmony")
|
||||
// TODO: Add hub unique id to this for people with multiple hubs so that it is really a guid.
|
||||
.setCharacteristic(Characteristic.SerialNumber, this.id);
|
||||
|
||||
this.addService(Service.Switch)
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('get', function(callback) {
|
||||
// Refreshed automatically by platform
|
||||
callback(null, self.isOn);
|
||||
})
|
||||
.on('set', this.setPowerState.bind(this));
|
||||
|
||||
}
|
||||
inherits(LogitechHarmonyActivityAccessory, Accessory);
|
||||
LogitechHarmonyActivityAccessory.prototype.parent = Accessory.prototype;
|
||||
LogitechHarmonyActivityAccessory.prototype.getServices = function() {
|
||||
return this.services;
|
||||
};
|
||||
|
||||
|
||||
LogitechHarmonyAccessory.prototype = {
|
||||
|
||||
// TODO: Somehow make this event driven so that it tells the user what activity is on
|
||||
getPowerState: function (callback) {
|
||||
var self = this;
|
||||
|
||||
if (this.isActivity) {
|
||||
hub.getCurrentActivity().then(function (currentActivity) {
|
||||
callback(currentActivity.id === self.id);
|
||||
}).except(function (err) {
|
||||
self.log('Unable to get current activity with error', err);
|
||||
callback(false);
|
||||
});
|
||||
} else {
|
||||
// TODO: Support onRead for devices
|
||||
this.log('TODO: Support onRead for devices');
|
||||
}
|
||||
},
|
||||
|
||||
setPowerState: function (state, callback) {
|
||||
var self = this;
|
||||
|
||||
if (this.isActivity) {
|
||||
this.log('Set activity ' + this.name + ' power state to ' + state);
|
||||
|
||||
// Activity id -1 is turn off all devices
|
||||
var id = state ? this.id : -1;
|
||||
|
||||
this.hub.startActivity(id)
|
||||
.then(function () {
|
||||
self.log('Finished setting activity ' + self.name + ' power state to ' + state);
|
||||
callback();
|
||||
})
|
||||
.catch(function (err) {
|
||||
self.log('Failed setting activity ' + self.name + ' power state to ' + state + ' with error ' + err);
|
||||
callback(err);
|
||||
});
|
||||
} else {
|
||||
// TODO: Support setting device power
|
||||
this.log('TODO: Support setting device power');
|
||||
callback();
|
||||
}
|
||||
},
|
||||
|
||||
getServices: function () {
|
||||
var self = this;
|
||||
|
||||
return [
|
||||
{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: [
|
||||
{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: self.name,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of the accessory",
|
||||
designedMaxLength: 255
|
||||
},
|
||||
{
|
||||
cType: types.MANUFACTURER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Logitech",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},
|
||||
{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: "Harmony",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},
|
||||
{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
// TODO: Add hub unique id to this for people with multiple hubs so that it is really a guid.
|
||||
initialValue: self.id,
|
||||
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: self.name,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
},
|
||||
{
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function (value) {
|
||||
self.setPowerState(value)
|
||||
},
|
||||
onRead: self.getPowerState,
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the power state",
|
||||
designedMaxLength: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
LogitechHarmonyActivityAccessory.prototype.updateActivityState = function (currentActivity) {
|
||||
this.isOn = (currentActivity === this.id);
|
||||
// Force get to trigger 'change' if needed
|
||||
this.getService(Service.Switch)
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.getValue();
|
||||
};
|
||||
|
||||
LogitechHarmonyActivityAccessory.prototype.setPowerState = function (state, callback) {
|
||||
this.changeCurrentActivity(state ? this.id : null, callback);
|
||||
};
|
||||
|
||||
module.exports.accessory = LogitechHarmonyAccessory;
|
||||
module.exports.platform = LogitechHarmonyPlatform;
|
||||
|
||||
|
||||
@@ -47,8 +47,8 @@ TODO:
|
||||
|
||||
*/
|
||||
|
||||
var Service = require("HAP-NodeJS").Service;
|
||||
var Characteristic = require("HAP-NodeJS").Characteristic;
|
||||
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;
|
||||
|
||||
|
||||
+7
-3
@@ -1,4 +1,4 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var nest = require('unofficial-nest-api');
|
||||
|
||||
function NestPlatform(log, config){
|
||||
@@ -44,7 +44,11 @@ NestPlatform.prototype = {
|
||||
|
||||
function NestThermostatAccessory(log, name, device, deviceId) {
|
||||
// device info
|
||||
this.name = name;
|
||||
if (name) {
|
||||
this.name = name;
|
||||
} else {
|
||||
this.name = "Nest" + device.serial_number;
|
||||
}
|
||||
this.model = device.model_version;
|
||||
this.serial = device.serial_number;
|
||||
this.deviceId = deviceId;
|
||||
@@ -390,4 +394,4 @@ NestThermostatAccessory.prototype = {
|
||||
}
|
||||
|
||||
module.exports.accessory = NestThermostatAccessory;
|
||||
module.exports.platform = NestPlatform;
|
||||
module.exports.platform = NestPlatform;
|
||||
|
||||
@@ -0,0 +1,401 @@
|
||||
'use strict';
|
||||
|
||||
// Netatmo weatherstation for HomeBridge
|
||||
// Wriiten by planetk (https://github.com/planetk)
|
||||
//
|
||||
// Remember to add platform to config.json. Example:
|
||||
// "platforms": [
|
||||
// {
|
||||
// "platform": "Netatmo",
|
||||
// "name": "Netatmo Weather",
|
||||
// "auth": {
|
||||
// "client_id": "",
|
||||
// "client_secret": "",
|
||||
// "username": "",
|
||||
// "password": ""
|
||||
// }
|
||||
// }
|
||||
// ],
|
||||
//
|
||||
// The default code for all HomeBridge accessories is 031-45-154.
|
||||
|
||||
var types = require('hap-nodejs/accessories/types.js');
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
// DECLARE SOME UUIDS WHICH SHOUL BE IN HAP-NODEJS TYPES LIB, BUT ARE NOT YET
|
||||
// REMOVE WHEN HAP LIB IS UPDATED!!
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
var stPre = "000000";
|
||||
var stPost = "-0000-1000-8000-0026BB765291";
|
||||
|
||||
types.BATTERY_SERVICE_STYPE = stPre + "96" + stPost;
|
||||
types.AIR_QUALITY_SENSOR_STYPE = stPre + "8D" + stPost;
|
||||
types.CARBON_DIOXIDE_SENSOR_STYPE = stPre + "97" + stPost;
|
||||
|
||||
types.AIR_PARTICULATE_DENISITY_CTYPE = stPre + "64" + stPost;
|
||||
types.CARBON_DIOXIDE_DETECTED_CTYPE = stPre + "92" + stPost;
|
||||
types.CARBON_DIOXIDE_LEVEL_CTYPE = stPre + "93" + stPost;
|
||||
types.AIR_QUALITY_CTYPE = stPre + "95" + stPost;
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
var netatmo = require('netatmo');
|
||||
var NodeCache = require( "node-cache" );
|
||||
|
||||
function NetAtmoRepository(log, api) {
|
||||
this.api = api;
|
||||
this.log = log;
|
||||
this.cache = new NodeCache();
|
||||
}
|
||||
|
||||
NetAtmoRepository.prototype = {
|
||||
refresh: function(callback) {
|
||||
var datasource = {
|
||||
devices: {},
|
||||
modules: {}
|
||||
};
|
||||
var that = this;
|
||||
that.api.getDevicelist(function(err, devices, modules) {
|
||||
for (var device of devices) {
|
||||
that.log("refreshing device " + device._id + " (" + device.module_name + ")");
|
||||
datasource.devices[device._id] = device;
|
||||
}
|
||||
for (var module of modules) {
|
||||
that.log("refreshing module " + module._id + " (" + module.module_name + ")");
|
||||
datasource.modules[module._id] = module;
|
||||
}
|
||||
that.cache.set( "datasource", datasource, 20 );
|
||||
callback(datasource);
|
||||
});
|
||||
},
|
||||
load: function(callback) {
|
||||
var that = this;
|
||||
this.cache.get( "datasource", function( err, datasource ) {
|
||||
if( !err ){
|
||||
if ( datasource == undefined ){
|
||||
that.refresh(callback);
|
||||
} else {
|
||||
callback(datasource)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function NetatmoPlatform(log, config) {
|
||||
this.log = log;
|
||||
var api = new netatmo(config["auth"]);
|
||||
this.repository = new NetAtmoRepository(this.log, api);
|
||||
api.on("error", function(error) {
|
||||
this.log('ERROR - Netatmo: ' + error);
|
||||
});
|
||||
api.on("warning", function(error) {
|
||||
this.log('WARN - Netatmo: ' + error);
|
||||
});
|
||||
}
|
||||
|
||||
NetatmoPlatform.prototype = {
|
||||
accessories: function(callback) {
|
||||
|
||||
var that = this;
|
||||
var foundAccessories = [];
|
||||
|
||||
this.repository.load(function(datasource) {
|
||||
for (var id in datasource.devices) {
|
||||
var device = datasource.devices[id];
|
||||
that.log("Adding accessory for device " + id + " (" + device.module_name + ")");
|
||||
var accessory = new NetatmoAccessory(that.log, that.repository, device._id, null, device);
|
||||
foundAccessories.push(accessory);
|
||||
}
|
||||
for (var id in datasource.modules) {
|
||||
var module = datasource.modules[id];
|
||||
that.log("Adding accessory for module " + module._id + " (" + module.module_name + ")");
|
||||
var accessory = new NetatmoAccessory(that.log, that.repository, module.main_device, module._id, module);
|
||||
foundAccessories.push(accessory);
|
||||
}
|
||||
callback(foundAccessories);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function NetatmoAccessory(log, repository, deviceId, moduleId, device) {
|
||||
this.log = log;
|
||||
this.repository = repository;
|
||||
this.deviceId = deviceId;
|
||||
this.moduleId = moduleId;
|
||||
this.serial = deviceId;
|
||||
if (moduleId) {
|
||||
this.serial = moduleId;
|
||||
}
|
||||
|
||||
// add station name to devices to avoid duplicate names
|
||||
if (device.station_name) {
|
||||
this.name = device.station_name + " " + device.module_name;
|
||||
} else {
|
||||
this.name = device.module_name;
|
||||
}
|
||||
|
||||
this.model = device.type;
|
||||
this.serviceTypes = device.data_type;
|
||||
if (device.battery_vp) {
|
||||
this.serviceTypes.push("Battery");
|
||||
}
|
||||
}
|
||||
|
||||
NetatmoAccessory.prototype = {
|
||||
|
||||
getData: function(callback) {
|
||||
var that = this;
|
||||
this.repository.load(function(datasource) {
|
||||
if(that.moduleId) {
|
||||
callback(datasource.modules[that.moduleId]);
|
||||
} else {
|
||||
callback(datasource.devices[that.deviceId]);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
getCurrentTemperature: function(callback) {
|
||||
this.getData(function(deviceData) {
|
||||
callback(deviceData.dashboard_data.Temperature);
|
||||
});
|
||||
},
|
||||
|
||||
getCurrentHumidity: function(callback) {
|
||||
this.getData(function(deviceData) {
|
||||
callback(deviceData.dashboard_data.Humidity);
|
||||
});
|
||||
},
|
||||
|
||||
getAirQuality: function(callback) {
|
||||
this.getData(function(deviceData) {
|
||||
var level = deviceData.dashboard_data.CO2;
|
||||
var quality = 0;
|
||||
if (level > 2000) quality = 5;
|
||||
else if (level > 1500) quality = 4;
|
||||
else if (level > 1000) quality = 3;
|
||||
else if (level > 500) quality = 2;
|
||||
else if (level > 250) quality = 1;
|
||||
callback(quality);
|
||||
});
|
||||
},
|
||||
getCurrentCO2Level: function(callback) {
|
||||
this.log("fetching co2");
|
||||
this.getData(function(deviceData) {
|
||||
callback(deviceData.dashboard_data.CO2);
|
||||
});
|
||||
},
|
||||
|
||||
informationCharacteristics: function() {
|
||||
return [
|
||||
{
|
||||
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: "Netatmo",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.model,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.serial,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "SN",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.IDENTIFY_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pw"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
humidityCharacteristics: function(that) {
|
||||
var cTypes = [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name +" Humidity",
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.CURRENT_RELATIVE_HUMIDITY_CTYPE,
|
||||
onRead: function(callback) { that.getCurrentHumidity(callback); },
|
||||
onUpdate: null,
|
||||
perms: ["pr","ev"],
|
||||
format: "int",
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Humidity"
|
||||
}];
|
||||
return cTypes;
|
||||
},
|
||||
|
||||
temperatureCharacteristics: function(that) {
|
||||
var cTypes = [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name + " Temperature",
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.CURRENT_TEMPERATURE_CTYPE,
|
||||
onRead: function(callback) { that.getCurrentTemperature(callback); },
|
||||
onUpdate: null,
|
||||
perms: ["pr","ev"],
|
||||
format: "float",
|
||||
initialValue: 0.0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Current Temperature",
|
||||
unit: "celsius"
|
||||
}];
|
||||
return cTypes;
|
||||
},
|
||||
|
||||
co2Characteristics: function(that) {
|
||||
var cTypes = [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name + "Carbon Dioxide Level",
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.CARBON_DIOXIDE_DETECTED_CTYPE,
|
||||
//onRead: function(callback) { that.getCurrentTemperature(callback); },
|
||||
onRead: function(callback) { callback(0); },
|
||||
onUpdate: null,
|
||||
perms: ["pr","ev"],
|
||||
format: "uint8",
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "CO2 detected"
|
||||
},{
|
||||
cType: types.CARBON_DIOXIDE_LEVEL_CTYPE,
|
||||
onRead: function(callback) { that.getCurrentCO2Level(callback); },
|
||||
onUpdate: null,
|
||||
perms: ["pr","ev"],
|
||||
format: "float",
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "CO2 level "
|
||||
}];
|
||||
return cTypes;
|
||||
},
|
||||
|
||||
airQualityCharacteristics: function(that) {
|
||||
var cTypes = [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.name + " Air Quality",
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.AIR_QUALITY_CTYPE,
|
||||
onRead: function(callback) { that.getAirQuality(callback); },
|
||||
onUpdate: null,
|
||||
perms: ["pr","ev"],
|
||||
format: "float",
|
||||
initialValue: 0,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Current Air Quality",
|
||||
}];
|
||||
return cTypes;
|
||||
},
|
||||
|
||||
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
var services = [{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: this.informationCharacteristics(),
|
||||
}];
|
||||
|
||||
// TEMPERATURE //////////////////////////////////////////////////
|
||||
if (this.serviceTypes.indexOf("Temperature") > -1) {
|
||||
var tempSensorSvc = {
|
||||
sType: types.TEMPERATURE_SENSOR_STYPE,
|
||||
characteristics: this.temperatureCharacteristics(that)
|
||||
}
|
||||
services.push(tempSensorSvc);
|
||||
}
|
||||
// HUMIDITY ////////////////////////////////////////////////////
|
||||
if (this.serviceTypes.indexOf("Humidity") > -1) {
|
||||
services.push({
|
||||
sType: types.HUMIDITY_SENSOR_STYPE,
|
||||
characteristics: this.humidityCharacteristics(that)
|
||||
});
|
||||
}
|
||||
// CO2 SENSOR /////////////////////////////////////////////////
|
||||
if (this.serviceTypes.indexOf("CO2") > -1) {
|
||||
services.push({
|
||||
sType: types.CARBON_DIOXIDE_SENSOR_STYPE,
|
||||
characteristics: this.co2Characteristics(that)
|
||||
});
|
||||
services.push({
|
||||
sType: types.AIR_QUALITY_SENSOR_STYPE,
|
||||
characteristics: this.airQualityCharacteristics(that)
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Pressure
|
||||
// TODO: Noise
|
||||
// TODO: Battery
|
||||
// TODO: Check Elgato Eve Characteristics (map min, max, time series, etc.)!
|
||||
|
||||
return services;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.accessory = NetatmoAccessory;
|
||||
module.exports.platform = NetatmoPlatform;
|
||||
@@ -0,0 +1,573 @@
|
||||
// OpenHAB 1 Platform Shim for HomeBridge
|
||||
// Written by Tommaso Marchionni
|
||||
// Based on many of the other HomeBridge platform modules
|
||||
//
|
||||
// Revisions:
|
||||
//
|
||||
// 17 October 2015 [tommasomarchionni]
|
||||
// - Initial release
|
||||
//
|
||||
// 25 October 2015 [tommasomarchionni]
|
||||
// - Added WS listener and new OOP structure
|
||||
//
|
||||
// Remember to add platform to config.json. Example:
|
||||
// "platforms": [
|
||||
// {
|
||||
// "platform": "Openhab",
|
||||
// "name": "Openhab",
|
||||
// "server": "127.0.0.1",
|
||||
// "port": "8080",
|
||||
// "sitemap": "demo"
|
||||
// }
|
||||
// ],
|
||||
//
|
||||
// Example of sitemap in OpenHAB:
|
||||
// sitemap homekit label="HomeKit" {
|
||||
// Switch item=Light_1 label="Light 1"
|
||||
// }
|
||||
//
|
||||
// Rollershutter is tested with this binding in OpenHAB:
|
||||
// command=SWITCH_MULTILEVEL,invert_percent=true,invert_state=false"
|
||||
// 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.
|
||||
//
|
||||
|
||||
//////// LIBS /////////
|
||||
|
||||
var WebSocket = require('ws');
|
||||
var request = require("request");
|
||||
var Service = require("hap-nodejs/lib/Service.js").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var currentModule = this;
|
||||
var util = require('core-util-is');
|
||||
util.inherits = require('inherits');
|
||||
|
||||
//////// PLATFORM /////////
|
||||
|
||||
function OpenhabPlatform(log, config){
|
||||
this.log = log;
|
||||
this.user = config["user"];
|
||||
this.password = config["password"];
|
||||
this.server = config["server"];
|
||||
this.port = config["port"];
|
||||
this.protocol = "http";
|
||||
this.sitemap = "demo";
|
||||
if (typeof config["sitemap"] != 'undefined') {
|
||||
this.sitemap = config["sitemap"];
|
||||
}
|
||||
}
|
||||
|
||||
OpenhabPlatform.prototype = {
|
||||
accessories: function(callback) {
|
||||
var that = this;
|
||||
this.log("Platform - Fetching OpenHAB devices.");
|
||||
var itemFactory = new ItemFactory(this);
|
||||
url = itemFactory.sitemapUrl();
|
||||
this.log("Platform - Connecting to " + url);
|
||||
request.get({
|
||||
url: url,
|
||||
json: true
|
||||
}, function(err, response, json) {
|
||||
if (!err && response.statusCode == 200) {
|
||||
callback(itemFactory.parseSitemap(json));
|
||||
} else {
|
||||
that.log("Platform - There was a problem connecting to OpenHAB.");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
//////// END PLATFORM /////////
|
||||
|
||||
///////// ACCESSORY /////////
|
||||
|
||||
function OpenhabAccessory(widget,platform) {}
|
||||
|
||||
///////// ABSTRACT ITEM /////////
|
||||
|
||||
function AbstractItem(widget,platform){
|
||||
|
||||
AbstractItem.super_.call(this,widget,platform);
|
||||
|
||||
this.widget = widget;
|
||||
this.label = widget.label;
|
||||
this.name = widget.item.name;
|
||||
this.url = widget.item.link;
|
||||
this.state = widget.item.state;
|
||||
this.platform = platform;
|
||||
this.log = platform.log;
|
||||
|
||||
this.setInitialState = false;
|
||||
this.setFromOpenHAB = false;
|
||||
this.informationService = undefined;
|
||||
this.otherService = undefined;
|
||||
this.listener = undefined;
|
||||
this.ws = undefined;
|
||||
};
|
||||
|
||||
util.inherits(AbstractItem, OpenhabAccessory);
|
||||
|
||||
AbstractItem.prototype.getInformationServices = function() {
|
||||
informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "OpenHAB")
|
||||
.setCharacteristic(Characteristic.Model, this.constructor.name)
|
||||
.setCharacteristic(Characteristic.SerialNumber, "N/A")
|
||||
.setCharacteristic(Characteristic.Name, this.name);
|
||||
return informationService;
|
||||
}
|
||||
|
||||
AbstractItem.prototype.checkListener = function() {
|
||||
|
||||
if (typeof this.listener == 'undefined' || typeof this.ws == 'undefined') {
|
||||
this.ws = undefined;
|
||||
this.listener = new WSListener(this, this.updateCharacteristics.bind(this));
|
||||
this.listener.startListener();
|
||||
}
|
||||
};
|
||||
|
||||
///////// END ABSTRACT ITEM /////////
|
||||
|
||||
///////// SWITCH ITEM /////////
|
||||
|
||||
function SwitchItem(widget,platform){
|
||||
SwitchItem.super_.call(this, widget,platform);
|
||||
};
|
||||
|
||||
util.inherits(SwitchItem, AbstractItem);
|
||||
|
||||
SwitchItem.prototype.getServices = function() {
|
||||
|
||||
this.checkListener();
|
||||
this.setInitialState = true;
|
||||
this.informationService = this.getInformationServices();
|
||||
|
||||
this.otherService = new Service.Lightbulb();
|
||||
this.otherService.getCharacteristic(Characteristic.On)
|
||||
.on('set', this.setItem.bind(this))
|
||||
.on('get', this.getItemPowerState.bind(this))
|
||||
.setValue(this.state === 'ON');
|
||||
|
||||
return [this.informationService, this.otherService];
|
||||
};
|
||||
|
||||
SwitchItem.prototype.updateCharacteristics = function(message) {
|
||||
|
||||
this.setFromOpenHAB = true;
|
||||
this.otherService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.setValue(message === 'ON' ? true : false,
|
||||
function() {
|
||||
this.setFromOpenHAB = false;
|
||||
}.bind(this)
|
||||
);
|
||||
};
|
||||
|
||||
SwitchItem.prototype.getItemPowerState = function(callback) {
|
||||
|
||||
var self = this;
|
||||
this.checkListener();
|
||||
|
||||
this.log("iOS - request power state from " + this.name);
|
||||
request(this.url + '/state?type=json', function (error, response, body) {
|
||||
if (!error && response.statusCode == 200) {
|
||||
self.log("OpenHAB HTTP - response from " + self.name + ": " + body);
|
||||
callback(undefined,body == "ON" ? true : false);
|
||||
} else {
|
||||
self.log("OpenHAB HTTP - error from " + self.name + ": " + error);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
SwitchItem.prototype.setItem = function(value, callback) {
|
||||
|
||||
var self = this;
|
||||
this.checkListener();
|
||||
|
||||
if (this.setInitialState) {
|
||||
this.setInitialState = false;
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.setFromOpenHAB) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
this.log("iOS - send message to " + this.name + ": " + value);
|
||||
var command = value ? 'ON' : 'OFF';
|
||||
request.post(
|
||||
this.url,
|
||||
{ body: command },
|
||||
function (error, response, body) {
|
||||
if (!error && response.statusCode == 201) {
|
||||
self.log("OpenHAB HTTP - response from " + self.name + ": " + body);
|
||||
} else {
|
||||
self.log("OpenHAB HTTP - error from " + self.name + ": " + error);
|
||||
}
|
||||
callback();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
///////// END SWITCH ITEM /////////
|
||||
|
||||
///////// DIMMER ITEM /////////
|
||||
|
||||
function DimmerItem(widget,platform){
|
||||
DimmerItem.super_.call(this, widget,platform);
|
||||
};
|
||||
|
||||
util.inherits(DimmerItem, AbstractItem);
|
||||
|
||||
DimmerItem.prototype.getServices = function() {
|
||||
|
||||
this.checkListener();
|
||||
this.setInitialState = true;
|
||||
|
||||
this.informationService = this.getInformationServices();
|
||||
|
||||
this.otherService = new Service.Lightbulb();
|
||||
this.otherService.getCharacteristic(Characteristic.On)
|
||||
.on('set', this.setItem.bind(this))
|
||||
.on('get', this.getItemPowerState.bind(this))
|
||||
.setValue(+this.state > 0);
|
||||
|
||||
this.setInitialState = true;
|
||||
|
||||
this.otherService.addCharacteristic(Characteristic.Brightness)
|
||||
.on('set', this.setItem.bind(this))
|
||||
.on('get', this.getItemBrightnessState.bind(this))
|
||||
.setValue(+this.state);
|
||||
|
||||
return [this.informationService, this.otherService];
|
||||
};
|
||||
|
||||
DimmerItem.prototype.updateCharacteristics = function(message) {
|
||||
|
||||
this.setFromOpenHAB = true;
|
||||
var brightness = +message;
|
||||
var steps = 2;
|
||||
if (brightness >= 0) {
|
||||
this.otherService.getCharacteristic(Characteristic.Brightness)
|
||||
.setValue(brightness,
|
||||
function() {
|
||||
steps--;
|
||||
if (!steps) {
|
||||
this.setFromOpenHAB = false;
|
||||
}
|
||||
}.bind(this));
|
||||
this.otherService.getCharacteristic(Characteristic.On)
|
||||
.setValue(brightness > 0 ? true : false,
|
||||
function() {
|
||||
steps--;
|
||||
if (!steps) {
|
||||
this.setFromOpenHAB = false;
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
DimmerItem.prototype.getItemPowerState = function(callback) {
|
||||
|
||||
var self = this;
|
||||
this.checkListener();
|
||||
|
||||
this.log("iOS - request power state from " + this.name);
|
||||
request(this.url + '/state?type=json', function (error, response, body) {
|
||||
if (!error && response.statusCode == 200) {
|
||||
self.log("OpenHAB HTTP - response from " + self.name + ": " + body);
|
||||
callback(undefined,+body > 0 ? true : false);
|
||||
} else {
|
||||
self.log("OpenHAB HTTP - error from " + self.name + ": " + error);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
DimmerItem.prototype.setItem = function(value, callback) {
|
||||
|
||||
var self = this;
|
||||
this.checkListener();
|
||||
|
||||
if (this.setInitialState) {
|
||||
this.setInitialState = false;
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.setFromOpenHAB) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
this.log("iOS - send message to " + this.name + ": " + value);
|
||||
var command = 0;
|
||||
if (typeof value === 'boolean') {
|
||||
command = value ? '100' : '0';
|
||||
} else {
|
||||
command = "" + value;
|
||||
}
|
||||
request.post(
|
||||
this.url,
|
||||
{
|
||||
body: command,
|
||||
headers: {'Content-Type': 'text/plain'}
|
||||
},
|
||||
function (error, response, body) {
|
||||
if (!error && response.statusCode == 201) {
|
||||
self.log("OpenHAB HTTP - response from " + self.name + ": " + body);
|
||||
} else {
|
||||
self.log("OpenHAB HTTP - error from " + self.name + ": " + error);
|
||||
}
|
||||
callback();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
DimmerItem.prototype.getItemBrightnessState = function(callback) {
|
||||
|
||||
var self = this;
|
||||
|
||||
this.log("iOS - request brightness state from " + this.name);
|
||||
request(this.url + '/state?type=json', function (error, response, body) {
|
||||
if (!error && response.statusCode == 200) {
|
||||
self.log("OpenHAB HTTP - response from " + self.name + ": " + body);
|
||||
callback(undefined,+body);
|
||||
} else {
|
||||
self.log("OpenHAB HTTP - error from " + self.name + ": " + error);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
///////// END DIMMER ITEM /////////
|
||||
|
||||
///////// ROLLERSHUTTER ITEM /////////
|
||||
|
||||
function RollershutterItem(widget,platform){
|
||||
RollershutterItem.super_.call(this, widget,platform);
|
||||
this.positionState = Characteristic.PositionState.STOPPED;
|
||||
this.currentPosition = 100;
|
||||
this.targetPosition = 100;
|
||||
this.startedPosition = 100;
|
||||
};
|
||||
|
||||
util.inherits(RollershutterItem, AbstractItem);
|
||||
|
||||
RollershutterItem.prototype.getServices = function() {
|
||||
|
||||
this.checkListener();
|
||||
|
||||
this.informationService = this.getInformationServices();
|
||||
|
||||
this.otherService = new Service.WindowCovering();
|
||||
|
||||
this.otherService.getCharacteristic(Characteristic.CurrentPosition)
|
||||
.on('get', this.getItemCurrentPosition.bind(this))
|
||||
.setValue(this.currentPosition);
|
||||
|
||||
this.setInitialState = true;
|
||||
|
||||
this.otherService.getCharacteristic(Characteristic.TargetPosition)
|
||||
.on('set', this.setItem.bind(this))
|
||||
.on('get', this.getItemTargetPosition.bind(this))
|
||||
.setValue(this.currentPosition);
|
||||
|
||||
this.otherService.getCharacteristic(Characteristic.PositionState)
|
||||
.on('get', this.getItemPositionState.bind(this))
|
||||
.setValue(this.positionState);
|
||||
|
||||
return [this.informationService, this.otherService];
|
||||
};
|
||||
|
||||
|
||||
|
||||
RollershutterItem.prototype.updateCharacteristics = function(message) {
|
||||
|
||||
console.log(message);
|
||||
console.log(this.targetPosition);
|
||||
|
||||
|
||||
|
||||
if (parseInt(message) == this.targetPosition) {
|
||||
var ps = Characteristic.PositionState.STOPPED;
|
||||
var cs = parseInt(message);
|
||||
} else if (parseInt(message) > this.targetPosition){
|
||||
var ps = Characteristic.PositionState.INCREASING;
|
||||
var cs = this.startedPosition;
|
||||
} else {
|
||||
var ps = Characteristic.PositionState.DECREASING;
|
||||
var cs = this.startedPosition;
|
||||
}
|
||||
|
||||
this.otherService
|
||||
.getCharacteristic(Characteristic.PositionState)
|
||||
.setValue(ps);
|
||||
|
||||
this.otherService
|
||||
.getCharacteristic(Characteristic.CurrentPosition)
|
||||
.setValue(parseInt(cs));
|
||||
this.currentPosition = parseInt(cs);
|
||||
};
|
||||
|
||||
RollershutterItem.prototype.setItem = function(value, callback) {
|
||||
|
||||
var self = this;
|
||||
this.checkListener();
|
||||
|
||||
if (this.setInitialState) {
|
||||
this.setInitialState = false;
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
this.startedPosition = this.currentPosition;
|
||||
|
||||
this.log("iOS - send message to " + this.name + ": " + value);
|
||||
|
||||
var command = 0;
|
||||
if (typeof value === 'boolean') {
|
||||
command = value ? '100' : '0';
|
||||
} else {
|
||||
command = "" + value;
|
||||
}
|
||||
request.post(
|
||||
this.url,
|
||||
{
|
||||
body: command,
|
||||
headers: {'Content-Type': 'text/plain'}
|
||||
},
|
||||
function (error, response, body) {
|
||||
if (!error && response.statusCode == 201) {
|
||||
self.log("OpenHAB HTTP - response from " + self.name + ": " + body);
|
||||
self.targetPosition = parseInt(value);
|
||||
} else {
|
||||
self.log("OpenHAB HTTP - error from " + self.name + ": " + error);
|
||||
}
|
||||
callback();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
RollershutterItem.prototype.getItemPositionState = function(callback) {
|
||||
this.log("iOS - request position state from " + this.name);
|
||||
this.log("Platform - response from " + this.name + ": " + this.positionState);
|
||||
callback(undefined,this.positionState);
|
||||
};
|
||||
|
||||
RollershutterItem.prototype.getItemTargetPosition = function(callback) {
|
||||
this.log("iOS - get target position state from " + this.name);
|
||||
this.log("Platform - response from " + this.name + ": " + this.targetPosition);
|
||||
callback(undefined,this.targetPosition);
|
||||
}
|
||||
|
||||
RollershutterItem.prototype.getItemCurrentPosition = function(callback) {
|
||||
var self = this;
|
||||
this.log("iOS - request current position state from " + this.name);
|
||||
|
||||
request(this.url + '/state?type=json', function (error, response, body) {
|
||||
if (!error && response.statusCode == 200) {
|
||||
|
||||
self.log("OpenHAB HTTP - response from " + self.name + ": " +body);
|
||||
self.currentPosition = parseInt(body);
|
||||
callback(undefined,parseInt(body));
|
||||
|
||||
} else {
|
||||
self.log("OpenHAB HTTP - error from " + self.name + ": " + error);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
///////// END ROLLERSHUTTER ITEM /////////
|
||||
|
||||
///////// ITEM UTILITY /////////
|
||||
|
||||
function ItemFactory(openhabPlatform){
|
||||
this.platform = openhabPlatform;
|
||||
this.log = this.platform.log;
|
||||
}
|
||||
|
||||
ItemFactory.prototype = {
|
||||
sitemapUrl: function() {
|
||||
var serverString = this.platform.server;
|
||||
//TODO da verificare
|
||||
if (this.platform.user && this.platform.password) {
|
||||
serverString = this.platform.user + ":" + this.platform.password + "@" + serverString;
|
||||
}
|
||||
|
||||
return this.platform.protocol + "://" + serverString + ":" + this.platform.port + "/rest/sitemaps/" + this.platform.sitemap + "?type=json";
|
||||
},
|
||||
|
||||
parseSitemap: function(jsonSitemap) {
|
||||
var widgets = [].concat(jsonSitemap.homepage.widget);
|
||||
|
||||
var result = [];
|
||||
for (var i = 0; i < widgets.length; i++) {
|
||||
var widget = widgets[i];
|
||||
if (!widget.item) {
|
||||
//TODO to handle frame
|
||||
this.log("Platform - The widget '" + widget.label + "' is not an item.");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentModule[widget.item.type] != undefined) {
|
||||
var accessory = new currentModule[widget.item.type](widget,this.platform);
|
||||
} else {
|
||||
this.log("Platform - The widget '" + widget.label + "' of type "+widget.item.type+" is an item not handled.");
|
||||
continue;
|
||||
}
|
||||
|
||||
this.log("Platform - Accessory Found: " + widget.label);
|
||||
result.push(accessory);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
///////// END ITEM UTILITY /////////
|
||||
|
||||
///////// WS LISTENER /////////
|
||||
|
||||
function WSListener(item, callback){
|
||||
this.item = item;
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
WSListener.prototype = {
|
||||
startListener: function() {
|
||||
var self = this;
|
||||
|
||||
if (typeof this.item.ws == 'undefined') {
|
||||
this.item.ws = new WebSocket(this.item.url.replace('http:', 'ws:') + '/state?type=json');
|
||||
}
|
||||
|
||||
this.item.ws.on('open', function() {
|
||||
self.item.log("OpenHAB WS - new connection for "+self.item.name);
|
||||
});
|
||||
|
||||
this.item.ws.on('message', function(message) {
|
||||
self.item.log("OpenHAB WS - message from " +self.item.name+": "+ message);
|
||||
self.callback(message);
|
||||
});
|
||||
|
||||
this.item.ws.on('close', function close() {
|
||||
self.item.log("OpenHAB WS - closed connection for "+self.item.name);
|
||||
self.item.listener = undefined;
|
||||
self.item.ws = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
///////// END WS LISTENER /////////
|
||||
|
||||
///////// SUPPORTED ITEMS /////////
|
||||
module.exports.SwitchItem = SwitchItem;
|
||||
module.exports.DimmerItem = DimmerItem;
|
||||
module.exports.RollershutterItem = RollershutterItem;
|
||||
///////// END SUPPORTED ITEMS /////////
|
||||
|
||||
module.exports.accessory = OpenhabAccessory;
|
||||
module.exports.platform = OpenhabPlatform;
|
||||
@@ -33,9 +33,6 @@ var hue = require("node-hue-api"),
|
||||
HueApi = hue.HueApi,
|
||||
lightState = hue.lightState;
|
||||
|
||||
/* // Oldschool
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
*/
|
||||
var Service = require("HAP-NodeJS").Service;
|
||||
var Characteristic = require("HAP-NodeJS").Characteristic;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// SmartThings JSON API SmartApp required
|
||||
// https://github.com/jnewland/SmartThings/blob/master/JSON.groovy
|
||||
//
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var request = require("request");
|
||||
|
||||
function SmartThingsPlatform(log, config){
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var sonos = require('sonos');
|
||||
|
||||
function SonosPlatform(log, config){
|
||||
@@ -34,7 +34,7 @@ SonosPlatform.prototype = {
|
||||
that.log("Found device at " + device.host);
|
||||
|
||||
device.deviceDescription(function (err, description) {
|
||||
if (description["zoneType"] != '11' && description["zoneType"] != '8') { // 8 is the Sonos SUB
|
||||
if (description["zoneType"] != '11' && description["zoneType"] != '8' && description["zoneType"] != '4') { // 8 is the Sonos SUB, 4 is the Sonos Bridge
|
||||
var roomName = description["roomName"];
|
||||
|
||||
if (!roomNamesFound[roomName]) {
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var telldus = require('telldus');
|
||||
|
||||
function TelldusPlatform(log, config) {
|
||||
var that = this;
|
||||
that.log = log;
|
||||
}
|
||||
|
||||
TelldusPlatform.prototype = {
|
||||
|
||||
accessories: function(callback) {
|
||||
var that = this;
|
||||
|
||||
that.log("Fetching devices...");
|
||||
|
||||
var devices = telldus.getDevicesSync();
|
||||
|
||||
that.log("Found " + devices.length + " devices...");
|
||||
|
||||
var foundAccessories = [];
|
||||
|
||||
// Clean non device
|
||||
for (var i = 0; i < devices.length; i++) {
|
||||
if (devices[i].type != 'DEVICE') {
|
||||
devices.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < devices.length; i++) {
|
||||
if (devices[i].type === 'DEVICE') {
|
||||
TelldusAccessory.create(that.log, devices[i], function(err, accessory) {
|
||||
if (!!err) that.log("Couldn't load device info");
|
||||
foundAccessories.push(accessory);
|
||||
if (foundAccessories.length >= devices.length) {
|
||||
callback(foundAccessories);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var TelldusAccessory = function TelldusAccessory(log, device) {
|
||||
|
||||
this.log = log;
|
||||
|
||||
var m = device.model.split(':');
|
||||
|
||||
this.dimTimeout = false;
|
||||
|
||||
// Set accessory info
|
||||
this.device = device;
|
||||
this.id = device.id;
|
||||
this.name = device.name;
|
||||
this.manufacturer = "Telldus"; // NOTE: Change this later
|
||||
this.model = device.model;
|
||||
this.status = device.status;
|
||||
switch (device.status.name) {
|
||||
case 'OFF':
|
||||
this.state = 0;
|
||||
this.stateValue = 0;
|
||||
break;
|
||||
case 'ON':
|
||||
this.state = 2;
|
||||
this.stateValue = 1;
|
||||
break;
|
||||
case 'DIM':
|
||||
this.state = 16;
|
||||
this.stateValue = device.status.level;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
TelldusAccessory.create = function (log, device, callback) {
|
||||
|
||||
callback(null, new TelldusAccessory(log, device));
|
||||
|
||||
};
|
||||
|
||||
TelldusAccessory.prototype = {
|
||||
|
||||
dimmerValue: function() {
|
||||
|
||||
if (this.state === 1) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
if (this.state === 16 && this.stateValue != "unde") {
|
||||
return parseInt(this.stateValue * 100 / 255);
|
||||
}
|
||||
|
||||
return 0;
|
||||
},
|
||||
|
||||
informationCharacteristics: function() {
|
||||
var that = this;
|
||||
|
||||
informationCharacteristics = [
|
||||
{
|
||||
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: that.manufacturer,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: that.model,
|
||||
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: function () {
|
||||
telldus.turnOff(that.id, function(err){
|
||||
if (!!err) that.log("Error: " + err.message);
|
||||
telldus.turnOn(that.id, function(err){
|
||||
if (!!err) that.log("Error: " + err.message);
|
||||
telldus.turnOff(that.id, function(err){
|
||||
if (!!err) that.log("Error: " + err.message);
|
||||
telldus.turnOn(that.id, function(err){
|
||||
if (!!err) that.log("Error: " + err.message);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
perms: ["pw"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Identify Accessory",
|
||||
designedMaxLength: 1
|
||||
}
|
||||
];
|
||||
return informationCharacteristics;
|
||||
},
|
||||
|
||||
controlCharacteristics: function() {
|
||||
var that = this;
|
||||
|
||||
cTypes = [{
|
||||
cType: types.NAME_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: that.name,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
}]
|
||||
|
||||
cTypes.push({
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) {
|
||||
if (value) {
|
||||
telldus.turnOn(that.id, function(err){
|
||||
if (!!err) {
|
||||
that.log("Error: " + err.message)
|
||||
} else {
|
||||
that.log(that.name + " - Updated power state: ON");
|
||||
}
|
||||
});
|
||||
} else {
|
||||
telldus.turnOff(that.id, function(err){
|
||||
if (!!err) {
|
||||
that.log("Error: " + err.message)
|
||||
} else {
|
||||
that.log(that.name + " - Updated power state: OFF");
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: (that.state != 2 && (that.state === 16 && that.stateValue != 0)) ? 1 : 0,
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the power state",
|
||||
designedMaxLength: 1
|
||||
})
|
||||
|
||||
if (that.model === "selflearning-dimmer") {
|
||||
cTypes.push({
|
||||
cType: types.BRIGHTNESS_CTYPE,
|
||||
onUpdate: function (value) {
|
||||
if (that.dimTimeout) {
|
||||
clearTimeout(that.dimTimeout);
|
||||
}
|
||||
|
||||
that.dimTimeout = setTimeout(function(){
|
||||
telldus.dim(that.id, (255 * (value / 100)), function(err, result){
|
||||
if (!!err) {
|
||||
that.log("Error: " + err.message);
|
||||
} else {
|
||||
that.log(that.name + " - Updated brightness: " + value);
|
||||
}
|
||||
});
|
||||
that.dimTimeout = false;
|
||||
}, 250);
|
||||
},
|
||||
perms: ["pw", "pr", "ev"],
|
||||
format: "int",
|
||||
initialValue: that.dimmerValue(),
|
||||
supportEvents: true,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Adjust Brightness of Light",
|
||||
designedMinValue: 0,
|
||||
designedMaxValue: 100,
|
||||
designedMinStep: 1,
|
||||
unit: "%"
|
||||
})
|
||||
}
|
||||
|
||||
return cTypes
|
||||
},
|
||||
|
||||
getServices: function() {
|
||||
|
||||
var services = [
|
||||
{
|
||||
sType: types.ACCESSORY_INFORMATION_STYPE,
|
||||
characteristics: this.informationCharacteristics()
|
||||
},
|
||||
{
|
||||
sType: types.LIGHTBULB_STYPE,
|
||||
characteristics: this.controlCharacteristics()
|
||||
}
|
||||
];
|
||||
|
||||
return services;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.platform = TelldusPlatform;
|
||||
module.exports.accessory = TelldusAccessory;
|
||||
@@ -1,4 +1,4 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var TellduAPI = require("telldus-live");
|
||||
|
||||
function TelldusLivePlatform(log, config) {
|
||||
@@ -58,7 +58,7 @@ var TelldusLiveAccessory = function TelldusLiveAccessory(log, cloud, device) {
|
||||
this.log = log;
|
||||
this.cloud = cloud;
|
||||
|
||||
var m = device.model.split(':');
|
||||
var m = device.model ? device.model.split(':') : ['unknown', 'unknown'] ;
|
||||
|
||||
// Set accessory info
|
||||
this.device = device;
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var wink = require('wink-js');
|
||||
|
||||
var model = {
|
||||
|
||||
+167
-100
@@ -1,5 +1,10 @@
|
||||
var types = require("HAP-NodeJS/accessories/types.js");
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var inherits = require('util').inherits;
|
||||
var debug = require('debug')('YamahaAVR');
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var Yamaha = require('yamaha-nodejs');
|
||||
var Q = require('q');
|
||||
var mdns = require('mdns');
|
||||
//workaround for raspberry pi
|
||||
var sequence = [
|
||||
@@ -12,10 +17,54 @@ function YamahaAVRPlatform(log, config){
|
||||
this.log = log;
|
||||
this.config = config;
|
||||
this.playVolume = config["play_volume"];
|
||||
this.minVolume = config["min_volume"] || -50.0;
|
||||
this.maxVolume = config["max_volume"] || -20.0;
|
||||
this.gapVolume = this.maxVolume - this.minVolume;
|
||||
this.setMainInputTo = config["setMainInputTo"];
|
||||
this.expectedDevices = config["expected_devices"] || 100;
|
||||
this.discoveryTimeout = config["discovery_timeout"] || 30;
|
||||
this.manualAddresses = config["manual_addresses"] || {};
|
||||
this.browser = mdns.createBrowser(mdns.tcp('http'), {resolverSequence: sequence});
|
||||
}
|
||||
|
||||
// Custom Characteristics and service...
|
||||
|
||||
YamahaAVRPlatform.AudioVolume = function() {
|
||||
Characteristic.call(this, 'Audio Volume', '00001001-0000-1000-8000-135D67EC4377');
|
||||
this.setProps({
|
||||
format: Characteristic.Formats.UINT8,
|
||||
unit: Characteristic.Units.PERCENTAGE,
|
||||
maxValue: 100,
|
||||
minValue: 0,
|
||||
minStep: 1,
|
||||
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY]
|
||||
});
|
||||
this.value = this.getDefaultValue();
|
||||
};
|
||||
inherits(YamahaAVRPlatform.AudioVolume, Characteristic);
|
||||
|
||||
YamahaAVRPlatform.Muting = function() {
|
||||
Characteristic.call(this, 'Muting', '00001002-0000-1000-8000-135D67EC4377');
|
||||
this.setProps({
|
||||
format: Characteristic.Formats.UINT8,
|
||||
perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY]
|
||||
});
|
||||
this.value = this.getDefaultValue();
|
||||
};
|
||||
inherits(YamahaAVRPlatform.Muting, Characteristic);
|
||||
|
||||
YamahaAVRPlatform.AudioDeviceService = function(displayName, subtype) {
|
||||
Service.call(this, displayName, '00000001-0000-1000-8000-135D67EC4377', subtype);
|
||||
|
||||
// Required Characteristics
|
||||
this.addCharacteristic(YamahaAVRPlatform.AudioVolume);
|
||||
|
||||
// Optional Characteristics
|
||||
this.addOptionalCharacteristic(YamahaAVRPlatform.Muting);
|
||||
};
|
||||
inherits(YamahaAVRPlatform.AudioDeviceService, Service);
|
||||
|
||||
|
||||
YamahaAVRPlatform.prototype = {
|
||||
accessories: function(callback) {
|
||||
this.log("Getting Yamaha AVR devices.");
|
||||
@@ -24,38 +73,86 @@ YamahaAVRPlatform.prototype = {
|
||||
var browser = this.browser;
|
||||
browser.stop();
|
||||
browser.removeAllListeners('serviceUp'); // cleanup listeners
|
||||
|
||||
browser.on('serviceUp', function(service){
|
||||
var accessories = [];
|
||||
var timer, timeElapsed = 0, checkCyclePeriod = 5000;
|
||||
|
||||
// Hmm... seems we need to prevent double-listing via manual and Bonjour...
|
||||
var sysIds = {};
|
||||
|
||||
var setupFromService = function(service){
|
||||
var name = service.name;
|
||||
//console.log('Found HTTP service "' + name + '"');
|
||||
// We can't tell just from mdns if this is an AVR...
|
||||
if (service.port != 80) return; // yamaha-nodejs assumes this, so finding one on another port wouldn't do any good anyway.
|
||||
var yamaha = new Yamaha(service.host);
|
||||
yamaha.getSystemConfig().then(function(sysConfig){
|
||||
var sysModel = sysConfig.YAMAHA_AV.System[0].Config[0].Model_Name[0];
|
||||
var sysId = sysConfig.YAMAHA_AV.System[0].Config[0].System_ID[0];
|
||||
that.log("Found Yamaha " + sysModel + " - " + sysId + ", \"" + name + "\"");
|
||||
var accessory = new YamahaAVRAccessory(that.log, that.config, service, yamaha, sysConfig);
|
||||
callback([accessory]);
|
||||
}, function(err){
|
||||
return;
|
||||
})
|
||||
});
|
||||
yamaha.getSystemConfig().then(
|
||||
function(sysConfig){
|
||||
var sysModel = sysConfig.YAMAHA_AV.System[0].Config[0].Model_Name[0];
|
||||
var sysId = sysConfig.YAMAHA_AV.System[0].Config[0].System_ID[0];
|
||||
if(sysIds[sysId]){
|
||||
this.log("WARN: Got multiple systems with ID " + sysId + "! Omitting duplicate!");
|
||||
return;
|
||||
}
|
||||
sysIds[sysId] = true;
|
||||
this.log("Found Yamaha " + sysModel + " - " + sysId + ", \"" + name + "\"");
|
||||
var accessory = new YamahaAVRAccessory(this.log, this.config, name, yamaha, sysConfig);
|
||||
accessories.push(accessory);
|
||||
if(accessories.length >= this.expectedDevices)
|
||||
timeoutFunction(); // We're done, call the timeout function now.
|
||||
}.bind(this)
|
||||
);
|
||||
}.bind(this);
|
||||
|
||||
// process manually specified devices...
|
||||
for(var key in this.manualAddresses){
|
||||
if(!this.manualAddresses.hasOwnProperty(key)) continue;
|
||||
setupFromService({
|
||||
name: key,
|
||||
host: this.manualAddresses[key],
|
||||
port: 80
|
||||
});
|
||||
}
|
||||
|
||||
browser.on('serviceUp', setupFromService);
|
||||
browser.start();
|
||||
|
||||
// The callback can only be called once...so we'll have to find as many as we can
|
||||
// in a fixed time and then call them in.
|
||||
var timeoutFunction = function(){
|
||||
if(accessories.length >= that.expectedDevices){
|
||||
clearTimeout(timer);
|
||||
} else {
|
||||
timeElapsed += checkCyclePeriod;
|
||||
if(timeElapsed > that.discoveryTimeout * 1000){
|
||||
that.log("Waited " + that.discoveryTimeout + " seconds, stopping discovery.");
|
||||
} else {
|
||||
timer = setTimeout(timeoutFunction, checkCyclePeriod);
|
||||
return;
|
||||
}
|
||||
}
|
||||
browser.stop();
|
||||
browser.removeAllListeners('serviceUp');
|
||||
that.log("Discovery finished, found " + accessories.length + " Yamaha AVR devices.");
|
||||
callback(accessories);
|
||||
};
|
||||
timer = setTimeout(timeoutFunction, checkCyclePeriod);
|
||||
}
|
||||
};
|
||||
|
||||
function YamahaAVRAccessory(log, config, mdnsService, yamaha, sysConfig) {
|
||||
function YamahaAVRAccessory(log, config, name, yamaha, sysConfig) {
|
||||
this.log = log;
|
||||
this.config = config;
|
||||
this.mdnsService = mdnsService;
|
||||
this.yamaha = yamaha;
|
||||
this.sysConfig = sysConfig;
|
||||
|
||||
this.name = mdnsService.name;
|
||||
this.serviceName = mdnsService.name + " Speakers";
|
||||
this.nameSuffix = config["name_suffix"] || " Speakers";
|
||||
this.name = name;
|
||||
this.serviceName = name + this.nameSuffix;
|
||||
this.setMainInputTo = config["setMainInputTo"];
|
||||
this.playVolume = this.config["play_volume"];
|
||||
this.minVolume = config["min_volume"] || -50.0;
|
||||
this.maxVolume = config["max_volume"] || -20.0;
|
||||
this.gapVolume = this.maxVolume - this.minVolume;
|
||||
}
|
||||
|
||||
YamahaAVRAccessory.prototype = {
|
||||
@@ -66,104 +163,74 @@ YamahaAVRAccessory.prototype = {
|
||||
|
||||
if (playing) {
|
||||
|
||||
yamaha.powerOn().then(function(){
|
||||
return yamaha.powerOn().then(function(){
|
||||
if (that.playVolume) return yamaha.setVolumeTo(that.playVolume*10);
|
||||
else return { then: function(f, r){ f(); } };
|
||||
else return Q();
|
||||
}).then(function(){
|
||||
if (that.setMainInputTo) return yamaha.setMainInputTo(that.setMainInputTo);
|
||||
else return { then: function(f, r){ f(); } };
|
||||
else return Q();
|
||||
}).then(function(){
|
||||
if (that.setMainInputTo == "AirPlay") return yamaha.SendXMLToReceiver(
|
||||
'<YAMAHA_AV cmd="PUT"><AirPlay><Play_Control><Playback>Play</Playback></Play_Control></AirPlay></YAMAHA_AV>'
|
||||
);
|
||||
else return { then: function(f, r){ f(); } };
|
||||
//else return Promise.fulfilled(undefined);
|
||||
else return Q();
|
||||
});
|
||||
}
|
||||
else {
|
||||
yamaha.powerOff();
|
||||
return yamaha.powerOff();
|
||||
}
|
||||
},
|
||||
|
||||
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: "Yamaha",
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Manufacturer",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.MODEL_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.sysConfig.YAMAHA_AV.System[0].Config[0].Model_Name[0],
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Model",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.SERIAL_NUMBER_CTYPE,
|
||||
onUpdate: null,
|
||||
perms: ["pr"],
|
||||
format: "string",
|
||||
initialValue: this.sysConfig.YAMAHA_AV.System[0].Config[0].System_ID[0],
|
||||
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.serviceName,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Name of service",
|
||||
designedMaxLength: 255
|
||||
},{
|
||||
cType: types.POWER_STATE_CTYPE,
|
||||
onUpdate: function(value) { that.setPlaying(value); },
|
||||
perms: ["pw","pr","ev"],
|
||||
format: "bool",
|
||||
initialValue: false,
|
||||
supportEvents: false,
|
||||
supportBonjour: false,
|
||||
manfDescription: "Change the playback state of the Yamaha AV Receiver",
|
||||
designedMaxLength: 1
|
||||
}]
|
||||
}];
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
var yamaha = this.yamaha;
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Name, this.name)
|
||||
.setCharacteristic(Characteristic.Manufacturer, "Yamaha")
|
||||
.setCharacteristic(Characteristic.Model, this.sysConfig.YAMAHA_AV.System[0].Config[0].Model_Name[0])
|
||||
.setCharacteristic(Characteristic.SerialNumber, this.sysConfig.YAMAHA_AV.System[0].Config[0].System_ID[0]);
|
||||
|
||||
var switchService = new Service.Switch("Power State");
|
||||
switchService.getCharacteristic(Characteristic.On)
|
||||
.on('get', function(callback, context){
|
||||
yamaha.isOn().then(function(result){
|
||||
callback(false, result);
|
||||
}.bind(this));
|
||||
}.bind(this))
|
||||
.on('set', function(powerOn, callback){
|
||||
this.setPlaying(powerOn).then(function(){
|
||||
callback(false, powerOn);
|
||||
}, function(error){
|
||||
callback(error, !powerOn); //TODO: Actually determine and send real new status.
|
||||
});
|
||||
}.bind(this));
|
||||
|
||||
var audioDeviceService = new YamahaAVRPlatform.AudioDeviceService("Audio Functions");
|
||||
audioDeviceService.getCharacteristic(YamahaAVRPlatform.AudioVolume)
|
||||
.on('get', function(callback, context){
|
||||
yamaha.getBasicInfo().done(function(basicInfo){
|
||||
var v = basicInfo.getVolume()/10.0;
|
||||
var p = 100 * ((v - that.minVolume) / that.gapVolume);
|
||||
p = p < 0 ? 0 : p > 100 ? 100 : Math.round(p);
|
||||
debug("Got volume percent of " + p + "%");
|
||||
callback(false, p);
|
||||
});
|
||||
})
|
||||
.on('set', function(p, callback){
|
||||
var v = ((p / 100) * that.gapVolume) + that.minVolume;
|
||||
v = Math.round(v*10.0);
|
||||
debug("Setting volume to " + v);
|
||||
yamaha.setVolumeTo(v).then(function(){
|
||||
callback(false, p);
|
||||
});
|
||||
})
|
||||
.getValue(null, null); // force an asynchronous get
|
||||
|
||||
|
||||
return [informationService, switchService, audioDeviceService];
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
+395
-55
@@ -1,7 +1,7 @@
|
||||
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 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");
|
||||
@@ -11,6 +11,7 @@ function ZWayServerPlatform(log, config){
|
||||
this.url = config["url"];
|
||||
this.login = config["login"];
|
||||
this.password = config["password"];
|
||||
this.opt_in = config["opt_in"];
|
||||
this.name_overrides = config["name_overrides"];
|
||||
this.batteryLow = config["battery_low_level"] || 15;
|
||||
this.pollInterval = config["poll_interval"] || 2;
|
||||
@@ -82,18 +83,31 @@ ZWayServerPlatform.prototype = {
|
||||
return deferred.promise;
|
||||
}
|
||||
,
|
||||
|
||||
getTagValue: function(vdev, tagStem){
|
||||
if(!(vdev.tags && vdev.tags.length > 0)) return false;
|
||||
var tagStem = "Homebridge." + tagStem;
|
||||
if(vdev.tags.indexOf(tagStem) >= 0) return true;
|
||||
var tags = vdev.tags, l = tags.length, tag;
|
||||
for(var i = 0; i < l; i++){
|
||||
tag = tags[i];
|
||||
if(tag.indexOf(tagStem + ":") === 0){
|
||||
return tag.substr(tagStem.length + 1);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
,
|
||||
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",
|
||||
"switchMultilevel",
|
||||
"switchBinary",
|
||||
"sensorBinary.Door/Window",
|
||||
"sensorMultilevel.Temperature",
|
||||
"switchMultilevel"
|
||||
"sensorMultilevel.Temperature"
|
||||
];
|
||||
|
||||
var that = this;
|
||||
@@ -109,14 +123,49 @@ ZWayServerPlatform.prototype = {
|
||||
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});
|
||||
if(this.getTagValue("Skip")) { debug("Tag says skip!"); continue; }
|
||||
if(this.opt_in && !this.getTagValue(vdev, "Include")) continue;
|
||||
|
||||
var gdid = this.getTagValue(vdev, "Accessory.Id");
|
||||
if(!gdid){
|
||||
gdid = vdev.id.replace(/^(.*?)_zway_(\d+-\d+)-\d.*/, '$1_$2');
|
||||
}
|
||||
|
||||
var gd = groupedDevices[gdid] || (groupedDevices[gdid] = { devices: [], types: {}, extras: {}, primary: undefined, cxmap: {} });
|
||||
|
||||
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
|
||||
var vdevIndex = gd.devices.length - 1;
|
||||
|
||||
var tk = ZWayServerPlatform.getVDevTypeKey(vdev);
|
||||
|
||||
// If this is explicitly set as primary, set it now...
|
||||
if(this.getTagValue(vdev, "IsPrimary")){
|
||||
// everybody out of the way! Can't be in "extras" if you're the primary...
|
||||
if(gd.types[tk] !== undefined){
|
||||
gd.extras[tk] = gd.extras[tk] || [];
|
||||
gd.extras[tk].push(gd.types[tk]);
|
||||
delete gd.types[tk]; // clear the way for this one to be set here below...
|
||||
}
|
||||
gd.primary = vdevIndex;
|
||||
//gd.types[tk] = gd.primary;
|
||||
}
|
||||
|
||||
if(gd.types[tk] === undefined){
|
||||
gd.types[tk] = vdevIndex;
|
||||
} else {
|
||||
gd.extras[tk] = gd.extras[tk] || [];
|
||||
gd.extras[tk].push(vdevIndex);
|
||||
}
|
||||
if(tk !== vdev.deviceType) gd.types[vdev.deviceType] = vdevIndex; // also include the deviceType only as a possibility
|
||||
|
||||
// Create a map entry when Homebridge.Characteristic.Type is set...
|
||||
var ctype = this.getTagValue(vdev, "Characteristic.Type");
|
||||
if(ctype && Characteristic[ctype]){
|
||||
var cx = new Characteristic[ctype]();
|
||||
gd.cxmap[cx.UUID] = vdevIndex;
|
||||
}
|
||||
}
|
||||
//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;
|
||||
|
||||
@@ -128,12 +177,17 @@ ZWayServerPlatform.prototype = {
|
||||
}
|
||||
|
||||
var accessory = null;
|
||||
for(var ti = 0; ti < primaryDeviceClasses.length; ti++){
|
||||
if(gd.primary !== undefined){
|
||||
var pd = gd.devices[gd.primary];
|
||||
var name = pd.metrics && pd.metrics.title ? pd.metrics.title : pd.id;
|
||||
accessory = new ZWayServerAccessory(name, gd, that);
|
||||
}
|
||||
else 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.");
|
||||
//debug("Using primary device with type " + primaryDeviceClasses[ti] + ", " + name + " (" + pd.id + ") as primary.");
|
||||
accessory = new ZWayServerAccessory(name, gd, that);
|
||||
break;
|
||||
}
|
||||
@@ -145,7 +199,6 @@ ZWayServerPlatform.prototype = {
|
||||
foundAccessories.push(accessory);
|
||||
|
||||
}
|
||||
//foundAccessories = foundAccessories.slice(0, 10); // Limit to a few devices for testing...
|
||||
callback(foundAccessories);
|
||||
|
||||
// Start the polling process...
|
||||
@@ -172,6 +225,11 @@ ZWayServerPlatform.prototype = {
|
||||
if(this.cxVDevMap[upd.id]){
|
||||
var vdev = this.vDevStore[upd.id];
|
||||
vdev.metrics.level = upd.metrics.level;
|
||||
if(upd.metrics.color){
|
||||
vdev.metrics.r = upd.metrics.r;
|
||||
vdev.metrics.g = upd.metrics.g;
|
||||
vdev.metrics.b = upd.metrics.b;
|
||||
}
|
||||
vdev.updateTime = upd.updateTime;
|
||||
var cxs = this.cxVDevMap[upd.id];
|
||||
for(var j = 0; j < cxs.length; j++){
|
||||
@@ -222,31 +280,109 @@ ZWayServerAccessory.prototype = {
|
||||
});
|
||||
},
|
||||
|
||||
rgb2hsv: function(obj) {
|
||||
// RGB: 0-255; H: 0-360, S,V: 0-100
|
||||
var r = obj.r/255, g = obj.g/255, b = obj.b/255;
|
||||
var max, min, d, h, s, v;
|
||||
|
||||
min = Math.min(r, Math.min(g, b));
|
||||
max = Math.max(r, Math.max(g, b));
|
||||
|
||||
if (min === max) {
|
||||
// shade of gray
|
||||
return {h: 0, s: 0, v: r * 100};
|
||||
}
|
||||
|
||||
var d = (r === min) ? g - b : ((b === min) ? r - g : b - r);
|
||||
h = (r === min) ? 3 : ((b === min) ? 1 : 5);
|
||||
h = 60 * (h - d/(max - min));
|
||||
s = (max - min) / max;
|
||||
v = max;
|
||||
return {"h": h, "s": s * 100, "v": v * 100};
|
||||
}
|
||||
,
|
||||
hsv2rgb: function(obj) {
|
||||
// H: 0-360; S,V: 0-100; RGB: 0-255
|
||||
var r, g, b;
|
||||
var sfrac = obj.s / 100;
|
||||
var vfrac = obj.v / 100;
|
||||
|
||||
if(sfrac === 0){
|
||||
var vbyte = Math.round(vfrac*255);
|
||||
return { r: vbyte, g: vbyte, b: vbyte };
|
||||
}
|
||||
|
||||
var hdb60 = (obj.h % 360) / 60;
|
||||
var sector = Math.floor(hdb60);
|
||||
var fpart = hdb60 - sector;
|
||||
var c = vfrac * (1 - sfrac);
|
||||
var x1 = vfrac * (1 - sfrac * fpart);
|
||||
var x2 = vfrac * (1 - sfrac * (1 - fpart));
|
||||
switch(sector){
|
||||
case 0:
|
||||
r = vfrac; g = x2; b = c; break;
|
||||
case 1:
|
||||
r = x1; g = vfrac; b = c; break;
|
||||
case 2:
|
||||
r = c; g = vfrac; b = x2; break;
|
||||
case 3:
|
||||
r = c; g = x1; b = vfrac; break;
|
||||
case 4:
|
||||
r = x2; g = c; b = vfrac; break;
|
||||
case 5:
|
||||
default:
|
||||
r = vfrac; g = c; b = x1; break;
|
||||
}
|
||||
|
||||
return { "r": Math.round(255 * r), "g": Math.round(255 * g), "b": Math.round(255 * b) };
|
||||
}
|
||||
,
|
||||
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));
|
||||
services.push(new Service.Thermostat(vdev.metrics.title, vdev.id));
|
||||
break;
|
||||
case "sensorMultilevel.Temperature":
|
||||
services.push(new Service.TemperatureSensor(vdev.metrics.title));
|
||||
case "switchBinary":
|
||||
services.push(new Service.Switch(vdev.metrics.title, vdev.id));
|
||||
break;
|
||||
case "switchRGBW":
|
||||
case "switchMultilevel":
|
||||
if(this.platform.getTagValue(vdev, "Service.Type") === "Switch"){
|
||||
services.push(new Service.Switch(vdev.metrics.title, vdev.id));
|
||||
} else {
|
||||
services.push(new Service.Lightbulb(vdev.metrics.title, vdev.id));
|
||||
}
|
||||
break;
|
||||
case "sensorBinary.Door/Window":
|
||||
services.push(new Service.GarageDoorOpener(vdev.metrics.title));
|
||||
var stype = this.platform.getTagValue(vdev, "Service.Type");
|
||||
if(stype === "ContactSensor"){
|
||||
services.push(new Service.ContactSensor(vdev.metrics.title, vdev.id));
|
||||
} else if(stype === "GarageDoorOpener"){
|
||||
services.push(new Service.GarageDoorOpener(vdev.metrics.title, vdev.id));
|
||||
} else if(stype === "Window"){
|
||||
services.push(new Service.GarageDoorOpener(vdev.metrics.title, vdev.id));
|
||||
} else {
|
||||
services.push(new Service.Door(vdev.metrics.title, vdev.id));
|
||||
}
|
||||
break;
|
||||
case "sensorMultilevel.Temperature":
|
||||
services.push(new Service.TemperatureSensor(vdev.metrics.title, vdev.id));
|
||||
break;
|
||||
case "battery.Battery":
|
||||
services.push(new Service.BatteryService(vdev.metrics.title));
|
||||
services.push(new Service.BatteryService(vdev.metrics.title, vdev.id));
|
||||
break;
|
||||
case "sensorMultilevel.Luminiscence":
|
||||
services.push(new Service.LightSensor(vdev.metrics.title));
|
||||
services.push(new Service.LightSensor(vdev.metrics.title, vdev.id));
|
||||
break;
|
||||
case "sensorBinary":
|
||||
var stype = this.platform.getTagValue(vdev, "Service.Type");
|
||||
if(stype === "MotionSensor"){
|
||||
services.push(new Service.MotionSensor(vdev.metrics.title, vdev.id));
|
||||
} else {
|
||||
services.push(new Service.ContactSensor(vdev.metrics.title, vdev.id));
|
||||
}
|
||||
}
|
||||
|
||||
var validServices =[];
|
||||
@@ -267,11 +403,19 @@ ZWayServerAccessory.prototype = {
|
||||
}
|
||||
,
|
||||
getVDevForCharacteristic: function(cx, vdevPreferred){
|
||||
|
||||
// If we know which vdev should be used for this Characteristic, we're done!
|
||||
if(this.devDesc.cxmap[cx.UUID] !== undefined){
|
||||
return this.devDesc.devices[this.devDesc.cxmap[cx.UUID]];
|
||||
}
|
||||
|
||||
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.Hue).UUID] = ["switchRGBW"];
|
||||
map[(new Characteristic.Saturation).UUID] = ["switchRGBW"];
|
||||
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
|
||||
@@ -279,6 +423,10 @@ ZWayServerAccessory.prototype = {
|
||||
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.ContactSensorState).UUID] = ["sensorBinary"];
|
||||
map[(new Characteristic.CurrentPosition).UUID] = ["sensorBinary.Door/Window","sensorBinary"];
|
||||
map[(new Characteristic.TargetPosition).UUID] = ["sensorBinary.Door/Window","sensorBinary"];
|
||||
map[(new Characteristic.PositionState).UUID] = ["sensorBinary.Door/Window","sensorBinary"];
|
||||
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"];
|
||||
@@ -287,7 +435,7 @@ ZWayServerAccessory.prototype = {
|
||||
}
|
||||
|
||||
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;
|
||||
//
|
||||
@@ -309,8 +457,8 @@ ZWayServerAccessory.prototype = {
|
||||
return null;
|
||||
}
|
||||
,
|
||||
configureCharacteristic: function(cx, vdev){
|
||||
var that = this;
|
||||
configureCharacteristic: function(cx, vdev, service){
|
||||
var accessory = this;
|
||||
|
||||
// Add this combination to the maps...
|
||||
if(!this.platform.cxVDevMap[vdev.id]) this.platform.cxVDevMap[vdev.id] = [];
|
||||
@@ -324,12 +472,17 @@ ZWayServerAccessory.prototype = {
|
||||
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);
|
||||
callback(false, accessory.name);
|
||||
});
|
||||
cx.writable = false;
|
||||
return cx;
|
||||
}
|
||||
|
||||
// We don't want to override "Name"'s name...so we just move this below that block.
|
||||
var descOverride = this.platform.getTagValue(vdev, "Characteristic.Description");
|
||||
if(descOverride){
|
||||
cx.displayName = descOverride;
|
||||
}
|
||||
|
||||
if(cx instanceof Characteristic.On){
|
||||
cx.zway_getValueFromVDev = function(vdev){
|
||||
var val = false;
|
||||
@@ -381,6 +534,64 @@ ZWayServerAccessory.prototype = {
|
||||
return cx;
|
||||
}
|
||||
|
||||
if(cx instanceof Characteristic.Hue){
|
||||
cx.zway_getValueFromVDev = function(vdev){
|
||||
debug("Derived value " + accessory.rgb2hsv(vdev.metrics.color).h + " for hue.");
|
||||
return accessory.rgb2hsv(vdev.metrics.color).h;
|
||||
};
|
||||
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(hue, callback){
|
||||
var scx = service.getCharacteristic(Characteristic.Saturation);
|
||||
var vcx = service.getCharacteristic(Characteristic.Brightness);
|
||||
if(!scx || !vcx){
|
||||
debug("Hue without Saturation and Brightness is not supported! Cannot set value!")
|
||||
callback(true, cx.value);
|
||||
}
|
||||
var rgb = this.hsv2rgb({ h: hue, s: scx.value, v: vcx.value });
|
||||
this.command(vdev, "exact", { red: rgb.r, green: rgb.g, blue: rgb.b }).then(function(result){
|
||||
callback();
|
||||
});
|
||||
}.bind(this));
|
||||
|
||||
return cx;
|
||||
}
|
||||
|
||||
if(cx instanceof Characteristic.Saturation){
|
||||
cx.zway_getValueFromVDev = function(vdev){
|
||||
debug("Derived value " + accessory.rgb2hsv(vdev.metrics.color).s + " for saturation.");
|
||||
return accessory.rgb2hsv(vdev.metrics.color).s;
|
||||
};
|
||||
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(saturation, callback){
|
||||
var hcx = service.getCharacteristic(Characteristic.Hue);
|
||||
var vcx = service.getCharacteristic(Characteristic.Brightness);
|
||||
if(!hcx || !vcx){
|
||||
debug("Saturation without Hue and Brightness is not supported! Cannot set value!")
|
||||
callback(true, cx.value);
|
||||
}
|
||||
var rgb = this.hsv2rgb({ h: hcx.value, s: saturation, v: vcx.value });
|
||||
this.command(vdev, "exact", { red: rgb.r, green: rgb.g, blue: rgb.b }).then(function(result){
|
||||
callback();
|
||||
});
|
||||
}.bind(this));
|
||||
|
||||
return cx;
|
||||
}
|
||||
|
||||
if(cx instanceof Characteristic.CurrentTemperature){
|
||||
cx.zway_getValueFromVDev = function(vdev){
|
||||
return vdev.metrics.level;
|
||||
@@ -393,8 +604,10 @@ ZWayServerAccessory.prototype = {
|
||||
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;
|
||||
cx.setProps({
|
||||
minValue: vdev.metrics && vdev.metrics.min !== undefined ? vdev.metrics.min : -40,
|
||||
maxValue: vdev.metrics && vdev.metrics.max !== undefined ? vdev.metrics.max : 999
|
||||
});
|
||||
return cx;
|
||||
}
|
||||
|
||||
@@ -416,8 +629,10 @@ ZWayServerAccessory.prototype = {
|
||||
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;
|
||||
cx.setProps({
|
||||
minValue: vdev.metrics && vdev.metrics.min !== undefined ? vdev.metrics.min : 5,
|
||||
maxValue: vdev.metrics && vdev.metrics.max !== undefined ? vdev.metrics.max : 40
|
||||
});
|
||||
return cx;
|
||||
}
|
||||
|
||||
@@ -431,7 +646,9 @@ ZWayServerAccessory.prototype = {
|
||||
debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"...");
|
||||
callback(false, Characteristic.TemperatureDisplayUnits.CELSIUS);
|
||||
});
|
||||
cx.writable = false;
|
||||
cx.setProps({
|
||||
perms: [Characteristic.Perms.READ]
|
||||
});
|
||||
return cx;
|
||||
}
|
||||
|
||||
@@ -459,7 +676,6 @@ ZWayServerAccessory.prototype = {
|
||||
callback(false, Characteristic.TargetHeatingCoolingState.HEAT);
|
||||
});
|
||||
// Hmm... apparently if this is not setable, we can't add a thermostat change to a scene. So, make it writable but a no-op.
|
||||
cx.writable = true;
|
||||
cx.on('set', function(newValue, callback){
|
||||
debug("WARN: Set of TargetHeatingCoolingState not yet implemented, resetting to HEAT!")
|
||||
callback(undefined, Characteristic.TargetHeatingCoolingState.HEAT);
|
||||
@@ -494,8 +710,9 @@ ZWayServerAccessory.prototype = {
|
||||
debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"...");
|
||||
callback(false, Characteristic.TargetDoorState.CLOSED);
|
||||
});
|
||||
//cx.readable = false;
|
||||
cx.writable = false;
|
||||
cx.setProps({
|
||||
perms: [Characteristic.Perms.READ]
|
||||
});
|
||||
}
|
||||
|
||||
if(cx instanceof Characteristic.ObstructionDetected){
|
||||
@@ -508,8 +725,6 @@ ZWayServerAccessory.prototype = {
|
||||
debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"...");
|
||||
callback(false, false);
|
||||
});
|
||||
//cx.readable = false;
|
||||
cx.writable = false;
|
||||
}
|
||||
|
||||
if(cx instanceof Characteristic.BatteryLevel){
|
||||
@@ -528,7 +743,7 @@ ZWayServerAccessory.prototype = {
|
||||
|
||||
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;
|
||||
return vdev.metrics.level <= accessory.platform.batteryLow ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL;
|
||||
};
|
||||
cx.value = cx.zway_getValueFromVDev(vdev);
|
||||
cx.on('get', function(callback, context){
|
||||
@@ -550,8 +765,6 @@ ZWayServerAccessory.prototype = {
|
||||
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){
|
||||
@@ -560,8 +773,8 @@ ZWayServerAccessory.prototype = {
|
||||
// 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;
|
||||
// Bounds checking now done upstream!
|
||||
//if(lux < cx.minimumValue) return cx.minimumValue; if(lux > cx.maximumValue) return cx.maximumValue;
|
||||
return lux;
|
||||
} else {
|
||||
return vdev.metrics.level;
|
||||
@@ -580,6 +793,105 @@ ZWayServerAccessory.prototype = {
|
||||
});
|
||||
return cx;
|
||||
}
|
||||
|
||||
if(cx instanceof Characteristic.MotionDetected){
|
||||
cx.zway_getValueFromVDev = function(vdev){
|
||||
return vdev.metrics.level === "off" ? false : true;
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
if(cx instanceof Characteristic.StatusTampered){
|
||||
cx.zway_getValueFromVDev = function(vdev){
|
||||
return vdev.metrics.level === "off" ? Characteristic.StatusTampered.NOT_TAMPERED : Characteristic.StatusTampered.TAMPERED;
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
if(cx instanceof Characteristic.ContactSensorState){
|
||||
cx.zway_getValueFromVDev = function(vdev){
|
||||
var boolval = vdev.metrics.level === "off" ? false : true;
|
||||
boolval = accessory.platform.getTagValue(vdev, "ContactSensorState.Invert") ? !boolval : boolval;
|
||||
return boolval ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED;
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
if(cx instanceof Characteristic.CurrentPosition){
|
||||
cx.zway_getValueFromVDev = function(vdev){
|
||||
return vdev.metrics.level === "off" ? 0 : 100 ;
|
||||
};
|
||||
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;
|
||||
}
|
||||
|
||||
if(cx instanceof Characteristic.TargetPosition){
|
||||
//TODO: Currently only Door sensors, so always return 0.
|
||||
cx.zway_getValueFromVDev = function(vdev){
|
||||
return 0;
|
||||
};
|
||||
cx.value = cx.zway_getValueFromVDev(vdev);
|
||||
cx.on('get', function(callback, context){
|
||||
debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"...");
|
||||
callback(false, cx.zway_getValueFromVDev(vdev));
|
||||
});
|
||||
}
|
||||
|
||||
if(cx instanceof Characteristic.PositionState){
|
||||
//TODO: Currently only Door sensors, so always return STOPPED.
|
||||
cx.zway_getValueFromVDev = function(vdev){
|
||||
return Characteristic.PositionState.STOPPED;
|
||||
};
|
||||
cx.value = cx.zway_getValueFromVDev(vdev);
|
||||
cx.on('get', function(callback, context){
|
||||
debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"...");
|
||||
callback(false, cx.zway_getValueFromVDev(vdev));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
,
|
||||
configureService: function(service, vdev){
|
||||
@@ -591,14 +903,29 @@ ZWayServerAccessory.prototype = {
|
||||
success = false;
|
||||
debug("ERROR! Failed to configure required characteristic \"" + service.characteristics[i].displayName + "\"!");
|
||||
}
|
||||
cx = this.configureCharacteristic(cx, vdev);
|
||||
cx = this.configureCharacteristic(cx, vdev, service);
|
||||
}
|
||||
for(var i = 0; i < service.optionalCharacteristics.length; i++){
|
||||
var cx = service.optionalCharacteristics[i];
|
||||
var vdev = this.getVDevForCharacteristic(cx);
|
||||
var vdev = this.getVDevForCharacteristic(cx, vdev);
|
||||
if(!vdev) continue;
|
||||
cx = this.configureCharacteristic(cx, vdev);
|
||||
if(cx) service.addCharacteristic(cx);
|
||||
|
||||
//NOTE: Questionable logic, but if the vdev has already been used for the same
|
||||
// characteristic type elsewhere, lets not duplicate it just for the sake of an
|
||||
// optional characteristic. This eliminates the problem with RGB+W+W bulbs
|
||||
// having the HSV controls shown again, but might have unintended consequences...
|
||||
var othercx, othercxs = this.platform.cxVDevMap[vdev.id];
|
||||
if(othercxs) for(var j = 0; j < othercxs.length; j++) if(othercxs[j].UUID === cx.UUID) othercx = othercxs[j];
|
||||
if(othercx)
|
||||
continue;
|
||||
|
||||
cx = this.configureCharacteristic(cx, vdev, service);
|
||||
try {
|
||||
if(cx) service.addCharacteristic(cx);
|
||||
}
|
||||
catch (ex) {
|
||||
debug('Adding Characteristic "' + cx.displayName + '" failed with message "' + ex.message + '". This may be expected.');
|
||||
}
|
||||
}
|
||||
return success;
|
||||
}
|
||||
@@ -606,17 +933,30 @@ ZWayServerAccessory.prototype = {
|
||||
getServices: function() {
|
||||
var that = this;
|
||||
|
||||
var vdevPrimary = this.devDesc.devices[this.devDesc.primary];
|
||||
var accId = this.platform.getTagValue(vdevPrimary, "Accessory.Id");
|
||||
if(!accId){
|
||||
accId = "VDev-" + vdevPrimary.h; //FIXME: Is this valid?
|
||||
}
|
||||
|
||||
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?);
|
||||
.setCharacteristic(Characteristic.SerialNumber, accId);
|
||||
|
||||
var services = [informationService];
|
||||
|
||||
services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.primary]));
|
||||
services = services.concat(this.getVDevServices(vdevPrimary));
|
||||
|
||||
// Any extra switchMultilevels? Could be a RGBW+W bulb, add them as additional services...
|
||||
if(this.devDesc.extras["switchMultilevel"]) for(var i = 0; i < this.devDesc.extras["switchMultilevel"].length; i++){
|
||||
var xvdev = this.devDesc.devices[this.devDesc.extras["switchMultilevel"][i]];
|
||||
var xservice = this.getVDevServices(xvdev);
|
||||
services = services.concat(xservice);
|
||||
}
|
||||
|
||||
if(this.platform.splitServices){
|
||||
if(this.devDesc.types["battery.Battery"]){
|
||||
@@ -655,7 +995,7 @@ ZWayServerAccessory.prototype = {
|
||||
extraCxs = []; // to wipe out any already setup cxs.
|
||||
break;
|
||||
}
|
||||
this.configureCharacteristic(cx, vdev2);
|
||||
this.configureCharacteristic(cx, vdev2, service);
|
||||
extraCxs.push(cx);
|
||||
}
|
||||
for(var j = 0; j < extraCxs.length; j++)
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
{
|
||||
"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",
|
||||
"hint2":"Replace all group addresses by current addresses of your installation, these are arbitrary examples!",
|
||||
"hint3":"For valid services and their characteristics have a look at the knxdevice.md file in folder accessories!"
|
||||
"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 types 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": "1/2/3",
|
||||
"Listen": "1/2/4"
|
||||
},
|
||||
"Current": {
|
||||
"Set": "1/3/1",
|
||||
"Listen": "1/3/2"
|
||||
},
|
||||
"PositionState": {
|
||||
"Listen": "2/7/1"
|
||||
}
|
||||
}
|
||||
]
|
||||
},{
|
||||
"accessory_type": "knxdevice",
|
||||
|
||||
"description":"sample contact sensor device",
|
||||
"name": "Office",
|
||||
"services": [
|
||||
{
|
||||
"type": "ContactSensor",
|
||||
"name": "Office Door",
|
||||
"ContactSensorState": {
|
||||
"Listen": "5/3/5"
|
||||
}
|
||||
},
|
||||
|
||||
]
|
||||
}
|
||||
],
|
||||
"accessories": []
|
||||
}
|
||||
@@ -0,0 +1,763 @@
|
||||
/*
|
||||
ISY-JS
|
||||
|
||||
ISY-99 REST / WebSockets based HomeBridge shim.
|
||||
|
||||
Supports the following Insteon devices: Lights (dimmable and non-dimmable), Fans, Outlets, Door/Window Sensors, MorningLinc locks, Inline Lincs and I/O Lincs.
|
||||
Also supports ZWave based locks. If elkEnabled is set to true then this will also expose your Elk Alarm Panel and all of your Elk Sensors.
|
||||
|
||||
Turns out that HomeBridge platforms can only return a maximum of 100 devices. So if you end up exposing more then 100 devices through HomeBridge the HomeKit
|
||||
software will fail adding the HomeBridge to your HomeKit network. To address this issue this platform provides an option to screen out devices based on
|
||||
criteria specified in the config.
|
||||
|
||||
Configuration sample:
|
||||
|
||||
"platforms": [
|
||||
{
|
||||
"platform": "isy-js",
|
||||
"name": "isy-js",
|
||||
"host": "10.0.1.12",
|
||||
"username": "admin",
|
||||
"password": "password",
|
||||
"elkEnabled": true,
|
||||
"ignoreDevices": [
|
||||
{ "nameContains": "ApplianceLinc", "lastAddressDigit": "", "address": ""},
|
||||
{ "nameContains": "Bedroom.Side Gate", "lastAddressDigit": "", "address": ""},
|
||||
{ "nameContains": "Remote", "lastAddressDigit": "", "address": "" },
|
||||
{ "nameContains": "Keypad", "lastAddressDigit": "2", "address": "" },
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Fields:
|
||||
"platform" - Must be set to isy-js
|
||||
"name" - Can be set to whatever you want
|
||||
"host" - IP address of the ISY
|
||||
"username" - Your ISY username
|
||||
"password" - Your ISY password
|
||||
"elkEnabled" - true if there is an elk alarm panel connected to your ISY
|
||||
"ignoreDevices" - Array of objects specifying criteria for screening out devices from the network. nameContains is the only required criteria. If the other criteria
|
||||
are blank all devices will match those criteria (providing they match the name criteria).
|
||||
"nameContains" - Specifies a substring to check against the names of the ISY devices. Required field for the criteria.
|
||||
"lastAddressDigit" - Specifies a single digit in the ISY address of a device which should be used to match the device. Example use of this is for composite
|
||||
devices like keypads so you can screen out the non-main buttons.
|
||||
"address" - ISY address to match.
|
||||
|
||||
Examples:
|
||||
|
||||
{ "nameContains": "Keypad", "lastAddressDigit": "2", "address": "" } - Ignore all devices which have the word Keypad in their name and whose last address digit is 2.
|
||||
{ "nameContains": "Remote", "lastAddressDigit": "", "address": "" } - Ignore all devices which have the word Remote in their name
|
||||
{ "nameContains": "", "lastAddressDigit": "", "address": "15 5 3 2"} - Ignore the device with an ISY address of 15 5 3 2.
|
||||
|
||||
TODOS: Implement identify functions (beep perhaps?) and more device types.
|
||||
*/
|
||||
|
||||
|
||||
var types = require("hap-nodejs/accessories/types.js");
|
||||
var isy = require('isy-js');
|
||||
var Service = require("hap-nodejs").Service;
|
||||
var Characteristic = require("hap-nodejs").Characteristic;
|
||||
var inherits = require('util').inherits;
|
||||
|
||||
// Global device map. Needed to map incoming notifications to the corresponding HomeKit device for update.
|
||||
var deviceMap = {};
|
||||
|
||||
// This function responds to changes in devices from the isy-js library. Uses the global device map to update
|
||||
// the state.
|
||||
// TODO: Move this to a member function of the ISYPlatform object so we don't need a global map.
|
||||
function ISYChangeHandler(isy,device) {
|
||||
var deviceToUpdate = deviceMap[device.address];
|
||||
if(deviceToUpdate != null) {
|
||||
deviceToUpdate.handleExternalChange();
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to have ISYJSDEBUG control if debug output appears
|
||||
function ISYJSDebugMessage(isy,message) {
|
||||
if(process.env.ISYJSDEBUG != undefined) {
|
||||
isy.log(message);
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// PLATFORM
|
||||
|
||||
// Construct the ISY platform. log = Logger, config = homebridge cofnig
|
||||
function ISYPlatform(log,config) {
|
||||
this.log = log;
|
||||
this.config = config;
|
||||
this.host = config.host;
|
||||
this.username = config.username;
|
||||
this.password = config.password;
|
||||
this.elkEnabled = config.elkEnabled;
|
||||
this.isy = new isy.ISY(this.host, this.username,this.password, config.elkEnabled, ISYChangeHandler);
|
||||
}
|
||||
|
||||
// Checks the device against the configuration to see if it should be ignored.
|
||||
ISYPlatform.prototype.shouldIgnore = function(device) {
|
||||
var deviceAddress = device.address;
|
||||
var deviceName = device.name;
|
||||
for(var index = 0; index < this.config.ignoreDevices.length; index++) {
|
||||
var rule = this.config.ignoreDevices[index];
|
||||
if(rule.nameContains != "") {
|
||||
if(deviceName.indexOf(rule.nameContains) == -1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if(rule.lastAddressDigit != "") {
|
||||
if(deviceAddress.indexOf(rule.lastAddressDigit,deviceAddress.length-2) == -1) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if(rule.address != "") {
|
||||
if(deviceAddress != rule.address) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ISYJSDebugMessage(this,"Ignoring device: "+deviceName+" ["+deviceAddress+"] because of rule ["+rule.nameContains+"] ["+rule.lastAddressDigit+"] ["+rule.address+"]");
|
||||
return true;
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calls the isy-js library, retrieves the list of devices, and maps them to appropriate ISYXXXXAccessory devices.
|
||||
ISYPlatform.prototype.accessories = function(callback) {
|
||||
var that = this;
|
||||
this.isy.initialize(function() {
|
||||
var results = [];
|
||||
var deviceList = that.isy.getDeviceList();
|
||||
for(var index = 0; index < deviceList.length; index++) {
|
||||
var device = deviceList[index];
|
||||
var homeKitDevice = null;
|
||||
if(!that.shouldIgnore(device)) {
|
||||
|
||||
if(device.deviceType == that.isy.DEVICE_TYPE_LIGHT || device.deviceType == that.isy.DEVICE_TYPE_DIMMABLE_LIGHT) {
|
||||
homeKitDevice = new ISYLightAccessory(that.log,device);
|
||||
} else if(device.deviceType == that.isy.DEVICE_TYPE_LOCK || device.deviceType == that.isy.DEVICE_TYPE_SECURE_LOCK) {
|
||||
homeKitDevice = new ISYLockAccessory(that.log,device);
|
||||
} else if(device.deviceType == that.isy.DEVICE_TYPE_OUTLET) {
|
||||
homeKitDevice = new ISYOutletAccessory(that.log,device);
|
||||
} else if(device.deviceType == that.isy.DEVICE_TYPE_FAN) {
|
||||
homeKitDevice = new ISYFanAccessory(that.log,device);
|
||||
} else if(device.deviceType == that.isy.DEVICE_TYPE_DOOR_WINDOW_SENSOR) {
|
||||
homeKitDevice = new ISYDoorWindowSensorAccessory(that.log,device);
|
||||
} else if(device.deviceType == that.isy.DEVICE_TYPE_ALARM_DOOR_WINDOW_SENSOR) {
|
||||
homeKitDevice = new ISYDoorWindowSensorAccessory(that.log,device);
|
||||
} else if(device.deviceType == that.isy.DEVICE_TYPE_ALARM_PANEL) {
|
||||
homeKitDevice = new ISYElkAlarmPanelAccessory(that.log,device);
|
||||
}
|
||||
if(homeKitDevice != null) {
|
||||
// Make sure the device is address to the global map
|
||||
deviceMap[device.address] = homeKitDevice;
|
||||
results.push(homeKitDevice);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(that.isy.elkEnabled) {
|
||||
var panelDevice = that.isy.getElkAlarmPanel();
|
||||
var panelDeviceHK = new ISYElkAlarmPanelAccessory(that.log,panelDevice);
|
||||
deviceMap[panelDevice.address] = panelDeviceHK;
|
||||
results.push(panelDeviceHK);
|
||||
}
|
||||
ISYJSDebugMessage(that,"Filtered device has: "+results.length+" devices");
|
||||
callback(results);
|
||||
});
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// BASE FOR ALL DEVICES
|
||||
|
||||
// Provides common constructor tasks
|
||||
function ISYAccessoryBaseSetup(accessory,log,device) {
|
||||
accessory.log = log;
|
||||
accessory.device = device;
|
||||
accessory.address = device.address;
|
||||
accessory.name = device.name;
|
||||
accessory.uuid_base = device.isy.address+":"+device.address;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// FANS - ISYFanAccessory
|
||||
// Implemetnts the fan service for an isy fan device.
|
||||
|
||||
// Constructs a fan accessory object. device is the isy-js device object and log is the logger.
|
||||
function ISYFanAccessory(log,device) {
|
||||
ISYAccessoryBaseSetup(this,log,device);
|
||||
}
|
||||
|
||||
ISYFanAccessory.prototype.identify = function(callback) {
|
||||
// Do the identify action
|
||||
callback();
|
||||
}
|
||||
|
||||
// Translates the fan speed as an isy-js string into the corresponding homekit constant level.
|
||||
// Homekit doesn't have steps for the fan speed and needs to have a value from 0 to 100. We
|
||||
// split the range into 4 steps and map them to the 4 isy-js levels.
|
||||
ISYFanAccessory.prototype.translateFanSpeedToHK = function(fanSpeed) {
|
||||
if(fanSpeed == this.device.FAN_LEVEL_OFF) {
|
||||
return 0;
|
||||
} else if(fanSpeed == this.device.FAN_LEVEL_LOW) {
|
||||
return 32;
|
||||
} else if(fanSpeed == this.device.FAN_LEVEL_MEDIUM) {
|
||||
return 67;
|
||||
} else if(fanSpeed == this.device.FAN_LEVEL_HIGH) {
|
||||
return 100;
|
||||
} else {
|
||||
ISYJSDebugMessage(this,"!!!! ERROR: Unknown fan speed: "+fanSpeed);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Translates the fan level from homebridge into the isy-js level. Maps from the 0-100
|
||||
// to the four isy-js fan speed levels.
|
||||
ISYFanAccessory.prototype.translateHKToFanSpeed = function(fanStateHK) {
|
||||
if(fanStateHK == 0) {
|
||||
return this.device.FAN_LEVEL_OFF;
|
||||
} else if(fanStateHK > 0 && fanStateHK <=32) {
|
||||
return this.device.FAN_LEVEL_LOW;
|
||||
} else if(fanStateHK >= 33 && fanStateHK <= 67) {
|
||||
return this.device.FAN_LEVEL_MEDIUM;
|
||||
} else if(fanStateHK > 67) {
|
||||
return this.device.FAN_LEVEL_HIGH;
|
||||
} else {
|
||||
ISYJSDebugMessage(this,"ERROR: Unknown fan state!");
|
||||
return this.device.FAN_LEVEL_OFF;
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the current state of the fan from the isy-js level to the 0-100 level of HK.
|
||||
ISYFanAccessory.prototype.getFanRotationSpeed = function(callback) {
|
||||
callback(null,this.translateFanSpeedToHK(this.device.getCurrentFanState()));
|
||||
}
|
||||
|
||||
// Sets the current state of the fan from the 0-100 level of HK to the isy-js level.
|
||||
ISYFanAccessory.prototype.setFanRotationSpeed = function(fanStateHK,callback) {
|
||||
var newFanState = this.translateHKToFanSpeed(fanStateHK);
|
||||
ISYJSDebugMessage(this,"Sending command to set fan state to: "+newFanState);
|
||||
if(newFanState != this.device.getCurrentFanState()) {
|
||||
this.device.sendFanCommand(newFanState, function(result) {
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
ISYJSDebugMessage(this,"Fan command does not change actual speed");
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if the fan is on
|
||||
ISYFanAccessory.prototype.getIsFanOn = function() {
|
||||
return (this.device.getCurrentFanState() != "Off");
|
||||
}
|
||||
|
||||
// Returns the state of the fan to the homebridge system for the On characteristic
|
||||
ISYFanAccessory.prototype.getFanOnState = function(callback) {
|
||||
callback(null,this.getIsFanOn());
|
||||
}
|
||||
|
||||
// Sets the fan state based on the value of the On characteristic. Default to Medium for on.
|
||||
ISYFanAccessory.prototype.setFanOnState = function(onState,callback) {
|
||||
if(onState != this.getIsFanOn()) {
|
||||
if(onState) {
|
||||
this.setFanRotationSpeed(this.translateFanSpeedToHK(this.device.FAN_LEVEL_MEDIUM), callback);
|
||||
} else {
|
||||
this.setFanRotationSpeed(this.translateFanSpeedToHK(this.device.FAN_LEVEL_OFF), callback);
|
||||
}
|
||||
} else {
|
||||
ISYJSDebugMessage(this,"Fan command does not change actual state");
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors change in the state of the underlying isj-js device object.
|
||||
ISYFanAccessory.prototype.handleExternalChange = function() {
|
||||
this.fanService
|
||||
.setCharacteristic(Characteristic.On, this.getIsFanOn());
|
||||
|
||||
this.fanService
|
||||
.setCharacteristic(Characteristic.RotationSpeed, this.translateFanSpeedToHK(this.device.getCurrentFanState()));
|
||||
}
|
||||
|
||||
// Returns the services supported by the fan device.
|
||||
ISYFanAccessory.prototype.getServices = function() {
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "SmartHome")
|
||||
.setCharacteristic(Characteristic.Model, this.device.deviceFriendlyName)
|
||||
.setCharacteristic(Characteristic.SerialNumber, this.device.address);
|
||||
|
||||
var fanService = new Service.Fan();
|
||||
|
||||
this.fanService = fanService;
|
||||
this.informationService = informationService;
|
||||
|
||||
fanService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('set', this.setFanOnState.bind(this));
|
||||
|
||||
fanService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('get', this.getFanOnState.bind(this));
|
||||
|
||||
fanService
|
||||
.addCharacteristic(new Characteristic.RotationSpeed())
|
||||
.on('get', this.getFanRotationSpeed.bind(this));
|
||||
|
||||
fanService
|
||||
.getCharacteristic(Characteristic.RotationSpeed)
|
||||
.on('set', this.setFanRotationSpeed.bind(this));
|
||||
|
||||
return [informationService, fanService];
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// OUTLETS - ISYOutletAccessory
|
||||
// Implements the Outlet service for ISY devices.
|
||||
|
||||
// Constructs an outlet. log = HomeBridge logger, device = isy-js device to wrap
|
||||
function ISYOutletAccessory(log,device) {
|
||||
ISYAccessoryBaseSetup(this,log,device);
|
||||
}
|
||||
|
||||
// Handles the identify command
|
||||
ISYOutletAccessory.prototype.identify = function(callback) {
|
||||
// Do the identify action
|
||||
callback();
|
||||
}
|
||||
|
||||
// Handles a request to set the outlet state. Ignores redundant sets based on current states.
|
||||
ISYOutletAccessory.prototype.setOutletState = function(outletState,callback) {
|
||||
ISYJSDebugMessage(this,"Sending command to set outlet state to: "+outletState);
|
||||
if(outletState != this.device.getCurrentOutletState()) {
|
||||
this.device.sendOutletCommand(outletState, function(result) {
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// Handles a request to get the current outlet state based on underlying isy-js device object.
|
||||
ISYOutletAccessory.prototype.getOutletState = function(callback) {
|
||||
callback(null,this.device.getCurrentOutletState());
|
||||
}
|
||||
|
||||
// Handles a request to get the current in use state of the outlet. We set this to true always as
|
||||
// there is no way to deterine this through the isy.
|
||||
ISYOutletAccessory.prototype.getOutletInUseState = function(callback) {
|
||||
callback(null, true);
|
||||
}
|
||||
|
||||
// Mirrors change in the state of the underlying isj-js device object.
|
||||
ISYOutletAccessory.prototype.handleExternalChange = function() {
|
||||
this.outletService
|
||||
.setCharacteristic(Characteristic.On, this.device.getCurrentOutletState());
|
||||
}
|
||||
|
||||
// Returns the set of services supported by this object.
|
||||
ISYOutletAccessory.prototype.getServices = function() {
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "SmartHome")
|
||||
.setCharacteristic(Characteristic.Model, this.device.deviceFriendlyName)
|
||||
.setCharacteristic(Characteristic.SerialNumber, this.device.address);
|
||||
|
||||
var outletService = new Service.Outlet();
|
||||
|
||||
this.outletService = outletService;
|
||||
this.informationService = informationService;
|
||||
|
||||
outletService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('set', this.setOutletState.bind(this));
|
||||
|
||||
outletService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('get', this.getOutletState.bind(this));
|
||||
|
||||
outletService
|
||||
.getCharacteristic(Characteristic.OutletInUse)
|
||||
.on('get', this.getOutletInUseState.bind(this));
|
||||
|
||||
return [informationService, outletService];
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// LOCKS - ISYLockAccessory
|
||||
// Implements the lock service for isy-js devices.
|
||||
|
||||
// Constructs a lock accessory. log = homebridge logger, device = isy-js device object being wrapped
|
||||
function ISYLockAccessory(log,device) {
|
||||
ISYAccessoryBaseSetup(this,log,device);
|
||||
}
|
||||
|
||||
// Handles an identify request
|
||||
ISYLockAccessory.prototype.identify = function(callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
// Handles a set to the target lock state. Will ignore redundant commands.
|
||||
ISYLockAccessory.prototype.setTargetLockState = function(lockState,callback) {
|
||||
ISYJSDebugMessage(this,"Sending command to set lock state to: "+lockState);
|
||||
if(lockState != this.getDeviceCurrentStateAsHK()) {
|
||||
var targetLockValue = (lockState == 0) ? false : true;
|
||||
this.device.sendLockCommand(targetLockValue, function(result) {
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// Translates underlying lock state into the corresponding homekit state
|
||||
ISYLockAccessory.prototype.getDeviceCurrentStateAsHK = function() {
|
||||
return (this.device.getCurrentLockState() ? 1 : 0);
|
||||
}
|
||||
|
||||
// Handles request to get the current lock state for homekit
|
||||
ISYLockAccessory.prototype.getLockCurrentState = function(callback) {
|
||||
callback(null, this.getDeviceCurrentStateAsHK());
|
||||
}
|
||||
|
||||
// Handles request to get the target lock state for homekit
|
||||
ISYLockAccessory.prototype.getTargetLockState = function(callback) {
|
||||
this.getLockCurrentState(callback);
|
||||
}
|
||||
|
||||
// Mirrors change in the state of the underlying isj-js device object.
|
||||
ISYLockAccessory.prototype.handleExternalChange = function() {
|
||||
this.lockService
|
||||
.setCharacteristic(Characteristic.LockTargetState, this.getDeviceCurrentStateAsHK());
|
||||
this.lockService
|
||||
.setCharacteristic(Characteristic.LockCurrentState, this.getDeviceCurrentStateAsHK());
|
||||
}
|
||||
|
||||
// Returns the set of services supported by this object.
|
||||
ISYLockAccessory.prototype.getServices = function() {
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "SmartHome")
|
||||
.setCharacteristic(Characteristic.Model, this.device.deviceFriendlyName)
|
||||
.setCharacteristic(Characteristic.SerialNumber, this.device.address);
|
||||
|
||||
var lockMechanismService = new Service.LockMechanism();
|
||||
|
||||
this.lockService = lockMechanismService;
|
||||
this.informationService = informationService;
|
||||
|
||||
lockMechanismService
|
||||
.getCharacteristic(Characteristic.LockTargetState)
|
||||
.on('set', this.setTargetLockState.bind(this));
|
||||
|
||||
lockMechanismService
|
||||
.getCharacteristic(Characteristic.LockTargetState)
|
||||
.on('get', this.getTargetLockState.bind(this));
|
||||
|
||||
lockMechanismService
|
||||
.getCharacteristic(Characteristic.LockCurrentState)
|
||||
.on('get', this.getLockCurrentState.bind(this));
|
||||
|
||||
return [informationService, lockMechanismService];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// LIGHTS
|
||||
// Implements the Light service for homekit based on an underlying isy-js device. Is dimmable or not depending
|
||||
// on if the underlying device is dimmable.
|
||||
|
||||
// Constructs the light accessory. log = homebridge logger, device = isy-js device object being wrapped
|
||||
function ISYLightAccessory(log,device) {
|
||||
ISYAccessoryBaseSetup(this,log,device);
|
||||
this.dimmable = (this.device.deviceType == "DimmableLight");
|
||||
}
|
||||
|
||||
// Handles the identify command
|
||||
ISYLightAccessory.prototype.identify = function(callback) {
|
||||
this.device.sendLightCommand(true, function(result) {
|
||||
this.device.sendLightCommand(false, function(result) {
|
||||
callback();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Handles request to set the current powerstate from homekit. Will ignore redundant commands.
|
||||
ISYLightAccessory.prototype.setPowerState = function(powerOn,callback) {
|
||||
ISYJSDebugMessage(this,"Setting powerstate to %s", powerOn);
|
||||
if(powerOn != this.device.getCurrentLightState()) {
|
||||
ISYJSDebugMessage(this,"Changing powerstate to "+powerOn);
|
||||
this.device.sendLightCommand(powerOn, function(result) {
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
ISYJSDebugMessage(this,"Ignoring redundant setPowerState");
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors change in the state of the underlying isj-js device object.
|
||||
ISYLightAccessory.prototype.handleExternalChange = function() {
|
||||
ISYJSDebugMessage(this,"Handling external change for light");
|
||||
this.lightService
|
||||
.setCharacteristic(Characteristic.On, this.device.getCurrentLightState());
|
||||
if(this.device.deviceType == this.device.isy.DEVICE_TYPE_DIMMABLE_LIGHT) {
|
||||
this.lightService
|
||||
.setCharacteristic(Characteristic.Brightness, this.device.getCurrentLightDimState() );
|
||||
}
|
||||
}
|
||||
|
||||
// Handles request to get the current on state
|
||||
ISYLightAccessory.prototype.getPowerState = function(callback) {
|
||||
callback(null,this.device.getCurrentLightState());
|
||||
}
|
||||
|
||||
// Handles request to set the brightness level of dimmable lights. Ignore redundant commands.
|
||||
ISYLightAccessory.prototype.setBrightness = function(level,callback) {
|
||||
ISYJSDebugMessage(this,"Setting brightness to %s", level);
|
||||
if(level != this.device.getCurrentLightDimState()) {
|
||||
ISYJSDebugMessage(this,"Changing Brightness to "+level);
|
||||
this.device.sendLightDimCommand(level, function(result) {
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
ISYJSDebugMessage(this,"Ignoring redundant setBrightness");
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// Handles a request to get the current brightness level for dimmable lights.
|
||||
ISYLightAccessory.prototype.getBrightness = function(callback) {
|
||||
callback(null,this.device.getCurrentLightDimState());
|
||||
}
|
||||
|
||||
// Returns the set of services supported by this object.
|
||||
ISYLightAccessory.prototype.getServices = function() {
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "SmartHome")
|
||||
.setCharacteristic(Characteristic.Model, this.device.deviceFriendlyName)
|
||||
.setCharacteristic(Characteristic.SerialNumber, this.device.address);
|
||||
|
||||
var lightBulbService = new Service.Lightbulb();
|
||||
|
||||
this.informationService = informationService;
|
||||
this.lightService = lightBulbService;
|
||||
|
||||
lightBulbService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('set', this.setPowerState.bind(this));
|
||||
|
||||
lightBulbService
|
||||
.getCharacteristic(Characteristic.On)
|
||||
.on('get', this.getPowerState.bind(this));
|
||||
|
||||
if(this.dimmable) {
|
||||
lightBulbService
|
||||
.addCharacteristic(new Characteristic.Brightness())
|
||||
.on('get', this.getBrightness.bind(this));
|
||||
|
||||
lightBulbService
|
||||
.getCharacteristic(Characteristic.Brightness)
|
||||
.on('set', this.setBrightness.bind(this));
|
||||
}
|
||||
|
||||
return [informationService, lightBulbService];
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// CONTACT SENSOR - ISYDoorWindowSensorAccessory
|
||||
// Implements the ContactSensor service.
|
||||
|
||||
// Constructs a Door Window Sensor (contact sensor) accessory. log = HomeBridge logger, device = wrapped isy-js device.
|
||||
function ISYDoorWindowSensorAccessory(log,device) {
|
||||
ISYAccessoryBaseSetup(this,log,device);
|
||||
this.doorWindowState = false;
|
||||
}
|
||||
|
||||
// Handles the identify command.
|
||||
ISYDoorWindowSensorAccessory.prototype.identify = function(callback) {
|
||||
// Do the identify action
|
||||
callback();
|
||||
}
|
||||
|
||||
// Translates the state of the underlying device object into the corresponding homekit compatible state
|
||||
ISYDoorWindowSensorAccessory.prototype.translateCurrentDoorWindowState = function() {
|
||||
return (this.device.getCurrentDoorWindowState()) ? Characteristic.ContactSensorState.CONTACT_NOT_DETECTED : Characteristic.ContactSensorState.CONTACT_DETECTED;
|
||||
}
|
||||
|
||||
// Handles the request to get he current door window state.
|
||||
ISYDoorWindowSensorAccessory.prototype.getCurrentDoorWindowState = function(callback) {
|
||||
callback(null,this.translateCurrentDoorWindowState());
|
||||
}
|
||||
|
||||
// Mirrors change in the state of the underlying isj-js device object.
|
||||
ISYDoorWindowSensorAccessory.prototype.handleExternalChange = function() {
|
||||
this.sensorService
|
||||
.setCharacteristic(Characteristic.ContactSensorState, this.translateCurrentDoorWindowState());
|
||||
}
|
||||
|
||||
// Returns the set of services supported by this object.
|
||||
ISYDoorWindowSensorAccessory.prototype.getServices = function() {
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "SmartHome")
|
||||
.setCharacteristic(Characteristic.Model, this.device.deviceFriendlyName)
|
||||
.setCharacteristic(Characteristic.SerialNumber, this.device.address);
|
||||
|
||||
var sensorService = new Service.ContactSensor();
|
||||
|
||||
this.sensorService = sensorService;
|
||||
this.informationService = informationService;
|
||||
|
||||
sensorService
|
||||
.getCharacteristic(Characteristic.ContactSensorState)
|
||||
.on('get', this.getCurrentDoorWindowState.bind(this));
|
||||
|
||||
return [informationService, sensorService];
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ELK SENSOR PANEL - ISYElkAlarmPanelAccessory
|
||||
// Implements the SecuritySystem service for an elk security panel connected to the isy system
|
||||
|
||||
// Constructs the alarm panel accessory. log = HomeBridge logger, device = underlying isy-js device being wrapped
|
||||
function ISYElkAlarmPanelAccessory(log,device) {
|
||||
ISYAccessoryBaseSetup(this,log,device);
|
||||
}
|
||||
|
||||
// Handles the identify command
|
||||
ISYElkAlarmPanelAccessory.prototype.identify = function(callback) {
|
||||
callback();
|
||||
}
|
||||
|
||||
// Handles the request to set the alarm target state
|
||||
ISYElkAlarmPanelAccessory.prototype.setAlarmTargetState = function(targetStateHK,callback) {
|
||||
ISYJSDebugMessage(this,"Sending command to set alarm panel state to: "+targetStateHK);
|
||||
var targetState = this.translateHKToAlarmTargetState(targetStateHK);
|
||||
ISYJSDebugMessage(this,"Would send the target state of: "+targetState);
|
||||
if(this.device.getAlarmMode() != targetState) {
|
||||
this.device.sendSetAlarmModeCommand(targetState, function(result) {
|
||||
callback();
|
||||
});
|
||||
} else {
|
||||
ISYJSDebugMessage(this,"Redundant command, already in that state.");
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
// Translates from the current state of the elk alarm system into a homekit compatible state. The elk panel has a lot more
|
||||
// possible states then can be directly represented by homekit so we map them. If the alarm is going off then it is tripped.
|
||||
// If it is arming or armed it is considered armed. Stay maps to the state state, away to the away state, night to the night
|
||||
// state.
|
||||
ISYElkAlarmPanelAccessory.prototype.translateAlarmCurrentStateToHK = function() {
|
||||
var tripState = this.device.getAlarmTripState();
|
||||
var sourceAlarmState = this.device.getAlarmState();
|
||||
var sourceAlarmMode = this.device.getAlarmMode();
|
||||
|
||||
if(tripState >= this.device.ALARM_TRIP_STATE_TRIPPED) {
|
||||
return Characteristic.SecuritySystemCurrentState.ALARM_TRIGGERED;
|
||||
} else if(sourceAlarmState == this.device.ALARM_STATE_NOT_READY_TO_ARM ||
|
||||
sourceAlarmState == this.device.ALARM_STATE_READY_TO_ARM ||
|
||||
sourceAlarmState == this.device.ALARM_STATE_READY_TO_ARM_VIOLATION) {
|
||||
return Characteristic.SecuritySystemCurrentState.DISARMED;
|
||||
} else {
|
||||
if(sourceAlarmMode == this.device.ALARM_MODE_STAY || sourceAlarmMode == this.device.ALARM_MODE_STAY_INSTANT ) {
|
||||
return Characteristic.SecuritySystemCurrentState.STAY_ARM;
|
||||
} else if(sourceAlarmMode == this.device.ALARM_MODE_AWAY || sourceAlarmMode == this.device.ALARM_MODE_VACATION) {
|
||||
return Characteristic.SecuritySystemCurrentState.AWAY_ARM;
|
||||
} else if(sourceAlarmMode == this.device.ALARM_MODE_NIGHT || sourceAlarmMode == this.device.ALARM_MODE_NIGHT_INSTANT) {
|
||||
return Characteristic.SecuritySystemCurrentState.NIGHT_ARM;
|
||||
} else {
|
||||
ISYJSDebugMessage(this,"Setting to disarmed because sourceAlarmMode is "+sourceAlarmMode);
|
||||
return Characteristic.SecuritySystemCurrentState.DISARMED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Translates the current target state of hthe underlying alarm into the appropriate homekit value
|
||||
ISYElkAlarmPanelAccessory.prototype.translateAlarmTargetStateToHK = function() {
|
||||
var sourceAlarmState = this.device.getAlarmMode();
|
||||
if(sourceAlarmState == this.device.ALARM_MODE_STAY || sourceAlarmState == this.device.ALARM_MODE_STAY_INSTANT ) {
|
||||
return Characteristic.SecuritySystemTargetState.STAY_ARM;
|
||||
} else if(sourceAlarmState == this.device.ALARM_MODE_AWAY || sourceAlarmState == this.device.ALARM_MODE_VACATION) {
|
||||
return Characteristic.SecuritySystemTargetState.AWAY_ARM;
|
||||
} else if(sourceAlarmState == this.device.ALARM_MODE_NIGHT || sourceAlarmState == this.device.ALARM_MODE_NIGHT_INSTANT) {
|
||||
return Characteristic.SecuritySystemTargetState.NIGHT_ARM;
|
||||
} else {
|
||||
return Characteristic.SecuritySystemTargetState.DISARM;
|
||||
}
|
||||
}
|
||||
|
||||
// Translates the homekit version of the alarm target state into the appropriate elk alarm panel state
|
||||
ISYElkAlarmPanelAccessory.prototype.translateHKToAlarmTargetState = function(state) {
|
||||
if(state == Characteristic.SecuritySystemTargetState.STAY_ARM) {
|
||||
return this.device.ALARM_MODE_STAY;
|
||||
} else if(state == Characteristic.SecuritySystemTargetState.AWAY_ARM) {
|
||||
return this.device.ALARM_MODE_AWAY;
|
||||
} else if(state == Characteristic.SecuritySystemTargetState.NIGHT_ARM) {
|
||||
return this.device.ALARM_MODE_NIGHT;
|
||||
} else {
|
||||
return this.device.ALARM_MODE_DISARMED;
|
||||
}
|
||||
}
|
||||
|
||||
// Handles request to get the target alarm state
|
||||
ISYElkAlarmPanelAccessory.prototype.getAlarmTargetState = function(callback) {
|
||||
callback(null,this.translateAlarmTargetStateToHK());
|
||||
}
|
||||
|
||||
// Handles request to get the current alarm state
|
||||
ISYElkAlarmPanelAccessory.prototype.getAlarmCurrentState = function(callback) {
|
||||
callback(null,this.translateAlarmCurrentStateToHK());
|
||||
}
|
||||
|
||||
// Mirrors change in the state of the underlying isj-js device object.
|
||||
ISYElkAlarmPanelAccessory.prototype.handleExternalChange = function() {
|
||||
ISYJSDebugMessage(this,"Source device. Currenty state locally -"+this.device.getAlarmStatusAsText());
|
||||
ISYJSDebugMessage(this,"Got alarm change notification. Setting HK target state to: "+this.translateAlarmTargetStateToHK()+" Setting HK Current state to: "+this.translateAlarmCurrentStateToHK());
|
||||
this.alarmPanelService
|
||||
.setCharacteristic(Characteristic.SecuritySystemTargetState, this.translateAlarmTargetStateToHK());
|
||||
this.alarmPanelService
|
||||
.setCharacteristic(Characteristic.SecuritySystemCurrentState, this.translateAlarmCurrentStateToHK());
|
||||
}
|
||||
|
||||
// Returns the set of services supported by this object.
|
||||
ISYElkAlarmPanelAccessory.prototype.getServices = function() {
|
||||
var informationService = new Service.AccessoryInformation();
|
||||
|
||||
informationService
|
||||
.setCharacteristic(Characteristic.Manufacturer, "SmartHome")
|
||||
.setCharacteristic(Characteristic.Model, this.device.deviceFriendlyName)
|
||||
.setCharacteristic(Characteristic.SerialNumber, this.device.address);
|
||||
|
||||
var alarmPanelService = new Service.SecuritySystem();
|
||||
|
||||
this.alarmPanelService = alarmPanelService;
|
||||
this.informationService = informationService;
|
||||
|
||||
alarmPanelService
|
||||
.getCharacteristic(Characteristic.SecuritySystemTargetState)
|
||||
.on('set', this.setAlarmTargetState.bind(this));
|
||||
|
||||
alarmPanelService
|
||||
.getCharacteristic(Characteristic.SecuritySystemTargetState)
|
||||
.on('get', this.getAlarmTargetState.bind(this));
|
||||
|
||||
alarmPanelService
|
||||
.getCharacteristic(Characteristic.SecuritySystemCurrentState)
|
||||
.on('get', this.getAlarmCurrentState.bind(this));
|
||||
|
||||
return [informationService, alarmPanelService];
|
||||
}
|
||||
|
||||
module.exports.platform = ISYPlatform;
|
||||
module.exports.accessory = ISYFanAccessory;
|
||||
module.exports.accessory = ISYLightAccessory;
|
||||
module.exports.accessory = ISYLockAccessory;
|
||||
module.exports.accessory = ISYOutletAccessory;
|
||||
module.exports.accessory = ISYDoorWindowSensorAccessory;
|
||||
module.exports.accessory = ISYElkAlarmPanelAccessory;
|
||||
Reference in New Issue
Block a user