Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Andre Schröter
2015-10-19 21:16:35 +02:00
47 changed files with 4103 additions and 549 deletions

View File

@@ -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){

View File

@@ -26,7 +26,6 @@ try {
Characteristic = require("hap-nodejs").Characteristic;
}
var util = require('util');

253
platforms/FibaroHC2.js Normal file
View File

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

View File

@@ -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')

1066
platforms/HomeSeer.js Normal file

File diff suppressed because it is too large Load Diff

View File

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

552
platforms/Indigo.js Normal file
View File

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

View File

@@ -1,154 +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 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",
"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": []
"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": [
]
}

View File

@@ -2,7 +2,7 @@
* based on Sonos platform
*/
'use strict';
var types = require("HAP-NodeJS/accessories/types.js");
var types = require("hap-nodejs/accessories/types.js");
//var hardware = require('myHardwareSupport'); //require any additional hardware packages
var knxd = require('eibd');
@@ -116,8 +116,8 @@ 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 });
}
/*
@@ -143,7 +143,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 +156,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 +185,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[i].match(/\d*\/\d*\/\d*(R)/) ? true:false);
}
}
// console.log("listeners now: " + subscriptions.length);
};

View File

@@ -47,7 +47,7 @@ You have to add services in the following syntax:
{
"type": "SERVICENAME",
"description": "This is just for you to remember things",
"name": "We need a name for each service, though it usually shows only if multiple services are present in one accessory",
"name": "beer tap thermostat",
"CHARACTERISTIC1": {
"Set": "1/1/6",
"Listen": [
@@ -68,11 +68,54 @@ Two kinds of addresses are supported: `"Set":"1/2/3"` is a writable group addres
`"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:`
# Supported Services and their characteristics
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 **OR**
- ContactSensorStateContact1: DPT 1.002, 1 as contact
- 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
@@ -113,10 +156,10 @@ Two kinds of addresses are supported: `"Set":"1/2/3"` is a writable group addres
- CurrentAmbientLightLevel: DPT 9.004, 0 to 100000 Lux
## LockMechanism (This is poorly mapped!)
- LockCurrentState: DPT 1, 1 as secured **OR (but not both:)**
- LockCurrentStateSecured0: DPT 1, 0 as secured
- LockTargetState: DPT 1, 1 as secured **OR**
- LockTargetStateSecured0: DPT 1, 0 as secured
- 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*
@@ -136,11 +179,11 @@ Two kinds of addresses are supported: `"Set":"1/2/3"` is a writable group addres
- On: DPT 1.001, 1 as on, 0 as off
## TemperatureSensor
- CurrentTemperature: DPT9.001 in °C [listen only]
- CurrentTemperature: DPT9.001 in °C [listen only]
## Thermostat
- CurrentTemperature: DPT9.001 in °C [listen only]
- TargetTemperature: DPT9.001, values 0..40°C only, all others are ignored
- 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
@@ -152,7 +195,7 @@ Two kinds of addresses are supported: `"Set":"1/2/3"` is a writable group addres
## WindowCovering
- CurrentPosition: DPT5 percentage
- TargetPosition: DPT5 percentage
- PositionState: DPT5 value [listen only]
- PositionState: DPT5 value [listen only: 0 Closing, 1 Opening, 2 Stopped]
### not yet supported
- HoldPosition

View File

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

View File

@@ -17,7 +17,7 @@
//
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');

View File

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

View File

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

347
platforms/Openhab.js Normal file
View File

@@ -0,0 +1,347 @@
// OpenHAB Platform Shim for HomeBridge
// Written by Tommaso Marchionni
// Based on many of the other HomeBridge platform modules
//
// Revisions:
//
// 17 October 2015 [tommasomarchionni]
// - Initial release
//
// 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"
// }
//
// 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 request = require("request");
var Service = require("hap-nodejs/lib/Service.js").Service;
var Characteristic = require("hap-nodejs").Characteristic;
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 = {
sitemapUrl: function() {
var serverString = this.server;
//TODO da verificare
if (this.user && this.password) {
serverString = this.user + ":" + this.password + "@" + serverString;
}
return this.protocol + "://" + serverString + ":" + this.port + "/rest/sitemaps/" + this.sitemap + "?type=json";
},
parseSitemap: function(sitemap) {
var widgets = [].concat(sitemap.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("WARN: The widget '" + widget.label + "' does not reference an item.");
continue;
}
if (widget.item.type=="SwitchItem" || widget.item.type=="DimmerItem" || widget.item.type == "RollershutterItem"){
accessory = new OpenhabAccessory(this.log,this,widget.widgetId,widget.label,widget.item)
this.log("Accessory Found: " + widget.label);
result.push(accessory);
}
}
return result;
},
accessories: function(callback) {
this.log("Fetching OpenHAB devices.");
var that = this;
url = that.sitemapUrl();
this.log("Connecting to " + url);
request.get({
url: url,
json: true
}, function(err, response, json) {
if (!err && response.statusCode == 200) {
callback(that.parseSitemap(json));
} else {
that.log("There was a problem connecting to OpenHAB.");
}
});
}
};
function OpenhabAccessory(log, platform, widgetId, label, detail) {
this.log = log;
this.platform = platform;
this.idx = widgetId;
this.name = label;
this.label = label;
this.type = detail.type;
this.deviceURL = detail.link;
this.addressStr = "n/a";
this.state = detail.state;
if (this.type == "DimmerItem") {
this.typeSupportsOnOff = true;
this.typeSupportsDim = true;
}
if (this.type == "SwitchItem") {
this.typeSupportsOnOff = true;
}
if (this.type == "RollershutterItem") {
this.typeSupportsWindowCovering = true;
}
}
OpenhabAccessory.prototype = {
updateStatus: function(command) {
var that = this;
var options = {
url: this.deviceURL,
method: 'POST',
body: "" + command
};
if (this.auth) {
options['auth'] = this.auth;
}
that.log("eseguo post");
request(options, function(error, response, body) {
if (error) {
console.trace("Updating Device Status.");
that.log(error);
return error;
}
that.log("updateStatus of " + that.name + ": " + command);
});
},
getServiceType: function() {
if (this.typeSupportsWindowCovering){
return new Service.WindowCovering;
} else if (this.typeSupportsDim) {
return new Service.Lightbulb;
} else if (this.typeSupportsOnOff) {
return new Service.Switch;
}
},
updateStatus: function(command, callback) {
var that = this;
var options = {
url: this.deviceURL,
method: 'POST',
body: "" + command
};
if (this.auth) {
options['auth'] = this.auth;
}
request(options, function(error, response, body) {
if (error) {
//console.trace("Updating Device Status.");
//that.log(error);
//return error;
callback(new Error(error));
} else {
that.log("updateStatus of " + that.name + ": " + command);
callback(true);
}
}.bind(this));
},
setPowerState: function(powerOn, callback) {
var that = this;
if (this.typeSupportsOnOff) {
if (powerOn) {
var command = "ON";
} else {
var command = "OFF";
}
this.log("Setting power state on the '"+this.name+"' to " + command);
this.updateStatus(command, function(noError){
if (noError) {
that.log("Successfully set '"+that.name+"' to " + command);
callback();
} else {
callback(new Error('Can not communicate with OpenHAB.'));
}
}.bind(this));
}else{
callback(new Error(this.name + " not supports ONOFF"));
}
},
getStatus: function(callback){
var that = this;
this.log("Fetching status brightness for: " + this.name);
var options = {
url: this.deviceURL + '/state?type=json',
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);
//return error;
callback(new Error('Can not communicate with Home Assistant.'));
} else {
that.log("getStatus of " + that.name + ": " + body);
callback(null,body);
}
}.bind(this));
},
getCurrentPosition: function(callback){
callback(100);
},
getPositionState: function(callback){
this.log("Fetching position state for: " + this.name);
callback(Characteristic.PositionState.STOPPED);
},
setTargetPosition: function(level, callback) {
var that = this;
this.log("Setting target position on the '"+this.name+"' to " + level);
this.updateStatus(level, function(noError){
if (noError) {
that.log("Successfully set position on the '"+that.name+"' to " + level);
callback();
} else {
callback(new Error('Can not communicate with OpenHAB.'));
}
}.bind(this));
},
setBrightness: function(level, callback) {
var that = this;
if (this.typeSupportsDim && level >= 0 && level <= 100) {
this.log("Setting brightness on the '"+this.name+"' to " + level);
this.updateStatus(level, function(noError){
if (noError) {
that.log("Successfully set brightness on the '"+that.name+"' to " + level);
callback();
} else {
callback(new Error('Can not communicate with OpenHAB.'));
}
}.bind(this));
}
},
getServices: function() {
var informationService = new Service.AccessoryInformation();
informationService
.setCharacteristic(Characteristic.Manufacturer, "OpenHAB")
.setCharacteristic(Characteristic.Model, this.type)
.setCharacteristic(Characteristic.SerialNumber, "1234567890")
.setCharacteristic(Characteristic.Name, this.label);
var otherService = this.getServiceType();
if (this.typeSupportsOnOff) {
otherService
.getCharacteristic(Characteristic.On)
.on('get', this.getStatus.bind(this))
.on('set', this.setPowerState.bind(this));
}
if (this.typeSupportsDim) {
otherService
.addCharacteristic(Characteristic.Brightness)
.on('get', this.getStatus.bind(this))
.on('set', this.setBrightness.bind(this));
}
if (this.typeSupportsWindowCovering) {
var currentPosition = 100;
otherService
.getCharacteristic(Characteristic.CurrentPosition)
.on('get', this.getCurrentPosition.bind(this))
.setValue(currentPosition);
otherService
.getCharacteristic(Characteristic.PositionState)
.on('get', this.getPositionState.bind(this))
.setValue(Characteristic.PositionState.STOPPED);
otherService
.getCharacteristic(Characteristic.TargetPosition)
.on('get', this.getCurrentPosition.bind(this))
.on('set', this.setTargetPosition.bind(this));
}
console.log(informationService);
return [informationService, otherService];
}
}
module.exports.accessory = OpenhabAccessory;
module.exports.platform = OpenhabPlatform;

View File

@@ -33,7 +33,7 @@ var hue = require("node-hue-api"),
HueApi = hue.HueApi,
lightState = hue.lightState;
var types = require("HAP-NodeJS/accessories/types.js");
var types = require("hap-nodejs/accessories/types.js");
function PhilipsHuePlatform(log, config) {
this.log = log;

View File

@@ -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){

View File

@@ -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){

265
platforms/Telldus.js Normal file
View File

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

View File

@@ -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) {

View File

@@ -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 = {

View File

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

View File

@@ -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,7 +83,20 @@ 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...");
@@ -90,10 +104,10 @@ ZWayServerPlatform.prototype = {
//Note: Order matters!
var primaryDeviceClasses = [
"thermostat",
"sensorMultilevel.Temperature",
"switchMultilevel",
"switchBinary",
"sensorBinary.Door/Window"
"sensorBinary.Door/Window",
"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,98 @@ 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 "thermostat":
services.push(new Service.Thermostat(vdev.metrics.title));
break;
case "sensorMultilevel.Temperature":
services.push(new Service.TemperatureSensor(vdev.metrics.title));
break;
case "switchMultilevel":
services.push(new Service.Lightbulb(vdev.metrics.title));
break;
case "battery.Battery":
services.push(new Service.BatteryService(vdev.metrics.title));
case "thermostat":
services.push(new Service.Thermostat(vdev.metrics.title, vdev.id));
break;
case "switchBinary":
services.push(new Service.Switch(vdev.metrics.title));
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));
services.push(new Service.GarageDoorOpener(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, 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));
}
}
var validServices =[];
@@ -267,11 +392,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
@@ -287,7 +420,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 +442,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 +457,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 +519,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 +589,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 +614,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 +631,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 +661,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 +695,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 +710,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 +728,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 +750,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 +758,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 +778,43 @@ 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;
}
}
,
configureService: function(service, vdev){
@@ -591,14 +826,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 +856,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 +918,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++)