diff --git a/accessories/AD2USB.js b/accessories/AD2USB.js index ea05377..bcef82d 100644 --- a/accessories/AD2USB.js +++ b/accessories/AD2USB.js @@ -1,4 +1,4 @@ -var types = require("HAP-NodeJS/accessories/types.js"); +var types = require("hap-nodejs/accessories/types.js"); var AD2USB = require('ad2usb'); var CUSTOM_PANEL_LCD_TEXT_CTYPE = "A3E7B8F9-216E-42C1-A21C-97D4E3BE52C8"; diff --git a/accessories/Carwings.js b/accessories/Carwings.js index 150c936..7d0cd82 100644 --- a/accessories/Carwings.js +++ b/accessories/Carwings.js @@ -1,4 +1,4 @@ -var types = require("HAP-NodeJS/accessories/types.js"); +var types = require("hap-nodejs/accessories/types.js"); var carwings = require("carwingsjs"); function CarwingsAccessory(log, config) { diff --git a/accessories/ELKM1.js b/accessories/ELKM1.js index d32cc1b..ead0f17 100644 --- a/accessories/ELKM1.js +++ b/accessories/ELKM1.js @@ -1,4 +1,4 @@ -var types = require("HAP-NodeJS/accessories/types.js"); +var types = require("hap-nodejs/accessories/types.js"); var elkington = require("elkington"); function ElkM1Accessory(log, config) { diff --git a/accessories/FileSensor.js b/accessories/FileSensor.js index e377dc6..112089e 100644 --- a/accessories/FileSensor.js +++ b/accessories/FileSensor.js @@ -1,5 +1,5 @@ -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 chokidar = require("chokidar"); var debug = require("debug")("FileSensorAccessory"); var crypto = require("crypto"); diff --git a/accessories/GenericRS232Device.js b/accessories/GenericRS232Device.js index b84e4cc..01fe80c 100644 --- a/accessories/GenericRS232Device.js +++ b/accessories/GenericRS232Device.js @@ -1,5 +1,5 @@ -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 SerialPort = require("serialport").SerialPort; module.exports = { diff --git a/accessories/HomeMatic.js b/accessories/HomeMatic.js index ce575b1..23a7ecb 100644 --- a/accessories/HomeMatic.js +++ b/accessories/HomeMatic.js @@ -1,4 +1,4 @@ -var types = require("HAP-NodeJS/accessories/types.js"); +var types = require("hap-nodejs/accessories/types.js"); var request = require("request"); function HomeMatic(log, config) { @@ -30,7 +30,31 @@ HomeMatic.prototype = { } }); }, + getPowerState: function(callback) { + var that = this; + + this.log("Getting Power State of CCU"); + request.get({ + url: "http://"+this.ccuIP+"/config/xmlapi/state.cgi?datapoint_id="+this.ccuID, + }, function(err, response, body) { + if (!err && response.statusCode == 200) { + + //that.log("Response:"+response.body); + var responseString = response.body.substring(83,87); + //that.log(responseString); + switch(responseString){ + case "true": {modvalue = "1";break;} + case "fals": {modvalue = "0";break;} + } + callback(parseInt(modvalue)); + that.log("Getting Power State complete."); + } + else { + that.log("Error '"+err+"' getting Power State: " + body); + } + }); + }, getServices: function() { var that = this; return [{ @@ -101,6 +125,7 @@ HomeMatic.prototype = { },{ cType: types.POWER_STATE_CTYPE, onUpdate: function(value) { that.setPowerState(value); }, + onRead: function(callback) { that.getPowerState(callback); }, perms: ["pw","pr","ev"], format: "bool", initialValue: false, diff --git a/accessories/HomeMaticThermo.js b/accessories/HomeMaticThermo.js new file mode 100644 index 0000000..86db229 --- /dev/null +++ b/accessories/HomeMaticThermo.js @@ -0,0 +1,264 @@ +var types = require("hap-nodejs/accessories/types.js"); +var request = require("request"); + +function HomeMaticThermo(log, config) { + this.log = log; + this.name = config["name"]; + this.ccuIDTargetTemp = config["ccu_id_TargetTemp"]; + this.ccuIDCurrentTemp = config["ccu_id_CurrentTemp"]; + this.ccuIDControlMode = config["ccu_id_ControlMode"]; + this.ccuIDManuMode = config["ccu_id_ManuMode"]; + this.ccuIDAutoMode = config["ccu_id_AutoMode"]; + this.ccuIP = config["ccu_ip"]; +} + +HomeMaticThermo.prototype = { + + setTargetTemperature: function(value) { + + var that = this; + + this.log("Setting target Temperature of CCU to " + value); + this.log(this.ccuIDTargetTemp + " " + value); + + request.put({ + url: "http://"+this.ccuIP+"/config/xmlapi/statechange.cgi?ise_id="+this.ccuIDTargetTemp+"&new_value="+ value, + }, function(err, response, body) { + + if (!err && response.statusCode == 200) { + that.log("State change complete."); + } + else { + that.log("Error '"+err+"' setting Temperature: " + body); + } + }); + }, + getCurrentTemperature: function(callback) { + + var that = this; + + this.log("Getting current Temperature of CCU"); + request.get({ + url: "http://"+this.ccuIP+"/config/xmlapi/state.cgi?datapoint_id="+this.ccuIDCurrentTemp, + }, function(err, response, body) { + + if (!err && response.statusCode == 200) { + + //that.log("Response:"+response.body); + var responseString = response.body.substring(83,87); + //that.log(responseString); + callback(parseFloat(responseString)); + //that.log("Getting current temperature complete."); + } + else { + that.log("Error '"+err+"' getting Temperature: " + body); + } + }); + }, + getTargetTemperature: function(callback) { + + var that = this; + +this.log("Getting target Temperature of CCU"); + request.get({ + url: "http://"+this.ccuIP+"/config/xmlapi/state.cgi?datapoint_id="+this.ccuIDTargetTemp, + }, function(err, response, body) { + + if (!err && response.statusCode == 200) { + + //that.log("Response:"+response.body); + var responseString = response.body.substring(83,87); + //that.log(responseString); + callback(parseFloat(responseString)); + //that.log("Getting target temperature complete."); + } + else { + that.log("Error '"+err+"' getting Temperature: " + body); + } + }); + }, + getMode: function(callback) { + + var that = this; + + //this.log("Getting target Mode of CCU"); + //this.log(this.ccuID+ value); + + request.get({ + url: "http://"+this.ccuIP+"/config/xmlapi/state.cgi?datapoint_id="+this.ccuIDControlMode, + }, function(err, response, body) { + + if (!err && response.statusCode == 200) { + + //that.log("Response:"+response.body); + var responseInt = response.body.substring(83,84); + //that.log(responseString); + if (responseInt == 1) + { callback(parseInt("0")); } + if (responseInt == 0) + { callback(parseInt("1")); } + //that.log("Getting mode complete."); + } + else { + that.log("Error '"+err+"' getting Mode: " + body); + } + }); + }, + setMode: function(value) { + + var that = this; + + //this.log("Seting target Mode of CCU:" + value); + var modvalue; + var dpID; + switch(value) { + case 3: {modvalue = "true";dpID=this.ccuIDAutoMode;break;} //auto + case 1: {modvalue = "true";dpID=this.ccuIDAutoMode;break;} //heating => auto + default: {modvalue = "1";dpID=this.ccuIDManuMode;} //default => off (manual) + } + + request.put({ + url: "http://"+this.ccuIP+"/config/xmlapi/statechange.cgi?ise_id="+dpID+"&new_value="+ modvalue, + }, function(err, response, body) { + + if (!err && response.statusCode == 200) { + //that.log("Setting Mode complete."); + } + else { + that.log("Error '"+err+"' setting Mode: " + body); + } + }); + }, + getServices: function() { + var that = this; + return [{ + sType: types.ACCESSORY_INFORMATION_STYPE, + characteristics: [{ + cType: types.NAME_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.name, + supportEvents: false, + supportBonjour: false, + manfDescription: "Name of the accessory", + designedMaxLength: 255 + },{ + cType: types.MANUFACTURER_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "test", + supportEvents: false, + supportBonjour: false, + manfDescription: "Manufacturer", + designedMaxLength: 255 + },{ + cType: types.MODEL_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "test", + supportEvents: false, + supportBonjour: false, + manfDescription: "Model", + designedMaxLength: 255 + },{ + cType: types.SERIAL_NUMBER_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "A1S2NREF88EW", + 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.THERMOSTAT_STYPE, + characteristics: [{ + cType: types.NAME_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.name, + supportEvents: false, + supportBonjour: false, + manfDescription: "Name of service", + designedMaxLength: 255 + },{ + cType: types.CURRENTHEATINGCOOLING_CTYPE, + onRead: function(callback) { that.getMode(callback); }, + perms: ["pr","ev"], + format: "int", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Current Mode", + designedMaxLength: 1, + designedMinValue: 0, + designedMaxValue: 2, + designedMinStep: 1, + },{ + cType: types.TARGETHEATINGCOOLING_CTYPE, + onRead: function(callback) { that.getMode(callback); }, + onUpdate: function(value) { that.setMode(value);}, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Target Mode", + designedMinValue: 0, + designedMaxValue: 3, + designedMinStep: 1, + },{ + cType: types.CURRENT_TEMPERATURE_CTYPE, + onRead: function(callback) { that.getCurrentTemperature(callback); }, + onUpdate: null, + perms: ["pr","ev"], + format: "float", + initialValue: 13.0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Current Temperature", + unit: "celsius" + },{ + cType: types.TARGET_TEMPERATURE_CTYPE, + onUpdate: function(value) { that.setTargetTemperature(value); }, + onRead: function(callback) { that.getTargetTemperature(callback); }, + perms: ["pw","pr","ev"], + format: "float", + initialValue: 19.0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Target Temperature", + designedMinValue: 4, + designedMaxValue: 25, + designedMinStep: 0.1, + unit: "celsius" + },{ + cType: types.TEMPERATURE_UNITS_CTYPE, + onUpdate: null, + perms: ["pr","ev"], + format: "int", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Unit" + }] + }]; + } +}; + +module.exports.accessory = HomeMaticThermo; diff --git a/accessories/HomeMaticWindow.js b/accessories/HomeMaticWindow.js new file mode 100644 index 0000000..6dcaf78 --- /dev/null +++ b/accessories/HomeMaticWindow.js @@ -0,0 +1,123 @@ +var types = require("hap-nodejs/accessories/types.js"); +var Characteristic = require("hap-nodejs").Characteristic; +var request = require("request"); + +function HomeMaticWindow(log, config) { + this.log = log; + this.name = config["name"]; + this.ccuID = config["ccu_id"]; + this.ccuIP = config["ccu_ip"]; +} + +HomeMaticWindow.prototype = { + + + getPowerState: function(callback) { + var that = this; + + this.log("Getting Window State of CCU"); + request.get({ + url: "http://"+this.ccuIP+"/config/xmlapi/state.cgi?datapoint_id="+this.ccuID, + }, function(err, response, body) { + + if (!err && response.statusCode == 200) { + + //that.log("Response:"+response.body); + var responseString = response.body.substring(83,84); + //that.log(responseString); + switch(responseString){ + case "0": {callback(Characteristic.ContactSensorState.CONTACT_DETECTED);break;} + case "1": {callback(Characteristic.ContactSensorState.CONTACT_NOT_DETECTED);break;} + case "2": {callback(Characteristic.ContactSensorState.CONTACT_NOT_DETECTED);break;} + } + that.log("Getting Window State complete."); + } + else { + that.log("Error '"+err+"' getting Window State: " + body); + } + }); + }, + + getServices: function() { + var that = this; + return [{ + sType: types.ACCESSORY_INFORMATION_STYPE, + characteristics: [{ + cType: types.NAME_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.name, + supportEvents: false, + supportBonjour: false, + manfDescription: "Name of the accessory", + designedMaxLength: 255 + },{ + cType: types.MANUFACTURER_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "Homematic", + supportEvents: false, + supportBonjour: false, + manfDescription: "Manufacturer", + designedMaxLength: 255 + },{ + cType: types.MODEL_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "HM-Sec-RHS", + supportEvents: false, + supportBonjour: false, + manfDescription: "Model", + designedMaxLength: 255 + },{ + cType: types.SERIAL_NUMBER_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "A1S2NASF88EW", + supportEvents: false, + supportBonjour: false, + manfDescription: "SN", + designedMaxLength: 255 + },{ + cType: types.IDENTIFY_CTYPE, + onUpdate: null, + perms: ["pw"], + format: "bool", + initialValue: false, + supportEvents: false, + supportBonjour: false, + manfDescription: "Identify Accessory", + designedMaxLength: 1 + }] + },{ + sType: types.CONTACT_SENSOR_STYPE, + characteristics: [{ + cType: types.NAME_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.name, + supportEvents: false, + supportBonjour: false, + manfDescription: "Name of service", + designedMaxLength: 255 + },{ + cType: types.CONTACT_SENSOR_STATE_CTYPE, + onRead: function(callback) { that.getPowerState(callback); }, + perms: ["pr","ev"], + format: "bool", + initialValue: false, + supportEvents: false, + supportBonjour: false, + manfDescription: "Get Window state of a Variable", + designedMaxLength: 1 + }] + }]; + } +}; + +module.exports.accessory = HomeMaticWindow; diff --git a/accessories/Http.js b/accessories/Http.js index e1859cf..fb708e3 100644 --- a/accessories/Http.js +++ b/accessories/Http.js @@ -1,5 +1,5 @@ -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 request = require("request"); module.exports = { diff --git a/accessories/HttpHygrometer.js b/accessories/HttpHygrometer.js new file mode 100644 index 0000000..f722fe8 --- /dev/null +++ b/accessories/HttpHygrometer.js @@ -0,0 +1,71 @@ +var Service = require("hap-nodejs").Service; +var Characteristic = require("hap-nodejs").Characteristic; +var request = require("request"); + +module.exports = { + accessory: HygrometerAccessory +} + +function HygrometerAccessory(log, config) { + this.log = log; + + // url info + this.url = config["url"]; + this.http_method = config["http_method"]; +} + + +HygrometerAccessory.prototype = { + + httpRequest: function(url, method, callback) { + request({ + url: url, + method: method + }, + function (error, response, body) { + callback(error, response, body) + }) + }, + + + identify: function(callback) { + this.log("Identify requested!"); + callback(); // success + }, + + getCurrentRelativeHumidity: function (callback) { + var that = this; + that.log ("getting CurrentCurrentRelativeHumidity"); + + this.httpRequest(this.url, this.http_method, function(error, response, body) { + if (error) { + this.log('HTTP function failed: %s', error); + callback(error); + } + else { + this.log('HTTP function succeeded - %s', body); + callback(null, Number(body)); + } + }.bind(this)); + }, + + getServices: function() { + + // you can OPTIONALLY create an information service if you wish to override + // the default values for things like serial number, model, etc. + var informationService = new Service.AccessoryInformation(); + + informationService + .setCharacteristic(Characteristic.Manufacturer, "HTTP Manufacturer") + .setCharacteristic(Characteristic.Model, "HTTP Hygrometer") + .setCharacteristic(Characteristic.SerialNumber, "HTTP Serial Number"); + + var humidityService = new Service.HumiditySensor(); + + humidityService + .getCharacteristic(Characteristic.CurrentRelativeHumidity) + .on('get', this.getCurrentRelativeHumidity.bind(this)); + + return [informationService, humidityService]; + } +}; diff --git a/accessories/HttpThermometer.js b/accessories/HttpThermometer.js new file mode 100644 index 0000000..9235a57 --- /dev/null +++ b/accessories/HttpThermometer.js @@ -0,0 +1,79 @@ +var Service = require("hap-nodejs").Service; +var Characteristic = require("hap-nodejs").Characteristic; +var request = require("request"); + +module.exports = { + accessory: ThermometerAccessory +} + +function ThermometerAccessory(log, config) { + this.log = log; + + // url info + this.url = config["url"]; + this.http_method = config["http_method"]; +} + + +ThermometerAccessory.prototype = { + + httpRequest: function(url, method, callback) { + request({ + url: url, + method: method + }, + function (error, response, body) { + callback(error, response, body) + }) + }, + + + identify: function(callback) { + this.log("Identify requested!"); + callback(); // success + }, + + getCurrentTemperature: function (callback) { + var that = this; + that.log ("getting CurrentTemperature"); + + + this.httpRequest(this.url, this.http_method, function(error, response, body) { + if (error) { + this.log('HTTP function failed: %s', error); + callback(error); + } + else { + this.log('HTTP function succeeded - %s', body); + callback(null, Number(body)); + } + }.bind(this)); + }, + + getTemperatureUnits: function (callback) { + var that = this; + that.log ("getTemperature Units"); + // 1 = F and 0 = C + callback (null, 0); + }, + + getServices: function() { + + // you can OPTIONALLY create an information service if you wish to override + // the default values for things like serial number, model, etc. + var informationService = new Service.AccessoryInformation(); + + informationService + .setCharacteristic(Characteristic.Manufacturer, "HTTP Manufacturer") + .setCharacteristic(Characteristic.Model, "HTTP Thermometer") + .setCharacteristic(Characteristic.SerialNumber, "HTTP Serial Number"); + + var temperatureService = new Service.TemperatureSensor(); + + temperatureService + .getCharacteristic(Characteristic.CurrentTemperature) + .on('get', this.getCurrentTemperature.bind(this)); + + return [informationService, temperatureService]; + } +}; diff --git a/accessories/Hyperion.js b/accessories/Hyperion.js index b01db55..8767b19 100644 --- a/accessories/Hyperion.js +++ b/accessories/Hyperion.js @@ -1,4 +1,4 @@ -var types = require("HAP-NodeJS/accessories/types.js"); +var types = require("hap-nodejs/accessories/types.js"); var net = require('net'); var Color = require('color'); diff --git a/accessories/LiftMaster.js b/accessories/LiftMaster.js index baf3be8..c239f24 100644 --- a/accessories/LiftMaster.js +++ b/accessories/LiftMaster.js @@ -1,4 +1,4 @@ -var types = require("HAP-NodeJS/accessories/types.js"); +var types = require("hap-nodejs/accessories/types.js"); var request = require("request"); // This seems to be the "id" of the official LiftMaster iOS app diff --git a/accessories/Lockitron.js b/accessories/Lockitron.js index 8088d14..ed95b7e 100644 --- a/accessories/Lockitron.js +++ b/accessories/Lockitron.js @@ -1,5 +1,5 @@ -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 request = require("request"); module.exports = { diff --git a/accessories/Tesla.js b/accessories/Tesla.js index 016a465..8d96e57 100644 --- a/accessories/Tesla.js +++ b/accessories/Tesla.js @@ -1,4 +1,4 @@ -var types = require("HAP-NodeJS/accessories/types.js"); +var types = require("hap-nodejs/accessories/types.js"); var tesla = require("teslams"); function TeslaAccessory(log, config) { diff --git a/accessories/WeMo.js b/accessories/WeMo.js index b97b041..5cbc3f4 100644 --- a/accessories/WeMo.js +++ b/accessories/WeMo.js @@ -1,5 +1,5 @@ -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 wemo = require('wemo'); module.exports = { diff --git a/accessories/X10.js b/accessories/X10.js index 6668a44..0cf781c 100644 --- a/accessories/X10.js +++ b/accessories/X10.js @@ -1,4 +1,4 @@ -var types = require("HAP-NodeJS/accessories/types.js"); +var types = require("hap-nodejs/accessories/types.js"); var request = require("request"); function X10(log, config) { diff --git a/accessories/iControl.js b/accessories/iControl.js index d948867..a4299b5 100644 --- a/accessories/iControl.js +++ b/accessories/iControl.js @@ -1,6 +1,6 @@ var iControl = require('node-icontrol').iControl; -var Service = require('HAP-NodeJS').Service; -var Characteristic = require('HAP-NodeJS').Characteristic; +var Service = require("hap-nodejs").Service; +var Characteristic = require("hap-nodejs").Characteristic; module.exports = { accessory: iControlAccessory diff --git a/accessories/knxdevice.js b/accessories/knxdevice.js index 6aa4ffd..92bb88c 100644 --- a/accessories/knxdevice.js +++ b/accessories/knxdevice.js @@ -21,8 +21,8 @@ New 2015-10-07: - Accept uuid_base parameter from config.json to use as unique identifier in UUIDs instead of name (optional) * */ -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 knxd = require("eibd"); var knxd_registerGA = require('../platforms/KNX.js').registerGA; var knxd_startMonitor = require('../platforms/KNX.js').startMonitor; diff --git a/accessories/mpdclient.js b/accessories/mpdclient.js index 8fee5eb..ac6bf5d 100644 --- a/accessories/mpdclient.js +++ b/accessories/mpdclient.js @@ -1,5 +1,5 @@ -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 request = require("request"); var komponist = require('komponist') diff --git a/app.js b/app.js index 126b7e3..20279fa 100644 --- a/app.js +++ b/app.js @@ -1,14 +1,14 @@ var fs = require('fs'); var path = require('path'); var storage = require('node-persist'); -var hap = require('HAP-NodeJS'); -var uuid = require('HAP-NodeJS').uuid; -var Bridge = require('HAP-NodeJS').Bridge; -var Accessory = require('HAP-NodeJS').Accessory; -var Service = require('HAP-NodeJS').Service; -var Characteristic = require('HAP-NodeJS').Characteristic; -var accessoryLoader = require('HAP-NodeJS').AccessoryLoader; -var once = require('HAP-NodeJS/lib/util/once').once; +var hap = require("hap-nodejs"); +var uuid = require("hap-nodejs").uuid; +var Bridge = require("hap-nodejs").Bridge; +var Accessory = require("hap-nodejs").Accessory; +var Service = require("hap-nodejs").Service; +var Characteristic = require("hap-nodejs").Characteristic; +var accessoryLoader = require("hap-nodejs").AccessoryLoader; +var once = require("hap-nodejs/lib/util/once").once; console.log("Starting HomeBridge server..."); diff --git a/config-sample.json b/config-sample.json index 7af74db..05b14ca 100644 --- a/config-sample.json +++ b/config-sample.json @@ -23,6 +23,10 @@ "token" : "telldus token", "token_secret" : "telldus token secret" }, + { + "platform" : "Telldus", + "name" : "Telldus" + }, { "platform": "Wink", "name": "Wink", @@ -149,6 +153,24 @@ "ccu_id": "The XMP-API id of your HomeMatic device", "ccu_ip": "The IP-Adress of your HomeMatic CCU device" }, + { + "accessory": "HomeMaticWindow", + "name": "Contact", + "description": "Control HomeMatic devices (The XMP-API addon for the CCU is required)", + "ccu_id": "The XMP-API id of your HomeMatic device (type HM-Sec-RHS)", + "ccu_ip": "The IP-Adress of your HomeMatic CCU device" + }, + { + "accessory": "HomeMaticThermo", + "name": "Contact", + "description": "Control HomeMatic devices (The XMP-API addon for the CCU is required)", + "ccu_id_TargetTemp": "The XMP-API id of your HomeMatic device (type HM-CC-RT-DN )", + "ccu_id_CurrentTemp": "The XMP-API id of your HomeMatic device (type HM-CC-RT-DN )", + "ccu_id_ControlMode": "The XMP-API id of your HomeMatic device (type HM-CC-RT-DN )", + "ccu_id_ManuMode": "The XMP-API id of your HomeMatic device (type HM-CC-RT-DN )", + "ccu_id_AutoMode": "The XMP-API id of your HomeMatic device (type HM-CC-RT-DN )", + "ccu_ip": "The IP-Adress of your HomeMatic CCU device" + }, { "accessory": "X10", "name": "Lamp", @@ -164,7 +186,20 @@ "off_url": "https://192.168.1.22:3030/devices/23222/off", "brightness_url": "https://192.168.1.22:3030/devices/23222/brightness/%b", "http_method": "POST" - },{ + }, + { + "accessory": "HttpHygrometer", + "name": "Kitchen", + "url": "http://host/URL", + "http_method": "GET" + }, + { + "accessory": "HttpThermometer", + "name": "Garage", + "url": "http://home/URL", + "http_method": "GET" + }, + { "accessory": "ELKM1", "name": "Security System", "description": "Allows basic control of Elk M1 security system. You can use 1 of 3 arm modes: Away, Stay, Night. If you need to access all 3, create 3 accessories with different names.", diff --git a/package.json b/package.json index 3a3ca74..aa5c82d 100644 --- a/package.json +++ b/package.json @@ -12,34 +12,38 @@ "license": "ISC", "dependencies": { "ad2usb": "git+https://github.com/alistairg/node-ad2usb.git#local", + "async": "^1.4.2", "carwingsjs": "0.0.x", "chokidar": "^1.0.5", "color": "0.10.x", + "debug": "^2.2.0", "eibd": "^0.3.1", "elkington": "kevinohara80/elkington", - "hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#4650e771f356a220868d873d16564a6be6603ff7", - "harmonyhubjs-client": "^1.1.4", + "hap-nodejs": "^0.0.2", + "harmonyhubjs-client": "^1.1.6", "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", - "lifx-api": "^1.0.1", + "isy-js": "", + "komponist": "0.1.0", "lifx": "git+https://github.com/magicmonkey/lifxjs.git", + "lifx-api": "^1.0.1", "mdns": "^2.2.4", "node-hue-api": "^1.0.5", - "node-icontrol": "^0.1.4", + "node-icontrol": "^0.1.5", "node-milight-promise": "0.0.x", "node-persist": "0.0.x", + "node-xmpp-client": "1.0.0-alpha23", "q": "1.4.x", - "tough-cookie": "^2.0.0", + "queue": "^3.1.0", "request": "2.49.x", "sonos": "0.8.x", - "telldus-live": "0.2.x", + "telldus-live": "^0.2.1", "teslams": "1.0.1", + "tough-cookie": "^2.0.0", "unofficial-nest-api": "git+https://github.com/hachidorii/unofficial_nodejs_nest.git#d8d48edc952b049ff6320ef99afa7b2f04cdee98", "wemo": "0.2.x", "wink-js": "0.0.5", "xml2js": "0.4.x", "xmldoc": "0.1.x", - "komponist" : "0.1.0", - "yamaha-nodejs": "0.4.x", - "debug": "^2.2.0" + "yamaha-nodejs": "0.4.x" } } diff --git a/platforms/Domoticz.js b/platforms/Domoticz.js index 8930011..948167d 100644 --- a/platforms/Domoticz.js +++ b/platforms/Domoticz.js @@ -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){ diff --git a/platforms/FHEM.js b/platforms/FHEM.js index 03f3ee1..08f337d 100644 --- a/platforms/FHEM.js +++ b/platforms/FHEM.js @@ -1,4 +1,5 @@ // FHEM Platform Shim for HomeBridge +// current version on https://github.com/justme-1968/homebridge // // Remember to add platform to config.json. Example: // "platforms": [ @@ -12,14 +13,20 @@ // 'filter': "room=xyz" // } // ], -// -// 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; +try { + Service = require("hap-nodejs").Service; +} catch(err) { + Service = require("HAP-NodeJS").Service; +} -var types = require('HAP-NodeJS/accessories/types.js'); +var Characteristic; +try { + Characteristic = require("hap-nodejs").Characteristic; +} catch(err) { + Characteristic = require("HAP-NodeJS").Characteristic; +} var util = require('util'); @@ -31,6 +38,22 @@ FHEM_subscribe(characteristic, inform_id, accessory) { FHEM_subscriptions[inform_id] = { 'characteristic': characteristic, 'accessory': accessory }; } +function +FHEM_isPublished(device) { + var keys = Object.keys(FHEM_subscriptions); + for( var i = 0; i < keys.length; i++ ) { + var key = keys[i]; + + var subscription = FHEM_subscriptions[key]; + var accessory = subscription.accessory; + + if( accessory.device === device ) + return true; + }; + + return false; +} + // cached readings from longpoll & query var FHEM_cached = {}; //var FHEM_internal = {}; @@ -44,26 +67,30 @@ FHEM_update(inform_id, value, no_update) { FHEM_cached[inform_id] = value; //FHEM_cached[inform_id] = { 'value': value, 'timestamp': Date.now() }; - console.log(" caching: " + inform_id + ": " + value + " as " + typeof(value) ); + var date = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, ''); + console.log(" " + date + " caching: " + inform_id + ": " + value + " as " + typeof(value) ); - if( !no_update ) - subscription.characteristic.setValue(value, undefined, 'fromFhem'); + if( !no_update && subscription.characteristic ) + subscription.characteristic.setValue(value, undefined, 'fromFHEM'); } } -var FHEM_lastEventTime; -var FHEM_longpoll_running = false; +var FHEM_lastEventTime = {}; +var FHEM_longpoll_running = {}; //FIXME: add filter function FHEM_startLongpoll(connection) { - if( FHEM_longpoll_running ) + if( FHEM_longpoll_running[connection.base_url] ) return; - FHEM_longpoll_running = true; + FHEM_longpoll_running[connection.base_url] = true; + + if( connection.disconnects == undefined ) + connection.disconnects = 0; var filter = ".*"; var since = "null"; - if( FHEM_lastEventTime ) - since = FHEM_lastEventTime/1000; + if( FHEM_lastEventTime[connection.base_url] ) + since = FHEM_lastEventTime[connection.base_url]/1000; var query = "/fhem.pl?XHR=1"+ "&inform=type=status;filter="+filter+";since="+since+";fmt=JSON"+ "×tamp="+Date.now() @@ -108,7 +135,7 @@ function FHEM_startLongpoll(connection) { var subscription = FHEM_subscriptions[d[0]]; if( subscription != undefined ) { //console.log( "Rcvd: "+(l.length>132 ? l.substring(0,132)+"...("+l.length+")":l) ); - FHEM_lastEventTime = lastEventTime; + FHEM_lastEventTime[connection.base_url] = lastEventTime; var accessory = subscription.accessory; var value = d[1]; @@ -149,6 +176,27 @@ function FHEM_startLongpoll(connection) { FHEM_update( device+'-pct', pct ); } + } else if( reading == 'activity') { + + FHEM_update( device+'-'+reading, value, true ); + + Object.keys(FHEM_subscriptions).forEach(function(key) { + var parts = key.split( '-', 3 ); + if( parts[0] != '#' + device ) + return; + if( parts[1] != reading ) + return; + + var subscription = FHEM_subscriptions[key]; + var accessory = subscription.accessory; + + var activity = parts[2]; + + subscription.characteristic.setValue(value==activity?1:0, undefined, 'fromFHEM'); + } ); + + continue; + } else if(accessory.mappings.rgb && reading == accessory.mappings.rgb.reading) { var hsv = FHEM_rgb2hsv(value); var hue = parseInt( hsv[0] * 360 ); @@ -160,6 +208,25 @@ function FHEM_startLongpoll(connection) { FHEM_update( device+'-sat', sat ); FHEM_update( device+'-bri', bri ); continue; + + } else if(accessory.mappings.colormode) { + //FIXME: add colormode ct + if( reading == 'xy') { + var xy = value.split(','); + var rgb = FHEM_xyY2rgb(xy[0], xy[1] , 1); + var hsv = FHEM_rgb2hsv(rgb); + var hue = parseInt( hsv[0] * 360 ); + var sat = parseInt( hsv[1] * 100 ); + var bri = parseInt( hsv[2] * 100 ); + + FHEM_update( device+'-hue', hue ); + FHEM_update( device+'-sat', sat ); + FHEM_update( device+'-bri', bri ); + } + + FHEM_update( device+'-'+reading, value, false ); + continue; + } value = accessory.reading2homekit(reading, value); @@ -173,23 +240,34 @@ function FHEM_startLongpoll(connection) { input = input.substr(FHEM_longpollOffset); FHEM_longpollOffset = 0; - } ).on( 'end', function() { - console.log( "longpoll ended" ); + connection.disconnects = 0; - FHEM_longpoll_running = false; - setTimeout( function(){FHEM_startLongpoll(connection)}, 2000 ); + } ).on( 'end', function() { + FHEM_longpoll_running[connection.base_url] = false; + + connection.disconnects++; + var timeout = 500 * connection.disconnects - 300; + if( timeout > 30000 ) timeout = 30000; + + console.log( "longpoll ended, reconnect in: " + timeout + "msec" ); + setTimeout( function(){FHEM_startLongpoll(connection)}, timeout ); } ).on( 'error', function(err) { - console.log( "longpoll error: " + err ); + FHEM_longpoll_running[connection.base_url] = false; - FHEM_longpoll_running = false; - setTimeout( function(){FHEM_startLongpoll(connection)}, 5000 ); + connection.disconnects++; + var timeout = 5000 * connection.disconnects; + if( timeout > 30000 ) timeout = 30000; + + console.log( "longpoll error: " + err + ", retry in: " + timeout + "msec" ); + setTimeout( function(){FHEM_startLongpoll(connection)}, timeout ); } ); } -function FHEMPlatform(log, config) { +function +FHEMPlatform(log, config) { this.log = log; this.server = config['server']; this.port = config['port']; @@ -280,6 +358,90 @@ FHEM_hsv2rgb(h,s,v) { return FHEM_rgb2hex( Math.round(r*255),Math.round(g*255),Math.round(b*255) ); } +function +FHEM_ct2rgb(ct) +{ + // calculation from http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code + // adjusted by 1000K + var temp = (1000000/ct)/100 + 10; + + var r = 0; + var g = 0; + var b = 0; + + r = 255; + if( temp > 66 ) + r = 329.698727446 * Math.pow(temp - 60, -0.1332047592); + if( r < 0 ) + r = 0; + if( r > 255 ) + r = 255; + + if( temp <= 66 ) + g = 99.4708025861 * Math.log(temp) - 161.1195681661; + else + g = 288.1221695283 * Math.pow(temp - 60, -0.0755148492); + if( g < 0 ) + g = 0; + if( g > 255 ); + g = 255; + + b = 255; + if( temp <= 19 ) + b = 0; + if( temp < 66 ) + b = 138.5177312231 * log(temp-10) - 305.0447927307; + if( b < 0 ) + b = 0; + if( b > 255 ) + b = 255; + + return FHEM_rgb2hex( Math.round(r*255),Math.round(g*255),Math.round(b*255) ); +} + +function +FHEM_xyY2rgb(x,y,Y) +{ + // calculation from http://www.brucelindbloom.com/index.html + + var r = 0; + var g = 0; + var b = 0; + + if( y > 0 ) { + var X = x * Y / y; + var Z = (1 - x - y)*Y / y; + + if( X > 1 + || Y > 1 + || Z > 1 ) { + var f = Math.max(X,Y,Z); + X /= f; + Y /= f; + Z /= f; + } + + r = 0.7982 * X + 0.3389 * Y - 0.1371 * Z; + g = -0.5918 * X + 1.5512 * Y + 0.0406 * Z; + b = 0.0008 * X + 0.0239 * Y + 0.9753 * Z; + + if( r > 1 + || g > 1 + || b > 1 ) { + var f = Math.max(r,g,b); + r /= f; + g /= f; + b /= f; + } + + r *= 255; + g *= 255; + b *= 255; + } + + return FHEM_rgb2hex( Math.round(r*255),Math.round(g*255),Math.round(b*255) ); +} + function FHEM_rgb2hsv(r,g,b){ @@ -319,9 +481,60 @@ FHEM_rgb2hsv(r,g,b){ return [h,s,v]; } +function +FHEM_execute(log,connection,cmd,callback) { + var url = encodeURI( connection.base_url + "/fhem?cmd=" + cmd + "&XHR=1"); + log( ' executing: ' + url ); + + connection.request + .get( { url: url, gzip: true }, + function(err, response, result) { + if( !err && response.statusCode == 200 ) { + result = result.replace(/[\r\n]/g, ""); + if( callback ) + callback( result ); + + } else { + log("There was a problem connecting to FHEM ("+ url +")."); + if( response ) + log( " " + response.statusCode + ": " + response.statusMessage ); + + } + + } ) + .on( 'error', function(err) { log("There was a problem connecting to FHEM ("+ url +"):"+ err); } ); +} FHEMPlatform.prototype = { + execute: function(cmd,callback) {FHEM_execute(this.log, this.connection, cmd, callback)}, + + checkAndSetGenericDeviceType: function() { + this.log("Checking genericDeviceType..."); + + var cmd = '{AttrVal("global","userattr","")}'; + + this.execute( cmd, + function(result) { + //if( result == undefined ) + //result = ""; + + if( !result.match(/(^| )genericDeviceType\b/) ) { + //FIXME: use addToAttrList + var cmd = 'attr global userattr ' + result + ' genericDeviceType:ignore,switch,outlet,light,blind,thermostat,garage,window,lock'; + this.execute( cmd, + function(result) { +console.log( result ); + console.log( 'genericDeviceType attribute was not known. please restart homebridge.' ); + process.exit(0); + } ); + } + }.bind(this) ); + + }, + accessories: function(callback) { + //this.checkAndSetGenericDeviceType(); + this.log("Fetching FHEM switchable devices..."); var foundAccessories = []; @@ -339,80 +552,85 @@ FHEMPlatform.prototype = { asyncCalls++; - var that = this; this.connection.request.get( { url: url, json: true, gzip: true }, function(err, response, json) { if( !err && response.statusCode == 200 ) { - that.log( 'got: ' + json['totalResultsReturned'] + ' results' ); -//that.log("got json: " + util.inspect(json) ); + this.log( 'got: ' + json['totalResultsReturned'] + ' results' ); +//this.log("got json: " + util.inspect(json) ); if( json['totalResultsReturned'] ) { var sArray=FHEM_sortByKey(json['Results'],"Name"); sArray.map(function(s) { var accessory; - if( s.Attributes.disable == 1 ) { - that.log( s.Internals.NAME + ' is disabled'); + if( FHEM_isPublished(s.Internals.NAME) ) + this.log( s.Internals.NAME + ' is already published'); + + else if( s.Attributes.disable == 1 ) { + this.log( s.Internals.NAME + ' is disabled'); } else if( s.Internals.TYPE == 'structure' ) { - that.log( s.Internals.NAME + ' is a structure'); + this.log( 'ignoring structure ' + s.Internals.NAME ); } else if( s.Attributes.genericDisplayType || s.Attributes.genericDeviceType ) { - accessory = new FHEMAccessory(that.log, that.connection, s); + accessory = new FHEMAccessory(this.log, this.connection, s); - } else if( s.PossibleSets.match(/[\^ ]on\b/) - && s.PossibleSets.match(/[\^ ]off\b/) ) { - accessory = new FHEMAccessory(that.log, that.connection, s); - - } else if( s.PossibleSets.match(/[\^ ]Volume\b/) ) { //FIXME: use sets [Pp]lay/[Pp]ause/[Ss]top - that.log( s.Internals.NAME + ' has volume'); - accessory = new FHEMAccessory(that.log, that.connection, s); + } else if( s.PossibleSets.match(/(^| )on\b/) + && s.PossibleSets.match(/(^| )off\b/) ) { + accessory = new FHEMAccessory(this.log, this.connection, s); } else if( s.Attributes.subType == 'thermostat' || s.Attributes.subType == 'blindActuator' || s.Attributes.subType == 'threeStateSensor' ) { - accessory = new FHEMAccessory(that.log, that.connection, s); + accessory = new FHEMAccessory(this.log, this.connection, s); } else if( s.Attributes.model == 'HM-SEC-WIN' ) { - accessory = new FHEMAccessory(that.log, that.connection, s); + accessory = new FHEMAccessory(this.log, this.connection, s); - } else if( s.Attributes.model == 'HM-SEC-KEY' ) { - accessory = new FHEMAccessory(that.log, that.connection, s); + } else if( s.Attributes.model && s.Attributes.model.match(/^HM-SEC-KEY/) ) { + accessory = new FHEMAccessory(this.log, this.connection, s); - } else if( s.Internals.TYPE == 'PRESENCE' ) { - accessory = new FHEMAccessory(that.log, that.connection, s); + } else if( s.Internals.TYPE == 'PRESENCE' + || s.Internals.TYPE == 'ROOMMATE' ) { + accessory = new FHEMAccessory(this.log, this.connection, s); + + } else if( s.Internals.TYPE == 'SONOSPLAYER' ) { + accessory = new FHEMAccessory(this.log, this.connection, s); } else if( s.Readings.temperature ) { - accessory = new FHEMAccessory(that.log, that.connection, s); + accessory = new FHEMAccessory(this.log, this.connection, s); } else if( s.Readings.humidity ) { - accessory = new FHEMAccessory(that.log, that.connection, s); + accessory = new FHEMAccessory(this.log, this.connection, s); } else if( s.Readings.voc ) { - accessory = new FHEMAccessory(that.log, that.connection, s); + accessory = new FHEMAccessory(this.log, this.connection, s); + + } else if( s.Internals.TYPE == 'harmony' ) { + accessory = new FHEMAccessory(this.log, this.connection, s); } else { - that.log( 'ignoring ' + s.Internals.NAME ); + this.log( 'ignoring ' + s.Internals.NAME + ' (' + s.Internals.TYPE + ')' ); } if( accessory && Object.getOwnPropertyNames(accessory).length ) foundAccessories.push(accessory); - }); + }.bind(this) ); } - //callback(foundAccessories); - callbackLater(); + callback(foundAccessories); + //callbackLater(); } else { - that.log("There was a problem connecting to FHEM (1)."); + this.log("There was a problem connecting to FHEM (1)."); if( response ) - that.log( " " + response.statusCode + ": " + response.statusMessage ); + this.log( " " + response.statusCode + ": " + response.statusMessage ); } - }); + }.bind(this) ); } } @@ -426,46 +644,61 @@ FHEMAccessory(log, connection, s) { return new FHEMAccessory(log, connection, s); if( s.Attributes.disable == 1 ) { - that.log( s.Internals.NAME + ' is disabled'); + log( s.Internals.NAME + ' is disabled'); return null; } else if( s.Internals.TYPE == 'structure' ) { - that.log( s.Internals.NAME + ' is a structure'); + log( 'ignoring ' + s.Internals.NAME + ' (' + s.Internals.TYPE + ')' ); return null; } + var genericType = s.Attributes.genericDeviceType; + if( !genericType ) + genericType = s.Attributes.genericDisplayType; + + if( genericType == 'ignore' ) { + log( 'ignoring ' + s.Internals.NAME ); + return null; + } + this.mappings = {}; var match; - if( match = s.PossibleSets.match(/[\^ ]pct\b/) ) { + if( match = s.PossibleSets.match(/(^| )pct\b/) ) { this.mappings.pct = { reading: 'pct', cmd: 'pct' }; - } else if( match = s.PossibleSets.match(/[\^ ]dim\d+%/) ) { + } else if( match = s.PossibleSets.match(/(^| )dim\d+%/) ) { s.hasDim = true; s.pctMax = 100; } - if( match = s.PossibleSets.match(/[\^ ]hue[^\b\s]*(,(\d+)?)+\b/) ) { + if( match = s.PossibleSets.match(/(^| )hue[^\b\s]*(,(\d+)?)+\b/) ) { s.isLight = true; var max = 360; - if( match[2] != undefined ) - max = match[2]; + if( match[3] != undefined ) + max = match[3]; this.mappings.hue = { reading: 'hue', cmd: 'hue', min: 0, max: max }; } - if( match = s.PossibleSets.match(/[\^ ]sat[^\b\s]*(,(\d+)?)+\b/) ) { + if( match = s.PossibleSets.match(/(^| )sat[^\b\s]*(,(\d+)?)+\b/) ) { s.isLight = true; var max = 100; - if( match[2] != undefined ) - max = match[2]; + if( match[3] != undefined ) + max = match[3]; this.mappings.sat = { reading: 'sat', cmd: 'sat', min: 0, max: max }; } - if( s.PossibleSets.match(/[\^ ]rgb\b/) ) { + if( s.Readings.colormode ) + this.mappings.colormode = { reading: 'colormode' }; + if( s.Readings.xy ) + this.mappings.xy = { reading: 'xy' }; + //FIXME: add ct/colortemperature + + if( s.PossibleSets.match(/(^| )rgb\b/) ) { s.isLight = true; this.mappings.rgb = { reading: 'rgb', cmd: 'rgb' }; if( s.Internals.TYPE == 'SWAP_0000002200000003' ) this.mappings.rgb = { reading: '0B-RGBlevel', cmd: 'rgb' }; - } else if( s.PossibleSets.match(/[\^ ]RGB\b/) ) { + } else if( s.PossibleSets.match(/(^| )RGB\b/) ) { s.isLight = true; this.mappings.rgb = { reading: 'RGB', cmd: 'RGB' }; } @@ -486,23 +719,31 @@ FHEMAccessory(log, connection, s) { if( s.Readings.humidity ) this.mappings.humidity = { reading: 'humidity' }; + if( s.Readings.luminosity ) + this.mappings.light = { reading: 'luminosity' }; + if( s.Readings.voc ) this.mappings.airquality = { reading: 'voc' }; if( s.Readings.motor ) this.mappings.motor = { reading: 'motor' }; + if( s.Readings.battery ) + this.mappings.battery = { reading: 'battery' }; + if( s.Readings.direction ) this.mappings.direction = { reading: 'direction' }; + if( s.Readings['D-firmware'] ) + this.mappings.firmware = { reading: 'D-firmware' }; - var genericType = s.Attributes.genericDeviceType; - if( !genericType ) - genericType = s.Attributes.genericDisplayType; if( genericType == 'switch' ) s.isSwitch = true; + else if( genericType == 'outlet' ) + s.isOutlet = true; + else if( genericType == 'garage' ) this.mappings.garage = { cmdOpen: 'on', cmdClose: 'off' }; @@ -512,46 +753,93 @@ FHEMAccessory(log, connection, s) { else if( genericType == 'blind' || s.Attributes.subType == 'blindActuator' ) { delete this.mappings.pct; - this.mappings.blind = { reading: 'pct', cmd: 'pct' }; + if( s.PossibleSets.match(/[\^ ]position\b/) ) + this.mappings.blind = { reading: 'position', cmd: 'position' }; + else + this.mappings.blind = { reading: 'pct', cmd: 'pct' }; } else if( genericType == 'window' - || s.Attributes.model == 'HM-SEC-WIN' ) { + || s.Attributes.model == 'HM-SEC-WIN' ) this.mappings.window = { reading: 'level', cmd: 'level' }; - } else if( genericType == 'lock' - || s.Attributes.model == 'HM-SEC-KEY' ) { - this.mappings.lock = { reading: 'lock' }; + else if( genericType == 'lock' + || ( s.Attributes.model && s.Attributes.model.match(/^HM-SEC-KEY/ ) ) ) { + this.mappings.lock = { reading: 'lock', cmdLock: 'lock', cmdUnlock: 'unlock', cmdOpen: 'open' }; + if( s.Internals.TYPE == 'dummy' ) + this.mappings.lock = { reading: 'lock', cmdLock: 'lock locked', cmdUnlock: 'lock unlocked', cmdOpen: 'open' }; } else if( genericType == 'thermostat' - || s.Attributes.subType == 'thermostat' ) { + || s.Attributes.subType == 'thermostat' ) s.isThermostat = true; - } else if( s.Internals.TYPE == 'CUL_FHTTK' ) { + else if( s.Internals.TYPE == 'CUL_FHTTK' ) this.mappings.contact = { reading: 'Window' }; - } else if( s.Attributes.subType == 'threeStateSensor' ) { + else if( s.Internals.TYPE == 'MAX' + && s.Internals.type == 'ShutterContact' ) + this.mappings.contact = { reading: 'state' }; + + else if( s.Attributes.subType == 'threeStateSensor' ) this.mappings.contact = { reading: 'contact' }; - } else if( s.Internals.TYPE == 'PRESENCE' ) + else if( s.Internals.TYPE == 'PRESENCE' ) this.mappings.occupancy = { reading: 'state' }; + else if( s.Internals.TYPE == 'ROOMMATE' ) + this.mappings.occupancy = { reading: 'presence' }; + else if( s.Attributes.model == 'fs20di' ) s.isLight = true; - if( s.PossibleSets.match(/[\^ ]desired-temp\b/) ) + //if( s.PossibleSets.match(/(^| )desired-temp\b/) ) { + if( match = s.PossibleSets.match(/(^| )desired-temp(:[^\d]*([^\$ ]*))?/) ) { this.mappings.thermostat = { reading: 'desired-temp', cmd: 'desired-temp' }; - else if( s.PossibleSets.match(/[\^ ]desiredTemperature\b/) ) + + if( s.Readings.controlMode ) + this.mappings.thermostat_mode = { reading: 'controlMode', cmd: 'controlMode' }; + + if( match[3] ) { + var values = match[3].split(','); + this.mappings.thermostat.min = parseFloat(values[0]); + this.mappings.thermostat.max = parseFloat(values[values.length-1]); + this.mappings.thermostat.step = values[1] - values[0]; + } + + //} else if( s.PossibleSets.match(/(^| )desiredTemperature\b/) ) { + } else if( match = s.PossibleSets.match(/(^| )desiredTemperature(:[^\d]*([^\$ ]*))?/) ) { this.mappings.thermostat = { reading: 'desiredTemperature', cmd: 'desiredTemperature' }; - else if( s.isThermostat ) { + if( s.Readings.mode ) + this.mappings.thermostat_mode = { reading: 'mode', cmd: 'desiredTemperature' }; + + if( match[3] ) { + var values = match[3].split(','); + this.mappings.thermostat.min = values[0]; + this.mappings.thermostat.max = values[values.length-2]; + this.mappings.thermostat.step = values[1] - values[0]; + } + + } else if( s.isThermostat ) { s.isThermostat = false; delete this.mappings.thermostat; log( s.Internals.NAME + ' is NOT a thermostat. set for target temperature missing' ); + } if( s.Internals.TYPE == 'SONOSPLAYER' ) //FIXME: use sets [Pp]lay/[Pp]ause/[Ss]top this.mappings.onOff = { reading: 'transportState', cmdOn: 'play', cmdOff: 'pause' }; - else if( s.PossibleSets.match(/[\^ ]on\b/) - && s.PossibleSets.match(/[\^ ]off\b/) ) { + + else if( s.Internals.TYPE == 'harmony' ) { + if( s.Internals.id != undefined ) { + if( s.Attributes.genericDeviceType ) + this.mappings.onOff = { reading: 'power', cmdOn: 'on', cmdOff: 'off' }; + else + return null; + + } else + this.mappings.onOff = { reading: 'activity', cmdOn: 'activity', cmdOff: 'off' }; + + } else if( s.PossibleSets.match(/(^| )on\b/) + && s.PossibleSets.match(/(^| )off\b/) ) { this.mappings.onOff = { reading: 'state', cmdOn: 'on', cmdOff: 'off' }; if( !s.Readings.state ) delete this.mappings.onOff.reading; @@ -582,7 +870,7 @@ FHEMAccessory(log, connection, s) { else if( this.mappings.blind ) log( s.Internals.NAME + ' is blind ['+ this.mappings.blind.reading +']' ); else if( this.mappings.thermostat ) - log( s.Internals.NAME + ' is thermostat ['+ this.mappings.thermostat.reading +']' ); + log( s.Internals.NAME + ' is thermostat ['+ this.mappings.thermostat.reading + ';' + this.mappings.thermostat.min + '-' + this.mappings.thermostat.max + ':' + this.mappings.thermostat.step +']' ); else if( this.mappings.contact ) log( s.Internals.NAME + ' is contact sensor [' + this.mappings.contact.reading +']' ); else if( this.mappings.occupancy ) @@ -595,6 +883,8 @@ FHEMAccessory(log, connection, s) { log( s.Internals.NAME + ' is dimable [0-'+ s.pctMax +']' ); else if( s.isLight ) log( s.Internals.NAME + ' is light' ); + else if( s.isOutlet ) + log( s.Internals.NAME + ' is outlet' ); else if( this.mappings.onOff || s.isSwitch ) log( s.Internals.NAME + ' is switchable' ); else if( !this.mappings ) @@ -604,19 +894,33 @@ FHEMAccessory(log, connection, s) { if( this.mappings.onOff ) log( s.Internals.NAME + ' has onOff [' + this.mappings.onOff.reading + ';' + this.mappings.onOff.cmdOn +',' + this.mappings.onOff.cmdOff + ']' ); if( this.mappings.hue ) - log( s.Internals.NAME + ' has hue [0-' + this.mappings.hue.max +']' ); + log( s.Internals.NAME + ' has hue [' + this.mappings.hue.reading + ';0-' + this.mappings.hue.max +']' ); if( this.mappings.sat ) - log( s.Internals.NAME + ' has sat [0-' + this.mappings.sat.max +']' ); + log( s.Internals.NAME + ' has sat [' + this.mappings.sat.reading + ';0-' + this.mappings.sat.max +']' ); + if( this.mappings.colormode ) + log( s.Internals.NAME + ' has colormode [' + this.mappings.colormode.reading +']' ); + if( this.mappings.xy ) + log( s.Internals.NAME + ' has xy [' + this.mappings.xy.reading +']' ); + if( this.mappings.thermostat_mode ) + log( s.Internals.NAME + ' has thermostat mode ['+ this.mappings.thermostat_mode.reading + ';' + this.mappings.thermostat_mode.cmd +']' ); if( this.mappings.temperature ) log( s.Internals.NAME + ' has temperature ['+ this.mappings.temperature.reading +']' ); if( this.mappings.humidity ) log( s.Internals.NAME + ' has humidity ['+ this.mappings.humidity.reading +']' ); + if( this.mappings.light ) + log( s.Internals.NAME + ' has light ['+ this.mappings.light.reading +']' ); if( this.mappings.airquality ) log( s.Internals.NAME + ' has voc ['+ this.mappings.airquality.reading +']' ); if( this.mappings.motor ) - log( s.Internals.NAME + ' has motor' ); + log( s.Internals.NAME + ' has motor ['+ this.mappings.motor.reading +']' ); + if( this.mappings.battery ) + log( s.Internals.NAME + ' has battery ['+ this.mappings.battery.reading +']' ); if( this.mappings.direction ) - log( s.Internals.NAME + ' has direction' ); + log( s.Internals.NAME + ' has direction ['+ this.mappings.direction.reading +']' ); + if( this.mappings.firmware ) + log( s.Internals.NAME + ' has firmware ['+ this.mappings.firmware.reading +']' ); + if( this.mappings.volume ) + log( s.Internals.NAME + ' has volume ['+ this.mappings.volume.reading + ':' + (this.mappings.volume.nocache ? 'not cached' : 'cached' ) +']' ); //log( util.inspect(s) ); @@ -631,49 +935,53 @@ FHEMAccessory(log, connection, s) { this.PossibleSets = s.PossibleSets; if( this.type == 'CUL_HM' ) { - this.serial = s.Internals.DEF; + this.serial = this.type + '.' + s.Internals.DEF; if( s.Attributes.serialNr ) this.serial = s.Attributes.serialNr; else if( s.Readings['D-serialNr'] && s.Readings['D-serialNr'].Value ) this.serial = s.Readings['D-serialNr'].Value; } else if( this.type == 'CUL_WS' ) - this.serial = s.Internals.DEF; + this.serial = this.type + '.' + s.Internals.DEF; else if( this.type == 'FS20' ) - this.serial = s.Internals.DEF; + this.serial = this.type + '.' + s.Internals.DEF; else if( this.type == 'IT' ) - this.serial = s.Internals.DEF; + this.serial = this.type + '.' + s.Internals.DEF; else if( this.type == 'HUEDevice' ) this.serial = s.Internals.uniqueid; else if( this.type == 'SONOSPLAYER' ) this.serial = s.Internals.UDN; + else if( this.type == 'EnOcean' ) + this.serial = this.type + '.' + s.Internals.DEF; + + this.uuid_base = this.serial; this.hasDim = s.hasDim; this.pctMax = s.pctMax; this.isLight = s.isLight; this.isSwitch = s.isSwitch; + this.isOutlet = s.isOutlet; //log( util.inspect(s.Readings) ); if( this.mappings.blind || this.mappings.door || this.mappings.garage || this.mappings.window || this.mappings.thermostat ) delete this.mappings.onOff; - var that = this; Object.keys(this.mappings).forEach(function(key) { - var reading = that.mappings[key].reading; + var reading = this.mappings[key].reading; if( s.Readings[reading] && s.Readings[reading].Value ) { var value = s.Readings[reading].Value; - value = that.reading2homekit(reading, value); + value = this.reading2homekit(reading, value); if( value != undefined ) { - var inform_id = that.device +'-'+ reading; - that.mappings[key].informId = inform_id; + var inform_id = this.device +'-'+ reading; + this.mappings[key].informId = inform_id; - if( !that.mappings[key].nocache ) + if( !this.mappings[key].nocache ) FHEM_cached[inform_id] = value; } } - } ); + }.bind(this) ); this.log = log; this.connection = connection; @@ -687,14 +995,17 @@ FHEMAccessory.prototype = { return undefined; if( reading == 'hue' ) { - value = Math.round(value * 360 / this.mappings.hue ? this.mappings.hue.max : 360); + value = Math.round(value * 360 / (this.mappings.hue ? this.mappings.hue.max : 360) ); } else if( reading == 'sat' ) { - value = Math.round(value * 100 / this.mappings.sat ? this.mappings.sat.max : 100); + value = Math.round(value * 100 / (this.mappings.sat ? this.mappings.sat.max : 100) ); } else if( reading == 'pct' ) { value = parseInt( value ); + } else if( reading == 'position' ) { + value = parseInt( value ); + } else if(reading == 'motor') { if( value.match(/^up/)) value = Characteristic.PositionState.INCREASING; @@ -703,6 +1014,20 @@ FHEMAccessory.prototype = { else value = Characteristic.PositionState.STOPPED; + } else if(reading == 'controlMode') { + if( value.match(/^auto/)) + value = Characteristic.TargetHeatingCoolingState.AUTO; + else if( value.match(/^manu/)) + value = Characteristic.TargetHeatingCoolingState.HEAT; + else + value = Characteristic.TargetHeatingCoolingState.OFF; + + } else if(reading == 'mode') { + if( value.match(/^auto/)) + value = Characteristic.TargetHeatingCoolingState.AUTO; + else + value = Characteristic.TargetHeatingCoolingState.HEAT; + } else if(reading == 'direction') { if( value.match(/^opening/)) value = PositionState.INCREASING; @@ -747,9 +1072,22 @@ FHEMAccessory.prototype = { || reading == 'desiredTemperature' ) { value = parseFloat( value ); + if( this.mappings.thermostat + && reading == this.mappings.thermostat.reading ) { + if( value < this.mappings.thermostat.min ) + value = this.mappings.thermostat.min; + else if( value > this.mappings.thermostat.max ) + value = this.mappings.thermostat.min; + + value = Math.round(value / this.mappings.thermostat.step) * this.mappings.thermostat.step; + } + } else if( reading == 'humidity' ) { value = parseInt( value ); + } else if( reading == 'luminosity' ) { + value = parseFloat( value ) / 0.265; + } else if( reading == 'voc' ) { value = parseInt( value ); if( value > 1500 ) @@ -765,6 +1103,18 @@ FHEMAccessory.prototype = { else Characteristic.AirQuality.UNKNOWN; + } else if( reading == 'battery' ) { + if( value == 'ok' ) + value = Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + else + value = Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + + } else if( reading == 'presence' ) { + if( value == 'present' ) + value = Characteristic.OccupancyDetected.OCCUPANCY_DETECTED; + else + value = Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED; + } else if( reading == 'state' ) { if( value.match(/^set-/ ) ) return undefined; @@ -777,13 +1127,21 @@ FHEMAccessory.prototype = { if( value == 'off' ) value = 0; + else if( value == 'opened' ) + value = Characteristic.ContactSensorState.CONTACT_NOT_DETECTED; + else if( value == 'closed' ) + value = Characteristic.ContactSensorState.CONTACT_DETECTED; else if( value == 'present' ) value = Characteristic.OccupancyDetected.OCCUPANCY_DETECTED; else if( value == 'absent' ) value = Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED; + else if( value == 'locked' ) + value = Characteristic.LockCurrentState.SECURED; + else if( value == 'unlocked' ) + value = Characteristic.LockCurrentState.UNSECURED; else if( value == '000000' ) value = 0; - else if( value.match( /^[A-D]0$/ ) ) //FIXME: is handled by event_map now + else if( value.match( /^[A-D]0$/ ) ) //FIXME: not necessary any more. handled by event_map now. value = 0; else value = 1; @@ -801,8 +1159,8 @@ FHEMAccessory.prototype = { } this.log(this.name + " delaying command " + c + " with value " + value); - var that = this; - this.delayed[c] = setTimeout( function(){clearTimeout(that.delayed[c]);that.command(c,value)}, delay?delay:1000 ); + this.delayed[c] = setTimeout( function(){clearTimeout(this.delayed[c]); this.command(c,value);}.bind(this), + delay?delay:1000 ); }, command: function(c,value) { @@ -868,6 +1226,27 @@ FHEMAccessory.prototype = { } else if( c == 'targetTemperature' ) { cmd = "set " + this.device + " " + this.mappings.thermostat.cmd + " " + value; + } else if( c == 'targetMode' ) { + var set = this.mappings.thermostat_mode.cmd; + if( value == Characteristic.TargetHeatingCoolingState.OFF ) { + value = 'off' + if( this.mappings.thermostat_mode.cmd == 'controlMode' ) + set = 'desired-temp'; + + } else if( value == Characteristic.TargetHeatingCoolingState.AUTO ) { + value = 'auto' + + }else { + if( this.mappings.thermostat_mode == 'controlMode' ) + value = 'manu'; + else { + value = FHEM_cached[this.mappings.thermostat.informId]; + set = 'desired-temp'; + } + + } + cmd = "set " + this.device + " " + set + " " + value; + } else if( c == 'targetPosition' ) { if( this.mappings.window ) { if( value == 0 ) @@ -890,29 +1269,7 @@ FHEMAccessory.prototype = { this.execute(cmd); }, - execute: function(cmd,callback) { - var url = encodeURI( this.connection.base_url + "/fhem?cmd=" + cmd + "&XHR=1"); - this.log( ' executing: ' + url ); - - var that = this; - this.connection.request.get( { url: url, gzip: true }, - function(err, response, result) { - if( !err && response.statusCode == 200 ) { - if( callback ) - callback( result ); - - } else { - that.log("There was a problem connecting to FHEM ("+ url +")."); - if( response ) - that.log( " " + response.statusCode + ": " + response.statusMessage ); - - } - - } ).on( 'error', function(err) { - that.log("There was a problem connecting to FHEM ("+ url +"):"+ err); - - } ); - }, + execute: function(cmd,callback) {FHEM_execute(this.log, this.connection, cmd, callback)}, query: function(reading, callback) { if( reading == undefined ) { @@ -950,17 +1307,16 @@ FHEMAccessory.prototype = { query_reading = 'state'; } else if( reading == 'lock' && this.mappings.lock ) { - query_reading = 'state'; + //query_reading = 'state'; } var cmd = '{ReadingsVal("'+this.device+'","'+query_reading+'","")}'; - var that = this; this.execute( cmd, function(result) { value = result.replace(/[\r\n]/g, ""); - that.log(" value: " + value); + this.log(" value: " + value); if( value == undefined ) return value; @@ -996,28 +1352,28 @@ FHEMAccessory.prototype = { else value = Characteristic.LockCurrentState.UNSECURED; - } else if(reading == 'hue' && query_reading == that.mappings.rgb) { - //FHEM_update( that.device+'-'+query_reading, value ); + } else if(reading == 'hue' && query_reading == this.mappings.rgb) { + //FHEM_update( this.device+'-'+query_reading, value ); value = parseInt( FHEM_rgb2hsv(value)[0] * 360 ); - } else if(reading == 'sat' && query_reading == that.mappings.rgb) { - //FHEM_update( that.device+'-'+query_reading, value ); + } else if(reading == 'sat' && query_reading == this.mappings.rgb) { + //FHEM_update( this.device+'-'+query_reading, value ); value = parseInt( FHEM_rgb2hsv(value)[1] * 100 ); - } else if(reading == 'bri' && query_reading == that.mappings.rgb) { - //FHEM_update( that.device+'-'+query_reading, value ); + } else if(reading == 'bri' && query_reading == this.mappings.rgb) { + //FHEM_update( this.device+'-'+query_reading, value ); value = parseInt( FHEM_rgb2hsv(value)[2] * 100 ); } } else { - value = that.reading2homekit(reading, value); + value = this.reading2homekit(reading, value); } - that.log(" mapped: " + value); - FHEM_update( that.device + '-' + reading, value, true ); + this.log(" mapped: " + value); + FHEM_update( this.device + '-' + reading, value, true ); if( callback != undefined ) { if( value == undefined ) @@ -1028,18 +1384,28 @@ FHEMAccessory.prototype = { return value ; - } ); + }.bind(this) ); }, - createDeviceService: function() { - var name = this.alias + 'xxx'; + createDeviceService: function(subtype) { + //var name = this.alias + ' (' + this.name + ')'; + var name = this.alias; + if( subtype ) + //name = subtype + ' (' + this.name + ')'; + name = subtype + ' (' + this.alias + ')'; if( this.isSwitch ) { this.log(" switch service for " + this.name) return new Service.Switch(name); + } else if( this.isOutlet ) { + this.log(" outlet service for " + this.name) + return new Service.Outlet(name); } else if( this.mappings.garage ) { this.log(" garage door opener service for " + this.name) return new Service.GarageDoorOpener(name); + } else if( this.mappings.lock ) { + this.log(" lock mechanism service for " + this.name) + return new Service.LockMechanism(name); } else if( this.mappings.window ) { this.log(" window service for " + this.name) return new Service.Window(name); @@ -1064,54 +1430,143 @@ FHEMAccessory.prototype = { } else if( this.mappings.humidity ) { this.log(" humidity sensor service for " + this.name) return new Service.HumiditySensor(name); + } else if( this.mappings.light ) { + this.log(" light sensor service for " + this.name) + return new Service.LightSensor(name); } else if( this.mappings.airquality ) { - this.log(" humidity sensor service for " + this.name) + this.log(" air quality sensor service for " + this.name) return new Service.AirQualitySensor(name); } - this.log(" switch service for " + this.name) - return new Service.Switch(name); + this.log(" switch service for " + this.name + ' (' + subtype + ')' ) + return new Service.Switch(name, subtype); }, identify: function(callback) { this.log('['+this.name+'] identify requested!'); - if( match = this.PossibleSets.match(/[\^ ]toggle\b/) ) { + if( match = this.PossibleSets.match(/(^| )toggle\b/) ) { this.command( 'identify' ); } callback(); }, getServices: function() { + var services = []; + this.log("creating services for " + this.name) this.log(" information service for " + this.name) var informationService = new Service.AccessoryInformation(); + services.push( informationService ); informationService .setCharacteristic(Characteristic.Manufacturer, "FHEM:"+this.type) - .setCharacteristic(Characteristic.Model, "FHEM:"+this.model ? this.model : '') + .setCharacteristic(Characteristic.Model, "FHEM:"+ (this.model ? this.model : '') ) .setCharacteristic(Characteristic.SerialNumber, this.serial ? this.serial : ''); - var controlService = this.createDeviceService(); - var that = this; + if( this.mappings.firmware ) { + this.log(" firmware revision characteristic for " + this.name) + + var characteristic = informationService.getCharacteristic(Characteristic.FirmwareRevision) + || informationService.addCharacteristic(Characteristic.FirmwareRevision); + + FHEM_subscribe(characteristic, this.mappings.firmware.informId, this); + + characteristic.value = FHEM_cached[this.mappings.firmware.informId]; + + characteristic + .on('get', function(callback) { + if( this.mappings.firmware ) + this.query(this.mappings.firmware.reading, callback); + }.bind(this) ); + } + + + // FIXME: allow multiple switch characteristics also for other types. check if this.mappings.onOff an array. + if( this.type == 'harmony' + && this.mappings.onOff.reading == 'activity' ) { + + FHEM_subscribe(undefined, this.mappings.onOff.informId, this); + + var match; + if( match = this.PossibleSets.match(/(^| )activity:([^\s]*)/) ) { + var activities = match[2].split(','); + for( var i = 0; i < activities.length; i++ ) { + var activity = activities[i]; + + var controlService = this.createDeviceService(activity); + services.push( controlService ); + + this.log(" on characteristic for " + this.name + ' ' + activity); + + var characteristic = controlService.getCharacteristic(Characteristic.On); + + FHEM_subscribe(characteristic, '#' + this.device + '-' + this.mappings.onOff.reading + '-' + activity, this); + + characteristic.displayName = activity; + characteristic.value = (FHEM_cached[this.mappings.onOff.informId]==activity?1:0); + + characteristic + .on('set', function(activity, value, callback, context) { + if( context !== 'fromFHEM' ) + this.command( 'set', value == 0 ? this.mappings.onOff.cmdOff : this.mappings.onOff.cmdOn + ' ' + activity ); + callback(); + }.bind(this, activity) ) + .on('get', function(activity, callback) { + var result = this.query(this.mappings.onOff.reading); + callback( undefined, result==activity?1:0 ); + }.bind(this, activity) ); + } + } + + return services; + } + + if( this.mappings.xy + && this.mappings.colormode ) { + FHEM_subscribe(undefined, this.mappings.xy.informId, this); + FHEM_subscribe(undefined, this.mappings.colormode.informId, this); + + + //FIXME: add colormode ct + if( FHEM_cached[this.mappings.colormode.informId] == 'xy' ) { + var value = FHEM_cached[this.mappings.xy.informId]; + var xy = value.split(','); + var rgb = FHEM_xyY2rgb(xy[0], xy[1] , 1); + var hsv = FHEM_rgb2hsv(rgb); + var hue = parseInt( hsv[0] * 360 ); + var sat = parseInt( hsv[1] * 100 ); + var bri = parseInt( hsv[2] * 100 ); + + //FHEM_update( device+'-'+reading, value, false ); + FHEM_update( this.device+'-hue', hue ); + FHEM_update( this.device+'-sat', sat ); + FHEM_update( this.device+'-bri', bri ); + } + } + + var controlService = this.createDeviceService(); + services.push( controlService ); + if( this.mappings.onOff ) { - this.log(" power characteristic for " + this.name) + this.log(" on characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.On); - FHEM_subscribe(characteristic, that.mappings.onOff.informId, that); - if( FHEM_cached[that.mappings.onOff.informId] != undefined ) - characteristic.value = FHEM_cached[that.mappings.onOff.informId]; + FHEM_subscribe(characteristic, this.mappings.onOff.informId, this); + + if( FHEM_cached[this.mappings.onOff.informId] != undefined ) + characteristic.value = FHEM_cached[this.mappings.onOff.informId]; characteristic .on('set', function(value, callback, context) { - if( context !== 'fromFhem' ) - that.command( 'set', value == 0 ? that.mappings.onOff.cmdOff : that.mappings.onOff.cmdOn ); + if( context !== 'fromFHEM' ) + this.command( 'set', value == 0 ? this.mappings.onOff.cmdOff : this.mappings.onOff.cmdOn ); callback(); }.bind(this) ) .on('get', function(callback) { - that.query(that.mappings.onOff.reading, callback); + this.query(this.mappings.onOff.reading, callback); }.bind(this) ); } @@ -1120,18 +1575,18 @@ FHEMAccessory.prototype = { var characteristic = controlService.addCharacteristic(Characteristic.Brightness); - FHEM_subscribe(characteristic, that.mappings.pct.informId, that); - if( FHEM_cached[that.mappings.pct.informId] != undefined ) - characteristic.value = FHEM_cached[that.mappings.pct.informId]; + FHEM_subscribe(characteristic, this.mappings.pct.informId, this); + if( FHEM_cached[this.mappings.pct.informId] != undefined ) + characteristic.value = FHEM_cached[this.mappings.pct.informId]; characteristic .on('set', function(value, callback, context) { - if( context !== 'fromFhem' ) - that.command('pct', value); + if( context !== 'fromFHEM' ) + this.command('pct', value); callback(); }.bind(this) ) .on('get', function(callback) { - that.query(that.mappings.pct.reading, callback); + this.query(this.mappings.pct.reading, callback); }.bind(this) ); } else if( this.hasDim ) { @@ -1139,39 +1594,38 @@ FHEMAccessory.prototype = { var characteristic = controlService.addCharacteristic(Characteristic.Brightness); - FHEM_subscribe(characteristic, that.name+'-pct', that); + FHEM_subscribe(characteristic, this.name+'-pct', this); characteristic.value = 0; characteristic.maximumValue = this.pctMax; characteristic .on('set', function(value, callback, context) { - if( context !== 'fromFhem' ) - that.delayed('dim', value); + if( context !== 'fromFHEM' ) + this.delayed('dim', value); callback(); }.bind(this) ) .on('get', function(callback) { - that.query('pct', callback); + this.query('pct', callback); }.bind(this) ); - } - if( that.mappings.hue ) { + if( this.mappings.hue ) { this.log(" hue characteristic for " + this.name) var characteristic = controlService.addCharacteristic(Characteristic.Hue); - FHEM_subscribe(characteristic, that.mappings.hue.informId, that); - if( FHEM_cached[that.mappings.hue.informId] != undefined ) - characteristic.value = FHEM_cached[that.mappings.hue.informId]; + FHEM_subscribe(characteristic, this.mappings.hue.informId, this); + if( FHEM_cached[this.mappings.hue.informId] != undefined ) + characteristic.value = FHEM_cached[this.mappings.hue.informId]; characteristic .on('set', function(value, callback, context) { - if( context !== 'fromFhem' ) - that.command('hue', value); + if( context !== 'fromFHEM' ) + this.command('hue', value); callback(); }.bind(this) ) .on('get', function(callback) { - that.query(that.mappings.hue.reading, callback); + this.query(this.mappings.hue.reading, callback); }.bind(this) ); } else if( this.mappings.rgb ) { @@ -1179,18 +1633,18 @@ FHEMAccessory.prototype = { var characteristic = controlService.addCharacteristic(Characteristic.Hue); - FHEM_subscribe(characteristic, that.name+'-hue', that); - FHEM_subscribe(characteristic, that.mappings.rgb.informId, that); + FHEM_subscribe(characteristic, this.name+'-hue', this); + FHEM_subscribe(characteristic, this.mappings.rgb.informId, this); characteristic.value = 0; characteristic .on('set', function(value, callback, context) { - if( context !== 'fromFhem' ) - that.command('H-rgb', value); + if( context !== 'fromFHEM' ) + this.command('H-rgb', value); callback(); }.bind(this) ) .on('get', function(callback) { - that.query('hue', callback); + this.query('hue', callback); }.bind(this) ); if( !this.mappings.sat ) { @@ -1198,17 +1652,17 @@ FHEMAccessory.prototype = { var characteristic = controlService.addCharacteristic(Characteristic.Saturation); - FHEM_subscribe(characteristic, that.name+'-sat', that); + FHEM_subscribe(characteristic, this.name+'-sat', this); characteristic.value = 100; characteristic .on('set', function(value, callback, context) { - if( context !== 'fromFhem' ) - that.command('S-rgb', value); + if( context !== 'fromFHEM' ) + this.command('S-rgb', value); callback(); }.bind(this) ) .on('get', function(callback) { - that.query('sat', callback); + this.query('sat', callback); }.bind(this) ); } @@ -1217,20 +1671,19 @@ FHEMAccessory.prototype = { var characteristic = controlService.addCharacteristic(Characteristic.Brightness); - FHEM_subscribe(characteristic, that.name+'-bri', that); + FHEM_subscribe(characteristic, this.name+'-bri', this); characteristic.value = 0; characteristic .on('set', function(value, callback, context) { - if( context !== 'fromFhem' ) - that.command('B-rgb', value); + if( context !== 'fromFHEM' ) + this.command('B-rgb', value); callback(); }.bind(this) ) .on('get', function(callback) { - that.query('bri', callback); + this.query('bri', callback); }.bind(this) ); } - } if( this.mappings.sat ) { @@ -1238,18 +1691,18 @@ FHEMAccessory.prototype = { var characteristic = controlService.addCharacteristic(Characteristic.Saturation); - FHEM_subscribe(characteristic, that.mappings.sat.informId, that); - if( FHEM_cached[that.mappings.sat.informId] != undefined ) - characteristic.value = FHEM_cached[that.mappings.sat.informId]; + FHEM_subscribe(characteristic, this.mappings.sat.informId, this); + if( FHEM_cached[this.mappings.sat.informId] != undefined ) + characteristic.value = FHEM_cached[this.mappings.sat.informId]; characteristic .on('set', function(value, callback, context) { - if( context !== 'fromFhem' ) - that.command('sat', value); + if( context !== 'fromFHEM' ) + this.command('sat', value); callback(); }.bind(this) ) .on('get', function(callback) { - that.query(that.mappings.sat.reading, callback); + this.query(this.mappings.sat.reading, callback); }.bind(this) ); } @@ -1259,32 +1712,35 @@ FHEMAccessory.prototype = { var characteristic = new Characteristic('Volume', '00000027-0000-1000-8000-0026BB765291'); // FIXME!!! controlService.addCharacteristic(characteristic); - if( !that.mappings.volume.nocache ) { - FHEM_subscribe(characteristic, that.mappings.volume.informId, that); - characteristic.value = FHEM_cached[that.mappings.volume.informId]; + if( !this.mappings.volume.nocache ) { + FHEM_subscribe(characteristic, this.mappings.volume.informId, this); + characteristic.value = FHEM_cached[this.mappings.volume.informId]; } else { characteristic.value = 10; } - characteristic.format = 'uint8'; - characteristic.unit = 'percentage'; - characteristic.maximumValue = 100; - characteristic.minimumValue = 0; - characteristic.stepValue = 1; + characteristic.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] + }); + characteristic.readable = true; characteristic.writable = true; characteristic.supportsEventNotification = true; characteristic .on('set', function(value, callback, context) { - if( context !== 'fromFhem' ) - that.delayed('volume', value); + if( context !== 'fromFHEM' ) + this.delayed('volume', value); callback(); }.bind(this) ) .on('get', function(callback) { - that.query(that.mappings.volume.reading, callback); + this.query(this.mappings.volume.reading, callback); }.bind(this) ); - } if( this.mappings.blind ) { @@ -1292,29 +1748,33 @@ FHEMAccessory.prototype = { var characteristic = controlService.getCharacteristic(Characteristic.CurrentPosition); - FHEM_subscribe(characteristic, that.mappings.blind.informId, that); - characteristic.value = FHEM_cached[that.mappings.blind.informId]; + var step = 1; + FHEM_subscribe(characteristic, this.mappings.blind.informId, this); + characteristic.value = Math.round(FHEM_cached[this.mappings.blind.informId] / step) * step; characteristic .on('get', function(callback) { - that.query(that.mappings.blind.reading, callback); + this.query(this.mappings.blind.reading, callback); }.bind(this) ); this.log(" target position characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.TargetPosition); + characteristic.setProps( { + minStep: step, + } ); - characteristic.value = FHEM_cached[that.mappings.blind.informId]; + characteristic.value = FHEM_cached[this.mappings.blind.informId]; characteristic .on('set', function(value, callback, context) { - if( context !== 'fromFhem' ) - that.delayed('targetPosition', value, 1500); + if( context !== 'fromFHEM' ) + this.delayed('targetPosition', value, 1500); callback(); }.bind(this) ) .on('get', function(callback) { - that.query(that.mappings.blind.reading, callback); + this.query(this.mappings.blind.reading, callback); }.bind(this) ); @@ -1322,16 +1782,15 @@ FHEMAccessory.prototype = { var characteristic = controlService.getCharacteristic(Characteristic.PositionState); - if( that.mappings.motor ) - FHEM_subscribe(characteristic, that.mappings.motor.informId, that); - characteristic.value = that.mappings.motor?FHEM_cached[that.mappings.motor.informId]:Characteristic.PositionState.STOPPED; + if( this.mappings.motor ) + FHEM_subscribe(characteristic, this.mappings.motor.informId, this); + characteristic.value = this.mappings.motor?FHEM_cached[this.mappings.motor.informId]:Characteristic.PositionState.STOPPED; characteristic .on('get', function(callback) { - if( that.mappings.motor ) - that.query(that.mappings.motor.reading, callback); + if( this.mappings.motor ) + this.query(this.mappings.motor.reading, callback); }.bind(this) ); - } if( this.mappings.window ) { @@ -1339,13 +1798,13 @@ FHEMAccessory.prototype = { var characteristic = controlService.getCharacteristic(Characteristic.CurrentPosition); - FHEM_subscribe(characteristic, that.name+'-state', that); - FHEM_subscribe(characteristic, that.mappings.window.informId, that); - characteristic.value = FHEM_cached[that.mappings.window.informId]; + FHEM_subscribe(characteristic, this.name+'-state', this); + FHEM_subscribe(characteristic, this.mappings.window.informId, this); + characteristic.value = FHEM_cached[this.mappings.window.informId]; characteristic .on('get', function(callback) { - that.query(that.mappings.window.reading, callback); + this.query(this.mappings.window.reading, callback); }.bind(this) ); @@ -1353,16 +1812,16 @@ FHEMAccessory.prototype = { var characteristic = controlService.getCharacteristic(Characteristic.TargetPosition); - characteristic.value = FHEM_cached[that.mappings.window.informId]; + characteristic.value = FHEM_cached[this.mappings.window.informId]; characteristic .on('set', function(value, callback, context) { - if( context !== 'fromFhem' ) - that.delayed('targetPosition', value, 1500); + if( context !== 'fromFHEM' ) + this.delayed('targetPosition', value, 1500); callback(); }.bind(this) ) .on('get', function(callback) { - that.query(that.mappings.window.reading, callback); + this.query(this.mappings.window.reading, callback); }.bind(this) ); @@ -1370,16 +1829,66 @@ FHEMAccessory.prototype = { var characteristic = controlService.getCharacteristic(Characteristic.PositionState); - if( that.mappings.direction ) - FHEM_subscribe(characteristic, that.mappings.direction.informId, that); - characteristic.value = that.mappings.direction?FHEM_cached[that.mappings.direction.informId]:Characteristic.PositionState.STOPPED; + if( this.mappings.direction ) + FHEM_subscribe(characteristic, this.mappings.direction.informId, this); + characteristic.value = this.mappings.direction?FHEM_cached[this.mappings.direction.informId]:Characteristic.PositionState.STOPPED; characteristic .on('get', function(callback) { - if( that.mappings.direction ) - that.query(that.mappings.direction.reading, callback); + if( this.mappings.direction ) + this.query(this.mappings.direction.reading, callback); + }.bind(this) ); + } + + if( this.mappings.lock ) { + this.log(" lock current state characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.LockCurrentState); + + //FHEM_subscribe(characteristic, this.name+'-state', this); + FHEM_subscribe(characteristic, this.mappings.lock.informId, this); + characteristic.value = FHEM_cached[this.mappings.lock.informId]; + + characteristic + .on('get', function(callback) { + this.query(this.mappings.lock.reading, callback); }.bind(this) ); + this.log(" lock target state characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.LockTargetState); + + characteristic.value = FHEM_cached[this.mappings.lock.informId]; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFHEM' ) + this.command( 'set', value == Characteristic.LockTargetState.UNSECURED ? this.mappings.lock.cmdUnlock : this.mappings.lock.cmdLock ); + callback(); + }.bind(this) ) + .on('get', function(callback) { + this.query(this.mappings.lock.reading, callback); + }.bind(this) ); + + if( this.mappings.lock.cmdOpen ) { + this.log(" target door state characteristic for " + this.name) + + var characteristic = controlService.addCharacteristic(Characteristic.TargetDoorState); + + characteristic.value = Characteristic.TargetDoorState.CLOSED; + + characteristic + .on('set', function(characteristic,value, callback, context) { + if( context !== 'fromFHEM' ) { + this.command( 'set', this.mappings.lock.cmdOpen ); + setTimeout( function(){characteristic.setValue(Characteristic.TargetDoorState.CLOSED, undefined, 'fromFHEM');}, 500 ); + } + if( callback ) callback(); + }.bind(this,characteristic) ) + .on('get', function(callback) { + callback(undefined,Characteristic.TargetDoorState.CLOSED); + }.bind(this) ); + } } if( this.mappings.garage ) { @@ -1403,8 +1912,8 @@ FHEMAccessory.prototype = { characteristic .on('set', function(value, callback, context) { - if( context !== 'fromFhem' ) - that.command( 'set', value == 0 ? that.mappings.garage.cmdOpen : that.mappings.garage.cmdClose ); + if( context !== 'fromFHEM' ) + this.command( 'set', value == 0 ? this.mappings.garage.cmdOpen : this.mappings.garage.cmdClose ); callback(); }.bind(this) ) .on('get', function(callback) { @@ -1412,18 +1921,19 @@ FHEMAccessory.prototype = { }.bind(this) ); + if( 0 ) { this.log(" obstruction detected characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.ObstructionDetected); - //FHEM_subscribe(characteristic, that.mappings.direction.informId, that); + //FHEM_subscribe(characteristic, this.mappings.direction.informId, this); characteristic.value = 0; characteristic .on('get', function(callback) { callback(undefined,1); }.bind(this) ); - + } } if( this.mappings.temperature ) { @@ -1432,14 +1942,13 @@ FHEMAccessory.prototype = { var characteristic = controlService.getCharacteristic(Characteristic.CurrentTemperature) || controlService.addCharacteristic(Characteristic.CurrentTemperature); - FHEM_subscribe(characteristic, that.mappings.temperature.informId, that); - characteristic.value = FHEM_cached[that.mappings.temperature.informId]; + FHEM_subscribe(characteristic, this.mappings.temperature.informId, this); + characteristic.value = FHEM_cached[this.mappings.temperature.informId]; characteristic .on('get', function(callback) { - that.query(that.mappings.temperature.reading, callback); + this.query(this.mappings.temperature.reading, callback); }.bind(this) ); - } if( this.mappings.humidity ) { @@ -1448,14 +1957,28 @@ FHEMAccessory.prototype = { var characteristic = controlService.getCharacteristic(Characteristic.CurrentRelativeHumidity) || controlService.addCharacteristic(Characteristic.CurrentRelativeHumidity); - FHEM_subscribe(characteristic, that.mappings.humidity.informId, that); - characteristic.value = FHEM_cached[that.mappings.humidity.informId]; + FHEM_subscribe(characteristic, this.mappings.humidity.informId, this); + characteristic.value = FHEM_cached[this.mappings.humidity.informId]; characteristic .on('get', function(callback) { - that.query(that.mappings.humidity.reading, callback); + this.query(this.mappings.humidity.reading, callback); }.bind(this) ); + } + if( this.mappings.light ) { + this.log(" light characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.CurrentAmbientLightLevel) + || controlService.addCharacteristic(Characteristic.CurrentAmbientLightLevel); + + FHEM_subscribe(characteristic, this.mappings.light.informId, this); + characteristic.value = FHEM_cached[this.mappings.light.informId]; + + characteristic + .on('get', function(callback) { + this.query(this.mappings.light.reading, callback); + }.bind(this) ); } if( this.mappings.airquality ) { @@ -1464,35 +1987,87 @@ FHEMAccessory.prototype = { var characteristic = controlService.getCharacteristic(Characteristic.AirQuality) || controlService.addCharacteristic(Characteristic.AirQuality); - FHEM_subscribe(characteristic, that.mappings.airquality.informId, that); - characteristic.value = FHEM_cached[that.mappings.airquality.informId]; + FHEM_subscribe(characteristic, this.mappings.airquality.informId, this); + characteristic.value = FHEM_cached[this.mappings.airquality.informId]; characteristic .on('get', function(callback) { - that.query(that.mappings.airquality.reading, callback); + this.query(this.mappings.airquality.reading, callback); + }.bind(this) ); + } + + if( this.mappings.battery ) { + this.log(" battery status characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.StatusLowBattery) + || controlService.addCharacteristic(Characteristic.StatusLowBattery); + + FHEM_subscribe(characteristic, this.mappings.battery.informId, this); + characteristic.value = FHEM_cached[this.mappings.battery.informId]; + + characteristic + .on('get', function(callback) { + this.query(this.mappings.battery.reading, callback); }.bind(this) ); } - //FIXME: parse range and set designedMinValue & designedMaxValue & designedMinStep if( this.mappings.thermostat ) { this.log(" target temperature characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.TargetTemperature); - FHEM_subscribe(characteristic, that.mappings.thermostat.informId, that); - characteristic.value = FHEM_cached[that.mappings.thermostat.informId]; + FHEM_subscribe(characteristic, this.mappings.thermostat.informId, this); + characteristic.value = FHEM_cached[this.mappings.thermostat.informId]; + + characteristic.setProps( { + maxValue: this.mappings.thermostat.max, + minValue: this.mappings.thermostat.min, + minStep: this.mappings.thermostat.step, + } ); characteristic .on('set', function(value, callback, context) { - if( context !== 'fromFhem' ) - that.delayed('targetTemperature', value, 1500); + if( context !== 'fromFHEM' ) + this.delayed('targetTemperature', value, 1500); callback(); }.bind(this) ) .on('get', function(callback) { - that.query(that.mappings.thermostat.reading, callback); + this.query(this.mappings.thermostat.reading, callback); }.bind(this) ); + if( this.mappings.thermostat_modex ) { + this.log(" current mode characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.CurrentHeatingCoolingState); + + FHEM_subscribe(characteristic, this.mappings.thermostat_mode.informId, this); + characteristic.value = FHEM_cached[this.mappings.thermostat_mode.informId]; + + characteristic + .on('get', function(callback) { + this.query(this.mappings.thermostat_mode.reading, callback); + }.bind(this) ); + } + + if( this.mappings.thermostat_modex ) { + this.log(" target mode characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.TargetHeatingCoolingState); + + FHEM_subscribe(characteristic, this.mappings.thermostat_mode.informId, this); + characteristic.value = FHEM_cached[this.mappings.thermostat_mode.informId]; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFHEM' ) + this.command('targetMode', value); + callback(); + }.bind(this) ) + .on('get', function(callback) { + this.query(this.mappings.thermostat_mode.reading, callback); + }.bind(this) ); + } } if( this.mappings.contact ) { @@ -1500,14 +2075,26 @@ FHEMAccessory.prototype = { var characteristic = controlService.getCharacteristic(Characteristic.ContactSensorState); - FHEM_subscribe(characteristic, that.mappings.contact.informId, that); - characteristic.value = FHEM_cached[that.mappings.contact.informId]; + FHEM_subscribe(characteristic, this.mappings.contact.informId, this); + characteristic.value = FHEM_cached[this.mappings.contact.informId]; characteristic .on('get', function(callback) { - that.query(that.mappings.contact.reading, callback); + this.query(this.mappings.contact.reading, callback); }.bind(this) ); + if( 1 ) { + this.log(" current door state characteristic for " + this.name) + + var characteristic = controlService.addCharacteristic(Characteristic.CurrentDoorState); + + characteristic.value = FHEM_cached[this.mappings.contact.informId]==Characteristic.ContactSensorState.CONTACT_DETECTED?Characteristic.CurrentDoorState.CLOSED:Characteristic.CurrentDoorState.OPEN; + + characteristic + .on('get', function(callback) { + callback(undefined, FHEM_cached[this.mappings.contact.informId]==Characteristic.ContactSensorState.CONTACT_DETECTED?Characteristic.CurrentDoorState.CLOSED:Characteristic.CurrentDoorState.OPEN); + }.bind(this) ); + } } if( this.mappings.occupancy ) { @@ -1515,17 +2102,16 @@ FHEMAccessory.prototype = { var characteristic = controlService.getCharacteristic(Characteristic.OccupancyDetected); - FHEM_subscribe(characteristic, that.mappings.occupancy.informId, that); - characteristic.value = FHEM_cached[that.mappings.occupancy.informId]; + FHEM_subscribe(characteristic, this.mappings.occupancy.informId, this); + characteristic.value = FHEM_cached[this.mappings.occupancy.informId]; characteristic .on('get', function(callback) { - that.query(that.mappings.occupancy.reading, callback); + this.query(this.mappings.occupancy.reading, callback); }.bind(this) ); - } - return [informationService, controlService]; + return services; } }; @@ -1546,7 +2132,10 @@ function FHEMdebug_handleRequest(request, response){ if( request.url == "/cached" ) { response.write( "home

" ); if( FHEM_lastEventTime ) - response.write( "FHEM_lastEventTime: "+ new Date(FHEM_lastEventTime) +"

" ); + var keys = Object.keys(FHEM_lastEventTime); + for( var i = 0; i < keys.length; i++ ) + response.write( "FHEM_lastEventTime " + keys[i] + ": "+ new Date(FHEM_lastEventTime[keys[i]]) +"
" ); + response.write( "
" ); response.end( "cached: " + util.inspect(FHEM_cached).replace(/\n/g, '
') ); } else if( request.url == "/subscriptions" ) { diff --git a/platforms/FibaroHC2.js b/platforms/FibaroHC2.js index 4567b40..57c880c 100644 --- a/platforms/FibaroHC2.js +++ b/platforms/FibaroHC2.js @@ -14,9 +14,9 @@ // 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 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){ diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index 61d3bc2..1bfdc6c 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -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"); diff --git a/platforms/HomeSeer.js b/platforms/HomeSeer.js new file mode 100644 index 0000000..7c899f8 --- /dev/null +++ b/platforms/HomeSeer.js @@ -0,0 +1,1066 @@ +'use strict'; + +// +// HomeSeer Platform Shim for HomeBridge by Jean-Michel Joudrier - (stipus at stipus dot com) +// V0.1 - 2015/10/07 +// - Initial version +// V0.2 - 2015/10/10 +// - Occupancy sensor fix +// V0.3 - 2015/10/11 +// - Added TemperatureUnit=F|C option to temperature sensors +// - Added negative temperature support to temperature sensors +// V0.4 - 2015/10/12 +// - Added thermostat support +// V0.5 - 2015/10/12 +// - Added Humidity sensor support +// V0.6 - 2015/10/12 +// - Added Battery support +// - Added low battery support for all sensors +// - Added HomeSeer event support (using HomeKit switches...) +// V0.7 - 2015/10/13 +// - You can add multiple HomeKit devices for the same HomeSeer device reference +// - Added CarbonMonoxide sensor +// - Added CarbonDioxide sensor +// - Added onValues option to all binary sensors +// V0.8 - 2015/10/14 +// - Added uuid_base parameter to all accessories +// V0.9 - 2015/10/16 +// - Smoke sensor battery fix +// - Added offEventGroup && offEventName to events (turn on launches one HS event. turn off can launch another HS event) +// - Added GarageDoorOpener support +// - Added Lock support +// +// +// Remember to add platform to config.json. +// +// You can get HomeSeer Device References by clicking a HomeSeer device name, then +// choosing the Advanced Tab. +// +// Example: +// "platforms": [ +// { +// "platform": "HomeSeer", // Required +// "name": "HomeSeer", // Required +// "host": "http://192.168.3.4:81", // Required - If you did setup HomeSeer authentication, use "http://user:password@ip_address:port" +// +// "events":[ // Optional - List of Events - Currently they are imported into HomeKit as switches +// { +// "eventGroup":"My Group", // Required - The HomeSeer event group +// "eventName":"My On Event", // Required - The HomeSeer event name +// "offEventGroup":"My Group", // Optional - The HomeSeer event group for turn-off +// "offEventName":"My Off Event", // Optional - The HomeSeer event name for turn-off +// "name":"Test", // Optional - HomeSeer event name is the default +// "uuid_base":"SomeUniqueId" // Optional - HomeKit identifier will be derived from this parameter instead of the name +// } +// ], +// +// "accessories":[ // Required - List of Accessories +// { +// "ref":8, // Required - HomeSeer Device Reference (To get it, select the HS Device - then Advanced Tab) +// "type":"Lightbulb", // Optional - Lightbulb is the default +// "name":"My Light", // Optional - HomeSeer device name is the default +// "offValue":"0", // Optional - 0 is the default +// "onValue":"100", // Optional - 100 is the default +// "can_dim":true, // Optional - true is the default - false for a non dimmable lightbulb +// "uuid_base":"SomeUniqueId2" // Optional - HomeKit identifier will be derived from this parameter instead of the name. You SHOULD add this parameter to all accessories ! +// }, +// { +// "ref":9 // This is a dimmable Lightbulb by default +// }, +// { +// "ref":58, // This is a controllable outlet +// "type":"Outlet" +// }, +// { +// "ref":111, // Required - HomeSeer Device Reference for your sensor +// "type":"TemperatureSensor", // Required for a temperature sensor +// "temperatureUnit":"F", // Optional - C is the default +// "name":"Bedroom temp", // Optional - HomeSeer device name is the default +// "batteryRef":112, // Optional - HomeSeer device reference for the sensor battery level +// "batteryThreshold":15 // Optional - If sensor battery level is below this value, the HomeKit LowBattery characteristic is set to 1. Default is 10 +// }, +// { +// "ref":34, // Required - HomeSeer Device Reference for your sensor +// "type":"SmokeSensor", // Required for a smoke sensor +// "name":"Kichen smoke detector", // Optional - HomeSeer device name is the default +// "batteryRef":35, // Optional - HomeSeer device reference for the sensor battery level +// "batteryThreshold":15, // Optional - If sensor battery level is below this value, the HomeKit LowBattery characteristic is set to 1. Default is 10 +// "onValues":[1,1.255] // Optional - List of all HomeSeer values triggering a "ON" sensor state - Default is any value different than 0 +// }, +// { +// "ref":34, // Required - HomeSeer Device Reference for your sensor (Here it's the same device as the SmokeSensor above) +// "type":"CarbonMonoxideSensor", // Required for a carbon monoxide sensor +// "name":"Kichen CO detector", // Optional - HomeSeer device name is the default +// "batteryRef":35, // Optional - HomeSeer device reference for the sensor battery level +// "batteryThreshold":15, // Optional - If sensor battery level is below this value, the HomeKit LowBattery characteristic is set to 1. Default is 10 +// "onValues":[2,2.255] // Optional - List of all HomeSeer values triggering a "ON" sensor state - Default is any value different than 0 +// }, +// { +// "ref":113, // Required - HomeSeer Device Reference of the Current Temperature Device +// "type":"Thermostat", // Required for a Thermostat +// "name":"Température Salon", // Optional - HomeSeer device name is the default +// "temperatureUnit":"C", // Optional - F for Fahrenheit, C for Celsius, C is the default +// "setPointRef":167, // Required - HomeSeer device reference for your thermostat Set Point. +// "setPointReadOnly":true, // Optional - Set to false if your SetPoint is read/write. true is the default +// "stateRef":166, // Required - HomeSeer device reference for your thermostat current state +// "stateOffValues":[0,4,5], // Required - List of the HomeSeer device values for a HomeKit state=OFF +// "stateHeatValues":[1], // Required - List of the HomeSeer device values for a HomeKit state=HEAT +// "stateCoolValues":[2], // Required - List of the HomeSeer device values for a HomeKit state=COOL +// "stateAutoValues":[3], // Required - List of the HomeSeer device values for a HomeKit state=AUTO +// "controlRef":168, // Required - HomeSeer device reference for your thermostat mode control (It can be the same as stateRef for some thermostats) +// "controlOffValue":0, // Required - HomeSeer device control value for OFF +// "controlHeatValue":1, // Required - HomeSeer device control value for HEAT +// "controlCoolValue":2, // Required - HomeSeer device control value for COOL +// "controlAutoValue":3, // Required - HomeSeer device control value for AUTO +// "coolingThresholdRef":169, // Optional - Not-implemented-yet - HomeSeer device reference for your thermostat cooling threshold +// "heatingThresholdRef":170 // Optional - Not-implemented-yet - HomeSeer device reference for your thermostat heating threshold +// }, +// { +// "ref":200, // Required - HomeSeer Device Reference of a garage door opener +// "type":"GarageDoorOpener", // Required for a Garage Door Opener +// "name":"Garage Door", // Optional - HomeSeer device name is the default +// "stateRef":201, // Required - HomeSeer device reference for your garage door opener current state (can be the same as ref) +// "stateOpenValues":[0], // Required - List of the HomeSeer device values for a HomeKit state=OPEN +// "stateClosedValues":[1], // Required - List of the HomeSeer device values for a HomeKit state=CLOSED +// "stateOpeningValues":[2], // Optional - List of the HomeSeer device values for a HomeKit state=OPENING +// "stateClosingValues":[3], // Optional - List of the HomeSeer device values for a HomeKit state=CLOSING +// "stateStoppedValues":[4], // Optional - List of the HomeSeer device values for a HomeKit state=STOPPED +// "controlRef":201, // Required - HomeSeer device reference for your garage door opener control (can be the same as ref and stateRef) +// "controlOpenValue":0, // Required - HomeSeer device control value for OPEN +// "controlCloseValue":1, // Required - HomeSeer device control value for CLOSE +// "obstructionRef":201, // Optional - HomeSeer device reference for your garage door opener obstruction state (can be the same as ref) +// "obstructionValues":[5], // Optional - List of the HomeSeer device values for a HomeKit obstruction state=OBSTRUCTION +// "lockRef":202, // Optional - HomeSeer device reference for your garage door lock (can be the same as ref) +// "lockUnsecuredValues":[0], // Optional - List of the HomeSeer device values for a HomeKit lock state=UNSECURED +// "lockSecuredValues":[1], // Optional - List of the HomeSeer device values for a HomeKit lock state=SECURED +// "lockJammedValues":[2], // Optional - List of the HomeSeer device values for a HomeKit lock state=JAMMED +// "unlockValue":0, // Optional - HomeSeer device control value to unlock the garage door opener +// "lockValue":1 // Optional - HomeSeer device control value to lock the garage door opener +// }, +// { +// "ref":210, // Required - HomeSeer Device Reference of a Lock +// "type":"Lock", // Required for a Lock +// "name":"Main Door Lock", // Optional - HomeSeer device name is the default +// "lockUnsecuredValues":[0], // Required - List of the HomeSeer device values for a HomeKit lock state=UNSECURED +// "lockSecuredValues":[1], // Required - List of the HomeSeer device values for a HomeKit lock state=SECURED +// "lockJammedValues":[2], // Optional - List of the HomeSeer device values for a HomeKit lock state=JAMMED +// "unlockValue":0, // Required - HomeSeer device control value to unlock +// "lockValue":1 // Required - HomeSeer device control value to lock +// }, +// { +// "ref":115, // Required - HomeSeer Device Reference for a device holding battery level (0-100) +// "type":"Battery", // Required for a Battery +// "name":"Roomba battery", // Optional - HomeSeer device name is the default +// "batteryThreshold":15 // Optional - If the level is below this value, the HomeKit LowBattery characteristic is set to 1. Default is 10 +// } +// ] +// } +// ], +// +// +// SUPORTED TYPES: +// - Lightbulb (can_dim, onValue, offValue options) +// - Fan (onValue, offValue options) +// - Switch (onValue, offValue options) +// - Outlet (onValue, offValue options) +// - Thermostat (temperatureUnit, setPoint, state, control options) +// - TemperatureSensor (temperatureUnit=C|F) +// - HumiditySensor (HomeSeer device value in % - batteryRef, batteryThreshold options) +// - LightSensor (HomeSeer device value in Lux - batteryRef, batteryThreshold options) +// - ContactSensor (onValues, batteryRef, batteryThreshold options) +// - MotionSensor (onValues, batteryRef, batteryThreshold options) +// - LeakSensor (onValues, batteryRef, batteryThreshold options) +// - OccupancySensor (onValues, batteryRef, batteryThreshold options) +// - SmokeSensor (onValues, batteryRef, batteryThreshold options) +// - CarbonMonoxideSensor (onValues, batteryRef, batteryThreshold options) +// - CarbonDioxideSensor (onValues, batteryRef, batteryThreshold options) +// - Battery (batteryThreshold option) +// - GarageDoorOpener (state, control, obstruction, lock options) +// - Lock (unsecured, secured, jammed options) +// - Door + + +var Service = require("hap-nodejs").Service; +var Characteristic = require("hap-nodejs").Characteristic; +var request = require("request"); + + +function httpRequest(url, method, callback) { + request({ + url: url, + method: method + }, + function (error, response, body) { + callback(error, response, body) + }) +} + + + +function HomeSeerPlatform(log, config){ + this.log = log; + this.config = config; +} + +HomeSeerPlatform.prototype = { + accessories: function(callback) { + var that = this; + var foundAccessories = []; + + if( this.config.events ) { + this.log("Creating HomeSeer events."); + for( var i=0; i 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; diff --git a/platforms/KNX.js b/platforms/KNX.js index 65f7a13..0ca4309 100644 --- a/platforms/KNX.js +++ b/platforms/KNX.js @@ -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'); diff --git a/platforms/LIFx.js b/platforms/LIFx.js index 79988eb..89de156 100644 --- a/platforms/LIFx.js +++ b/platforms/LIFx.js @@ -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; diff --git a/platforms/LogitechHarmony.js b/platforms/LogitechHarmony.js index a5c5e22..c1ee15a 100644 --- a/platforms/LogitechHarmony.js +++ b/platforms/LogitechHarmony.js @@ -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; diff --git a/platforms/MiLight.js b/platforms/MiLight.js index 3869e74..0325352 100644 --- a/platforms/MiLight.js +++ b/platforms/MiLight.js @@ -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; diff --git a/platforms/Nest.js b/platforms/Nest.js index 414fcef..9d225c0 100644 --- a/platforms/Nest.js +++ b/platforms/Nest.js @@ -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; \ No newline at end of file +module.exports.platform = NestPlatform; diff --git a/platforms/Openhab.js b/platforms/Openhab.js new file mode 100644 index 0000000..a148240 --- /dev/null +++ b/platforms/Openhab.js @@ -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; diff --git a/platforms/PhilipsHue.js b/platforms/PhilipsHue.js index 9bceaf0..40dcc33 100644 --- a/platforms/PhilipsHue.js +++ b/platforms/PhilipsHue.js @@ -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; diff --git a/platforms/SmartThings.js b/platforms/SmartThings.js index 580e70b..470ee55 100644 --- a/platforms/SmartThings.js +++ b/platforms/SmartThings.js @@ -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){ diff --git a/platforms/Sonos.js b/platforms/Sonos.js index 1d19c2f..8e7581d 100644 --- a/platforms/Sonos.js +++ b/platforms/Sonos.js @@ -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]) { diff --git a/platforms/Telldus.js b/platforms/Telldus.js new file mode 100644 index 0000000..c192a31 --- /dev/null +++ b/platforms/Telldus.js @@ -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; diff --git a/platforms/TelldusLive.js b/platforms/TelldusLive.js index b6a88f1..0e861b5 100644 --- a/platforms/TelldusLive.js +++ b/platforms/TelldusLive.js @@ -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) { diff --git a/platforms/Wink.js b/platforms/Wink.js index e2ec455..d9de07f 100644 --- a/platforms/Wink.js +++ b/platforms/Wink.js @@ -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 = { diff --git a/platforms/YamahaAVR.js b/platforms/YamahaAVR.js index 1659c0f..8d79058 100644 --- a/platforms/YamahaAVR.js +++ b/platforms/YamahaAVR.js @@ -1,8 +1,8 @@ -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 Service = require("hap-nodejs").Service; +var Characteristic = require("hap-nodejs").Characteristic; var Yamaha = require('yamaha-nodejs'); var Q = require('q'); var mdns = require('mdns'); @@ -23,6 +23,7 @@ function YamahaAVRPlatform(log, config){ 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}); } @@ -30,24 +31,24 @@ function YamahaAVRPlatform(log, config){ YamahaAVRPlatform.AudioVolume = function() { Characteristic.call(this, 'Audio Volume', '00001001-0000-1000-8000-135D67EC4377'); - this.format = 'uint8'; - this.unit = 'percentage'; - this.maximumValue = 100; - this.minimumValue = 0; - this.stepValue = 1; - this.readable = true; - this.writable = true; - this.supportsEventNotification = true; + 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.format = 'bool'; - this.readable = true; - this.writable = true; - this.supportsEventNotification = true; + this.setProps({ + format: Characteristic.Formats.UINT8, + perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] + }); this.value = this.getDefaultValue(); }; inherits(YamahaAVRPlatform.Muting, Characteristic); @@ -75,25 +76,44 @@ YamahaAVRPlatform.prototype = { var accessories = []; var timer, timeElapsed = 0, checkCyclePeriod = 5000; - browser.on('serviceUp', function(service){ + // 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); - accessories.push(accessory); - if(accessories.length >= this.expectedDevices) - timeoutFunction(); // We're done, call the timeout function now. - //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 @@ -119,15 +139,15 @@ YamahaAVRPlatform.prototype = { } }; -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; diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 70c2d6e..2d8adac 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -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,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 "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)); + 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, 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++) diff --git a/platforms/isy-js.js b/platforms/isy-js.js new file mode 100644 index 0000000..172bd32 --- /dev/null +++ b/platforms/isy-js.js @@ -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;