From c98e7a1d54a77a98164e5e2fda3f8a04a64f5cdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=20Schr=C3=B6ter?= Date: Fri, 14 Aug 2015 10:13:23 +0200 Subject: [PATCH 01/71] added zwave model more cleanups --- platforms/FHEM.js | 76 +++++++++++++++++++++++++++++++---------------- 1 file changed, 50 insertions(+), 26 deletions(-) diff --git a/platforms/FHEM.js b/platforms/FHEM.js index ada3c47..f16f72b 100644 --- a/platforms/FHEM.js +++ b/platforms/FHEM.js @@ -38,17 +38,17 @@ FHEM_update(inform_id, value, no_update) { || FHEM_cached[inform_id] === value ) return; - //if( FHEM_internal['.'+subscription.accessory.device+'-homekitID'] == undefined ) { - // var info = subscription.characteristic.accessoryController.tcpServer.accessoryInfo; + //if( FHEM_internal['.'+subscription.accessory.device+'-homekitID'] == undefined ) { + // var info = subscription.characteristic.accessoryController.tcpServer.accessoryInfo; - // if( info.username ) { - // var accessory = subscription.accessory; - // var cmd = '{$defs{'+ accessory.device +'}->{homekitID} = "'+info.username+'" if(defined($defs{'+ accessory.device +'}));;}'; - // //accessory.execute( cmd ); + // if( info.username ) { + // var accessory = subscription.accessory; + // var cmd = '{$defs{'+ accessory.device +'}->{homekitID} = "'+info.username+'" if(defined($defs{'+ accessory.device +'}));;}'; + // //accessory.execute( cmd ); - // FHEM_internal['.'+accessory.device+'-homekitID'] = info.username; - // } - //} + // FHEM_internal['.'+accessory.device+'-homekitID'] = info.username; + // } + //} FHEM_cached[inform_id] = value; //FHEM_cached[inform_id] = { 'value': value, 'timestamp': Date.now() }; @@ -60,9 +60,9 @@ FHEM_update(inform_id, value, no_update) { } -var FHEM_lastEventTimestamp; +var FHEM_lastEventTime; var FHEM_longpoll_running = false; -//FIXME: force reconnect on xxx bytes received ?, add filter, add since +//FIXME: add filter function FHEM_startLongpoll(connection) { if( FHEM_longpoll_running ) return; @@ -70,6 +70,8 @@ function FHEM_startLongpoll(connection) { var filter = ".*"; var since = "null"; + if( FHEM_lastEventTime ) + since = FHEM_lastEventTime/1000; var query = "/fhem.pl?XHR=1"+ "&inform=type=status;filter="+filter+";since="+since+";fmt=JSON"+ "×tamp="+Date.now() @@ -85,8 +87,9 @@ function FHEM_startLongpoll(connection) { return; input += data; + var lastEventTime = Date.now(); for(;;) { - var nOff = input.indexOf("\n", FHEM_longpollOffset); + var nOff = input.indexOf('\n', FHEM_longpollOffset); if(nOff < 0) break; var l = input.substr(FHEM_longpollOffset, nOff-FHEM_longpollOffset); @@ -113,7 +116,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_lastEventTimestamp = Date.now(); + FHEM_lastEventTime = lastEventTime; var accessory = subscription.accessory; var value = d[1]; @@ -393,11 +396,12 @@ FHEMPlatform.prototype = { } - if( accessory ) + if( accessory && Object.getOwnPropertyNames(accessory).length ) foundAccessories.push(accessory); }); } + callback(foundAccessories); } else { @@ -417,6 +421,20 @@ FHEMAccessory(log, connection, s) { //log("got json: " + util.inspect(s) ); //log("got json: " + util.inspect(s.Internals) ); + if( !(this instanceof FHEMAccessory) ) + return new FHEMAccessory(log, connection, s); + + if( s.Attributes.disable == 1 ) { + that.log( s.Internals.NAME + ' is disabled'); + return null; + + } else if( s.Internals.TYPE == 'structure' ) { + that.log( s.Internals.NAME + ' is a structure'); + return null; + + } + + this.mappings = {}; var match; @@ -570,12 +588,14 @@ FHEMAccessory(log, connection, s) { log( s.Internals.NAME + ' is dimable [0-'+ s.pctMax +']' ); else if( s.isLight ) log( s.Internals.NAME + ' is light' ); - else + else if( this.mappings.onOff || s.isSwitch ) log( s.Internals.NAME + ' is switchable' ); + else + return {}; - if( this.hasOnOff ) - log( s.Internals.NAME + ' has OnOff [' + this.hasOnOff + ']' ); + if( this.mappings.onOff ) + log( s.Internals.NAME + ' has onOff [' + this.mappings.onOff + ']' ); if( this.mappings.hue ) log( s.Internals.NAME + ' has hue [0-' + this.mappings.hue.max +']' ); if( this.mappings.sat ) @@ -594,7 +614,7 @@ FHEMAccessory(log, connection, s) { this.alias = s.Attributes.alias ? s.Attributes.alias : s.Internals.NAME; this.device = s.Internals.NAME; this.type = s.Internals.TYPE; - this.model = s.Attributes.model ? s.Attributes.model : s.Internals.model; + this.model = s.Attributes.model ? s.Attributes.model : (s.Internals.model ? s.Internals.model : s.Readings.model.Value); this.PossibleSets = s.PossibleSets; if( this.type == 'CUL_HM' ) { @@ -635,6 +655,7 @@ FHEMAccessory(log, connection, s) { if( value != undefined ) { var inform_id = that.device +'-'+ reading; that.mappings[key].informId = inform_id; + if( !that.mappings[key].nocache ) FHEM_cached[inform_id] = value; } @@ -643,6 +664,10 @@ FHEMAccessory(log, connection, s) { this.log = log; this.connection = connection; + + this.onRegister = function(accessory) { +console.log( ">>>>>>>>>>>>:" + util.inspect(accessory) ); + }; } FHEM_dim_values = [ 'dim06%', 'dim12%', 'dim18%', 'dim25%', 'dim31%', 'dim37%', 'dim43%', 'dim50%', 'dim56%', 'dim62%', 'dim68%', 'dim75%', 'dim81%', 'dim87%', 'dim93%' ]; @@ -732,7 +757,7 @@ FHEMAccessory.prototype = { value = 0; else if( value == '000000' ) value = 0; - else if( value.match( /^[A-D]0$/ ) ) // FIXME: should be handled by event_map now + else if( value.match( /^[A-D]0$/ ) ) //FIXME: is handled by event_map now value = 0; else value = 1; @@ -760,7 +785,7 @@ FHEMAccessory.prototype = { if( this.type == 'HUEDevice' ) cmd = "set " + this.device + "alert select"; else - cmd = "set " + this.device + " toggle;; sleep 1;; set "+ this.device + " toggle"; + cmd = "set " + this.device + " toggle; sleep 1; set "+ this.device + " toggle"; } else if( c == 'set' ) { cmd = "set " + this.device + " " + value; @@ -971,7 +996,7 @@ FHEMAccessory.prototype = { } ); }, - informationCharacteristics: function() { + informationCharacteristics: function(that) { return [ { cType: types.NAME_CTYPE, @@ -1016,7 +1041,7 @@ FHEMAccessory.prototype = { },{ cType: types.IDENTIFY_CTYPE, onUpdate: function(value) { - if( this.mappings.onOff ) + if( that.mappings.onOff ) that.command( 'identify' ); }, perms: ["pw"], @@ -1244,7 +1269,6 @@ FHEMAccessory.prototype = { }); } - //FIXME: use mapping.volume if( this.mappings.volume ) { cTypes.push({ cType: types.OUTPUTVOLUME_CTYPE, @@ -1658,7 +1682,7 @@ FHEMAccessory.prototype = { var that = this; var services = [{ sType: types.ACCESSORY_INFORMATION_STYPE, - characteristics: this.informationCharacteristics(), + characteristics: this.informationCharacteristics(that), }, { sType: this.sType(), @@ -1684,8 +1708,8 @@ function FHEMdebug_handleRequest(request, response){ if( request.url == "/cached" ) { response.write( "home

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

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

" ); response.end( "cached: " + util.inspect(FHEM_cached).replace(/\n/g, '
') ); } else if( request.url == "/subscriptions" ) { From 5782ff997f38870ffaa68c7da94df1fb73f13c4e Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Fri, 7 Aug 2015 20:42:28 +0200 Subject: [PATCH 02/71] Initial commit of ZWayServer platform. Not much working, having problems with authenticating. --- package.json | 1 + platforms/ZWayServer.js | 300 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 301 insertions(+) create mode 100644 platforms/ZWayServer.js diff --git a/package.json b/package.json index 2814343..75a45b1 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "node-hue-api": "^1.0.5", "node-milight-promise": "0.0.2", "node-persist": "0.0.x", + "q": "1.4.x", "request": "2.49.x", "sonos": "0.8.x", "telldus-live": "0.2.x", diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js new file mode 100644 index 0000000..539e7df --- /dev/null +++ b/platforms/ZWayServer.js @@ -0,0 +1,300 @@ +var types = require("HAP-NodeJS/accessories/types.js"); +var request = require("request"); +var Q = require("q"); + +function ZWayServerPlatform(log, config){ + this.log = log; + this.url = config["url"]; + this.login = config["login"]; + this.password = config["password"]; + this.name_overrides = config["name_overrides"]; + + this.jar = request.jar(); +} + +ZWayServerPlatform.prototype = { + + zwayRequest: function(verb, opts){ + var that = this; + var deferred = Q.defer(); + + opts.jar = this.jar; + opts.json = true; +//opts.proxy = 'http://localhost:8888'; + + var rmethod = request[verb]; + rmethod(opts) + .on('response', function(response){ + if(response.statusCode == 401){ +that.log("Authenticating..."); + request.post({ + url: that.url + 'ZAutomation/api/v1/login', + body: { //JSON.stringify({ + "form": true, + "login": that.login, + "password": that.password, + "keepme": false, + "default_ui": 1 + }, + headers: { + "Accept": "application/json", + "Content-Type": "application/json" + }, + json: true, + jar: that.jar + }).on('response', function(response){ + if(response.statusCode == 200){ + that.log("Authenticated. Resubmitting original request..."); + rmethod(opts).on('response', function(response){ + if(response.statusCode == 200){ + deferred.resolve(response); + } else { + deferred.reject(response); + } + }); + } else { + deferred.reject(response); + } + }); + } else if(response.statusCode == 200) { + deferred.resolve(response); + } else { + deferred.reject(response); + } + }); + return deferred.promise; + } + , + + accessories: function(callback) { + this.log("Fetching Z-Way devices..."); + + var that = this; + var foundAccessories = []; + + this.zwayRequest('get', { + url: this.url + 'ZAutomation/api/v1/devices' + }).then(function(result){ + var devices = result.data.devices; + var groupedDevices = {}; + for(var i = 0; i < devices.length; i++){ + var dentry = devices[i]; + var gdid = dentry.id.replace(/^(.*?)_zwqy_(\d+-\d+)-\d/, '$1_$2'); + var gd = groupedDevices[gdid] || (groupedDevices[gdid] = []); + gd.push(dentry); + } + for(var gdid in groupedDevices) { + if(!groupedDevices.hasOwnProperty(gdid)) continue; + this.log('Got grouped device ' + gdid + ' consiting of devices:'); + var gd = groupedDevices[gdid]; + for(var j = 0; j < gd.length; j++){ + this.log(gd[j].id); + } + var accessory = new ZWayServerAccessory(); + foundAccessories.push(accessory); + } + //callback(foundAccessories); + }); + + } + +} + +function ZWayServerAccessory(log, name, commands) { + // device info + this.name = name; + this.commands = commands; + this.log = log; +} +/* +SmartThingsAccessory.prototype = { + + command: function(c,value) { + this.log(this.name + " sending command " + c); + var url = this.commands[c]; + if (value != undefined) { + url = this.commands[c] + "&value="+value + } + + var that = this; + request.put({ + url: url + }, function(err, response) { + if (err) { + that.log("There was a problem sending command " + c + " to" + that.name); + that.log(url); + } else { + that.log(that.name + " sent command " + c); + } + }) + }, + + informationCharacteristics: function() { + return [ + { + cType: types.NAME_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.name, + supportEvents: false, + supportBonjour: false, + manfDescription: "Name of the accessory", + designedMaxLength: 255 + },{ + cType: types.MANUFACTURER_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "SmartThings", + supportEvents: false, + supportBonjour: false, + manfDescription: "Manufacturer", + designedMaxLength: 255 + },{ + cType: types.MODEL_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "Rev-1", + supportEvents: false, + supportBonjour: false, + manfDescription: "Model", + designedMaxLength: 255 + },{ + cType: types.SERIAL_NUMBER_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "A1S2NASF88EW", + supportEvents: false, + supportBonjour: false, + manfDescription: "SN", + designedMaxLength: 255 + },{ + cType: types.IDENTIFY_CTYPE, + onUpdate: null, + perms: ["pw"], + format: "bool", + initialValue: false, + supportEvents: false, + supportBonjour: false, + manfDescription: "Identify Accessory", + designedMaxLength: 1 + } + ] + }, + + controlCharacteristics: function(that) { + cTypes = [{ + cType: types.NAME_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.name, + supportEvents: true, + supportBonjour: false, + manfDescription: "Name of service", + designedMaxLength: 255 + }] + + if (this.commands['on'] != undefined) { + cTypes.push({ + cType: types.POWER_STATE_CTYPE, + onUpdate: function(value) { + if (value == 0) { + that.command("off") + } else { + that.command("on") + } + }, + perms: ["pw","pr","ev"], + format: "bool", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Change the power state", + designedMaxLength: 1 + }) + } + + if (this.commands['on'] != undefined) { + cTypes.push({ + cType: types.BRIGHTNESS_CTYPE, + onUpdate: function(value) { that.command("setLevel", value); }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Adjust Brightness of Light", + designedMinValue: 0, + designedMaxValue: 100, + designedMinStep: 1, + unit: "%" + }) + } + + if (this.commands['setHue'] != undefined) { + cTypes.push({ + cType: types.HUE_CTYPE, + onUpdate: function(value) { that.command("setHue", value); }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Adjust Hue of Light", + designedMinValue: 0, + designedMaxValue: 360, + designedMinStep: 1, + unit: "arcdegrees" + }) + } + + if (this.commands['setSaturation'] != undefined) { + cTypes.push({ + cType: types.SATURATION_CTYPE, + onUpdate: function(value) { that.command("setSaturation", value); }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Adjust Brightness of Light", + designedMinValue: 0, + designedMaxValue: 100, + designedMinStep: 1, + unit: "%" + }) + } + + return cTypes + }, + + sType: function() { + if (this.commands['setLevel'] != undefined) { + return types.LIGHTBULB_STYPE + } else { + return types.SWITCH_STYPE + } + }, + + getServices: function() { + var that = this; + var services = [{ + sType: types.ACCESSORY_INFORMATION_STYPE, + characteristics: this.informationCharacteristics(), + }, + { + sType: this.sType(), + characteristics: this.controlCharacteristics(that) + }]; + this.log("Loaded services for " + this.name) + return services; + } +}; +*/ + +module.exports.accessory = ZWayServerAccessory; +module.exports.platform = ZWayServerPlatform; From c6877193cc1c8346b5460f9c287d91845dc46363 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Sat, 15 Aug 2015 06:17:58 +0200 Subject: [PATCH 03/71] Early, but lots done This works (sometimes at least) but has lots of flaws, including ones that make the whole bridge unreachable. --- package.json | 1 + platforms/ZWayServer.js | 691 +++++++++++++++++++++++++++------------- 2 files changed, 475 insertions(+), 217 deletions(-) diff --git a/package.json b/package.json index 75a45b1..873fc73 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "node-milight-promise": "0.0.2", "node-persist": "0.0.x", "q": "1.4.x", + "tough-cookie": "^2.0.0", "request": "2.49.x", "sonos": "0.8.x", "telldus-live": "0.2.x", diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 539e7df..c81b093 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -1,34 +1,121 @@ var types = require("HAP-NodeJS/accessories/types.js"); var request = require("request"); +var tough = require('tough-cookie'); var Q = require("q"); -function ZWayServerPlatform(log, config){ - this.log = log; - this.url = config["url"]; - this.login = config["login"]; - this.password = config["password"]; - this.name_overrides = config["name_overrides"]; +var zwshkDeviceClasses = [ + { + primaryType: "switchBinary", + subTypes: { + "battery": true, + "sensorMultilevel.Electric": true + }, + tcType: types.SWITCH_TCTYPE + } + , + { + primaryType: "thermostat", + subTypes: { + "sensorMultiLevel.Temperature": true, + "battery": true + }, + tcType: types.THERMOSTAT_TCTYPE + } + , + { + primaryType: "sensorBinary.Door/Window", + subTypes: { + "battery": true + }, + tcType: types.SENSOR_TCTYPE + } + , + { + primaryType: "sensorMultilevel.Temperature", + subTypes: { + "battery": true + }, + tcType: types.SENSOR_TCTYPE + } + , + { + primaryType: "switchMultilevel", + subTypes: { + "battery": true + }, + tcType: types.LIGHTBULB_TCTYPE + } +]; - this.jar = request.jar(); +function ZWayServerPlatform(log, config){ + this.log = log; + this.url = config["url"]; + this.login = config["login"]; + this.password = config["password"]; + this.name_overrides = config["name_overrides"]; + this.userAgent = "HomeBridge/-1^0.5"; + this.sessionId = ""; + this.jar = request.jar(new tough.CookieJar()); +} + +ZWayServerPlatform.getVDevTypeKey = function(vdev){ + return vdev.deviceType + (vdev.metrics && vdev.metrics.probeTitle ? "." + vdev.metrics.probeTitle : "") +} + +ZWayServerPlatform.getVDevServiceTypes = function(vdev){ + var typeKey = ZWayServerPlatform.getVDevTypeKey(vdev); + switch (typeKey) { + case "switchBinary": + return [types.SWITCH_STYPE]; + case "switchMultilevel": + return [types.LIGHTBULB_STYPE]; + case "thermostat": + return [types.THERMOSTAT_STYPE]; + case "sensorMultilevel.Temperature": + return [types.TEMPERATURE_SENSOR_STYPE]; + case "sensorBinary.Door/Window": + return [types.GARAGE_DOOR_OPENER_STYPE]; + } +} + +ZWayServerPlatform.getVDevCharacteristicsTypes = function(vdev){ + var typeKey = ZWayServerPlatform.getVDevTypeKey(vdev); + switch (typeKey) { + case "switchBinary": + return [types.POWER_STATE_CTYPE]; + case "switchMultilevel": + return [types.POWER_STATE_CTYPE, types.BRIGHTNESS_CTYPE]; + case "thermostat": + return [types.TARGET_TEMPERATURE_CTYPE, types.TEMPERATURE_UNITS_CTYPE, types.CURRENTHEATINGCOOLING_CTYPE, types.TARGETHEATINGCOOLING_CTYPE]; + case "sensorMultilevel.Temperature": + return [types.CURRENT_TEMPERATURE_CTYPE, types.TEMPERATURE_UNITS_CTYPE]; + case "sensorBinary.Door/Window": + return [types.CURRENT_DOOR_STATE_CTYPE]; + case "battery.Battery": + return [types.BATTERY_LEVEL_CTYPE, types.STATUS_LOW_BATTERY_CTYPE]; + } } ZWayServerPlatform.prototype = { - zwayRequest: function(verb, opts){ + zwayRequest: function(opts){ var that = this; var deferred = Q.defer(); - opts.jar = this.jar; + opts.jar = true;//this.jar; opts.json = true; -//opts.proxy = 'http://localhost:8888'; + opts.headers = { + "Cookie": "ZWAYSession=" + this.sessionId + }; +opts.proxy = 'http://localhost:8888'; - var rmethod = request[verb]; - rmethod(opts) - .on('response', function(response){ + request(opts, function(error, response, body){ if(response.statusCode == 401){ that.log("Authenticating..."); - request.post({ + request({ + method: "POST", url: that.url + 'ZAutomation/api/v1/login', +proxy: 'http://localhost:8888', body: { //JSON.stringify({ "form": true, "login": that.login, @@ -38,16 +125,19 @@ that.log("Authenticating..."); }, headers: { "Accept": "application/json", - "Content-Type": "application/json" + "Content-Type": "application/json", + "User-Agent": that.userAgent }, json: true, - jar: that.jar - }).on('response', function(response){ + jar: true//that.jar + }, function(error, response, body){ if(response.statusCode == 200){ + that.sessionId = body.data.sid; + opts.headers["Cookie"] = "ZWAYSession=" + that.sessionId; that.log("Authenticated. Resubmitting original request..."); - rmethod(opts).on('response', function(response){ + request(opts, function(error, response, body){ if(response.statusCode == 200){ - deferred.resolve(response); + deferred.resolve(body); } else { deferred.reject(response); } @@ -57,7 +147,7 @@ that.log("Authenticating..."); } }); } else if(response.statusCode == 200) { - deferred.resolve(response); + deferred.resolve(body); } else { deferred.reject(response); } @@ -72,229 +162,396 @@ that.log("Authenticating..."); var that = this; var foundAccessories = []; - this.zwayRequest('get', { + this.zwayRequest({ + method: "GET", url: this.url + 'ZAutomation/api/v1/devices' }).then(function(result){ var devices = result.data.devices; var groupedDevices = {}; for(var i = 0; i < devices.length; i++){ - var dentry = devices[i]; - var gdid = dentry.id.replace(/^(.*?)_zwqy_(\d+-\d+)-\d/, '$1_$2'); - var gd = groupedDevices[gdid] || (groupedDevices[gdid] = []); - gd.push(dentry); + var vdev = devices[i]; + var gdid = vdev.id.replace(/^(.*?)_zway_(\d+-\d+)-\d.*/, '$1_$2'); + var gd = groupedDevices[gdid] || (groupedDevices[gdid] = {devices: [], types: {}, primary: undefined}); + gd.devices.push(vdev); + gd.types[ZWayServerPlatform.getVDevTypeKey(vdev)] = gd.devices.length - 1; + gd.types[vdev.deviceType] = gd.devices.length - 1; // also include the deviceType only as a possibility } + //TODO: Make a second pass, re-splitting any devices that don't make sense together for(var gdid in groupedDevices) { if(!groupedDevices.hasOwnProperty(gdid)) continue; - this.log('Got grouped device ' + gdid + ' consiting of devices:'); + + // Debug/log... + that.log('Got grouped device ' + gdid + ' consiting of devices:'); var gd = groupedDevices[gdid]; - for(var j = 0; j < gd.length; j++){ - this.log(gd[j].id); + for(var j = 0; j < gd.devices.length; j++){ + that.log(gd.devices[j].id + " - " + gd.devices[j].deviceType + (gd.devices[j].metrics && gd.devices[j].metrics.probeTitle ? "." + gd.devices[j].metrics.probeTitle : "")); } - var accessory = new ZWayServerAccessory(); + + var accessory = null; + for(var ti = 0; ti < zwshkDeviceClasses.length; ti++){ + if(gd.types[zwshkDeviceClasses[ti].primaryType] !== undefined){ + gd.primary = gd.types[zwshkDeviceClasses[ti].primaryType]; + var pd = gd.devices[gd.primary]; + var name = pd.metrics && pd.metrics.title ? pd.metrics.title : pd.id; + that.log("Using class with primaryType " + zwshkDeviceClasses[ti].primaryType + ", " + name + " (" + pd.id + ") as primary."); + accessory = new ZWayServerAccessory(name, zwshkDeviceClasses[ti], gd, that); + break; + } + } + if(!accessory) that.log("WARN: Didn't find suitable device class!"); + + //var accessory = new ZWayServerAccessory(); foundAccessories.push(accessory); } - //callback(foundAccessories); +foundAccessories = [foundAccessories[0], foundAccessories[1], foundAccessories[2], foundAccessories[3]]; // Limit to a few devices for testing... + callback(foundAccessories); }); } } -function ZWayServerAccessory(log, name, commands) { +function ZWayServerAccessory(name, dclass, devDesc, platform) { // device info this.name = name; - this.commands = commands; - this.log = log; + this.dclass = dclass; + this.devDesc = devDesc; + this.platform = platform; + this.log = platform.log; } -/* -SmartThingsAccessory.prototype = { - command: function(c,value) { - this.log(this.name + " sending command " + c); - var url = this.commands[c]; - if (value != undefined) { - url = this.commands[c] + "&value="+value - } - var that = this; - request.put({ - url: url - }, function(err, response) { - if (err) { - that.log("There was a problem sending command " + c + " to" + that.name); - that.log(url); - } else { - that.log(that.name + " sent command " + c); - } - }) - }, +ZWayServerAccessory.prototype = { - informationCharacteristics: function() { - return [ - { - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.name, - supportEvents: false, - supportBonjour: false, - manfDescription: "Name of the accessory", - designedMaxLength: 255 - },{ - cType: types.MANUFACTURER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "SmartThings", - supportEvents: false, - supportBonjour: false, - manfDescription: "Manufacturer", - designedMaxLength: 255 - },{ - cType: types.MODEL_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Rev-1", - supportEvents: false, - supportBonjour: false, - manfDescription: "Model", - designedMaxLength: 255 - },{ - cType: types.SERIAL_NUMBER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "A1S2NASF88EW", - supportEvents: false, - supportBonjour: false, - manfDescription: "SN", - designedMaxLength: 255 - },{ - cType: types.IDENTIFY_CTYPE, - onUpdate: null, - perms: ["pw"], - format: "bool", - initialValue: false, - supportEvents: false, - supportBonjour: false, - manfDescription: "Identify Accessory", - designedMaxLength: 1 - } - ] - }, - - controlCharacteristics: function(that) { - cTypes = [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.name, - supportEvents: true, - supportBonjour: false, - manfDescription: "Name of service", - designedMaxLength: 255 - }] - - if (this.commands['on'] != undefined) { - cTypes.push({ - cType: types.POWER_STATE_CTYPE, - onUpdate: function(value) { - if (value == 0) { - that.command("off") - } else { - that.command("on") - } - }, - perms: ["pw","pr","ev"], - format: "bool", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Change the power state", - designedMaxLength: 1 - }) - } - - if (this.commands['on'] != undefined) { - cTypes.push({ - cType: types.BRIGHTNESS_CTYPE, - onUpdate: function(value) { that.command("setLevel", value); }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust Brightness of Light", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }) - } - - if (this.commands['setHue'] != undefined) { - cTypes.push({ - cType: types.HUE_CTYPE, - onUpdate: function(value) { that.command("setHue", value); }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust Hue of Light", - designedMinValue: 0, - designedMaxValue: 360, - designedMinStep: 1, - unit: "arcdegrees" - }) - } - - if (this.commands['setSaturation'] != undefined) { - cTypes.push({ - cType: types.SATURATION_CTYPE, - onUpdate: function(value) { that.command("setSaturation", value); }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust Brightness of Light", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }) - } - - return cTypes - }, - - sType: function() { - if (this.commands['setLevel'] != undefined) { - return types.LIGHTBULB_STYPE - } else { - return types.SWITCH_STYPE - } - }, - - getServices: function() { - var that = this; - var services = [{ - sType: types.ACCESSORY_INFORMATION_STYPE, - characteristics: this.informationCharacteristics(), + command: function(vdev, command, value) { + this.platform.zwayRequest({ + method: "GET", + url: this.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + '/command/' + command + (value === undefined ? "" : "/" + value) + }); }, - { - sType: this.sType(), - characteristics: this.controlCharacteristics(that) - }]; - this.log("Loaded services for " + this.name) - return services; - } + + informationCharacteristics: function() { + return [ + { + cType: types.NAME_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.name, + supportEvents: false, + supportBonjour: false, + manfDescription: "Name of the accessory", + designedMaxLength: 255 + },{ + cType: types.MANUFACTURER_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "Z-Wave.me", + supportEvents: false, + supportBonjour: false, + manfDescription: "Manufacturer", + designedMaxLength: 255 + },{ + cType: types.MODEL_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "VDev", + supportEvents: false, + supportBonjour: false, + manfDescription: "Model", + designedMaxLength: 255 + },{ + cType: types.SERIAL_NUMBER_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "VDev-" + this.devDesc.devices[this.devDesc.primary].h, //TODO: Is this valid? + 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 + } + ] + }, + + controlCharacteristics: function(vdev) { + var that = this; + var cTypes = []; + + var cxs = ZWayServerPlatform.getVDevCharacteristicsTypes(vdev); + + if(!cxs || cxs.length <= 0) return cTypes; + + if (cxs.indexOf(types.POWER_STATE_CTYPE) >= 0) { + cTypes.push({ + cType: types.POWER_STATE_CTYPE, + onUpdate: function(value) { + if (value == 0) { + that.command(vdev, "off"); + } else { + that.command(vdev, "on"); + } + }, + perms: ["pw","pr","ev"], + format: "bool", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Change the power state", + designedMaxLength: 1 + }); + } + + if (cxs.indexOf(types.BRIGHTNESS_CTYPE) >= 0) { + cTypes.push({ + cType: types.BRIGHTNESS_CTYPE, + onUpdate: function(value) { + that.command(vdev, "exact", value); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Adjust Brightness of Light", + designedMinValue: 0, + designedMaxValue: 100, + designedMinStep: 1, + unit: "%" + }); + } + + if (cxs.indexOf(types.CURRENT_TEMPERATURE_CTYPE) >= 0) { + cTypes.push({ + cType: types.CURRENT_TEMPERATURE_CTYPE, + onUpdate: null, + onRead: function(callback) { + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(result.data.metrics.level); + }); + }, + perms: ["pr","ev"], + format: "int", + initialValue: 20, + supportEvents: false, + supportBonjour: false, + manfDescription: "Current Temperature", + unit: "celsius" + }); + } + + if (cxs.indexOf(types.TARGET_TEMPERATURE_CTYPE) >= 0) { + cTypes.push({ + cType: types.TARGET_TEMPERATURE_CTYPE, + onUpdate: function(value) { + that.command(vdev, "exact", value); + }, + onRead: function(callback) { + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(result.data.metrics.level); + }); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 20, + supportEvents: false, + supportBonjour: false, + manfDescription: "Target Temperature", + designedMinValue: 2, + designedMaxValue: 38, + designedMinStep: 1, + unit: "celsius" + }); + } + + if (cxs.indexOf(types.TEMPERATURE_UNITS_CTYPE) >= 0) { + cTypes.push({ + cType: types.TEMPERATURE_UNITS_CTYPE, + perms: ["pr"], + format: "int", + //TODO: Let this update from the device if it changes after startup. + initialValue: vdev.metrics.scaleTitle.indexOf("F") >= 0 ? 1 : 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Unit", + }); + } + + if (cxs.indexOf(types.CURRENTHEATINGCOOLING_CTYPE) >= 0) { + cTypes.push({ + cType: types.CURRENTHEATINGCOOLING_CTYPE, + //TODO: Support multifunction thermostats...only heating supported now. + /* + onUpdate: null, + onRead: function(callback) { + that.getCurrentHeatingCooling(function(currentHeatingCooling){ + callback(currentHeatingCooling); + }); + }, + */ + perms: ["pr"], + format: "int", + initialValue: 1, + supportEvents: false, + supportBonjour: false, + manfDescription: "Current Mode", + designedMaxLength: 1, + designedMinValue: 0, + designedMaxValue: 2, + designedMinStep: 1, + }); + } + + if (cxs.indexOf(types.TARGETHEATINGCOOLING_CTYPE) >= 0) { + cTypes.push({ + cType: types.TARGETHEATINGCOOLING_CTYPE, + //TODO: Support multifunction thermostats...only heating supported now. + /* + onUpdate: function(value) { + that.setTargetHeatingCooling(value); + }, + onRead: function(callback) { + that.getTargetHeatingCoooling(function(targetHeatingCooling){ + callback(targetHeatingCooling); + }); + }, + */ + perms: ["pr"], + format: "int", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Target Mode", + designedMinValue: 0, + designedMaxValue: 3, + designedMinStep: 1, + }); + } + + if (cxs.indexOf(types.CONTACT_SENSOR_STATE_CTYPE) >= 0) { + cTypes.push({ + cType: types.CONTACT_SENSOR_STATE_CTYPE, + onUpdate: null, + onRead: function(callback) { + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(result.data.metrics.level == "off" ? 0 : 1); + }); + }, + perms: ["pr","ev"], + format: "bool", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Contact State", + designedMaxLength: 1 + }); + } + + if (cxs.indexOf(types.CURRENT_DOOR_STATE_CTYPE) >= 0) { + cTypes.push({ + cType: types.CURRENT_DOOR_STATE_CTYPE, + onRead: function(callback) { + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(result.data.metrics.level == "off" ? 0 : 1); + }); + }, + perms: ["pr","ev"], + format: "int", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Current Door State", + designedMinValue: 0, + designedMaxValue: 4, + designedMinStep: 1, + designedMaxLength: 1 + }); + } + + return cTypes; + }, + + getServices: function() { + var that = this; + var services = [{ + sType: types.ACCESSORY_INFORMATION_STYPE, + characteristics: this.informationCharacteristics(), + }]; + + // rearrange the array so the primary is first + var vdevs = this.devDesc.devices.concat(); + var p = vdevs.splice(this.devDesc.primary, 1)[0]; + vdevs.unshift(p); + /* + for(var i = 0; i < vdevs.length; i++){ + var sTypes = ZWayServerPlatform.getVDevServiceTypes(vdevs[i]); + if(!sTypes) continue; + for(var j = 0; j < sTypes.length; j++){ + services.push({ + sType: sTypes[j], + characteristics: this.controlCharacteristics(vdevs[i]) + }); + } + } + */ + + var sTypes = ZWayServerPlatform.getVDevServiceTypes(vdevs[0]); + var cTypes = [{ + cType: types.NAME_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.name, + supportEvents: true, + supportBonjour: false, + manfDescription: "Name of service", + designedMaxLength: 255 + }]; + if(sTypes) for(var i = 0; i < vdevs.length; i++){ + cTypes = cTypes.concat(this.controlCharacteristics(vdevs[i])); + } + + // Scrub/eliminate duplicate cTypes? This is a lot of guesswork ATM... + var hits = {}; + for (var i = 0; i < cTypes.length; i++){ + if(hits[cTypes[i].cType]) cTypes.splice(i--, 1); // Remember postfix means post-evaluate! + hits[cTypes[i].cType] = true; + } + + services.push({ + sType: sTypes[0], + characteristics: cTypes + }); + //... + + this.log("Loaded services for " + this.name); + return services; + } }; -*/ module.exports.accessory = ZWayServerAccessory; module.exports.platform = ZWayServerPlatform; From 9d7a6768b889bcc1f37ecf1243a86e4c87de3323 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Sat, 15 Aug 2015 07:21:48 +0200 Subject: [PATCH 04/71] Working at the moment. Support for several device types. Gotta be careful to not throw the bridge out of compliance! --- platforms/ZWayServer.js | 48 +++++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index c81b093..b6fd29d 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -90,7 +90,7 @@ ZWayServerPlatform.getVDevCharacteristicsTypes = function(vdev){ case "sensorMultilevel.Temperature": return [types.CURRENT_TEMPERATURE_CTYPE, types.TEMPERATURE_UNITS_CTYPE]; case "sensorBinary.Door/Window": - return [types.CURRENT_DOOR_STATE_CTYPE]; + return [types.CURRENT_DOOR_STATE_CTYPE, types.TARGET_DOORSTATE_CTYPE, types.OBSTRUCTION_DETECTED_CTYPE]; case "battery.Battery": return [types.BATTERY_LEVEL_CTYPE, types.STATUS_LOW_BATTERY_CTYPE]; } @@ -203,7 +203,7 @@ proxy: 'http://localhost:8888', //var accessory = new ZWayServerAccessory(); foundAccessories.push(accessory); } -foundAccessories = [foundAccessories[0], foundAccessories[1], foundAccessories[2], foundAccessories[3]]; // Limit to a few devices for testing... +foundAccessories = [foundAccessories[0], foundAccessories[1], foundAccessories[2], foundAccessories[3], foundAccessories[4]]; // Limit to a few devices for testing... callback(foundAccessories); }); @@ -455,12 +455,12 @@ ZWayServerAccessory.prototype = { method: "GET", url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id }).then(function(result){ - callback(result.data.metrics.level == "off" ? 0 : 1); + callback(result.data.metrics.level == "off" ? 1 : 0); }); }, perms: ["pr","ev"], format: "bool", - initialValue: 0, + initialValue: 1, supportEvents: false, supportBonjour: false, manfDescription: "Contact State", @@ -476,12 +476,12 @@ ZWayServerAccessory.prototype = { method: "GET", url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id }).then(function(result){ - callback(result.data.metrics.level == "off" ? 0 : 1); + callback(result.data.metrics.level == "off" ? 1 : 0); }); }, perms: ["pr","ev"], format: "int", - initialValue: 0, + initialValue: 1, supportEvents: false, supportBonjour: false, manfDescription: "Current Door State", @@ -492,6 +492,42 @@ ZWayServerAccessory.prototype = { }); } + if (cxs.indexOf(types.TARGET_DOORSTATE_CTYPE) >= 0) { + cTypes.push({ + cType: types.TARGET_DOORSTATE_CTYPE, + onRead: function(callback) { + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(result.data.metrics.level == "off" ? 0 : 1); + }); + }, + perms: ["pr","ev"], //TODO: If we support some non-sensor device that can actually open, add "pw"! + format: "int", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Target Door State", + designedMinValue: 0, + designedMaxValue: 1, + designedMinStep: 1, + designedMaxLength: 1 + }); + } + + if (cxs.indexOf(types.OBSTRUCTION_DETECTED_CTYPE) >= 0) { + cTypes.push({ + cType: types.OBSTRUCTION_DETECTED_CTYPE, + perms: ["pr","ev"], + format: "bool", + initialValue: false, + supportEvents: false, + supportBonjour: false, + manfDescription: "Obstruction Detected" + }); + } + return cTypes; }, From f72cb43043bfdec4636c9a8250afc400c28ebc25 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Mon, 24 Aug 2015 06:39:00 +0200 Subject: [PATCH 05/71] Numerous fixes and improvements. Committing before merging back to master, since the upstream branches have merged into master! --- platforms/ZWayServer.js | 102 ++++++++++++++++++++++++++++++++++------ 1 file changed, 87 insertions(+), 15 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index b6fd29d..6053632 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -53,6 +53,7 @@ function ZWayServerPlatform(log, config){ this.login = config["login"]; this.password = config["password"]; this.name_overrides = config["name_overrides"]; + this.batteryLow = config["battery_low_level"]; this.userAgent = "HomeBridge/-1^0.5"; this.sessionId = ""; this.jar = request.jar(new tough.CookieJar()); @@ -107,15 +108,15 @@ ZWayServerPlatform.prototype = { opts.headers = { "Cookie": "ZWAYSession=" + this.sessionId }; -opts.proxy = 'http://localhost:8888'; +//opts.proxy = 'http://localhost:8888'; request(opts, function(error, response, body){ if(response.statusCode == 401){ -that.log("Authenticating..."); + that.log("Authenticating..."); request({ method: "POST", url: that.url + 'ZAutomation/api/v1/login', -proxy: 'http://localhost:8888', +//proxy: 'http://localhost:8888', body: { //JSON.stringify({ "form": true, "login": that.login, @@ -170,6 +171,7 @@ proxy: 'http://localhost:8888', var groupedDevices = {}; for(var i = 0; i < devices.length; i++){ var vdev = devices[i]; + if(vdev.tags.indexOf("HomeBridge:Skip") >= 0) { that.log("Tag says skip!"); continue; } var gdid = vdev.id.replace(/^(.*?)_zway_(\d+-\d+)-\d.*/, '$1_$2'); var gd = groupedDevices[gdid] || (groupedDevices[gdid] = {devices: [], types: {}, primary: undefined}); gd.devices.push(vdev); @@ -198,12 +200,14 @@ proxy: 'http://localhost:8888', break; } } - if(!accessory) that.log("WARN: Didn't find suitable device class!"); - - //var accessory = new ZWayServerAccessory(); - foundAccessories.push(accessory); + + if(!accessory) + that.log("WARN: Didn't find suitable device class!"); + else + foundAccessories.push(accessory); + } -foundAccessories = [foundAccessories[0], foundAccessories[1], foundAccessories[2], foundAccessories[3], foundAccessories[4]]; // Limit to a few devices for testing... +//foundAccessories = foundAccessories.slice(0, 10); // Limit to a few devices for testing... callback(foundAccessories); }); @@ -224,9 +228,10 @@ function ZWayServerAccessory(name, dclass, devDesc, platform) { ZWayServerAccessory.prototype = { command: function(vdev, command, value) { - this.platform.zwayRequest({ + return this.platform.zwayRequest({ method: "GET", - url: this.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + '/command/' + command + (value === undefined ? "" : "/" + value) + url: this.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + '/command/' + command, + qs: (value === undefined ? undefined : value) }); }, @@ -318,7 +323,7 @@ ZWayServerAccessory.prototype = { cTypes.push({ cType: types.BRIGHTNESS_CTYPE, onUpdate: function(value) { - that.command(vdev, "exact", value); + that.command(vdev, "exact", {level: parseInt(value, 10)}); }, perms: ["pw","pr","ev"], format: "int", @@ -359,7 +364,12 @@ ZWayServerAccessory.prototype = { cTypes.push({ cType: types.TARGET_TEMPERATURE_CTYPE, onUpdate: function(value) { - that.command(vdev, "exact", value); + try { + that.command(vdev, "exact", {level: parseFloat(value)}); + } + catch (e) { + that.log(e); + } }, onRead: function(callback) { that.platform.zwayRequest({ @@ -375,8 +385,8 @@ ZWayServerAccessory.prototype = { supportEvents: false, supportBonjour: false, manfDescription: "Target Temperature", - designedMinValue: 2, - designedMaxValue: 38, + designedMinValue: vdev.metrics && vdev.metrics.min !== undefined ? vdev.metrics.min : 5, + designedMaxValue: vdev.metrics && vdev.metrics.max !== undefined ? vdev.metrics.max : 40, designedMinStep: 1, unit: "celsius" }); @@ -528,6 +538,52 @@ ZWayServerAccessory.prototype = { }); } + if (cxs.indexOf(types.BATTERY_LEVEL_CTYPE) >= 0) { + cTypes.push({ + cType: types.BATTERY_LEVEL_CTYPE, + onRead: function(callback) { + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(result.data.metrics.level); + }); + }, + perms: ["pr","ev"], + format: "int", + initialValue: 100, + supportEvents: true, + supportBonjour: false, + manfDescription: "Battery Level", + designedMinValue: 0, + designedMaxValue: 100, + designedMinStep: 1, + unit: "%" + }); + } + + if (cxs.indexOf(types.STATUS_LOW_BATTERY_CTYPE) >= 0) { + cTypes.push({ + cType: types.STATUS_LOW_BATTERY_CTYPE, + onUpdate: null, + onRead: function(callback) { + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(result.data.metrics.level <= that.platform.batteryLow ? 1 : 0); + }); + }, + perms: ["pr","ev"], + format: "bool", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Battery is low", + designedMaxLength: 1 + }); + } + return cTypes; }, @@ -575,9 +631,25 @@ ZWayServerAccessory.prototype = { var hits = {}; for (var i = 0; i < cTypes.length; i++){ if(hits[cTypes[i].cType]) cTypes.splice(i--, 1); // Remember postfix means post-evaluate! - hits[cTypes[i].cType] = true; + else hits[cTypes[i].cType] = cTypes[i]; } + // Thermostats MUST include current temperature...so, for the Danfoss/Devolo radiator + // thermostats, we have to fake one... + if (hits[types.TARGET_TEMPERATURE_CTYPE] && !hits[types.CURRENT_TEMPERATURE_CTYPE]) { + // Copy the "target" device to the "current" one, with necessary tweaks... + var tcx = hits[types.TARGET_TEMPERATURE_CTYPE]; + var ccx = {}; + for(var p in tcx){ + if(tcx.hasOwnProperty(p)) ccx[p] = tcx[p]; + } + ccx.cType = types.CURRENT_TEMPERATURE_CTYPE; + ccx.onUpdate = null; + ccx.perms = ["pr"]; + //ccx.onRead = null; // Override this?? + cTypes.push(ccx); + } + services.push({ sType: sTypes[0], characteristics: cTypes From b56d9346c8c3ddd725ce9b5fcda8340b3784077f Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Sun, 30 Aug 2015 14:24:43 +0200 Subject: [PATCH 06/71] Refactored for new API. Mostly working, but door sensors need further work, battery service still isn't right, and I'm losing the bridge periodically...merging from master to see if I need some bugfixes. --- platforms/ZWayServer.js | 393 ++++++++++++++++++++++++++++++++-------- 1 file changed, 320 insertions(+), 73 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 6053632..93ade7b 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -1,3 +1,5 @@ +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'); @@ -79,6 +81,7 @@ ZWayServerPlatform.getVDevServiceTypes = function(vdev){ } } +/* ZWayServerPlatform.getVDevCharacteristicsTypes = function(vdev){ var typeKey = ZWayServerPlatform.getVDevTypeKey(vdev); switch (typeKey) { @@ -96,6 +99,7 @@ ZWayServerPlatform.getVDevCharacteristicsTypes = function(vdev){ return [types.BATTERY_LEVEL_CTYPE, types.STATUS_LOW_BATTERY_CTYPE]; } } +*/ ZWayServerPlatform.prototype = { @@ -108,7 +112,7 @@ ZWayServerPlatform.prototype = { opts.headers = { "Cookie": "ZWAYSession=" + this.sessionId }; -//opts.proxy = 'http://localhost:8888'; +opts.proxy = 'http://localhost:8888'; request(opts, function(error, response, body){ if(response.statusCode == 401){ @@ -116,7 +120,7 @@ ZWayServerPlatform.prototype = { request({ method: "POST", url: that.url + 'ZAutomation/api/v1/login', -//proxy: 'http://localhost:8888', +proxy: 'http://localhost:8888', body: { //JSON.stringify({ "form": true, "login": that.login, @@ -235,6 +239,7 @@ ZWayServerAccessory.prototype = { }); }, + /* informationCharacteristics: function() { return [ { @@ -272,7 +277,7 @@ ZWayServerAccessory.prototype = { onUpdate: null, perms: ["pr"], format: "string", - initialValue: "VDev-" + this.devDesc.devices[this.devDesc.primary].h, //TODO: Is this valid? + initialValue: "", supportEvents: false, supportBonjour: false, manfDescription: "SN", @@ -409,14 +414,14 @@ ZWayServerAccessory.prototype = { cTypes.push({ cType: types.CURRENTHEATINGCOOLING_CTYPE, //TODO: Support multifunction thermostats...only heating supported now. - /* + / * onUpdate: null, onRead: function(callback) { that.getCurrentHeatingCooling(function(currentHeatingCooling){ callback(currentHeatingCooling); }); }, - */ + * / perms: ["pr"], format: "int", initialValue: 1, @@ -434,7 +439,7 @@ ZWayServerAccessory.prototype = { cTypes.push({ cType: types.TARGETHEATINGCOOLING_CTYPE, //TODO: Support multifunction thermostats...only heating supported now. - /* + / * onUpdate: function(value) { that.setTargetHeatingCooling(value); }, @@ -443,7 +448,7 @@ ZWayServerAccessory.prototype = { callback(targetHeatingCooling); }); }, - */ + * / perms: ["pr"], format: "int", initialValue: 0, @@ -550,7 +555,7 @@ ZWayServerAccessory.prototype = { }); }, perms: ["pr","ev"], - format: "int", + format: "uint8", initialValue: 100, supportEvents: true, supportBonjour: false, @@ -575,7 +580,7 @@ ZWayServerAccessory.prototype = { }); }, perms: ["pr","ev"], - format: "bool", + format: "uint8", initialValue: 0, supportEvents: false, supportBonjour: false, @@ -586,75 +591,317 @@ ZWayServerAccessory.prototype = { return cTypes; }, + */ + + getVDevServices: function(vdev){ + var typeKey = ZWayServerPlatform.getVDevTypeKey(vdev); + var services = [], service; + switch (typeKey) { + case "switchBinary": + services.push(new Service.Switch(vdev.metrics.title)); + break; + case "switchMultilevel": + services.push(new Service.Lightbulb(vdev.metrics.title)); + break; + case "thermostat": + services.push(new Service.Thermostat(vdev.metrics.title)); + break; + case "sensorMultilevel.Temperature": + services.push(new Service.TemperatureSensor(vdev.metrics.title)); + break; + case "sensorBinary.Door/Window": + services.push(new Service.Door(vdev.metrics.title)); + break; + case "battery.Battery": + services.push(new Service.BatteryService(vdev.metrics.title)); + break; + } + + var validServices =[]; + for(var i = 0; i < services.length; i++){ + if(this.configureService(services[i], vdev)) + validServices.push(services[i]); + } + + return validServices; + } + , + uuidToTypeKeyMap: null + , + getVDevForCharacteristic: function(cx, vdevPreferred){ + var map = this.uuidToTypeKeyMap; + if(!map){ + this.uuidToTypeKeyMap = map = {}; + map[(new Characteristic.On).UUID] = ["switchBinary","switchMultilevel"]; + map[(new Characteristic.Brightness).UUID] = ["switchMultilevel"]; + map[(new Characteristic.CurrentTemperature).UUID] = ["sensorMultilevel.Temperature","thermostat"]; + map[(new Characteristic.TargetTemperature).UUID] = ["thermostat"]; + map[(new Characteristic.TemperatureDisplayUnits).UUID] = ["sensorMultilevel.Temperature","thermostat"]; //TODO: Always a fixed result + map[(new Characteristic.CurrentHeatingCoolingState).UUID] = ["thermostat"]; //TODO: Always a fixed result + map[(new Characteristic.TargetHeatingCoolingState).UUID] = ["thermostat"]; //TODO: Always a fixed result + map[(new Characteristic.CurrentDoorState).UUID] = ["sensorBinary.Door/Window","sensorBinary"]; + map[(new Characteristic.TargetDoorState).UUID] = ["sensorBinary.Door/Window","sensorBinary"]; //TODO: Always a fixed result + map[(new Characteristic.ObstructionDetected).UUID] = ["sensorBinary.Door/Window","sensorBinary"]; //TODO: Always a fixed result + map[(new Characteristic.BatteryLevel).UUID] = ["battery.Battery"]; + map[(new Characteristic.StatusLowBattery).UUID] = ["battery.Battery"]; + map[(new Characteristic.ChargingState).UUID] = ["battery.Battery"]; //TODO: Always a fixed result + } + + var typekeys = map[cx.UUID]; + if(typekeys === undefined) return null; + + if(vdevPreferred && typekeys.indexOf(ZWayServerPlatform.getVDevTypeKey(vdevPreferred)) >= 0){ + return vdevPreferred; + } + + var candidates = this.devDesc.devices; + for(var i = 0; i < typekeys.length; i++){ + for(var j = 0; j < candidates.length; j++){ + if(ZWayServerPlatform.getVDevTypeKey(candidates[j]) === typekeys[i]) return candidates[j]; + } + } + + return null; + } + , + configureCharacteristic: function(cx, vdev){ + var that = this; + var gdv = function(){ + that.log("Default value for " + vdev.metrics.title + " is " + vdev.metrics.level); + return vdev.metrics.level; + }; + + if(cx instanceof Characteristic.On){ + cx.getDefaultValue = gdv; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + that.log("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); + var val; + if(result.data.metrics.level === "off"){ + val = false; + } else if(val <= 5) { + val = false; + } else if (val > 5) { + val = true; + } + callback(false, val); + }); + }.bind(this)); + cx.on('set', function(powerOn, callback){ + this.command(vdev, powerOn ? "on" : "off").then(function(result){ + callback(); + }); + }.bind(this)); + return cx; + } + + if(cx instanceof Characteristic.Brightness){ + cx.getDefaultValue = gdv; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + that.log("Got value " + result.data.metrics.level + " for " + vdev.metrics.title + "."); + callback(false, result.data.metrics.level); + }); + }.bind(this)); + cx.on('set', function(level, callback){ + this.command(vdev, "exact", {level: parseInt(level, 10)}).then(function(result){ + callback(); + }); + }.bind(this)); + return cx; + } + + if(cx instanceof Characteristic.CurrentTemperature){ + cx.getDefaultValue = gdv; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(false, result.data.metrics.level); + }); + }.bind(this)); + cx.minimumValue = vdev.metrics && vdev.metrics.min !== undefined ? vdev.metrics.min : -40; + cx.maximumValue = vdev.metrics && vdev.metrics.max !== undefined ? vdev.metrics.max : 999; + return cx; + } + + if(cx instanceof Characteristic.TargetTemperature){ + cx.getDefaultValue = gdv; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + this.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(false, result.data.metrics.level); + }); + }.bind(this)); + cx.on('set', function(level, callback){ + this.command(vdev, "exact", {level: parseInt(level, 10)}).then(function(result){ + callback(); + }); + }.bind(this)); + cx.minimumValue = vdev.metrics && vdev.metrics.min !== undefined ? vdev.metrics.min : 5; + cx.maximumValue = vdev.metrics && vdev.metrics.max !== undefined ? vdev.metrics.max : 40; + return cx; + } + + if(cx instanceof Characteristic.TemperatureDisplayUnits){ + //TODO: Always in °C for now. + cx.getDefaultValue = function(){ return Characteristic.TemperatureDisplayUnits.CELCIUS; }; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, Characteristic.TemperatureDisplayUnits.CELCIUS); + }); + cx.writable = false; + return cx; + } + + if(cx instanceof Characteristic.CurrentHeatingCoolingState){ + //TODO: Always HEAT for now, we don't have an example to work with that supports another function. + cx.getDefaultValue = function(){ return Characteristic.CurrentHeatingCoolingState.HEAT; }; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, Characteristic.CurrentHeatingCoolingState.HEAT); + }); + return cx; + } + + if(cx instanceof Characteristic.TargetHeatingCoolingState){ + //TODO: Always HEAT for now, we don't have an example to work with that supports another function. + cx.getDefaultValue = function(){ return Characteristic.TargetHeatingCoolingState.HEAT; }; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, Characteristic.TargetHeatingCoolingState.HEAT); + }); + cx.writable = false; + return cx; + } + + if(cx instanceof Characteristic.CurrentDoorState){ + cx.getDefaultValue = function(){ + return vdev.metrics.level == "off" ? Characteristic.CurrentDoorState.CLOSED : Characteristic.CurrentDoorState.OPEN; + }; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + this.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(false, result.data.metrics.level == "off" ? Characteristic.CurrentDoorState.CLOSED : Characteristic.CurrentDoorState.OPEN); + }); + }.bind(this)); + } + + if(cx instanceof Characteristic.TargetDoorState){ + //TODO: We only support this for Door sensors now, so it's a fixed value. + cx.getDefaultValue = function(){ return Characteristic.TargetDoorState.CLOSED; }; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, Characteristic.TargetDoorState.CLOSED); + }); + //cx.readable = false; + cx.writable = false; + } + + if(cx instanceof Characteristic.ObstructionDetected){ + //TODO: We only support this for Door sensors now, so it's a fixed value. + cx.getDefaultValue = function(){ return false; }; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, false); + }); + //cx.readable = false; + cx.writable = false; + } + + if(cx instanceof Characteristic.BatteryLevel){ + cx.getDefaultValue = gdv; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(false, result.data.metrics.level); + }); + }.bind(this)); + } + + if(cx instanceof Characteristic.StatusLowBattery){ + cx.getDefaultValue = function(){ return Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; }; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + that.platform.zwayRequest({ + method: "GET", + url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + }).then(function(result){ + callback(false, result.data.metrics.level <= that.platform.batteryLow ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); + }); + }.bind(this)); + } + + if(cx instanceof Characteristic.ChargingState){ + //TODO: No known chargeable devices(?), so always return false. + cx.getDefaultValue = function(){ return Characteristic.ChargingState.NOT_CHARGING; }; + cx.on('get', function(callback, context){ + that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, Characteristic.ChargingState.NOT_CHARGING); + }); + //cx.readable = false; + cx.writable = false; + } + + } + , + configureService: function(service, vdev){ + var success = true; + for(var i = 0; i < service.characteristics.length; i++){ + var cx = service.characteristics[i]; + var vdev = this.getVDevForCharacteristic(cx, vdev); + if(!vdev){ + success = false; + this.log("ERROR! Failed to configure required characteristic \"" + service.characteristics[i].displayName + "\"!"); + } + cx = this.configureCharacteristic(cx, vdev); + } + for(var i = 0; i < service.optionalCharacteristics.length; i++){ + var cx = service.optionalCharacteristics[i]; + var vdev = this.getVDevForCharacteristic(cx); + if(!vdev) continue; + cx = this.configureCharacteristic(cx, vdev); + } + return success; + } + , getServices: function() { var that = this; - var services = [{ - sType: types.ACCESSORY_INFORMATION_STYPE, - characteristics: this.informationCharacteristics(), - }]; + + var informationService = new Service.AccessoryInformation(); - // rearrange the array so the primary is first - var vdevs = this.devDesc.devices.concat(); - var p = vdevs.splice(this.devDesc.primary, 1)[0]; - vdevs.unshift(p); - /* - for(var i = 0; i < vdevs.length; i++){ - var sTypes = ZWayServerPlatform.getVDevServiceTypes(vdevs[i]); - if(!sTypes) continue; - for(var j = 0; j < sTypes.length; j++){ - services.push({ - sType: sTypes[j], - characteristics: this.controlCharacteristics(vdevs[i]) - }); - } - } - */ - - var sTypes = ZWayServerPlatform.getVDevServiceTypes(vdevs[0]); - var cTypes = [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.name, - supportEvents: true, - supportBonjour: false, - manfDescription: "Name of service", - designedMaxLength: 255 - }]; - if(sTypes) for(var i = 0; i < vdevs.length; i++){ - cTypes = cTypes.concat(this.controlCharacteristics(vdevs[i])); - } - - // Scrub/eliminate duplicate cTypes? This is a lot of guesswork ATM... - var hits = {}; - for (var i = 0; i < cTypes.length; i++){ - if(hits[cTypes[i].cType]) cTypes.splice(i--, 1); // Remember postfix means post-evaluate! - else hits[cTypes[i].cType] = cTypes[i]; - } - - // Thermostats MUST include current temperature...so, for the Danfoss/Devolo radiator - // thermostats, we have to fake one... - if (hits[types.TARGET_TEMPERATURE_CTYPE] && !hits[types.CURRENT_TEMPERATURE_CTYPE]) { - // Copy the "target" device to the "current" one, with necessary tweaks... - var tcx = hits[types.TARGET_TEMPERATURE_CTYPE]; - var ccx = {}; - for(var p in tcx){ - if(tcx.hasOwnProperty(p)) ccx[p] = tcx[p]; - } - ccx.cType = types.CURRENT_TEMPERATURE_CTYPE; - ccx.onUpdate = null; - ccx.perms = ["pr"]; - //ccx.onRead = null; // Override this?? - cTypes.push(ccx); - } + 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?); - services.push({ - sType: sTypes[0], - characteristics: cTypes - }); - //... + var services = [informationService]; + + services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.primary])); + + if(this.devDesc.types["battery.Battery"]) + services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.types["battery.Battery"]])); this.log("Loaded services for " + this.name); return services; From 30a705e79f0e9de2245f310080809c5bc164ca99 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Wed, 2 Sep 2015 06:27:13 +0200 Subject: [PATCH 07/71] Fixes, cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Got rid of old code I clearly don’t need anymore. Switched `this.log`s to `debug`s. Fix for picking wrong vDev for thermostat current temp. Added configuration for `Name` characteristic…which I guess became required. --- package.json | 3 +- platforms/ZWayServer.js | 449 +++++----------------------------------- 2 files changed, 51 insertions(+), 401 deletions(-) diff --git a/package.json b/package.json index ca43ef5..69784c8 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "xml2js": "0.4.x", "xmldoc": "0.1.x", "yamaha-nodejs": "0.4.x", - "teslams": "1.0.1" + "teslams": "1.0.1", + "debug": "^2.2.0" } } diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 93ade7b..f595408 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -1,3 +1,4 @@ +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"); @@ -81,26 +82,6 @@ ZWayServerPlatform.getVDevServiceTypes = function(vdev){ } } -/* -ZWayServerPlatform.getVDevCharacteristicsTypes = function(vdev){ - var typeKey = ZWayServerPlatform.getVDevTypeKey(vdev); - switch (typeKey) { - case "switchBinary": - return [types.POWER_STATE_CTYPE]; - case "switchMultilevel": - return [types.POWER_STATE_CTYPE, types.BRIGHTNESS_CTYPE]; - case "thermostat": - return [types.TARGET_TEMPERATURE_CTYPE, types.TEMPERATURE_UNITS_CTYPE, types.CURRENTHEATINGCOOLING_CTYPE, types.TARGETHEATINGCOOLING_CTYPE]; - case "sensorMultilevel.Temperature": - return [types.CURRENT_TEMPERATURE_CTYPE, types.TEMPERATURE_UNITS_CTYPE]; - case "sensorBinary.Door/Window": - return [types.CURRENT_DOOR_STATE_CTYPE, types.TARGET_DOORSTATE_CTYPE, types.OBSTRUCTION_DETECTED_CTYPE]; - case "battery.Battery": - return [types.BATTERY_LEVEL_CTYPE, types.STATUS_LOW_BATTERY_CTYPE]; - } -} -*/ - ZWayServerPlatform.prototype = { zwayRequest: function(opts){ @@ -116,7 +97,7 @@ opts.proxy = 'http://localhost:8888'; request(opts, function(error, response, body){ if(response.statusCode == 401){ - that.log("Authenticating..."); + debug("Authenticating..."); request({ method: "POST", url: that.url + 'ZAutomation/api/v1/login', @@ -139,7 +120,7 @@ proxy: 'http://localhost:8888', if(response.statusCode == 200){ that.sessionId = body.data.sid; opts.headers["Cookie"] = "ZWAYSession=" + that.sessionId; - that.log("Authenticated. Resubmitting original request..."); + debug("Authenticated. Resubmitting original request..."); request(opts, function(error, response, body){ if(response.statusCode == 200){ deferred.resolve(body); @@ -162,7 +143,7 @@ proxy: 'http://localhost:8888', , accessories: function(callback) { - this.log("Fetching Z-Way devices..."); + debug("Fetching Z-Way devices..."); var that = this; var foundAccessories = []; @@ -175,7 +156,7 @@ proxy: 'http://localhost:8888', var groupedDevices = {}; for(var i = 0; i < devices.length; i++){ var vdev = devices[i]; - if(vdev.tags.indexOf("HomeBridge:Skip") >= 0) { that.log("Tag says skip!"); continue; } + if(vdev.tags.indexOf("HomeBridge:Skip") >= 0) { debug("Tag says skip!"); continue; } var gdid = vdev.id.replace(/^(.*?)_zway_(\d+-\d+)-\d.*/, '$1_$2'); var gd = groupedDevices[gdid] || (groupedDevices[gdid] = {devices: [], types: {}, primary: undefined}); gd.devices.push(vdev); @@ -187,10 +168,10 @@ proxy: 'http://localhost:8888', if(!groupedDevices.hasOwnProperty(gdid)) continue; // Debug/log... - that.log('Got grouped device ' + gdid + ' consiting of devices:'); + debug('Got grouped device ' + gdid + ' consiting of devices:'); var gd = groupedDevices[gdid]; for(var j = 0; j < gd.devices.length; j++){ - that.log(gd.devices[j].id + " - " + gd.devices[j].deviceType + (gd.devices[j].metrics && gd.devices[j].metrics.probeTitle ? "." + gd.devices[j].metrics.probeTitle : "")); + debug(gd.devices[j].id + " - " + gd.devices[j].deviceType + (gd.devices[j].metrics && gd.devices[j].metrics.probeTitle ? "." + gd.devices[j].metrics.probeTitle : "")); } var accessory = null; @@ -199,14 +180,14 @@ proxy: 'http://localhost:8888', gd.primary = gd.types[zwshkDeviceClasses[ti].primaryType]; var pd = gd.devices[gd.primary]; var name = pd.metrics && pd.metrics.title ? pd.metrics.title : pd.id; - that.log("Using class with primaryType " + zwshkDeviceClasses[ti].primaryType + ", " + name + " (" + pd.id + ") as primary."); + debug("Using class with primaryType " + zwshkDeviceClasses[ti].primaryType + ", " + name + " (" + pd.id + ") as primary."); accessory = new ZWayServerAccessory(name, zwshkDeviceClasses[ti], gd, that); break; } } if(!accessory) - that.log("WARN: Didn't find suitable device class!"); + debug("WARN: Didn't find suitable device class!"); else foundAccessories.push(accessory); @@ -238,360 +219,6 @@ ZWayServerAccessory.prototype = { qs: (value === undefined ? undefined : value) }); }, - - /* - informationCharacteristics: function() { - return [ - { - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.name, - supportEvents: false, - supportBonjour: false, - manfDescription: "Name of the accessory", - designedMaxLength: 255 - },{ - cType: types.MANUFACTURER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Z-Wave.me", - supportEvents: false, - supportBonjour: false, - manfDescription: "Manufacturer", - designedMaxLength: 255 - },{ - cType: types.MODEL_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "VDev", - supportEvents: false, - supportBonjour: false, - manfDescription: "Model", - designedMaxLength: 255 - },{ - cType: types.SERIAL_NUMBER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "", - 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 - } - ] - }, - - controlCharacteristics: function(vdev) { - var that = this; - var cTypes = []; - - var cxs = ZWayServerPlatform.getVDevCharacteristicsTypes(vdev); - - if(!cxs || cxs.length <= 0) return cTypes; - - if (cxs.indexOf(types.POWER_STATE_CTYPE) >= 0) { - cTypes.push({ - cType: types.POWER_STATE_CTYPE, - onUpdate: function(value) { - if (value == 0) { - that.command(vdev, "off"); - } else { - that.command(vdev, "on"); - } - }, - perms: ["pw","pr","ev"], - format: "bool", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Change the power state", - designedMaxLength: 1 - }); - } - - if (cxs.indexOf(types.BRIGHTNESS_CTYPE) >= 0) { - cTypes.push({ - cType: types.BRIGHTNESS_CTYPE, - onUpdate: function(value) { - that.command(vdev, "exact", {level: parseInt(value, 10)}); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust Brightness of Light", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - } - - if (cxs.indexOf(types.CURRENT_TEMPERATURE_CTYPE) >= 0) { - cTypes.push({ - cType: types.CURRENT_TEMPERATURE_CTYPE, - onUpdate: null, - onRead: function(callback) { - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - callback(result.data.metrics.level); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: 20, - supportEvents: false, - supportBonjour: false, - manfDescription: "Current Temperature", - unit: "celsius" - }); - } - - if (cxs.indexOf(types.TARGET_TEMPERATURE_CTYPE) >= 0) { - cTypes.push({ - cType: types.TARGET_TEMPERATURE_CTYPE, - onUpdate: function(value) { - try { - that.command(vdev, "exact", {level: parseFloat(value)}); - } - catch (e) { - that.log(e); - } - }, - onRead: function(callback) { - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - callback(result.data.metrics.level); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 20, - supportEvents: false, - supportBonjour: false, - manfDescription: "Target Temperature", - designedMinValue: vdev.metrics && vdev.metrics.min !== undefined ? vdev.metrics.min : 5, - designedMaxValue: vdev.metrics && vdev.metrics.max !== undefined ? vdev.metrics.max : 40, - designedMinStep: 1, - unit: "celsius" - }); - } - - if (cxs.indexOf(types.TEMPERATURE_UNITS_CTYPE) >= 0) { - cTypes.push({ - cType: types.TEMPERATURE_UNITS_CTYPE, - perms: ["pr"], - format: "int", - //TODO: Let this update from the device if it changes after startup. - initialValue: vdev.metrics.scaleTitle.indexOf("F") >= 0 ? 1 : 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Unit", - }); - } - - if (cxs.indexOf(types.CURRENTHEATINGCOOLING_CTYPE) >= 0) { - cTypes.push({ - cType: types.CURRENTHEATINGCOOLING_CTYPE, - //TODO: Support multifunction thermostats...only heating supported now. - / * - onUpdate: null, - onRead: function(callback) { - that.getCurrentHeatingCooling(function(currentHeatingCooling){ - callback(currentHeatingCooling); - }); - }, - * / - perms: ["pr"], - format: "int", - initialValue: 1, - supportEvents: false, - supportBonjour: false, - manfDescription: "Current Mode", - designedMaxLength: 1, - designedMinValue: 0, - designedMaxValue: 2, - designedMinStep: 1, - }); - } - - if (cxs.indexOf(types.TARGETHEATINGCOOLING_CTYPE) >= 0) { - cTypes.push({ - cType: types.TARGETHEATINGCOOLING_CTYPE, - //TODO: Support multifunction thermostats...only heating supported now. - / * - onUpdate: function(value) { - that.setTargetHeatingCooling(value); - }, - onRead: function(callback) { - that.getTargetHeatingCoooling(function(targetHeatingCooling){ - callback(targetHeatingCooling); - }); - }, - * / - perms: ["pr"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Target Mode", - designedMinValue: 0, - designedMaxValue: 3, - designedMinStep: 1, - }); - } - - if (cxs.indexOf(types.CONTACT_SENSOR_STATE_CTYPE) >= 0) { - cTypes.push({ - cType: types.CONTACT_SENSOR_STATE_CTYPE, - onUpdate: null, - onRead: function(callback) { - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - callback(result.data.metrics.level == "off" ? 1 : 0); - }); - }, - perms: ["pr","ev"], - format: "bool", - initialValue: 1, - supportEvents: false, - supportBonjour: false, - manfDescription: "Contact State", - designedMaxLength: 1 - }); - } - - if (cxs.indexOf(types.CURRENT_DOOR_STATE_CTYPE) >= 0) { - cTypes.push({ - cType: types.CURRENT_DOOR_STATE_CTYPE, - onRead: function(callback) { - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - callback(result.data.metrics.level == "off" ? 1 : 0); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: 1, - supportEvents: false, - supportBonjour: false, - manfDescription: "Current Door State", - designedMinValue: 0, - designedMaxValue: 4, - designedMinStep: 1, - designedMaxLength: 1 - }); - } - - if (cxs.indexOf(types.TARGET_DOORSTATE_CTYPE) >= 0) { - cTypes.push({ - cType: types.TARGET_DOORSTATE_CTYPE, - onRead: function(callback) { - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - callback(result.data.metrics.level == "off" ? 0 : 1); - }); - }, - perms: ["pr","ev"], //TODO: If we support some non-sensor device that can actually open, add "pw"! - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Target Door State", - designedMinValue: 0, - designedMaxValue: 1, - designedMinStep: 1, - designedMaxLength: 1 - }); - } - - if (cxs.indexOf(types.OBSTRUCTION_DETECTED_CTYPE) >= 0) { - cTypes.push({ - cType: types.OBSTRUCTION_DETECTED_CTYPE, - perms: ["pr","ev"], - format: "bool", - initialValue: false, - supportEvents: false, - supportBonjour: false, - manfDescription: "Obstruction Detected" - }); - } - - if (cxs.indexOf(types.BATTERY_LEVEL_CTYPE) >= 0) { - cTypes.push({ - cType: types.BATTERY_LEVEL_CTYPE, - onRead: function(callback) { - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - callback(result.data.metrics.level); - }); - }, - perms: ["pr","ev"], - format: "uint8", - initialValue: 100, - supportEvents: true, - supportBonjour: false, - manfDescription: "Battery Level", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - } - - if (cxs.indexOf(types.STATUS_LOW_BATTERY_CTYPE) >= 0) { - cTypes.push({ - cType: types.STATUS_LOW_BATTERY_CTYPE, - onUpdate: null, - onRead: function(callback) { - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - callback(result.data.metrics.level <= that.platform.batteryLow ? 1 : 0); - }); - }, - perms: ["pr","ev"], - format: "uint8", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Battery is low", - designedMaxLength: 1 - }); - } - - return cTypes; - }, - */ getVDevServices: function(vdev){ var typeKey = ZWayServerPlatform.getVDevTypeKey(vdev); @@ -647,6 +274,12 @@ ZWayServerAccessory.prototype = { map[(new Characteristic.ChargingState).UUID] = ["battery.Battery"]; //TODO: Always a fixed result } + if(cx instanceof Characteristic.Name) return vdevPreferred; + + // Special case!: If cx is a CurrentTemperature, ignore the preferred device...we want the sensor if available! + if(cx instanceof Characteristic.CurrentTemperature) vdevPreferred = null; + // + var typekeys = map[cx.UUID]; if(typekeys === undefined) return null; @@ -668,19 +301,29 @@ ZWayServerAccessory.prototype = { var that = this; var gdv = function(){ - that.log("Default value for " + vdev.metrics.title + " is " + vdev.metrics.level); + debug("Default value for " + vdev.metrics.title + " is " + vdev.metrics.level); return vdev.metrics.level; }; + if(cx instanceof Characteristic.Name){ + cx.getDefaultValue = function(){ return this.name; }; + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + callback(false, that.name); + }); + cx.writable = false; + return cx; + } + if(cx instanceof Characteristic.On){ cx.getDefaultValue = gdv; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); that.platform.zwayRequest({ method: "GET", url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id }).then(function(result){ - that.log("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); + debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); var val; if(result.data.metrics.level === "off"){ val = false; @@ -703,12 +346,12 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.Brightness){ cx.getDefaultValue = gdv; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); that.platform.zwayRequest({ method: "GET", url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id }).then(function(result){ - that.log("Got value " + result.data.metrics.level + " for " + vdev.metrics.title + "."); + debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); callback(false, result.data.metrics.level); }); }.bind(this)); @@ -723,11 +366,12 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.CurrentTemperature){ cx.getDefaultValue = gdv; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); that.platform.zwayRequest({ method: "GET", url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id }).then(function(result){ + debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); callback(false, result.data.metrics.level); }); }.bind(this)); @@ -739,7 +383,7 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.TargetTemperature){ cx.getDefaultValue = gdv; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); this.platform.zwayRequest({ method: "GET", url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id @@ -749,6 +393,7 @@ ZWayServerAccessory.prototype = { }.bind(this)); cx.on('set', function(level, callback){ this.command(vdev, "exact", {level: parseInt(level, 10)}).then(function(result){ + debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); callback(); }); }.bind(this)); @@ -761,7 +406,7 @@ ZWayServerAccessory.prototype = { //TODO: Always in °C for now. cx.getDefaultValue = function(){ return Characteristic.TemperatureDisplayUnits.CELCIUS; }; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.TemperatureDisplayUnits.CELCIUS); }); cx.writable = false; @@ -772,7 +417,7 @@ ZWayServerAccessory.prototype = { //TODO: Always HEAT for now, we don't have an example to work with that supports another function. cx.getDefaultValue = function(){ return Characteristic.CurrentHeatingCoolingState.HEAT; }; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.CurrentHeatingCoolingState.HEAT); }); return cx; @@ -782,7 +427,7 @@ ZWayServerAccessory.prototype = { //TODO: Always HEAT for now, we don't have an example to work with that supports another function. cx.getDefaultValue = function(){ return Characteristic.TargetHeatingCoolingState.HEAT; }; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.TargetHeatingCoolingState.HEAT); }); cx.writable = false; @@ -794,11 +439,12 @@ ZWayServerAccessory.prototype = { return vdev.metrics.level == "off" ? Characteristic.CurrentDoorState.CLOSED : Characteristic.CurrentDoorState.OPEN; }; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); this.platform.zwayRequest({ method: "GET", url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id }).then(function(result){ + debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); callback(false, result.data.metrics.level == "off" ? Characteristic.CurrentDoorState.CLOSED : Characteristic.CurrentDoorState.OPEN); }); }.bind(this)); @@ -808,7 +454,7 @@ ZWayServerAccessory.prototype = { //TODO: We only support this for Door sensors now, so it's a fixed value. cx.getDefaultValue = function(){ return Characteristic.TargetDoorState.CLOSED; }; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.TargetDoorState.CLOSED); }); //cx.readable = false; @@ -819,7 +465,7 @@ ZWayServerAccessory.prototype = { //TODO: We only support this for Door sensors now, so it's a fixed value. cx.getDefaultValue = function(){ return false; }; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, false); }); //cx.readable = false; @@ -829,11 +475,12 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.BatteryLevel){ cx.getDefaultValue = gdv; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); that.platform.zwayRequest({ method: "GET", url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id }).then(function(result){ + debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); callback(false, result.data.metrics.level); }); }.bind(this)); @@ -842,11 +489,12 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.StatusLowBattery){ cx.getDefaultValue = function(){ return Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; }; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); that.platform.zwayRequest({ method: "GET", url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id }).then(function(result){ + debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); callback(false, result.data.metrics.level <= that.platform.batteryLow ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); }); }.bind(this)); @@ -856,7 +504,7 @@ ZWayServerAccessory.prototype = { //TODO: No known chargeable devices(?), so always return false. cx.getDefaultValue = function(){ return Characteristic.ChargingState.NOT_CHARGING; }; cx.on('get', function(callback, context){ - that.log("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.ChargingState.NOT_CHARGING); }); //cx.readable = false; @@ -872,7 +520,7 @@ ZWayServerAccessory.prototype = { var vdev = this.getVDevForCharacteristic(cx, vdev); if(!vdev){ success = false; - this.log("ERROR! Failed to configure required characteristic \"" + service.characteristics[i].displayName + "\"!"); + debug("ERROR! Failed to configure required characteristic \"" + service.characteristics[i].displayName + "\"!"); } cx = this.configureCharacteristic(cx, vdev); } @@ -881,6 +529,7 @@ ZWayServerAccessory.prototype = { var vdev = this.getVDevForCharacteristic(cx); if(!vdev) continue; cx = this.configureCharacteristic(cx, vdev); + if(cx) service.addCharacteristic(cx); } return success; } @@ -903,7 +552,7 @@ ZWayServerAccessory.prototype = { if(this.devDesc.types["battery.Battery"]) services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.types["battery.Battery"]])); - this.log("Loaded services for " + this.name); + debug("Loaded services for " + this.name); return services; } }; From a677edb2cf4c44d39fde54623f410711d1260897 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Fri, 4 Sep 2015 06:18:23 +0200 Subject: [PATCH 08/71] Trying for a stable baseline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switched Door sensors back to GarageDoorOpener services for now, and disabled battery service…lets see if we can get this stabilized. --- platforms/ZWayServer.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index f595408..9a515ee 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -237,7 +237,7 @@ ZWayServerAccessory.prototype = { services.push(new Service.TemperatureSensor(vdev.metrics.title)); break; case "sensorBinary.Door/Window": - services.push(new Service.Door(vdev.metrics.title)); + services.push(new Service.GarageDoorOpener(vdev.metrics.title)); break; case "battery.Battery": services.push(new Service.BatteryService(vdev.metrics.title)); @@ -388,6 +388,7 @@ ZWayServerAccessory.prototype = { method: "GET", url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id }).then(function(result){ + debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); callback(false, result.data.metrics.level); }); }.bind(this)); @@ -549,8 +550,9 @@ ZWayServerAccessory.prototype = { services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.primary])); - if(this.devDesc.types["battery.Battery"]) - services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.types["battery.Battery"]])); + if(this.devDesc.types["battery.Battery"]){ + //services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.types["battery.Battery"]])); + } debug("Loaded services for " + this.name); return services; From 70b5a9142a55c7e35da3c9ca3506903b387824b4 Mon Sep 17 00:00:00 2001 From: Kai Date: Sun, 6 Sep 2015 04:31:05 +0200 Subject: [PATCH 09/71] adding resolver chain workaround for raspberry pi adding the workaround from https://github.com/agnat/node_mdns/issues/130 to fix #140 --- platforms/YamahaAVR.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/platforms/YamahaAVR.js b/platforms/YamahaAVR.js index f0d10c1..f554fa0 100644 --- a/platforms/YamahaAVR.js +++ b/platforms/YamahaAVR.js @@ -1,13 +1,19 @@ var types = require("HAP-NodeJS/accessories/types.js"); var Yamaha = require('yamaha-nodejs'); var mdns = require('mdns'); +//workaround for raspberry pi +var sequence = [ + mdns.rst.DNSServiceResolve(), + 'DNSServiceGetAddrInfo' in mdns.dns_sd ? mdns.rst.DNSServiceGetAddrInfo() : mdns.rst.getaddrinfo({families:[4]}), + mdns.rst.makeAddressesUnique() +]; function YamahaAVRPlatform(log, config){ this.log = log; this.config = config; this.playVolume = config["play_volume"]; this.setMainInputTo = config["setMainInputTo"]; - this.browser = mdns.createBrowser(mdns.tcp('http')); + this.browser = mdns.createBrowser(mdns.tcp('http'), {resolverSequence: sequence}); } YamahaAVRPlatform.prototype = { From 6614705d6c96fcdb9b7a3bc5d7cd0dc019cc3769 Mon Sep 17 00:00:00 2001 From: Raoul Date: Mon, 7 Sep 2015 15:27:30 +0200 Subject: [PATCH 10/71] missing comma should have used fsonlint.com after adding the comments and secriptions --- config-sample-knx.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config-sample-knx.json b/config-sample-knx.json index df0de60..a8e52b1 100644 --- a/config-sample-knx.json +++ b/config-sample-knx.json @@ -72,7 +72,7 @@ }, { "accessory_type": "knxdevice", - "description":"sample device with multiple services. Multiple services of different types are widely supported" + "description":"sample device with multiple services. Multiple services of different types are widely supported", "name": "Office", "services": [ { @@ -118,4 +118,4 @@ } ], "accessories": [] -} \ No newline at end of file +} From 78987a775fc31436154ca62328ca6f4c0534f818 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Mon, 7 Sep 2015 19:16:51 +0200 Subject: [PATCH 11/71] Might be stable now for Switches, Dimmers, & Thermostats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Basically, I needed to provide a default value instead of a `getDefaultValue` function. Keeping with this for a while before trying battery stats and door sensors again, but I think I figured out my compliance problems…don’t know how it ever successfully added before, actually. Also removed some more cruft from the earlier old-API version and started laying some groundwork for polling/updating from ZWay. --- platforms/ZWayServer.js | 152 +++++++++++++++++++--------------------- 1 file changed, 74 insertions(+), 78 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 9a515ee..da1496e 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -57,6 +57,7 @@ function ZWayServerPlatform(log, config){ this.password = config["password"]; this.name_overrides = config["name_overrides"]; this.batteryLow = config["battery_low_level"]; + this.pollInterval = config["poll_interval"] || 2; this.userAgent = "HomeBridge/-1^0.5"; this.sessionId = ""; this.jar = request.jar(new tough.CookieJar()); @@ -66,22 +67,6 @@ ZWayServerPlatform.getVDevTypeKey = function(vdev){ return vdev.deviceType + (vdev.metrics && vdev.metrics.probeTitle ? "." + vdev.metrics.probeTitle : "") } -ZWayServerPlatform.getVDevServiceTypes = function(vdev){ - var typeKey = ZWayServerPlatform.getVDevTypeKey(vdev); - switch (typeKey) { - case "switchBinary": - return [types.SWITCH_STYPE]; - case "switchMultilevel": - return [types.LIGHTBULB_STYPE]; - case "thermostat": - return [types.THERMOSTAT_STYPE]; - case "sensorMultilevel.Temperature": - return [types.TEMPERATURE_SENSOR_STYPE]; - case "sensorBinary.Door/Window": - return [types.GARAGE_DOOR_OPENER_STYPE]; - } -} - ZWayServerPlatform.prototype = { zwayRequest: function(opts){ @@ -211,7 +196,14 @@ function ZWayServerAccessory(name, dclass, devDesc, platform) { ZWayServerAccessory.prototype = { - + + getVDev: function(vdev){ + return this.platform.zwayRequest({ + method: "GET", + url: this.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id + })//.then(function()); + } + , command: function(vdev, command, value) { return this.platform.zwayRequest({ method: "GET", @@ -237,7 +229,7 @@ ZWayServerAccessory.prototype = { services.push(new Service.TemperatureSensor(vdev.metrics.title)); break; case "sensorBinary.Door/Window": - services.push(new Service.GarageDoorOpener(vdev.metrics.title)); + //services.push(new Service.GarageDoorOpener(vdev.metrics.title)); break; case "battery.Battery": services.push(new Service.BatteryService(vdev.metrics.title)); @@ -316,23 +308,23 @@ ZWayServerAccessory.prototype = { } if(cx instanceof Characteristic.On){ - cx.getDefaultValue = gdv; + cx.zway_getValueFromVDev = function(vdev){ + var val = false; + if(vdev.metrics.level === "off"){ + val = false; + } else if(vdev.metrics.level <= 5) { + val = false; + } else if (vdev.metrics.level > 5) { + val = true; + } + return val; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); - var val; - if(result.data.metrics.level === "off"){ - val = false; - } else if(val <= 5) { - val = false; - } else if (val > 5) { - val = true; - } - callback(false, val); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); cx.on('set', function(powerOn, callback){ @@ -344,15 +336,15 @@ ZWayServerAccessory.prototype = { } if(cx instanceof Characteristic.Brightness){ - cx.getDefaultValue = gdv; + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.level; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); - callback(false, result.data.metrics.level); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); cx.on('set', function(level, callback){ @@ -364,15 +356,15 @@ ZWayServerAccessory.prototype = { } if(cx instanceof Characteristic.CurrentTemperature){ - cx.getDefaultValue = gdv; + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.level; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); - callback(false, result.data.metrics.level); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); cx.minimumValue = vdev.metrics && vdev.metrics.min !== undefined ? vdev.metrics.min : -40; @@ -381,15 +373,15 @@ ZWayServerAccessory.prototype = { } if(cx instanceof Characteristic.TargetTemperature){ - cx.getDefaultValue = gdv; + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.level; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); - this.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); - callback(false, result.data.metrics.level); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); cx.on('set', function(level, callback){ @@ -405,10 +397,11 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.TemperatureDisplayUnits){ //TODO: Always in °C for now. - cx.getDefaultValue = function(){ return Characteristic.TemperatureDisplayUnits.CELCIUS; }; + cx.getDefaultValue = function(){ return Characteristic.TemperatureDisplayUnits.CELSIUS; }; + cx.value = cx.getDefaultValue(); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); - callback(false, Characteristic.TemperatureDisplayUnits.CELCIUS); + callback(false, Characteristic.TemperatureDisplayUnits.CELSIUS); }); cx.writable = false; return cx; @@ -417,6 +410,7 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.CurrentHeatingCoolingState){ //TODO: Always HEAT for now, we don't have an example to work with that supports another function. cx.getDefaultValue = function(){ return Characteristic.CurrentHeatingCoolingState.HEAT; }; + cx.value = cx.getDefaultValue(); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.CurrentHeatingCoolingState.HEAT); @@ -427,6 +421,7 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.TargetHeatingCoolingState){ //TODO: Always HEAT for now, we don't have an example to work with that supports another function. cx.getDefaultValue = function(){ return Characteristic.TargetHeatingCoolingState.HEAT; }; + cx.value = cx.getDefaultValue(); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.TargetHeatingCoolingState.HEAT); @@ -436,17 +431,15 @@ ZWayServerAccessory.prototype = { } if(cx instanceof Characteristic.CurrentDoorState){ - cx.getDefaultValue = function(){ - return vdev.metrics.level == "off" ? Characteristic.CurrentDoorState.CLOSED : Characteristic.CurrentDoorState.OPEN; + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.level === "off" ? Characteristic.CurrentDoorState.CLOSED : Characteristic.CurrentDoorState.OPEN; }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); - this.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); - callback(false, result.data.metrics.level == "off" ? Characteristic.CurrentDoorState.CLOSED : Characteristic.CurrentDoorState.OPEN); + 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)); } @@ -454,6 +447,7 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.TargetDoorState){ //TODO: We only support this for Door sensors now, so it's a fixed value. cx.getDefaultValue = function(){ return Characteristic.TargetDoorState.CLOSED; }; + cx.value = cx.getDefaultValue(); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.TargetDoorState.CLOSED); @@ -465,6 +459,7 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.ObstructionDetected){ //TODO: We only support this for Door sensors now, so it's a fixed value. cx.getDefaultValue = function(){ return false; }; + cx.value = cx.getDefaultValue(); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, false); @@ -474,29 +469,29 @@ ZWayServerAccessory.prototype = { } if(cx instanceof Characteristic.BatteryLevel){ - cx.getDefaultValue = gdv; + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.level; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); - callback(false, result.data.metrics.level); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); } if(cx instanceof Characteristic.StatusLowBattery){ - cx.getDefaultValue = function(){ return Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; }; + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.level <= that.platform.batteryLow ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); - that.platform.zwayRequest({ - method: "GET", - url: that.platform.url + 'ZAutomation/api/v1/devices/' + vdev.id - }).then(function(result){ - debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); - callback(false, result.data.metrics.level <= that.platform.batteryLow ? Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL); + 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)); } @@ -504,6 +499,7 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.ChargingState){ //TODO: No known chargeable devices(?), so always return false. cx.getDefaultValue = function(){ return Characteristic.ChargingState.NOT_CHARGING; }; + cx.value = cx.getDefaultValue(); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.ChargingState.NOT_CHARGING); From a4c3f73eb54620abfe73ad95627110d34f8df0bb Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Mon, 7 Sep 2015 13:29:51 -0700 Subject: [PATCH 12/71] [MiLight] Fix scope issue that prevented config from working, added documentation, and added correct pause for night mode commands --- accessories/MiLight.js | 68 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 7 deletions(-) diff --git a/accessories/MiLight.js b/accessories/MiLight.js index 85b09d6..cfe339a 100644 --- a/accessories/MiLight.js +++ b/accessories/MiLight.js @@ -1,3 +1,53 @@ +/* + +MiLight accessory shim for Homebridge +Written by Sam Edwards (https://samedwards.ca/) + +Uses the node-milight-promise library (https://github.com/mwittig/node-milight-promise) which features some code from +applamp.nl (http://www.applamp.nl/service/applamp-api/) and uses other details from (http://www.limitlessled.com/dev/) + +Configure in config.json as follows: + +"accessories": [ + { + "accessory":"MiLight", + "name": "Lamp", + "ip_address": "255.255.255.255", + "port": 8899, + "zone": 1, + "type": "rgbw", + "delay": 30, + "repeat": 3 + } +] + +Where the parameters are: + *accessory (required): This must be "MiLight", and refers to the name of the accessory as exported from this file + *name (required): The name for this light/zone, as passed on to Homebridge and HomeKit + *ip_address (optional): The IP address of the WiFi Bridge. Default to the broadcast address of 255.255.255.255 if not specified + *port (optional): Port of the WiFi bridge. Defaults to 8899 if not specified + *zone (required): The zone to target with this accessory. "0" for all zones on the bridge, otherwise 1-4 for a specific zone + *type (required): One of either "rgbw", "rgb", or "white", depending on the type of bulb being controlled + *delay (optional): Delay between commands sent over UDP. Default 30ms + *repeat (optional): Number of times to repeat the UDP command for better reliability. Default 3 + +Tips and Tricks: + *Setting the brightness of an rgbw or a white bulb will set it to "night mode", which is dimmer than the lowest brightness setting + *White and rgb bulbs don't support absolute brightness setting, so we just send a brightness up/brightness down command depending + if we got a percentage above/below 50% respectively + *The only exception to the above is that white bulbs support a "maximum brightness" command, so we send that when we get 100% + *Implemented warmer/cooler for white lamps in a similar way to brightnes, except this time above/below 180 degrees on the colour wheel + *I welcome feedback on a better way to work the brightness/hue for white and rgb bulbs + +Troubleshooting: +The node-milight-promise library provides additional debugging output when the MILIGHT_DEBUG environmental variable is set + +TODO: + *Probably convert this module to a platform that can configure an entire bridge at once, just passing a name for each zone + *Possibly build in some sort of state logging and persistance so that we can answswer HomeKit status queries to the best of our ability + +*/ + var Service = require("HAP-NodeJS").Service; var Characteristic = require("HAP-NodeJS").Characteristic; var Milight = require('node-milight-promise').MilightController; @@ -18,15 +68,15 @@ function MiLight(log, config) { this.type = config["type"]; this.delay = config["delay"]; this.repeat = config["repeat"]; -} -var light = new Milight({ - ip: this.ip_address, - port: this.port, - delayBetweenCommands: this.delay, - commandRepeat: this.repeat + var light = new Milight({ + ip: this.ip_address, + port: this.port, + delayBetweenCommands: this.delay, + commandRepeat: this.repeat }); +} MiLight.prototype = { setPowerState: function(powerOn, callback) { @@ -42,12 +92,15 @@ MiLight.prototype = { setBrightness: function(level, callback) { if (level <= 2 && (this.type == "rgbw" || this.type == "white")) { + // If setting brightness to 2 or lower, instead set night mode for lamps that support it this.log("Setting night mode", level); light.sendCommands(commands[this.type].off(this.zone)); - // Not sure if this timing is going to work or not? It's supposed to be 100ms after the off command + // Ensure we're pausing for 100ms between these commands as per the spec + light.pause(100); light.sendCommands(commands[this.type].nightMode(this.zone)); + } else { this.log("Setting brightness to %s", level); @@ -58,6 +111,7 @@ MiLight.prototype = { if (this.type == "rgbw") { light.sendCommands(commands.rgbw.brightness(level)); } else { + // If this is an rgb or a white lamp, they only support brightness up and down. // Set brightness up when value is >50 and down otherwise. Not sure how well this works real-world. if (level >= 50) { From 2710412ca669055a042338bebd12c3adf1671893 Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Mon, 7 Sep 2015 16:45:27 -0700 Subject: [PATCH 13/71] [MiLight] Correctly reference light object, and also fix bug with brightness=0 setting the lamp to night mode --- accessories/MiLight.js | 46 +++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/accessories/MiLight.js b/accessories/MiLight.js index cfe339a..c5ce67c 100644 --- a/accessories/MiLight.js +++ b/accessories/MiLight.js @@ -69,60 +69,62 @@ function MiLight(log, config) { this.delay = config["delay"]; this.repeat = config["repeat"]; - var light = new Milight({ + this.light = new Milight({ ip: this.ip_address, port: this.port, delayBetweenCommands: this.delay, commandRepeat: this.repeat -}); + }); } MiLight.prototype = { setPowerState: function(powerOn, callback) { if (powerOn) { - light.sendCommands(commands[this.type].on(this.zone)); this.log("Setting power state to on"); + this.light.sendCommands(commands[this.type].on(this.zone)); } else { - light.sendCommands(commands[this.type].off(this.zone)); this.log("Setting power state to off"); + this.light.sendCommands(commands[this.type].off(this.zone)); } callback(); }, setBrightness: function(level, callback) { - if (level <= 2 && (this.type == "rgbw" || this.type == "white")) { - + if (level == 0) { + // If brightness is set to 0, turn off the lamp + this.log("Setting brightness to 0 (off)"); + this.light.sendCommands(commands[this.type].off(this.zone)); + } else if (level <= 2 && (this.type == "rgbw" || this.type == "white")) { // If setting brightness to 2 or lower, instead set night mode for lamps that support it this.log("Setting night mode", level); - light.sendCommands(commands[this.type].off(this.zone)); + this.light.sendCommands(commands[this.type].off(this.zone)); // Ensure we're pausing for 100ms between these commands as per the spec - light.pause(100); - light.sendCommands(commands[this.type].nightMode(this.zone)); + this.light.pause(100); + this.light.sendCommands(commands[this.type].nightMode(this.zone)); } else { this.log("Setting brightness to %s", level); // Send on command to ensure we're addressing the right bulb - light.sendCommands(commands[this.type].on(this.zone)); + this.light.sendCommands(commands[this.type].on(this.zone)); // If this is an rgbw lamp, set the absolute brightness specified if (this.type == "rgbw") { - light.sendCommands(commands.rgbw.brightness(level)); + this.light.sendCommands(commands.rgbw.brightness(level)); } else { - // If this is an rgb or a white lamp, they only support brightness up and down. // Set brightness up when value is >50 and down otherwise. Not sure how well this works real-world. if (level >= 50) { if (this.type == "white" && level == 100) { // But the white lamps do have a "maximum brightness" command - light.sendCommands(commands.white.maxBright(this.zone)); + this.light.sendCommands(commands.white.maxBright(this.zone)); } else { - light.sendCommands(commands[this.type].brightUp()); + this.light.sendCommands(commands[this.type].brightUp()); } } else { - light.sendCommands(commands[this.type].brightDown()); + this.light.sendCommands(commands[this.type].brightDown()); } } } @@ -132,23 +134,25 @@ MiLight.prototype = { setHue: function(value, callback) { this.log("Setting hue to %s", value); + var hue = Array(value, 0, 0); + // Send on command to ensure we're addressing the right bulb - light.sendCommands(commands[this.type].on(this.zone)); + this.light.sendCommands(commands[this.type].on(this.zone)); if (this.type == "rgbw") { if (value == 0) { - light.sendCommands(commands.rgbw.whiteMode(this.zone)); + this.light.sendCommands(commands.rgbw.whiteMode(this.zone)); } else { - light.sendCommands(commands.rgbw.hue(commands.rgbw.hsvToMilightColor(Array(value, 0, 0)))); + this.light.sendCommands(commands.rgbw.hue(commands.rgbw.hsvToMilightColor(hue))); } } else if (this.type == "rgb") { - light.sendCommands(commands.rgb.hue(commands.rgbw.hsvToMilightColor(Array(value, 0, 0)))); + this.light.sendCommands(commands.rgb.hue(commands.rgbw.hsvToMilightColor(hue))); } else if (this.type == "white") { // Again, white lamps don't support setting an absolue colour temp, so trying to do warmer/cooler step at a time based on colour if (value >= 180) { - light.sendCommands(commands.white.warmer()); + this.light.sendCommands(commands.white.warmer()); } else { - light.sendCommands(commands.white.cooler()); + this.light.sendCommands(commands.white.cooler()); } } From 1df411d916e73b73faea5787c417e1a39f69bd85 Mon Sep 17 00:00:00 2001 From: Raoul Date: Tue, 8 Sep 2015 09:25:44 +0200 Subject: [PATCH 14/71] TESTING NEEDED Proposing idea to de-asynchronize Sonos device discovery with a timeout event for push. I have no IDE at hand right now, so please check syntax before merging! I have no Sonos devices, so please check somebody with the hardware at hand! Thanks Raoul --- platforms/Sonos.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/platforms/Sonos.js b/platforms/Sonos.js index 67d086e..1d19c2f 100644 --- a/platforms/Sonos.js +++ b/platforms/Sonos.js @@ -6,6 +6,8 @@ function SonosPlatform(log, config){ this.config = config; this.name = config["name"]; this.playVolume = config["play_volume"]; + // timeout for device discovery + this.discoveryTimeout = (config.deviceDiscoveryTimeout || 10)*1000; // assume 10sec as a default } SonosPlatform.prototype = { @@ -16,6 +18,18 @@ SonosPlatform.prototype = { // track found devices so we don't add duplicates var roomNamesFound = {}; + // collector array for the devices from callbacks + var devicesFound = []; + // tell the sonos callbacks if timeout already occured + var timeout = false; + + // the timeout event will push the accessories back + setTimeout(function(){ + timeout=true; + callback(devicesFound); + }, this.discoveryTimeout); + + sonos.search(function (device) { that.log("Found device at " + device.host); @@ -26,9 +40,13 @@ SonosPlatform.prototype = { if (!roomNamesFound[roomName]) { roomNamesFound[roomName] = true; that.log("Found playable device - " + roomName); + if (timeout) { + that.log("Ignored: Discovered after timeout (Set deviceDiscoveryTimeout parameter in Sonos section of config.json)"); + } // device is an instance of sonos.Sonos var accessory = new SonosAccessory(that.log, that.config, device, description); - callback([accessory]); + // add it to the collector array + devicesFound.push(accessory); } else { that.log("Ignoring playable device with duplicate room name - " + roomName); From abe88b75020c73551747228355ccdcf03701f6d0 Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Tue, 8 Sep 2015 10:41:03 -0700 Subject: [PATCH 15/71] [MiLight] Converted accessory to platform. Not fully tested yet --- {accessories => platforms}/MiLight.js | 81 ++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 14 deletions(-) rename {accessories => platforms}/MiLight.js (73%) diff --git a/accessories/MiLight.js b/platforms/MiLight.js similarity index 73% rename from accessories/MiLight.js rename to platforms/MiLight.js index c5ce67c..d38cb3f 100644 --- a/accessories/MiLight.js +++ b/platforms/MiLight.js @@ -1,6 +1,6 @@ /* -MiLight accessory shim for Homebridge +MiLight platform shim for Homebridge Written by Sam Edwards (https://samedwards.ca/) Uses the node-milight-promise library (https://github.com/mwittig/node-milight-promise) which features some code from @@ -8,28 +8,28 @@ applamp.nl (http://www.applamp.nl/service/applamp-api/) and uses other details f Configure in config.json as follows: -"accessories": [ +"platforms": [ { - "accessory":"MiLight", - "name": "Lamp", + "platform":"MiLight", + "name":"MiLight", "ip_address": "255.255.255.255", "port": 8899, - "zone": 1, "type": "rgbw", "delay": 30, - "repeat": 3 + "repeat": 3, + "zones":["Kitchen Lamp","Bedroom Lamp","Living Room Lamp","Hallway Lamp"] } ] Where the parameters are: - *accessory (required): This must be "MiLight", and refers to the name of the accessory as exported from this file - *name (required): The name for this light/zone, as passed on to Homebridge and HomeKit + *platform (required): This must be "MiLight", and refers to the name of the accessory as exported from this file + *name (optional): The display name used for logging output by Homebridge. Best to set to "MiLight" *ip_address (optional): The IP address of the WiFi Bridge. Default to the broadcast address of 255.255.255.255 if not specified *port (optional): Port of the WiFi bridge. Defaults to 8899 if not specified - *zone (required): The zone to target with this accessory. "0" for all zones on the bridge, otherwise 1-4 for a specific zone - *type (required): One of either "rgbw", "rgb", or "white", depending on the type of bulb being controlled + *type (optional): One of either "rgbw", "rgb", or "white", depending on the type of bulb being controlled. This applies to all zones. Defaults to rgbw. *delay (optional): Delay between commands sent over UDP. Default 30ms *repeat (optional): Number of times to repeat the UDP command for better reliability. Default 3 + *zones (required): An array of the names of the zones, in order, 1-4. Use null if a zone is skipped. RGB lamps can only have a single zone. Tips and Tricks: *Setting the brightness of an rgbw or a white bulb will set it to "night mode", which is dimmer than the lowest brightness setting @@ -43,7 +43,6 @@ Troubleshooting: The node-milight-promise library provides additional debugging output when the MILIGHT_DEBUG environmental variable is set TODO: - *Probably convert this module to a platform that can configure an entire bridge at once, just passing a name for each zone *Possibly build in some sort of state logging and persistance so that we can answswer HomeKit status queries to the best of our ability */ @@ -54,10 +53,64 @@ var Milight = require('node-milight-promise').MilightController; var commands = require('node-milight-promise').commands; module.exports = { - accessory: MiLight + accessory: MiLightAccessory, + platform: MiLightPlatform } -function MiLight(log, config) { +function MiLightPlatform(log, config) { + this.log = log; + + this.config = config; +} + +MiLightPlatform.prototype = { + accessories: function(callback) { + var that = this; + var zones = []; + + // Various error checking + if (this.config.zones) { + var zoneLength = this.config.zones.length; + } else { + this.log("ERROR: Could not read zones from configuration."); + return; + } + + if (!this.config["type"]) { + this.log("INFO: Type not specified, defaulting to rgbw"); + this.config["type"] = "rgbw"; + } + + if (zoneLength == 0) { + this.log("ERROR: No zones found in configuration."); + return; + } else if (this.config["type"] == "rgb" && zoneLength > 1) { + this.log("WARNING: RGB lamps only have a single zone. Only the first defined zone will be used."); + zoneLength = 1; + } else if (zoneLength > 4) { + this.log("WARNING: Only a maximum of 4 zones are supported per bridge. Only recognizing the first 4 zones."); + zoneLength = 4; + } + + // Create lamp accessories for all of the defined zones + for (var i=0; i < zoneLength; i++) { + if (!!this.config.zones[i]) { + this.config["name"] = this.config.zones[i]; + this.config["zone"] = i+1; + lamp = new MiLightAccessory(this.log, this.config); + zones.push(lamp); + } + } + if (zones.length > 0) { + callback(zones); + } else { + this.log("ERROR: Unable to find any valid zones"); + return; + } + } +} + +function MiLightAccessory(log, config) { this.log = log; // config info @@ -77,7 +130,7 @@ function MiLight(log, config) { }); } -MiLight.prototype = { +MiLightAccessory.prototype = { setPowerState: function(powerOn, callback) { if (powerOn) { From 5cccd3f916d93b0dd576a3b31d6f72d4bfb63b89 Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Tue, 8 Sep 2015 11:01:30 -0700 Subject: [PATCH 16/71] [MiLight] Modify logging to show the zone name when used as a platform accessory --- platforms/MiLight.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/platforms/MiLight.js b/platforms/MiLight.js index d38cb3f..7cbf1c9 100644 --- a/platforms/MiLight.js +++ b/platforms/MiLight.js @@ -134,10 +134,10 @@ MiLightAccessory.prototype = { setPowerState: function(powerOn, callback) { if (powerOn) { - this.log("Setting power state to on"); + this.log("["+this.name+"] Setting power state to on"); this.light.sendCommands(commands[this.type].on(this.zone)); } else { - this.log("Setting power state to off"); + this.log("["+this.name+"] Setting power state to off"); this.light.sendCommands(commands[this.type].off(this.zone)); } callback(); @@ -146,11 +146,11 @@ MiLightAccessory.prototype = { setBrightness: function(level, callback) { if (level == 0) { // If brightness is set to 0, turn off the lamp - this.log("Setting brightness to 0 (off)"); + this.log("["+this.name+"] Setting brightness to 0 (off)"); this.light.sendCommands(commands[this.type].off(this.zone)); } else if (level <= 2 && (this.type == "rgbw" || this.type == "white")) { // If setting brightness to 2 or lower, instead set night mode for lamps that support it - this.log("Setting night mode", level); + this.log("["+this.name+"] Setting night mode", level); this.light.sendCommands(commands[this.type].off(this.zone)); // Ensure we're pausing for 100ms between these commands as per the spec @@ -158,7 +158,7 @@ MiLightAccessory.prototype = { this.light.sendCommands(commands[this.type].nightMode(this.zone)); } else { - this.log("Setting brightness to %s", level); + this.log("["+this.name+"] Setting brightness to %s", level); // Send on command to ensure we're addressing the right bulb this.light.sendCommands(commands[this.type].on(this.zone)); @@ -185,7 +185,7 @@ MiLightAccessory.prototype = { }, setHue: function(value, callback) { - this.log("Setting hue to %s", value); + this.log("["+this.name+"] Setting hue to %s", value); var hue = Array(value, 0, 0); @@ -212,7 +212,7 @@ MiLightAccessory.prototype = { }, identify: function(callback) { - this.log("Identify requested!"); + this.log("["+this.name+"] Identify requested!"); callback(); // success }, From 18333242ff3866fb66750123caf001bfe9f74760 Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Tue, 8 Sep 2015 11:20:36 -0700 Subject: [PATCH 17/71] [MiLight] Add missing callback from hue function --- platforms/MiLight.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/platforms/MiLight.js b/platforms/MiLight.js index 7cbf1c9..4382dea 100644 --- a/platforms/MiLight.js +++ b/platforms/MiLight.js @@ -65,7 +65,6 @@ function MiLightPlatform(log, config) { MiLightPlatform.prototype = { accessories: function(callback) { - var that = this; var zones = []; // Various error checking @@ -208,7 +207,7 @@ MiLightAccessory.prototype = { this.light.sendCommands(commands.white.cooler()); } } - + callback(); }, identify: function(callback) { From 7dc168e9dc51178df4e84793488c3ad06192d76f Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Tue, 8 Sep 2015 11:33:17 -0700 Subject: [PATCH 18/71] [MiLight] Update config-sample.json to replace MiLight accessory with MiLight platform --- config-sample.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/config-sample.json b/config-sample.json index 4245bd5..d684c9d 100644 --- a/config-sample.json +++ b/config-sample.json @@ -71,7 +71,17 @@ "platform": "YamahaAVR", "play_volume": -35, "setMainInputTo": "AirPlay" - } + }, + { + "platform": "MiLight", + "name": "MiLight", + "ip_address": "255.255.255.255", + "port": 8899, + "type": "rgbw", + "delay": 30, + "repeat": 3, + "zones":["Kitchen Lamp","Bedroom Lamp","Living Room Lamp","Hallway Lamp"] + } ], "accessories": [ @@ -152,16 +162,6 @@ "port" : 4999, // Port the SER2SOCK process is running on "pin": "1234" // PIN used for arming / disarming }, - { - "accessory":"MiLight", - "name": "Lamp", - "ip_address": "255.255.255.255", - "port": 8899, - "zone": 1, - "type": "rgbw", - "delay": 35, - "repeat": 3 - }, { "accessory": "Tesla", "name": "Tesla", From 4170b8a533bf5aaa3657afa45b6033817f4b7f8e Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Wed, 9 Sep 2015 06:28:17 +0200 Subject: [PATCH 19/71] Polling! Updates from ZWay are now reflected in HomeKit! --- platforms/ZWayServer.js | 113 +++++++++++++++++++++++++++++++--------- 1 file changed, 87 insertions(+), 26 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index da1496e..80da4f8 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -58,7 +58,9 @@ function ZWayServerPlatform(log, config){ this.name_overrides = config["name_overrides"]; this.batteryLow = config["battery_low_level"]; this.pollInterval = config["poll_interval"] || 2; - this.userAgent = "HomeBridge/-1^0.5"; + this.lastUpdate = 0; + this.cxVDevMap = {}; + this.vDevStore = {}; this.sessionId = ""; this.jar = request.jar(new tough.CookieJar()); } @@ -96,8 +98,7 @@ proxy: 'http://localhost:8888', }, headers: { "Accept": "application/json", - "Content-Type": "application/json", - "User-Agent": that.userAgent + "Content-Type": "application/json" }, json: true, jar: true//that.jar @@ -137,6 +138,8 @@ proxy: 'http://localhost:8888', method: "GET", url: this.url + 'ZAutomation/api/v1/devices' }).then(function(result){ + this.lastUpdate = result.data.updateTime; + var devices = result.data.devices; var groupedDevices = {}; for(var i = 0; i < devices.length; i++){ @@ -179,9 +182,52 @@ proxy: 'http://localhost:8888', } //foundAccessories = foundAccessories.slice(0, 10); // Limit to a few devices for testing... callback(foundAccessories); - }); + + // Start the polling process... + this.pollingTimer = setTimeout(this.pollUpdate.bind(this), this.pollInterval*1000); + + }.bind(this)); } + , + + pollUpdate: function(){ + //debug("Polling for updates since " + this.lastUpdate + "..."); + return this.zwayRequest({ + method: "GET", + url: this.url + 'ZAutomation/api/v1/devices', + qs: {since: this.lastUpdate} + }).then(function(result){ + this.lastUpdate = result.data.updateTime; + if(result.data && result.data.devices && result.data.devices.length){ + var updates = result.data.devices; + debug("Got " + updates.length + " updates."); + for(var i = 0; i < updates.length; i++){ + var upd = updates[i]; + if(this.cxVDevMap[upd.id]){ + var vdev = this.vDevStore[upd.id]; + vdev.metrics.level = upd.metrics.level; + vdev.updateTime = upd.updateTime; + var cxs = this.cxVDevMap[upd.id]; + for(var j = 0; j < cxs.length; j++){ + var cx = cxs[j]; + if(typeof cx.zway_getValueFromVDev !== "function") continue; + var oldValue = cx.value; + var newValue = cx.zway_getValueFromVDev(vdev); + if(oldValue !== newValue){ + cx.value = newValue; + cx.emit('change', { oldValue:oldValue, newValue:cx.value, context:null }); + debug("Updated characteristic " + cx.displayName + " on " + vdev.metrics.title); + } + } + } + } + } + + // setup next poll... + this.pollingTimer = setTimeout(this.pollUpdate.bind(this), this.pollInterval*1000); + }.bind(this)); + } } @@ -291,14 +337,17 @@ ZWayServerAccessory.prototype = { , configureCharacteristic: function(cx, vdev){ var that = this; - - var gdv = function(){ - debug("Default value for " + vdev.metrics.title + " is " + vdev.metrics.level); - return vdev.metrics.level; - }; + + // Add this combination to the maps... + if(!this.platform.cxVDevMap[vdev.id]) this.platform.cxVDevMap[vdev.id] = []; + this.platform.cxVDevMap[vdev.id].push(cx); + if(!this.platform.vDevStore[vdev.id]) this.platform.vDevStore[vdev.id] = vdev; if(cx instanceof Characteristic.Name){ - cx.getDefaultValue = function(){ return this.name; }; + cx.zway_getValueFromVDev = function(vdev){ + return vdev.metrics.title; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, that.name); @@ -310,8 +359,8 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.On){ cx.zway_getValueFromVDev = function(vdev){ var val = false; - if(vdev.metrics.level === "off"){ - val = false; + if(vdev.metrics.level === "on"){ + val = true; } else if(vdev.metrics.level <= 5) { val = false; } else if (vdev.metrics.level > 5) { @@ -386,7 +435,7 @@ ZWayServerAccessory.prototype = { }.bind(this)); cx.on('set', function(level, callback){ this.command(vdev, "exact", {level: parseInt(level, 10)}).then(function(result){ - debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); + //debug("Got value: " + result.data.metrics.level + ", for " + vdev.metrics.title + "."); callback(); }); }.bind(this)); @@ -397,8 +446,10 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.TemperatureDisplayUnits){ //TODO: Always in °C for now. - cx.getDefaultValue = function(){ return Characteristic.TemperatureDisplayUnits.CELSIUS; }; - cx.value = cx.getDefaultValue(); + cx.zway_getValueFromVDev = function(vdev){ + return Characteristic.TemperatureDisplayUnits.CELSIUS; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.TemperatureDisplayUnits.CELSIUS); @@ -409,8 +460,10 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.CurrentHeatingCoolingState){ //TODO: Always HEAT for now, we don't have an example to work with that supports another function. - cx.getDefaultValue = function(){ return Characteristic.CurrentHeatingCoolingState.HEAT; }; - cx.value = cx.getDefaultValue(); + cx.zway_getValueFromVDev = function(vdev){ + return Characteristic.CurrentHeatingCoolingState.HEAT; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.CurrentHeatingCoolingState.HEAT); @@ -420,8 +473,10 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.TargetHeatingCoolingState){ //TODO: Always HEAT for now, we don't have an example to work with that supports another function. - cx.getDefaultValue = function(){ return Characteristic.TargetHeatingCoolingState.HEAT; }; - cx.value = cx.getDefaultValue(); + cx.zway_getValueFromVDev = function(vdev){ + return Characteristic.TargetHeatingCoolingState.HEAT; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.TargetHeatingCoolingState.HEAT); @@ -446,8 +501,10 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.TargetDoorState){ //TODO: We only support this for Door sensors now, so it's a fixed value. - cx.getDefaultValue = function(){ return Characteristic.TargetDoorState.CLOSED; }; - cx.value = cx.getDefaultValue(); + cx.zway_getValueFromVDev = function(vdev){ + return Characteristic.TargetDoorState.CLOSED; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.TargetDoorState.CLOSED); @@ -458,8 +515,10 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.ObstructionDetected){ //TODO: We only support this for Door sensors now, so it's a fixed value. - cx.getDefaultValue = function(){ return false; }; - cx.value = cx.getDefaultValue(); + cx.zway_getValueFromVDev = function(vdev){ + return false; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, false); @@ -498,8 +557,10 @@ ZWayServerAccessory.prototype = { if(cx instanceof Characteristic.ChargingState){ //TODO: No known chargeable devices(?), so always return false. - cx.getDefaultValue = function(){ return Characteristic.ChargingState.NOT_CHARGING; }; - cx.value = cx.getDefaultValue(); + cx.zway_getValueFromVDev = function(vdev){ + return Characteristic.ChargingState.NOT_CHARGING; + }; + cx.value = cx.zway_getValueFromVDev(vdev); cx.on('get', function(callback, context){ debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.ChargingState.NOT_CHARGING); @@ -547,7 +608,7 @@ ZWayServerAccessory.prototype = { services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.primary])); if(this.devDesc.types["battery.Battery"]){ - //services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.types["battery.Battery"]])); + services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.types["battery.Battery"]])); } debug("Loaded services for " + this.name); From 62cabc23f3cde3f55639e5be917bb22470009ff6 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Wed, 9 Sep 2015 06:48:33 +0200 Subject: [PATCH 20/71] Added Door/Window sensors and ancillary temperature sensors. Door/Window sensors are still implemented as garage door openers, because that seems to make the most sense at the moment. --- platforms/ZWayServer.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 80da4f8..416d431 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -80,7 +80,7 @@ ZWayServerPlatform.prototype = { opts.headers = { "Cookie": "ZWAYSession=" + this.sessionId }; -opts.proxy = 'http://localhost:8888'; +//opts.proxy = 'http://localhost:8888'; request(opts, function(error, response, body){ if(response.statusCode == 401){ @@ -88,7 +88,7 @@ opts.proxy = 'http://localhost:8888'; request({ method: "POST", url: that.url + 'ZAutomation/api/v1/login', -proxy: 'http://localhost:8888', +//proxy: 'http://localhost:8888', body: { //JSON.stringify({ "form": true, "login": that.login, @@ -275,7 +275,7 @@ ZWayServerAccessory.prototype = { services.push(new Service.TemperatureSensor(vdev.metrics.title)); break; case "sensorBinary.Door/Window": - //services.push(new Service.GarageDoorOpener(vdev.metrics.title)); + services.push(new Service.GarageDoorOpener(vdev.metrics.title)); break; case "battery.Battery": services.push(new Service.BatteryService(vdev.metrics.title)); @@ -611,6 +611,12 @@ ZWayServerAccessory.prototype = { services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.types["battery.Battery"]])); } + // Odds and ends...if there are sensors that haven't been used, add services for them... + var tempSensor = this.devDesc.types["sensorMultilevel.Temperature"] !== undefined ? this.devDesc.devices[this.devDesc.types["sensorMultilevel.Temperature"]] : false; + if(tempSensor && !this.platform.cxVDevMap[tempSensor.id]){ + services = services.concat(this.getVDevServices(tempSensor)); + } + debug("Loaded services for " + this.name); return services; } From d6e31b4aa71d388e6f2905893acfa4273c423b4a Mon Sep 17 00:00:00 2001 From: Sam Edwards Date: Wed, 9 Sep 2015 08:13:22 -0700 Subject: [PATCH 21/71] [MiLight] Swap cooler/warmer direction for white bulbs, and add note about delay --- platforms/MiLight.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/platforms/MiLight.js b/platforms/MiLight.js index 4382dea..3869e74 100644 --- a/platforms/MiLight.js +++ b/platforms/MiLight.js @@ -27,7 +27,7 @@ Where the parameters are: *ip_address (optional): The IP address of the WiFi Bridge. Default to the broadcast address of 255.255.255.255 if not specified *port (optional): Port of the WiFi bridge. Defaults to 8899 if not specified *type (optional): One of either "rgbw", "rgb", or "white", depending on the type of bulb being controlled. This applies to all zones. Defaults to rgbw. - *delay (optional): Delay between commands sent over UDP. Default 30ms + *delay (optional): Delay between commands sent over UDP. Default 30ms. May cause delays when sending a lot of commands. Try decreasing to improve. *repeat (optional): Number of times to repeat the UDP command for better reliability. Default 3 *zones (required): An array of the names of the zones, in order, 1-4. Use null if a zone is skipped. RGB lamps can only have a single zone. @@ -202,9 +202,9 @@ MiLightAccessory.prototype = { } else if (this.type == "white") { // Again, white lamps don't support setting an absolue colour temp, so trying to do warmer/cooler step at a time based on colour if (value >= 180) { - this.light.sendCommands(commands.white.warmer()); - } else { this.light.sendCommands(commands.white.cooler()); + } else { + this.light.sendCommands(commands.white.warmer()); } } callback(); From c73e22984d06d2668e96667eb096de91ec73c34d Mon Sep 17 00:00:00 2001 From: Nelson Melo Date: Wed, 9 Sep 2015 15:26:21 -0400 Subject: [PATCH 22/71] Implemented LIFx bulb platform --- package.json | 3 +- platforms/LIFx.js | 174 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 1 deletion(-) create mode 100644 platforms/LIFx.js diff --git a/package.json b/package.json index 9fa4cb5..8427666 100644 --- a/package.json +++ b/package.json @@ -14,11 +14,12 @@ "ad2usb": "git+https://github.com/alistairg/node-ad2usb.git#local", "carwingsjs": "0.0.x", "color": "0.10.x", - "elkington": "kevinohara80/elkington", "eibd": "^0.3.1", + "elkington": "kevinohara80/elkington", "hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#6bf0f9eaaa2d87db8d1768114c61f4acbb095c41", "harmonyhubjs-client": "^1.1.4", "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", + "lifx-api": "^1.0.1", "mdns": "^2.2.4", "node-hue-api": "^1.0.5", "node-icontrol": "^0.1.4", diff --git a/platforms/LIFx.js b/platforms/LIFx.js new file mode 100644 index 0000000..55d66f2 --- /dev/null +++ b/platforms/LIFx.js @@ -0,0 +1,174 @@ +var types = require("HAP-NodeJS/accessories/types.js"); +var lifxObj = require('lifx-api'); +var lifx; + +function LIFxPlatform(log, config){ + + // auth info + this.access_token = config["access_token"]; + + lifx = new lifxObj(this.access_token); + + this.log = log; +} + +LIFxPlatform.prototype = { + accessories: function(callback) { + this.log("Fetching LIFx devices."); + + var that = this; + var foundAccessories = []; + + lifx.listLights("all", function(body) { + var bulbs = JSON.parse(body); + + for(var i = 0; i < bulbs.length; i ++) { + var bulb = bulbs[i]; + var accessory = new LIFxBulbAccessory( + that.log, + bulb.label, + bulb.uuid, + bulb.model, + bulb.id + ); + foundAccessories.push(accessory); + } + callback(foundAccessories) + }); + } +} + +function LIFxBulbAccessory(log, label, serial, model, deviceId) { + // device info + this.name = label; + this.model = model; + this.deviceId = deviceId; + this.serial = serial; + this.log = log; +} + +LIFxBulbAccessory.prototype = { + getPower: function(callback){ + var that = this; + + lifx.listLights("all", function(body) { + var bulbs = JSON.parse(body); + + for(var i = 0; i < bulbs.length; i ++) { + var bulb = bulbs[i]; + + if(bulb.deviceId == that.deviceId) { + return bulb.state; + } + } + return "off"; + }); + + nest.fetchStatus(function (data) { + var device = data.shared[that.deviceId]; + that.log("Target temperature for " + this.name + " is: " + device.target_temperature); + callback(device.target_temperature); + }); + }, + setPower: function(state){ + var that = this; + this.log("Setting power state for heating cooling for " + this.name + " to: " + targetTemperatureType); + lifx.setPower("all", state, 1, function (body) { + this.log("body"); + }); + }, + + getServices: function() { + var that = this; + var chars= [{ + 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: "LIFx", + supportEvents: false, + supportBonjour: false, + manfDescription: "Manufacturer", + designedMaxLength: 255 + },{ + cType: types.MODEL_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.model, + supportEvents: false, + supportBonjour: false, + manfDescription: "Model", + designedMaxLength: 255 + },{ + cType: types.SERIAL_NUMBER_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.serial, + supportEvents: false, + supportBonjour: false, + manfDescription: "SN", + designedMaxLength: 255 + },{ + cType: types.IDENTIFY_CTYPE, + onUpdate: null, + perms: ["pw"], + format: "bool", + initialValue: true, + supportEvents: false, + supportBonjour: false, + manfDescription: "Identify Accessory", + designedMaxLength: 1 + }] + }, { + sType: types.LIGHTBULB_STYPE, + characteristics: [{ + cType: types.NAME_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.name, + supportEvents: false, + supportBonjour: false, + manfDescription: "Name of LIFx bulb", + designedMaxLength: 255 + }, { + cType: types.POWER_STATE_CTYPE, + onUpdate: function (value) { + that.setPower(value); + }, + onRead: function (callback) { + that.getPower(function (state) { + callback(state); + }); + }, + perms: ["pw", "pr", "ev"], + format: "int", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Power state", + designedMinValue: 0, + designedMaxValue: 1, + designedMinStep: 1 + }] + }]; + return chars; + } +} + +module.exports.accessory = LIFxBulbAccessory; +module.exports.platform = LIFxPlatform; From 4b1637152219a27aadbad5a3cbed01918ab7206d Mon Sep 17 00:00:00 2001 From: Nelson Melo Date: Wed, 9 Sep 2015 15:31:11 -0400 Subject: [PATCH 23/71] Added LIFx on Readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4fae78b..f11dd75 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Since Siri supports devices added through HomeKit, this means that with Homebrid * _Siri, turn off the Speakers._ ([Sonos](http://www.sonos.com)) * _Siri, turn on the Dehumidifier._ ([WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/)) * _Siri, turn on Away Mode._ ([Xfinity Home](http://www.comcast.com/home-security.html)) - * _Siri, turn on the living room lights._ ([Wink](http://www.wink.com), [SmartThings](http://www.smartthings.com), [X10](http://github.com/edc1591/rest-mochad), [Philips Hue](http://meethue.com), [LimitlessLED/MiLight/Easybulb](http://www.limitlessled.com/)) + * _Siri, turn on the living room lights._ ([Wink](http://www.wink.com), [SmartThings](http://www.smartthings.com), [X10](http://github.com/edc1591/rest-mochad), [Philips Hue](http://meethue.com), [LimitlessLED/MiLight/Easybulb](http://www.limitlessled.com/), [LIFx](http://www.lifx.com/)) * _Siri, set the movie scene._ ([Logitech Harmony](http://myharmony.com/)) If you would like to support any other devices, please write a shim and create a pull request and I'd be happy to add it to this official list. From 09f5e2bed0274e885b9c0198186f4492d23345fa Mon Sep 17 00:00:00 2001 From: David Parry Date: Thu, 10 Sep 2015 12:50:55 +1000 Subject: [PATCH 24/71] add support for WeMo motion sensor --- accessories/WeMo.js | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/accessories/WeMo.js b/accessories/WeMo.js index 26ef822..df16f56 100644 --- a/accessories/WeMo.js +++ b/accessories/WeMo.js @@ -30,6 +30,30 @@ WeMoAccessory.prototype.search = function() { }.bind(this)); } +WeMoAccessory.prototype.getMotion = function(callback) { + + if (!this.device) { + this.log("No '%s' device found (yet?)", this.wemoName); + callback(new Error("Device not found"), false); + return; + } + + this.log("Getting motion state on the '%s'...", this.wemoName); + + this.device.getBinaryState(function(err, result) { + if (!err) { + var binaryState = parseInt(result); + var powerOn = binaryState > 0; + this.log("Motion state for the '%s' is %s", this.wemoName, binaryState); + callback(null, powerOn); + } + else { + this.log("Error getting motion state on the '%s': %s", this.wemoName, err.message); + callback(err); + } + }.bind(this)); +} + WeMoAccessory.prototype.getPowerOn = function(callback) { if (!this.device) { @@ -122,6 +146,15 @@ WeMoAccessory.prototype.getServices = function() { return [garageDoorService]; } + else if (this.service == "MotionSensor") { + var motionSensorService = new Service.MotionSensor(this.name); + + motionSensorService + .getCharacteristic(Characteristic.MotionDetected) + .on('get', this.getMotion.bind(this)); + + return [motionSensorService]; + } else { throw new Error("Unknown service type '%s'", this.service); } From 3c35311c4aff446ec4b2b49e82df80714c26ddd8 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Thu, 10 Sep 2015 06:55:07 +0200 Subject: [PATCH 25/71] Added Luminiscence sensors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Though there doesn’t seem to be much app support for it yet…and my math (% -> lux) is complete guesswork. --- platforms/ZWayServer.js | 41 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 416d431..6c68de9 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -280,6 +280,9 @@ ZWayServerAccessory.prototype = { case "battery.Battery": services.push(new Service.BatteryService(vdev.metrics.title)); break; + case "sensorMultilevel.Luminiscence": + services.push(new Service.LightSensor(vdev.metrics.title)); + break; } var validServices =[]; @@ -310,6 +313,7 @@ ZWayServerAccessory.prototype = { map[(new Characteristic.BatteryLevel).UUID] = ["battery.Battery"]; map[(new Characteristic.StatusLowBattery).UUID] = ["battery.Battery"]; map[(new Characteristic.ChargingState).UUID] = ["battery.Battery"]; //TODO: Always a fixed result + map[(new Characteristic.CurrentAmbientLightLevel).UUID] = ["sensorMultilevel.Luminiscence"]; } if(cx instanceof Characteristic.Name) return vdevPreferred; @@ -381,6 +385,9 @@ ZWayServerAccessory.prototype = { callback(); }); }.bind(this)); + cx.on('change', function(ev){ + debug("Device " + vdev.metrics.title + ", characteristic " + cx.displayName + " changed from " + ev.oldValue + " to " + ev.newValue); + }); return cx; } @@ -497,6 +504,9 @@ ZWayServerAccessory.prototype = { callback(false, cx.zway_getValueFromVDev(result.data)); }); }.bind(this)); + cx.on('change', function(ev){ + debug("Device " + vdev.metrics.title + ", characteristic " + cx.displayName + " changed from " + ev.oldValue + " to " + ev.newValue); + }); } if(cx instanceof Characteristic.TargetDoorState){ @@ -568,7 +578,30 @@ ZWayServerAccessory.prototype = { //cx.readable = false; cx.writable = false; } - + + if(cx instanceof Characteristic.CurrentAmbientLightLevel){ + cx.zway_getValueFromVDev = function(vdev){ + if(vdev.metrics.scaleTitle === "%"){ + // Completely unscientific guess, based on test-fit data and Wikipedia real-world lux values. + // This will probably change! + return 0.0005 * (vdev.metrics.level^3.6); + } else { + return vdev.metrics.level; + } + }; + cx.value = cx.zway_getValueFromVDev(vdev); + cx.on('get', function(callback, context){ + debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); + this.getVDev(vdev).then(function(result){ + debug("Got value: " + cx.zway_getValueFromVDev(result.data) + ", for " + vdev.metrics.title + "."); + callback(false, cx.zway_getValueFromVDev(result.data)); + }); + }.bind(this)); + cx.on('change', function(ev){ + debug("Device " + vdev.metrics.title + ", characteristic " + cx.displayName + " changed from " + ev.oldValue + " to " + ev.newValue); + }); + return cx; + } } , configureService: function(service, vdev){ @@ -612,11 +645,17 @@ ZWayServerAccessory.prototype = { } // Odds and ends...if there are sensors that haven't been used, add services for them... + var tempSensor = this.devDesc.types["sensorMultilevel.Temperature"] !== undefined ? this.devDesc.devices[this.devDesc.types["sensorMultilevel.Temperature"]] : false; if(tempSensor && !this.platform.cxVDevMap[tempSensor.id]){ services = services.concat(this.getVDevServices(tempSensor)); } + var lightSensor = this.devDesc.types["sensorMultilevel.Luminiscence"] !== undefined ? this.devDesc.devices[this.devDesc.types["sensorMultilevel.Luminiscence"]] : false; + if(lightSensor && !this.platform.cxVDevMap[lightSensor.id]){ + services = services.concat(this.getVDevServices(lightSensor)); + } + debug("Loaded services for " + this.name); return services; } From 7e6df6191e923d44b8b829a89524ba20520d36e5 Mon Sep 17 00:00:00 2001 From: David Parry Date: Thu, 10 Sep 2015 22:19:41 +1000 Subject: [PATCH 26/71] [LiFX] fix/enhance the LiFX platform --- platforms/LIFx.js | 218 +++++++++++++++++++++------------------------- 1 file changed, 98 insertions(+), 120 deletions(-) diff --git a/platforms/LIFx.js b/platforms/LIFx.js index 55d66f2..8f4ef0d 100644 --- a/platforms/LIFx.js +++ b/platforms/LIFx.js @@ -1,4 +1,5 @@ -var types = require("HAP-NodeJS/accessories/types.js"); +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; var lifxObj = require('lifx-api'); var lifx; @@ -23,14 +24,7 @@ LIFxPlatform.prototype = { var bulbs = JSON.parse(body); for(var i = 0; i < bulbs.length; i ++) { - var bulb = bulbs[i]; - var accessory = new LIFxBulbAccessory( - that.log, - bulb.label, - bulb.uuid, - bulb.model, - bulb.id - ); + var accessory = new LIFxBulbAccessory(that.log, bulbs[i]); foundAccessories.push(accessory); } callback(foundAccessories) @@ -38,135 +32,119 @@ LIFxPlatform.prototype = { } } -function LIFxBulbAccessory(log, label, serial, model, deviceId) { +function LIFxBulbAccessory(log, bulb) { // device info - this.name = label; - this.model = model; - this.deviceId = deviceId; - this.serial = serial; + this.name = bulb.label; + this.model = bulb.product_name; + this.deviceId = bulb.id; + this.serial = bulb.uuid; + this.capabilities = bulb.capabilities; this.log = log; } LIFxBulbAccessory.prototype = { - getPower: function(callback){ + get: function(type, callback){ var that = this; - lifx.listLights("all", function(body) { - var bulbs = JSON.parse(body); + lifx.listLights("id:"+ that.deviceId, function(body) { + var bulb = JSON.parse(body); - for(var i = 0; i < bulbs.length; i ++) { - var bulb = bulbs[i]; - - if(bulb.deviceId == that.deviceId) { - return bulb.state; - } + if (bulb.connected != true) { + callback(new Error("Device not found"), false); + return; } - return "off"; - }); - nest.fetchStatus(function (data) { - var device = data.shared[that.deviceId]; - that.log("Target temperature for " + this.name + " is: " + device.target_temperature); - callback(device.target_temperature); + switch(type) { + case "power": + callback(null, bulb.power == "on" ? 1 : 0); + break; + case "brightness": + callback(null, Math.round(bulb.brightness * 100)); + break; + case "hue": + callback(null, bulb.color.hue); + break; + case "saturation": + callback(null, Math.round(bulb.color.saturation * 100)); + break; + } }); }, - setPower: function(state){ + identify: function(callback) { var that = this; - this.log("Setting power state for heating cooling for " + this.name + " to: " + targetTemperatureType); - lifx.setPower("all", state, 1, function (body) { - this.log("body"); + + lifx.breatheEffect("id:"+ that.deviceId, 'green', null, 1, 3, false, true, 0.5, function (body) { + callback(); + }); + }, + setColor: function(type, state, callback){ + var that = this; + var color; + + switch(type) { + case "brightness": + color = "brightness:" + (state / 100); + break; + case "hue": + color = "hue:" + state; + break; + case "saturation": + color = "saturation:" + (state / 100); + break; + } + + lifx.setColor("id:"+ that.deviceId, color, 0, null, function (body) { + callback(); + }); + }, + setPower: function(state, callback){ + var that = this; + + lifx.setPower("id:"+ that.deviceId, (state == 1 ? "on" : "off"), 0, function (body) { + callback(); }); }, getServices: function() { var that = this; - var chars= [{ - 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: "LIFx", - supportEvents: false, - supportBonjour: false, - manfDescription: "Manufacturer", - designedMaxLength: 255 - },{ - cType: types.MODEL_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.model, - supportEvents: false, - supportBonjour: false, - manfDescription: "Model", - designedMaxLength: 255 - },{ - cType: types.SERIAL_NUMBER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.serial, - supportEvents: false, - supportBonjour: false, - manfDescription: "SN", - designedMaxLength: 255 - },{ - cType: types.IDENTIFY_CTYPE, - onUpdate: null, - perms: ["pw"], - format: "bool", - initialValue: true, - supportEvents: false, - supportBonjour: false, - manfDescription: "Identify Accessory", - designedMaxLength: 1 - }] - }, { - sType: types.LIGHTBULB_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.name, - supportEvents: false, - supportBonjour: false, - manfDescription: "Name of LIFx bulb", - designedMaxLength: 255 - }, { - cType: types.POWER_STATE_CTYPE, - onUpdate: function (value) { - that.setPower(value); - }, - onRead: function (callback) { - that.getPower(function (state) { - callback(state); - }); - }, - perms: ["pw", "pr", "ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Power state", - designedMinValue: 0, - designedMaxValue: 1, - designedMinStep: 1 - }] - }]; - return chars; + var services = [] + var service = new Service.Lightbulb(this.name); + + service + .getCharacteristic(Characteristic.On) + .on('identify', function(callback) {}) + .on('get', function(callback) { that.get("power", callback);}) + .on('set', function(value, callback) {that.setPower(value, callback);}); + + service + .addCharacteristic(Characteristic.Brightness) + .on('get', function(callback) { that.get("brightness", callback);}) + .on('set', function(value, callback) { that.setColor("brightness", value, callback);}); + + if (this.capabilities.has_color == true) { + service + .addCharacteristic(Characteristic.Hue) + .on('get', function(callback) { that.get("hue", callback);}) + .on('set', function(value, callback) { that.setColor("hue", value, callback);}); + + service + .addCharacteristic(Characteristic.Saturation) + .on('get', function(callback) { that.get("saturation", callback);}) + .on('set', function(value, callback) { that.setColor("saturation", value, callback);}); + } + + services.push(service); + + service = new Service.AccessoryInformation(); + + service + .setCharacteristic(Characteristic.Manufacturer, "LiFX") + .setCharacteristic(Characteristic.Model, this.model) + .setCharacteristic(Characteristic.SerialNumber, this.serial); + + services.push(service); + + return services; } } From 17fc8f1829ee884c097cda3127122858220c509a Mon Sep 17 00:00:00 2001 From: David Parry Date: Fri, 11 Sep 2015 13:04:09 +1000 Subject: [PATCH 27/71] implement the LiFX LAN API as a configurable option for higher lantency connections --- package.json | 1 + platforms/LIFx.js | 245 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 200 insertions(+), 46 deletions(-) diff --git a/package.json b/package.json index 8427666..3732b9d 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "harmonyhubjs-client": "^1.1.4", "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", "lifx-api": "^1.0.1", + "lifx": "https://github.com/magicmonkey/lifxjs.git", "mdns": "^2.2.4", "node-hue-api": "^1.0.5", "node-icontrol": "^0.1.4", diff --git a/platforms/LIFx.js b/platforms/LIFx.js index 8f4ef0d..62167f6 100644 --- a/platforms/LIFx.js +++ b/platforms/LIFx.js @@ -1,16 +1,45 @@ +'use strict'; + +// LiFX Platform Shim for HomeBridge +// +// Remember to add platform to config.json. Example: +// "platforms": [ +// { +// "platform": "LIFx", // required +// "name": "LIFx", // required +// "access_token": "access token", // required +// "use_lan": "true" // optional set to "true" (gets and sets over the lan) or "get" (gets only over the lan) +// } +// ], +// +// When you attempt to add a device, it will ask for a "PIN code". +// The default code for all HomeBridge accessories is 031-45-154. +// + var Service = require("HAP-NodeJS").Service; var Characteristic = require("HAP-NodeJS").Characteristic; -var lifxObj = require('lifx-api'); -var lifx; +var lifxRemoteObj = require('lifx-api'); +var lifx_remote; + +var lifxLanObj; +var lifx_lan; +var use_lan; function LIFxPlatform(log, config){ + // auth info + this.access_token = config["access_token"]; - // auth info - this.access_token = config["access_token"]; + lifx_remote = new lifxRemoteObj(this.access_token); - lifx = new lifxObj(this.access_token); + // use remote or lan api ? + use_lan = config["use_lan"] || false; - this.log = log; + if (use_lan != false) { + lifxLanObj = require('lifx'); + lifx_lan = lifxLanObj.init(); + } + + this.log = log; } LIFxPlatform.prototype = { @@ -20,7 +49,7 @@ LIFxPlatform.prototype = { var that = this; var foundAccessories = []; - lifx.listLights("all", function(body) { + lifx_remote.listLights("all", function(body) { var bulbs = JSON.parse(body); for(var i = 0; i < bulbs.length; i ++) { @@ -33,20 +62,54 @@ LIFxPlatform.prototype = { } function LIFxBulbAccessory(log, bulb) { - // device info - this.name = bulb.label; - this.model = bulb.product_name; - this.deviceId = bulb.id; - this.serial = bulb.uuid; - this.capabilities = bulb.capabilities; - this.log = log; + // device info + this.name = bulb.label; + this.model = bulb.product_name; + this.deviceId = bulb.id; + this.serial = bulb.uuid; + this.capabilities = bulb.capabilities; + this.log = log; } LIFxBulbAccessory.prototype = { - get: function(type, callback){ + getLan: function(type, callback){ var that = this; - lifx.listLights("id:"+ that.deviceId, function(body) { + if (!lifx_lan.bulbs[this.deviceId]) { + callback(new Error("Device not found"), false); + return; + } + + lifx_lan.requestStatus(); + lifx_lan.on('bulbstate', function(bulb) { + if (callback == null) { + return; + } + + if (bulb.addr.toString('hex') == that.deviceId) { + switch(type) { + case "power": + callback(null, bulb.state.power > 0); + break; + case "brightness": + callback(null, Math.round(bulb.state.brightness * 100 / 65535)); + break; + case "hue": + callback(null, Math.round(bulb.state.hue * 360 / 65535)); + break; + case "saturation": + callback(null, Math.round(bulb.state.saturation * 100 / 65535)); + break; + } + + callback = null + } + }); + }, + getRemote: function(type, callback){ + var that = this; + + lifx_remote.listLights("id:"+ that.deviceId, function(body) { var bulb = JSON.parse(body); if (bulb.connected != true) { @@ -71,66 +134,156 @@ LIFxBulbAccessory.prototype = { }); }, identify: function(callback) { - var that = this; - - lifx.breatheEffect("id:"+ that.deviceId, 'green', null, 1, 3, false, true, 0.5, function (body) { + lifx_remote.breatheEffect("id:"+ this.deviceId, 'green', null, 1, 3, false, true, 0.5, function (body) { callback(); }); }, - setColor: function(type, state, callback){ - var that = this; + setLanColor: function(type, value, callback){ + var bulb = lifx_lan.bulbs[this.deviceId]; + + if (!bulb) { + callback(new Error("Device not found"), false); + return; + } + + var state = { + hue: bulb.state.hue, + saturation: bulb.state.saturation, + brightness: bulb.state.brightness, + kelvin: bulb.state.kelvin + }; + + var scale = type == "hue" ? 360 : 100; + + state[type] = Math.round(value * 65535 / scale) & 0xffff; + lifx_lan.lightsColour(state.hue, state.saturation, state.brightness, state.kelvin, 0, bulb); + + callback(null); + }, + setLanPower: function(state, callback){ + var bulb = lifx_lan.bulbs[this.deviceId]; + + if (!bulb) { + callback(new Error("Device not found"), false); + return; + } + + if (state) { + lifx_lan.lightsOn(bulb); + } + else { + lifx_lan.lightsOff(bulb); + } + + callback(null); + }, + setRemoteColor: function(type, value, callback){ var color; switch(type) { case "brightness": - color = "brightness:" + (state / 100); + color = "brightness:" + (value / 100); break; case "hue": - color = "hue:" + state; + color = "hue:" + value; break; case "saturation": - color = "saturation:" + (state / 100); + color = "saturation:" + (value / 100); break; } - lifx.setColor("id:"+ that.deviceId, color, 0, null, function (body) { + lifx_remote.setColor("id:"+ this.deviceId, color, 0, null, function (body) { callback(); }); }, - setPower: function(state, callback){ + setRemotePower: function(state, callback){ var that = this; - lifx.setPower("id:"+ that.deviceId, (state == 1 ? "on" : "off"), 0, function (body) { + lifx_remote.setPower("id:"+ that.deviceId, (state == 1 ? "on" : "off"), 0, function (body) { callback(); }); }, - getServices: function() { var that = this; var services = [] var service = new Service.Lightbulb(this.name); - service - .getCharacteristic(Characteristic.On) - .on('identify', function(callback) {}) - .on('get', function(callback) { that.get("power", callback);}) - .on('set', function(value, callback) {that.setPower(value, callback);}); + switch(use_lan) { + case true: + case "true": + // gets and sets over the lan api + service + .getCharacteristic(Characteristic.On) + .on('identify', function(callback) {}) + .on('get', function(callback) { that.getLan("power", callback);}) + .on('set', function(value, callback) {that.setLanPower(value, callback);}); - service - .addCharacteristic(Characteristic.Brightness) - .on('get', function(callback) { that.get("brightness", callback);}) - .on('set', function(value, callback) { that.setColor("brightness", value, callback);}); + service + .addCharacteristic(Characteristic.Brightness) + .on('get', function(callback) { that.getLan("brightness", callback);}) + .on('set', function(value, callback) { that.setLanColor("brightness", value, callback);}); - if (this.capabilities.has_color == true) { - service - .addCharacteristic(Characteristic.Hue) - .on('get', function(callback) { that.get("hue", callback);}) - .on('set', function(value, callback) { that.setColor("hue", value, callback);}); + if (this.capabilities.has_color == true) { + service + .addCharacteristic(Characteristic.Hue) + .on('get', function(callback) { that.getLan("hue", callback);}) + .on('set', function(value, callback) { that.setLanColor("hue", value, callback);}); - service - .addCharacteristic(Characteristic.Saturation) - .on('get', function(callback) { that.get("saturation", callback);}) - .on('set', function(value, callback) { that.setColor("saturation", value, callback);}); + service + .addCharacteristic(Characteristic.Saturation) + .on('get', function(callback) { that.getLan("saturation", callback);}) + .on('set', function(value, callback) { that.setLanColor("saturation", value, callback);}); + } + break; + case "get": + // gets over the lan api, sets over the remote api + service + .getCharacteristic(Characteristic.On) + .on('identify', function(callback) {}) + .on('get', function(callback) { that.getLan("power", callback);}) + .on('set', function(value, callback) {that.setRemotePower(value, callback);}); + + service + .addCharacteristic(Characteristic.Brightness) + .on('get', function(callback) { that.getLan("brightness", callback);}) + .on('set', function(value, callback) { that.setRemoteColor("brightness", value, callback);}); + + if (this.capabilities.has_color == true) { + service + .addCharacteristic(Characteristic.Hue) + .on('get', function(callback) { that.getLan("hue", callback);}) + .on('set', function(value, callback) { that.setRemoteColor("hue", value, callback);}); + + service + .addCharacteristic(Characteristic.Saturation) + .on('get', function(callback) { that.getLan("saturation", callback);}) + .on('set', function(value, callback) { that.setRemoteColor("saturation", value, callback);}); + } + break; + default: + // gets and sets over the remote api + service + .getCharacteristic(Characteristic.On) + .on('identify', function(callback) {}) + .on('get', function(callback) { that.getRemote("power", callback);}) + .on('set', function(value, callback) {that.setRemotePower(value, callback);}); + + service + .addCharacteristic(Characteristic.Brightness) + .on('get', function(callback) { that.getRemote("brightness", callback);}) + .on('set', function(value, callback) { that.setRemoteColor("brightness", value, callback);}); + + if (this.capabilities.has_color == true) { + service + .addCharacteristic(Characteristic.Hue) + .on('get', function(callback) { that.getRemote("hue", callback);}) + .on('set', function(value, callback) { that.setRemoteColor("hue", value, callback);}); + + service + .addCharacteristic(Characteristic.Saturation) + .on('get', function(callback) { that.getRemote("saturation", callback);}) + .on('set', function(value, callback) { that.setRemoteColor("saturation", value, callback);}); + } } services.push(service); From 3da6fcb5108cd1405633a3833545670682a1cbf7 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Fri, 11 Sep 2015 05:43:33 +0200 Subject: [PATCH 28/71] FIX: Prevent light sensor values from going out of bounds --- platforms/ZWayServer.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 6c68de9..77842a9 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -584,7 +584,10 @@ ZWayServerAccessory.prototype = { if(vdev.metrics.scaleTitle === "%"){ // Completely unscientific guess, based on test-fit data and Wikipedia real-world lux values. // This will probably change! - return 0.0005 * (vdev.metrics.level^3.6); + var lux = 0.0005 * (vdev.metrics.level^3.6); + if(lux < cx.minimumValue) return cx.minimumValue; + if(lux > cx.maximumValue) return cx.maximumValue; + return lux; } else { return vdev.metrics.level; } From 7d5a992c98173d8486df8ae4ee68a15db346119f Mon Sep 17 00:00:00 2001 From: David Parry Date: Fri, 11 Sep 2015 17:30:25 +1000 Subject: [PATCH 29/71] minor cleanup --- platforms/LIFx.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/platforms/LIFx.js b/platforms/LIFx.js index 62167f6..79988eb 100644 --- a/platforms/LIFx.js +++ b/platforms/LIFx.js @@ -214,7 +214,6 @@ LIFxBulbAccessory.prototype = { // gets and sets over the lan api service .getCharacteristic(Characteristic.On) - .on('identify', function(callback) {}) .on('get', function(callback) { that.getLan("power", callback);}) .on('set', function(value, callback) {that.setLanPower(value, callback);}); @@ -239,7 +238,6 @@ LIFxBulbAccessory.prototype = { // gets over the lan api, sets over the remote api service .getCharacteristic(Characteristic.On) - .on('identify', function(callback) {}) .on('get', function(callback) { that.getLan("power", callback);}) .on('set', function(value, callback) {that.setRemotePower(value, callback);}); @@ -264,7 +262,6 @@ LIFxBulbAccessory.prototype = { // gets and sets over the remote api service .getCharacteristic(Characteristic.On) - .on('identify', function(callback) {}) .on('get', function(callback) { that.getRemote("power", callback);}) .on('set', function(value, callback) {that.setRemotePower(value, callback);}); @@ -291,7 +288,7 @@ LIFxBulbAccessory.prototype = { service = new Service.AccessoryInformation(); service - .setCharacteristic(Characteristic.Manufacturer, "LiFX") + .setCharacteristic(Characteristic.Manufacturer, "LIFX") .setCharacteristic(Characteristic.Model, this.model) .setCharacteristic(Characteristic.SerialNumber, this.serial); From 17573524ce55c91db3618c319dabe132d356f0de Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Sat, 12 Sep 2015 14:23:17 +0200 Subject: [PATCH 30/71] Prep for initial release. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cleaned up some more old cruft, added config-sample.json entry, and now there’s a different default grouping of characteristics, which makes for more optional characteristics on fewer services. The older behavior (more services per accessory) can be switched on in config.json. The new default works better in Eve, other clients not so much. --- config-sample.json | 8 ++++++++ platforms/ZWayServer.js | 1 - 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/config-sample.json b/config-sample.json index 4245bd5..3f8b136 100644 --- a/config-sample.json +++ b/config-sample.json @@ -71,6 +71,14 @@ "platform": "YamahaAVR", "play_volume": -35, "setMainInputTo": "AirPlay" + }, + { + "platform": "ZWayServer", + "url": "http://192.168.1.10:8083/", + "login": "zwayusername", + "password": "zwayuserpassword", + "poll_interval": 2, + "split_services": false } ], diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 77842a9..799c08d 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -56,7 +56,6 @@ function ZWayServerPlatform(log, config){ this.login = config["login"]; this.password = config["password"]; this.name_overrides = config["name_overrides"]; - this.batteryLow = config["battery_low_level"]; this.pollInterval = config["poll_interval"] || 2; this.lastUpdate = 0; this.cxVDevMap = {}; From 2b1aa5e296e2f038636dcb96b7806456f1aa01a7 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Sat, 12 Sep 2015 14:24:31 +0200 Subject: [PATCH 31/71] Erm...missing modifications that should have been in the last commit. --- platforms/ZWayServer.js | 138 ++++++++++++++++++++-------------------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 799c08d..322b9fb 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -6,57 +6,15 @@ var request = require("request"); var tough = require('tough-cookie'); var Q = require("q"); -var zwshkDeviceClasses = [ - { - primaryType: "switchBinary", - subTypes: { - "battery": true, - "sensorMultilevel.Electric": true - }, - tcType: types.SWITCH_TCTYPE - } - , - { - primaryType: "thermostat", - subTypes: { - "sensorMultiLevel.Temperature": true, - "battery": true - }, - tcType: types.THERMOSTAT_TCTYPE - } - , - { - primaryType: "sensorBinary.Door/Window", - subTypes: { - "battery": true - }, - tcType: types.SENSOR_TCTYPE - } - , - { - primaryType: "sensorMultilevel.Temperature", - subTypes: { - "battery": true - }, - tcType: types.SENSOR_TCTYPE - } - , - { - primaryType: "switchMultilevel", - subTypes: { - "battery": true - }, - tcType: types.LIGHTBULB_TCTYPE - } -]; - function ZWayServerPlatform(log, config){ this.log = log; this.url = config["url"]; this.login = config["login"]; this.password = config["password"]; this.name_overrides = config["name_overrides"]; + this.batteryLow = config["battery_low_level"] || 15; this.pollInterval = config["poll_interval"] || 2; + this.splitServices= config["split_services"] || false; this.lastUpdate = 0; this.cxVDevMap = {}; this.vDevStore = {}; @@ -79,7 +37,6 @@ ZWayServerPlatform.prototype = { opts.headers = { "Cookie": "ZWAYSession=" + this.sessionId }; -//opts.proxy = 'http://localhost:8888'; request(opts, function(error, response, body){ if(response.statusCode == 401){ @@ -87,7 +44,6 @@ ZWayServerPlatform.prototype = { request({ method: "POST", url: that.url + 'ZAutomation/api/v1/login', -//proxy: 'http://localhost:8888', body: { //JSON.stringify({ "form": true, "login": that.login, @@ -130,6 +86,16 @@ ZWayServerPlatform.prototype = { accessories: function(callback) { debug("Fetching Z-Way devices..."); + //TODO: Unify this with getVDevServices, so there's only one place with mapping between service and vDev type. + //Note: Order matters! + var primaryDeviceClasses = [ + "switchBinary", + "thermostat", + "sensorBinary.Door/Window", + "sensorMultilevel.Temperature", + "switchMultilevel" + ]; + var that = this; var foundAccessories = []; @@ -143,7 +109,7 @@ 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; } + if(vdev.tags.indexOf("Homebridge:Skip") >= 0) { debug("Tag says skip!"); continue; } var gdid = vdev.id.replace(/^(.*?)_zway_(\d+-\d+)-\d.*/, '$1_$2'); var gd = groupedDevices[gdid] || (groupedDevices[gdid] = {devices: [], types: {}, primary: undefined}); gd.devices.push(vdev); @@ -162,13 +128,13 @@ ZWayServerPlatform.prototype = { } var accessory = null; - for(var ti = 0; ti < zwshkDeviceClasses.length; ti++){ - if(gd.types[zwshkDeviceClasses[ti].primaryType] !== undefined){ - gd.primary = gd.types[zwshkDeviceClasses[ti].primaryType]; + 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 class with primaryType " + zwshkDeviceClasses[ti].primaryType + ", " + name + " (" + pd.id + ") as primary."); - accessory = new ZWayServerAccessory(name, zwshkDeviceClasses[ti], gd, that); + debug("Using primary device with type " + primaryDeviceClasses[ti] + ", " + name + " (" + pd.id + ") as primary."); + accessory = new ZWayServerAccessory(name, gd, that); break; } } @@ -230,10 +196,9 @@ ZWayServerPlatform.prototype = { } -function ZWayServerAccessory(name, dclass, devDesc, platform) { +function ZWayServerAccessory(name, devDesc, platform) { // device info this.name = name; - this.dclass = dclass; this.devDesc = devDesc; this.platform = platform; this.log = platform.log; @@ -295,6 +260,12 @@ ZWayServerAccessory.prototype = { , uuidToTypeKeyMap: null , + extraCharacteristicsMap: { + "battery.Battery": [Characteristic.BatteryLevel, Characteristic.StatusLowBattery], + "sensorMultilevel.Temperature": [Characteristic.CurrentTemperature, Characteristic.TemperatureDisplayUnits], + "sensorMultilevel.Luminiscence": [Characteristic.CurrentAmbientLightLevel] + } + , getVDevForCharacteristic: function(cx, vdevPreferred){ var map = this.uuidToTypeKeyMap; if(!map){ @@ -641,21 +612,50 @@ ZWayServerAccessory.prototype = { var services = [informationService]; services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.primary])); - - if(this.devDesc.types["battery.Battery"]){ - services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.types["battery.Battery"]])); - } - - // Odds and ends...if there are sensors that haven't been used, add services for them... - - var tempSensor = this.devDesc.types["sensorMultilevel.Temperature"] !== undefined ? this.devDesc.devices[this.devDesc.types["sensorMultilevel.Temperature"]] : false; - if(tempSensor && !this.platform.cxVDevMap[tempSensor.id]){ - services = services.concat(this.getVDevServices(tempSensor)); - } - - var lightSensor = this.devDesc.types["sensorMultilevel.Luminiscence"] !== undefined ? this.devDesc.devices[this.devDesc.types["sensorMultilevel.Luminiscence"]] : false; - if(lightSensor && !this.platform.cxVDevMap[lightSensor.id]){ - services = services.concat(this.getVDevServices(lightSensor)); + + if(this.platform.splitServices){ + if(this.devDesc.types["battery.Battery"]){ + services = services.concat(this.getVDevServices(this.devDesc.devices[this.devDesc.types["battery.Battery"]])); + } + + // Odds and ends...if there are sensors that haven't been used, add services for them... + + var tempSensor = this.devDesc.types["sensorMultilevel.Temperature"] !== undefined ? this.devDesc.devices[this.devDesc.types["sensorMultilevel.Temperature"]] : false; + if(tempSensor && !this.platform.cxVDevMap[tempSensor.id]){ + services = services.concat(this.getVDevServices(tempSensor)); + } + + var lightSensor = this.devDesc.types["sensorMultilevel.Luminiscence"] !== undefined ? this.devDesc.devices[this.devDesc.types["sensorMultilevel.Luminiscence"]] : false; + if(lightSensor && !this.platform.cxVDevMap[lightSensor.id]){ + services = services.concat(this.getVDevServices(lightSensor)); + } + } else { + // Everything outside the primary service gets added as optional characteristics... + var service = services[1]; + var existingCxUUIDs = {}; + for(var i = 0; i < service.characteristics.length; i++) existingCxUUIDs[service.characteristics[i].UUID] = true; + + for(var i = 0; i < this.devDesc.devices.length; i++){ + var vdev = this.devDesc.devices[i]; + if(this.platform.cxVDevMap[vdev.id]) continue; // Don't double-use anything + var extraCxClasses = this.extraCharacteristicsMap[ZWayServerPlatform.getVDevTypeKey(vdev)]; + var extraCxs = []; + if(!extraCxClasses || extraCxClasses.length === 0) continue; + for(var j = 0; j < extraCxClasses.length; j++){ + var cx = new extraCxClasses[j](); + if(existingCxUUIDs[cx.UUID]) continue; // Don't have two of the same Characteristic type in one service! + var vdev2 = this.getVDevForCharacteristic(cx, vdev); // Just in case...will probably return vdev. + if(!vdev2){ + // Uh oh... one of the extraCxClasses can't be configured! Abort all extras for this vdev! + extraCxs = []; // to wipe out any already setup cxs. + break; + } + this.configureCharacteristic(cx, vdev2); + extraCxs.push(cx); + } + for(var j = 0; j < extraCxs.length; j++) + service.addCharacteristic(extraCxs[j]); + } } debug("Loaded services for " + this.name); From 197311534b15fd9a3cbe5d2cf75b6f235482580f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=20Schr=C3=B6ter?= Date: Sat, 12 Sep 2015 20:07:18 +0200 Subject: [PATCH 32/71] started porting to new bridge api --- platforms/FHEM.js | 1582 +++++++-------------------------------------- 1 file changed, 241 insertions(+), 1341 deletions(-) diff --git a/platforms/FHEM.js b/platforms/FHEM.js index f16f72b..a62e4b6 100644 --- a/platforms/FHEM.js +++ b/platforms/FHEM.js @@ -3,227 +3,26 @@ // Remember to add platform to config.json. Example: // "platforms": [ // { -// 'platform': "FHEM", -// 'name': "FHEM", -// 'server': "127.0.0.1", -// 'port': 8083, -// 'ssl': true, -// 'auth': {'user': "fhem", 'pass': "fhempassword"}, -// 'filter': "room=xyz" +// "platform": "FHEM", +// "name": "FHEM", +// "server": "127.0.0.1", +// "port": "8083", +// "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 types = require('HAP-NodeJS/accessories/types.js'); +// +var types = require("HAP-NodeJS/accessories/types.js"); +var request = require("request"); var util = require('util'); - -// subscriptions to fhem longpoll evens -var FHEM_subscriptions = {}; -function -FHEM_subscribe(characteristic, inform_id, accessory) { - FHEM_subscriptions[inform_id] = { 'characteristic': characteristic, 'accessory': accessory }; -} - -// cached readings from longpoll & query -var FHEM_cached = {}; -//var FHEM_internal = {}; -function -FHEM_update(inform_id, value, no_update) { - var subscription = FHEM_subscriptions[inform_id]; - if( subscription != undefined ) { - if( value == undefined - || FHEM_cached[inform_id] === value ) - return; - - //if( FHEM_internal['.'+subscription.accessory.device+'-homekitID'] == undefined ) { - // var info = subscription.characteristic.accessoryController.tcpServer.accessoryInfo; - - // if( info.username ) { - // var accessory = subscription.accessory; - // var cmd = '{$defs{'+ accessory.device +'}->{homekitID} = "'+info.username+'" if(defined($defs{'+ accessory.device +'}));;}'; - // //accessory.execute( cmd ); - - // FHEM_internal['.'+accessory.device+'-homekitID'] = info.username; - // } - //} - - FHEM_cached[inform_id] = value; - //FHEM_cached[inform_id] = { 'value': value, 'timestamp': Date.now() }; - console.log(" caching: " + inform_id + ": " + value + " as " + typeof(value) ); - - if( !no_update ) - subscription.characteristic.updateValue(value, null); - } -} - - -var FHEM_lastEventTime; -var FHEM_longpoll_running = false; -//FIXME: add filter -function FHEM_startLongpoll(connection) { - if( FHEM_longpoll_running ) - return; - FHEM_longpoll_running = true; - - var filter = ".*"; - var since = "null"; - if( FHEM_lastEventTime ) - since = FHEM_lastEventTime/1000; - var query = "/fhem.pl?XHR=1"+ - "&inform=type=status;filter="+filter+";since="+since+";fmt=JSON"+ - "×tamp="+Date.now() - - var url = encodeURI( connection.base_url + query ); - console.log( 'starting longpoll: ' + url ); - - var FHEM_longpollOffset = 0; - var input = ""; - connection.request.get( { url: url } ).on( 'data', function(data) { -//console.log( 'data: '+ data ); - if( !data ) - return; - - input += data; - var lastEventTime = Date.now(); - for(;;) { - var nOff = input.indexOf('\n', FHEM_longpollOffset); - if(nOff < 0) - break; - var l = input.substr(FHEM_longpollOffset, nOff-FHEM_longpollOffset); - FHEM_longpollOffset = nOff+1; -//console.log( "Rcvd: "+(l.length>132 ? l.substring(0,132)+"...("+l.length+")":l) ); - if(!l.length) - continue; - - var d; - if( l.substr(0,1) == '[' ) - d = JSON.parse(l); - else - d = l.split("<<", 3); - - //console.log(d); - - if(d.length != 3) - continue; - if(d[0].match(/-ts$/)) - continue; - -//console.log( "Rcvd: "+(l.length>132 ? l.substring(0,132)+"...("+l.length+")":l) ); - - 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; - var accessory = subscription.accessory; - - var value = d[1]; - if( value.match( /^set-/ ) ) - continue; - if( value.match( /^set_/ ) ) - continue; - - var match = d[0].match(/([^-]*)-(.*)/); - var device = match[1]; - var reading = match[2]; - if( reading == undefined ) - continue; - - if( reading == 'state') { - if( accessory.mappings.window ) { - var level = 50; - if( match = value.match(/^(\d+)/ ) ) - level = parseInt( match[1] ); - else if( value == 'locked' ) - level = 0; - - FHEM_update( accessory.mappings.window.informId, level ); - continue; - - } else if( accessory.mappings.lock ) { - var lock = 0; - if( value.match( /^locked/ ) ) - lock = 1; - - if( value.match( /uncertain/ ) ) - level = 4; - - FHEM_update( accessory.mappings.lock.informId, lock ); - continue; - - } else if( match = value.match(/dim(\d+)%/ ) ) { - var pct = parseInt( match[1] ); - - FHEM_update( device+'-pct', pct ); - } - - } else if(accessory.mappings.rgb && reading == accessory.mappings.rgb.reading) { - var hsv = FHEM_rgb2hsv(value); - 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( device+'-hue', hue ); - FHEM_update( device+'-sat', sat ); - FHEM_update( device+'-bri', bri ); - continue; - } - - value = accessory.reading2homekit(reading, value); - FHEM_update( device+'-'+reading, value ); - - } else { - } - - } - - input = input.substr(FHEM_longpollOffset); - FHEM_longpollOffset = 0; - - } ).on( 'end', function() { - console.log( "longpoll ended" ); - - FHEM_longpoll_running = false; - setTimeout( function(){FHEM_startLongpoll(connection)}, 2000 ); - - } ).on( 'error', function(err) { - console.log( "longpoll error: " + err ); - - FHEM_longpoll_running = false; - setTimeout( function(){FHEM_startLongpoll(connection)}, 5000 ); - - } ); -} - - function FHEMPlatform(log, config) { this.log = log; - this.server = config['server']; - this.port = config['port']; - this.filter = config['filter']; - - var base_url; - if( config['ssl'] ) - base_url = 'https://'; - else - base_url = 'http://'; - base_url += this.server + ':' + this.port; - - var request = require('request'); - var auth = config['auth']; - if( auth ) { - if( auth.sendImmediately == undefined ) - auth.sendImmediately = false; - - request = request.defaults( { 'auth': auth, 'rejectUnauthorized': false } ); - } - - this.connection = { 'base_url': base_url, 'request': request }; - - FHEM_startLongpoll( this.connection ); + this.server = config["server"]; + this.port = config["port"]; + this.filter = config["filter"]; } function @@ -292,7 +91,7 @@ FHEM_hsv2rgb(h,s,v) { } function -FHEM_rgb2hsv(r,g,b){ +FHEM_rgb2h(r,g,b){ if( r == undefined ) return; @@ -318,15 +117,17 @@ FHEM_rgb2hsv(r,g,b){ h = ( 60 * ( ( r - g ) / c ) + 240 ) / 360; } + return h; + if( M == 0 ) { s = 0; } else { s = c / M; } - v = M/255; + v = M; - return [h,s,v]; + return h; } @@ -334,16 +135,16 @@ FHEMPlatform.prototype = { accessories: function(callback) { this.log("Fetching FHEM switchable devices..."); + var that = this; var foundAccessories = []; - var cmd = 'jsonlist2'; + var cmd = 'jsonlist'; if( this.filter ) cmd += " " + this.filter; - var url = encodeURI( this.connection.base_url + "/fhem?cmd=" + cmd + "&XHR=1"); + var url = encodeURI("http://" + this.server + ":" + this.port + "/fhem?cmd=" + cmd + "&XHR=1"); this.log( 'fetching: ' + url ); - var that = this; - this.connection.request.get( { url: url, json: true, gzip: true }, + request.get( { url: url, json: true }, function(err, response, json) { if( !err && response.statusCode == 200 ) { that.log( 'got: ' + json['totalResultsReturned'] + ' results' ); @@ -351,63 +152,37 @@ FHEMPlatform.prototype = { 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'); - } else if( s.Internals.TYPE == 'structure' ) { - that.log( s.Internals.NAME + ' is a structure'); - - } else if( s.Attributes.genericDisplayType - || s.Attributes.genericDeviceType ) { - accessory = new FHEMAccessory(that.log, that.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.Attributes.subType == 'thermostat' - || s.Attributes.subType == 'blindActuator' - || s.Attributes.subType == 'threeStateSensor' ) { - accessory = new FHEMAccessory(that.log, that.connection, s); - - } else if( s.Attributes.model == 'HM-SEC-WIN' ) { - accessory = new FHEMAccessory(that.log, that.connection, s); - - } else if( s.Attributes.model == 'HM-SEC-KEY' ) { - accessory = new FHEMAccessory(that.log, that.connection, s); - - } else if( s.Internals.TYPE == 'PRESENCE' ) { - accessory = new FHEMAccessory(that.log, that.connection, s); - - } else if( s.Readings.temperature ) { - accessory = new FHEMAccessory(that.log, that.connection, s); - - } else if( s.Readings.humidity ) { - accessory = new FHEMAccessory(that.log, that.connection, s); - - } else { - that.log( 'ignoring ' + s.Internals.NAME ); - - } - - if( accessory && Object.getOwnPropertyNames(accessory).length ) + } else if( s.PossibleSets.match(/\bon\b/) + && s.PossibleSets.match(/\boff\b/) ) { + accessory = new FHEMAccessory(that.log, that.server, that.port, s); foundAccessories.push(accessory); + } else if( s.PossibleSets.match(/\bvolume\b/) ) { + that.log( s.Internals.NAME + ' has volume'); + accessory = new FHEMAccessory(that.log, that.server, that.port, s); + foundAccessories.push(accessory); + + } else if( s.Readings.temperature ) { + accessory = new FHEMAccessory(that.log, that.server, that.port, s); + foundAccessories.push(accessory); + + } else if( s.Readings.humidity ) { + accessory = new FHEMAccessory(that.log, that.server, that.port, s); + foundAccessories.push(accessory); + + } else { + that.log( s.Internals.NAME + ' is not switchable'); + + } }); } - callback(foundAccessories); } else { - that.log("There was a problem connecting to FHEM (1)."); - if( response ) - that.log( " " + response.statusCode + ": " + response.statusMessage ); + that.log("There was a problem connecting to FHEM."); } @@ -416,594 +191,217 @@ FHEMPlatform.prototype = { } function -FHEMAccessory(log, connection, s) { +FHEMAccessory(log, server, port, s) { //log( 'sets: ' + s.PossibleSets ); //log("got json: " + util.inspect(s) ); //log("got json: " + util.inspect(s.Internals) ); - if( !(this instanceof FHEMAccessory) ) - return new FHEMAccessory(log, connection, s); - - if( s.Attributes.disable == 1 ) { - that.log( s.Internals.NAME + ' is disabled'); - return null; - - } else if( s.Internals.TYPE == 'structure' ) { - that.log( s.Internals.NAME + ' is a structure'); - return null; - - } - - - this.mappings = {}; - var match; - if( match = s.PossibleSets.match(/[\^ ]pct\b/) ) { - this.mappings.pct = { reading: 'pct', cmd: 'pct' }; - } else if( match = s.PossibleSets.match(/[\^ ]dim\d+%/) ) { - s.hasDim = true; + if( match = s.PossibleSets.match(/\bpct\b/) ) { + s.hasPct = true; s.pctMax = 100; } - if( match = s.PossibleSets.match(/[\^ ]hue[^\b\s]*(,(\d+)?)+\b/) ) { - s.isLight = true; - var max = 360; + if( match = s.PossibleSets.match(/\bhue[^\b\s]*(,(\d*)?)+\b/) ) { + s.hasHue = true; + s.hueMax = 360; if( match[2] != undefined ) - max = match[2]; - this.mappings.hue = { reading: 'hue', cmd: 'hue', min: 0, max: max }; + s.hueMax = match[2]; } - if( match = s.PossibleSets.match(/[\^ ]sat[^\b\s]*(,(\d+)?)+\b/) ) { - s.isLight = true; - var max = 100; + if( match = s.PossibleSets.match(/\bsat[^\b\s]*(,(\d*)?)+\b/) ) { + s.hasSat = true; + s.satMax = 100; if( match[2] != undefined ) - max = match[2]; - this.mappings.sat = { reading: 'sat', cmd: 'sat', min: 0, max: max }; - } - - 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/) ) { - s.isLight = true; - this.mappings.rgb = { reading: 'RGB', cmd: 'RGB' }; - } - - if( s.Readings['measured-temp'] ) - this.mappings.temperature = { reading: 'measured-temp' }; - else if( s.Readings.temperature ) - this.mappings.temperature = { reading: 'temperature' }; - - if( s.Readings.volume ) - this.mappings.volume = { reading: 'volume', cmd: 'volume' }; - else if( s.Readings.Volume ) { - this.mappings.volume = { reading: 'Volume', cmd: 'Volume', nocache: true }; - if( s.Attributes.generateVolumeEvent == 1 ) - delete this.mappings.volume.nocache; + s.satMax = match[2]; } + if( s.PossibleSets.match(/\brgb\b/) ) { + s.hasRGB = true; + } + if( s.Readings.temperature ) + s.hasTemperature = true; if( s.Readings.humidity ) - this.mappings.humidity = { reading: 'humidity' }; + s.hasHumidity = true; - if( s.Readings.motor ) - this.mappings.motor = { reading: 'motor' }; - - if( s.Readings.direction ) - this.mappings.direction = { reading: 'direction' }; - - - var genericType = s.Attributes.genericDeviceType; - if( !genericType ) - genericType = s.Attributes.genericDisplayType; - - if( genericType == 'switch' ) - s.isSwitch = true; - - else if( genericType == 'garage' ) - this.mappings.garage = { cmdOpen: 'on', cmdClose: 'off' }; - - else if( genericType == 'light' ) - s.isLight = true; - - else if( genericType == 'blind' - || s.Attributes.subType == 'blindActuator' ) { - delete this.mappings.pct; - this.mappings.blind = { reading: 'pct', cmd: 'pct' }; - - } else if( genericType == 'window' - || 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 == 'thermostat' - || s.Attributes.subType == 'thermostat' ) { - s.isThermostat = true; - - } else if( s.Internals.TYPE == 'CUL_FHTTK' ) { - this.mappings.contact = { reading: 'Window' }; - - } else if( s.Attributes.subType == 'threeStateSensor' ) { - this.mappings.contact = { reading: 'contact' }; - - } else if( s.Internals.TYPE == 'PRESENCE' ) - this.mappings.occupancy = { reading: 'state' }; - - else if( s.Attributes.model == 'fs20di' ) - s.isLight = true; - - if( s.PossibleSets.match(/[\^ ]desired-temp\b/) ) - this.mappings.thermostat = { reading: 'desired-temp', cmd: 'desired-temp' }; - else if( s.PossibleSets.match(/[\^ ]desiredTemperature\b/) ) - this.mappings.thermostat = { reading: 'desiredTemperature', cmd: 'desiredTemperature' }; - 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/) ) - this.mappings.onOff = { reading: 'state', cmdOn: 'on', cmdOff: 'off' }; - - var event_map = s.Attributes.eventMap; - if( event_map ) { - var parts = event_map.split( ' ' ); - for( var p = 0; p < parts.length; p++ ) { - var map = parts[p].split( ':' ); - if( map[1] == 'on' - || map[1] == 'off' ) { - if( !this.event_map ) - this.event_map = {} - this.event_map[map[0]] = map[1]; - } - } - } - - if( this.mappings.door ) - log( s.Internals.NAME + ' is door' ); - else if( this.mappings.garage ) - log( s.Internals.NAME + ' is garage' ); - else if( this.mappings.lock ) - log( s.Internals.NAME + ' is lock ['+ this.mappings.lock.reading +']' ); - else if( this.mappings.window ) - log( s.Internals.NAME + ' is window' ); - 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 +']' ); - else if( this.mappings.contact ) - log( s.Internals.NAME + ' is contactsensor [' + this.mappings.contact.reading +']' ); - else if( this.mappings.occupancy ) - log( s.Internals.NAME + ' is occupancysensor' ); - else if( this.mappings.rgb ) - log( s.Internals.NAME + ' has RGB [0-' + this.mappings.rgb.reading +']'); - else if( this.mappings.pct ) - log( s.Internals.NAME + ' is dimable ['+ this.mappings.pct.reading +']' ); - else if( s.hasDim ) - log( s.Internals.NAME + ' is dimable [0-'+ s.pctMax +']' ); - else if( s.isLight ) - log( s.Internals.NAME + ' is light' ); - else if( this.mappings.onOff || s.isSwitch ) - log( s.Internals.NAME + ' is switchable' ); + if( s.hasHue ) + log( s.Internals.NAME + ' has hue [0-' + s.hueMax +']'); + else if( s.hasRGB ) + log( s.Internals.NAME + ' has RGB'); + else if( s.hasPct ) + log( s.Internals.NAME + ' is dimable [' + s.pctMax +']'); + else if( s.hasTemperature ) + log( s.Internals.NAME + ' has temperature' ); else - return {}; + log( s.Internals.NAME + ' is switchable'); - - if( this.mappings.onOff ) - log( s.Internals.NAME + ' has onOff [' + this.mappings.onOff + ']' ); - if( this.mappings.hue ) - log( s.Internals.NAME + ' has hue [0-' + this.mappings.hue.max +']' ); - if( this.mappings.sat ) - log( s.Internals.NAME + ' has sat [0-' + this.mappings.sat.max +']' ); - 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.motor ) - log( s.Internals.NAME + ' has motor' ); - if( this.mappings.direction ) - log( s.Internals.NAME + ' has direction' ); + if( s.hasHumidity ) + log( s.Internals.NAME + ' has humidity' ); // device info + this.name = s.Attributes.alias ? s.Attributes.alias : s.Internals.NAME; this.name = s.Internals.NAME; - this.alias = s.Attributes.alias ? s.Attributes.alias : s.Internals.NAME; this.device = s.Internals.NAME; this.type = s.Internals.TYPE; - this.model = s.Attributes.model ? s.Attributes.model : (s.Internals.model ? s.Internals.model : s.Readings.model.Value); + this.model = s.Attributes.model ? s.Attributes.model : s.Internals.model; this.PossibleSets = s.PossibleSets; - if( this.type == 'CUL_HM' ) { - this.serial = 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; - else if( this.type == 'FS20' ) - this.serial = s.Internals.DEF; - else if( this.type == 'IT' ) + if( this.type == 'CUL_HM' ) this.serial = s.Internals.DEF; else if( this.type == 'HUEDevice' ) this.serial = s.Internals.uniqueid; else if( this.type == 'SONOSPLAYER' ) this.serial = s.Internals.UDN; - this.hasDim = s.hasDim; + this.hasPct = s.hasPct; this.pctMax = s.pctMax; + this.hasHue = s.hasHue; + this.hueMax = s.hueMax; + this.hasSat = s.hasSat; + this.satMax = s.satMax; + this.hasRGB = s.hasRGB; - this.isLight = s.isLight; - this.isSwitch = s.isSwitch; + this.hasTemperature = s.hasTemperature; + this.hasHumidity = s.hasHumidity; -//log( util.inspect(s.Readings) ); +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; - if( s.Readings[reading] && s.Readings[reading].Value ) { - var value = s.Readings[reading].Value; - value = that.reading2homekit(reading, value); - - if( value != undefined ) { - var inform_id = that.device +'-'+ reading; - that.mappings[key].informId = inform_id; - - if( !that.mappings[key].nocache ) - FHEM_cached[inform_id] = value; - } - } - } ); - - this.log = log; - this.connection = connection; - - this.onRegister = function(accessory) { -console.log( ">>>>>>>>>>>>:" + util.inspect(accessory) ); - }; + this.log = log; + this.server = server; + this.port = port; } -FHEM_dim_values = [ 'dim06%', 'dim12%', 'dim18%', 'dim25%', 'dim31%', 'dim37%', 'dim43%', 'dim50%', 'dim56%', 'dim62%', 'dim68%', 'dim75%', 'dim81%', 'dim87%', 'dim93%' ]; - FHEMAccessory.prototype = { - reading2homekit: function(reading,value) { - if( reading == 'hue' ) { - 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); - - } else if( reading == 'pct' ) { - value = parseInt( value ); - - } else if(reading == 'direction') { - if( value.match(/^up/)) - value = 1; - else if( value.match(/^down/)) - value = 0; - else - value = 2; - - } else if(reading == 'motor') { - if( value.match(/^opening/)) - value = 1; - else if( value.match(/^closing/)) - value = 0; - else - value = 2; - - } else if( reading == 'transportState' ) { - if( value == 'PLAYING' ) - value = 1; - else - value = 0; - - } else if( reading == 'volume' - || reading == 'Volume' ) { - value = parseInt( value ); - - } else if( reading == 'contact' ) { - if( value.match( /^closed/ ) ) - value = 1; - else - value = 0; - - } else if( reading == 'Window' ) { - if( value.match( /^Closed/ ) ) - value = 1; - else - value = 0; - - } else if( reading == 'lock' ) { - if( value.match( /^locked/ ) ) - value = 1; - else - value = 0; - - if( value.match( /uncertain/ ) ) - value = 4; - - } else if( reading == 'temperature' - || reading == 'measured-temp' - || reading == 'desired-temp' - || reading == 'desiredTemperature' ) { - value = parseFloat( value ); - - } else if( reading == 'humidity' ) { - value = parseInt( value ); - - } else if( reading == 'state' ) { - if( value.match(/^set-/ ) ) - return undefined; - if( value.match(/^set_/ ) ) - return undefined; - - if( this.event_map != undefined ) { - var mapped = this.event_map[value]; - if( mapped != undefined ) - value = mapped; - } - - if( value == 'off' ) - value = 0; - else if( value == 'absent' ) - value = 0; - else if( value == '000000' ) - value = 0; - else if( value.match( /^[A-D]0$/ ) ) //FIXME: is handled by event_map now - value = 0; - else - value = 1; - - } - - return(value); - }, - - delayed: function(c,value,delay) { - var timer = this.delayed[c]; - if( timer ) { - //this.log(this.name + " removing old command " + c); - clearTimeout( timer ); - } - - 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 ); - }, - command: function(c,value) { this.log(this.name + " sending command " + c + " with value " + value); - if( c == 'identify' ) { - if( this.type == 'HUEDevice' ) - cmd = "set " + this.device + "alert select"; - else - cmd = "set " + this.device + " toggle; sleep 1; set "+ this.device + " toggle"; - - } else if( c == 'set' ) { - cmd = "set " + this.device + " " + value; - - } else if( c == 'volume' ) { - cmd = "set " + this.device + " volume " + value; - - } else if( c == 'pct' ) { - cmd = "set " + this.device + " pct " + value; - - } else if( c == 'dim' ) { - //if( value < 3 ) - // cmd = "set " + this.device + " off"; - //else - if( value > 97 ) - cmd = "set " + this.device + " on"; - else - cmd = "set " + this.device + " " + FHEM_dim_values[Math.round(value/6.25)]; - - } else if( c == 'H-rgb' || c == 'S-rgb' || c == 'B-rgb' ) { - var h = FHEM_cached[this.device + '-hue' ] / 360; - var s = FHEM_cached[this.device + '-sat' ] / 100; - var v = FHEM_cached[this.device + '-bri' ] / 100; - //this.log( this.name + ' cached : [' + h + ',' + s + ',' + v + ']' ); - if( h == undefined ) h = 0.0; - if( s == undefined ) s = 1.0; - if( v == undefined ) v = 1.0; - //this.log( this.name + ' old : [' + h + ',' + s + ',' + v + ']' ); - - if( c == 'H-rgb' ) { - FHEM_update(this.device + '-hue', value, false ); - h = value / 360; - } else if( c == 'S-rgb' ) { - FHEM_update(this.device + '-sat', value, false ); - s = value / 100; - } else if( c == 'B-rgb' ) { - FHEM_update(this.device + '-bri', value, false ); - v = value / 100; - } - //this.log( this.name + ' new : [' + h + ',' + s + ',' + v + ']' ); - - value = FHEM_hsv2rgb( h, s, v ); - //this.log( this.name + ' rgb : [' + value + ']' ); - cmd = "set " + this.device + " " + this.mappings.rgb.cmd + " " + value; - - } else if( c == 'hue' ) { - value = Math.round(value * this.mappings.hue.max / 360); - cmd = "set " + this.device + " hue " + value; - - } else if( c == 'sat' ) { - value = value / 100 * this.mappings.sat.max; - cmd = "set " + this.device + " sat " + value; - - } else if( c == 'targetTemperature' ) { - cmd = "set " + this.device + " " + this.mappings.thermostat.cmd + " " + value; - - } else if( c == 'targetPosition' ) { - if( this.mappings.window ) { - if( value == 0 ) - value = 'lock'; - - cmd = "set " + this.device + " " + this.mappings.window.cmd + " " + value; - - } else if( this.mappings.blind ) - cmd = "set " + this.device + " " + this.mappings.blind.cmd + " " + value; - + if( c == 'on' ) { + if( this.PossibleSets.match(/\bplay\b/i) ) + url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " play&XHR=1"; + else if( this.PossibleSets.match(/\bon\b/) ) + url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " on&XHR=1"; else this.log(this.name + " Unhandled command! cmd=" + c + ", value=" + value); - } else { + } else if( c == 'off' ) { + if( this.PossibleSets.match(/\bpause\b/i) ) + url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " pause&XHR=1"; + else if( this.PossibleSets.match(/\boff\b/) ) + url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " off&XHR=1"; + else + this.log(this.device + " Unhandled command! cmd=" + c + ", value=" + value); + + } else if( c == 'pct' ) { + url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " pct " + value + "&XHR=1"; + + } else if( c == 'hue' ) { + if( !this.hasHue ) { + value = FHEM_hsv2rgb( value/360.0, this.sat?this.sat/100.0:1.0, this.pct?this.pct/100.0:1.0 ); + url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " rgb " + value + "&XHR=1"; + + } else { + value = Math.round(value * this.hueMax / 360); + url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " hue " + value + "&XHR=1"; + } + + } else if( c == 'sat' ) { + value = value / 100 * this.satMax; + url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " sat " + value + "&XHR=1"; + + } else if( value != undefined ) { this.log(this.name + " Unhandled command! cmd=" + c + ", value=" + value); - return; } - 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); - - } ); + request.put( { url: encodeURI(url) }, + function(err, response) { + if( err ) { + that.log("There was a problem sending command " + c + " to" + that.name); + that.log(url); + } else { + that.log(that.name + " sent command " + c); + that.log(url); + } + } ); }, query: function(reading, callback) { - this.log("query: " + this.name + "-" + reading); + this.log("query: " + reading); - var result = FHEM_cached[this.device + '-' + reading]; - if( result != undefined ) { - this.log(" cached: " + result); - if( callback != undefined ) - callback( result ); - return( result ); - } else - this.log(" not cached" ); - - var query_reading = reading; - if( reading == 'hue' && !this.mappings.hue && this.mappings.rgb ) { - query_reading = this.mappings.rgb.reading; - - } else if( reading == 'sat' && !this.mappings.sat && this.mappings.rgb ) { - query_reading = this.mappings.rgb.reading; - - } else if( reading == 'bri' && !this.mappings.pct && this.mappings.rgb ) { - query_reading = this.mappings.rgb.reading; - - } else if( reading == 'pct' && !this.mappings.pct && this.hasDim ) { - query_reading = 'state'; - - } else if( reading == 'level' && this.mappings.window ) { - query_reading = 'state'; - - } else if( reading == 'lock' && this.mappings.lock ) { - query_reading = 'state'; + var rgb_to_hue = false; + if( reading == 'hue' && !this.hasHue && this.hasRGB ) { + reading = 'rgb'; + rgb_to_hue = true; + } else if( reading == 'state' + && this.type == 'SONOSPLAYER' ) { + reading = 'transportState'; + } - var cmd = '{ReadingsVal("'+this.device+'","'+query_reading+'","")}'; + if( reading == 'rgb' + && this.type == 'SWAP_0000002200000003' ) { + reading = '0B-RGBlevel'; + + } + + var cmd = '{ReadingsVal("'+this.device+'","'+reading+'","")}'; + var url = encodeURI("http://" + this.server + ":" + this.port + "/fhem?cmd=" + cmd + "&XHR=1"); + this.log( ' querying: ' + url ); var that = this; - this.execute( cmd, - function(result) { - value = result.replace(/[\r\n]/g, ""); - that.log(" value: " + value); + request.get( { url: url }, + function(err, response, result) { + if( !err && response.statusCode == 200 ) { + result = result.replace(/[\r\n]/g, ""); + that.log(" result: " + result); - if( value == undefined ) - return value; + if( rgb_to_hue ) { + result = FHEM_rgb2h(result) * 360; + that.hue = result; + } else if( reading == 'hue' ) { + result = Math.round(result * 360 / that.hueMax); + that.hue = result; + } else if( reading == 'sat' ) { + result = Math.round(result * 100 / that.satMax); + that.sat = result; + } else if( reading == 'pct' ) { + that.pct = result; + } else if( reading == 'transportState' ) { + if( result == 'PLAYING' ) + result = 1; + else + result = 0; + that.state = result; + } else if( reading == 'state' ) { + if( result == 'off' ) + result = 0; + else if( result == 'on' ) + result = 1; + else if( result == '000000' ) + result = 0; + else + result = 1; - if( reading != query_reading ) { - if( reading == 'pct' - && query_reading == 'state') { + that.state = result; + } + that.log(" mapped: " + result); - if( match = value.match(/dim(\d+)%/ ) ) - value = parseInt( match[1] ); - else if( value == 'off' ) - value = 0; - else - value = 100; + callback(result); - } else if( reading == 'level' - && query_reading == 'state') { + } else { + that.log("There was a problem connecting to FHEM."); - if( match = value.match(/^(\d+)/ ) ) - value = parseInt( match[1] ); - else if( value == 'locked' ) - value = 0; - else - value = 50; - - } else if( reading == 'lock' - && query_reading == 'state') { - - if( value.match( /^locked/ ) ) - value = 1; - else - value = 0; - - if( value.match( /uncertain/ ) ) - value = 4; - - } else if(reading == 'hue' && query_reading == that.mappings.rgb) { - //FHEM_update( that.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 ); - - value = parseInt( FHEM_rgb2hsv(value)[1] * 100 ); - - } else if(reading == 'bri' && query_reading == that.mappings.rgb) { - //FHEM_update( that.device+'-'+query_reading, value ); - - value = parseInt( FHEM_rgb2hsv(value)[2] * 100 ); - - } - } else { - value = that.reading2homekit(reading, value); - } - - that.log(" mapped: " + value); - FHEM_update( that.device + '-' + reading, value, true ); - - if( value == undefined ) - return; - if( callback != undefined ) - callback(value); - return(value); - - } ); + } + } ); }, - informationCharacteristics: function(that) { + informationCharacteristics: function() { return [ { cType: types.NAME_CTYPE, onUpdate: null, perms: ["pr"], format: "string", - initialValue: this.alias, + initialValue: this.name, supportEvents: false, supportBonjour: false, manfDescription: "Name of the accessory", @@ -1033,17 +431,14 @@ FHEMAccessory.prototype = { onUpdate: null, perms: ["pr"], format: "string", - initialValue: this.serial ? this.serial : "", + initialValue: this.serial ? this.serial : "A1S2NASF88EW", supportEvents: false, supportBonjour: false, manfDescription: "SN", designedMaxLength: 255 },{ cType: types.IDENTIFY_CTYPE, - onUpdate: function(value) { - if( that.mappings.onOff ) - that.command( 'identify' ); - }, + onUpdate: null, perms: ["pw"], format: "bool", initialValue: false, @@ -1051,7 +446,8 @@ FHEMAccessory.prototype = { supportBonjour: false, manfDescription: "Identify Accessory", designedMaxLength: 1 - }]; + } + ] }, controlCharacteristics: function(that) { @@ -1060,69 +456,46 @@ FHEMAccessory.prototype = { onUpdate: null, perms: ["pr"], format: "string", - initialValue: this.alias, + initialValue: this.name, supportEvents: true, supportBonjour: false, manfDescription: "Name of service", designedMaxLength: 255 }] - if( this.mappings.onOff ) { + if( this.name != undefined + && !this.hasTemperature ) { cTypes.push({ cType: types.POWER_STATE_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.onOff.informId, that); + onRegister: function(assignedCharacteristic) { +//that.log("onRegister: " + util.inspect(assignedCharacteristic) ); }, onUpdate: function(value) { - that.command( 'set', value == 0 ? that.mappings.onOff.cmdOff : that.mappings.onOff.cmdOn ); + if( value == 0 ) { + that.command("off") + } else { + that.command("on") + } }, onRead: function(callback) { - that.query( that.mappings.onOff.reading, function(state){ callback(state) } ); + that.query('state', function(powerState){ + callback(powerState); + }); }, perms: ["pw","pr","ev"], format: "bool", - initialValue: FHEM_cached[that.mappings.onOff.informId], + initialValue: 0, supportEvents: true, supportBonjour: false, manfDescription: "Change the power state", designedMaxLength: 1 - }); + }) } - if( this.mappings.pct ) { + if( this.hasPct == true ) { cTypes.push({ cType: types.BRIGHTNESS_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.pct.informId, that); - }, onUpdate: function(value) { that.command('pct', value); }, - onRead: function(callback) { - that.query(that.mappings.pct.reading, function(pct){ - callback(pct); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: FHEM_cached[that.mappings.pct.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust Brightness of the Light", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - } else if( this.hasDim ) { - cTypes.push({ - cType: types.BRIGHTNESS_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - // state is alreadi subscribed from POWER_STATE_CTYPE - FHEM_subscribe(characteristic, that.name+'-pct', that); - }, - onUpdate: function(value) { that.delayed('dim', value); }, onRead: function(callback) { that.query('pct', function(pct){ callback(pct); @@ -1131,7 +504,6 @@ FHEMAccessory.prototype = { perms: ["pw","pr","ev"], format: "int", initialValue: 0, - //initialValue: FHEM_cached[that.mappings.dim.informId], supportEvents: true, supportBonjour: false, manfDescription: "Adjust Brightness of the Light", @@ -1139,42 +511,13 @@ FHEMAccessory.prototype = { designedMaxValue: this.pctMax, designedMinStep: 1, unit: "%" - }); + }) } - if( that.mappings.hue ) { + if( this.hasHue == true || this.hasRGB == true ) { cTypes.push({ cType: types.HUE_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.hue.informId, that); - }, onUpdate: function(value) { that.command('hue', value); }, - onRead: function(callback) { - that.query(that.mappings.hue.reading, function(hue){ - callback(hue); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: FHEM_cached[that.mappings.hue.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust the Hue of the Light", - designedMinValue: 0, - designedMaxValue: 360, - designedMinStep: 1, - unit: "arcdegrees" - }); - } else if( this.mappings.rgb ) { - cTypes.push({ - cType: types.HUE_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.name+'-hue', that); - FHEM_subscribe(characteristic, that.mappings.rgb.informId, that); - }, - onUpdate: function(value) { that.command('H-rgb', value); }, onRead: function(callback) { that.query('hue', function(hue){ callback(hue); @@ -1190,16 +533,13 @@ FHEMAccessory.prototype = { designedMaxValue: 360, designedMinStep: 1, unit: "arcdegrees" - }); + }) + } - if( !this.mappings.sat ) + if( this.hasSat == true ) { cTypes.push({ cType: types.SATURATION_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.name+'-sat', that); - }, - onUpdate: function(value) { that.command('S-rgb', value); }, + onUpdate: function(value) { that.command('sat', value); }, onRead: function(callback) { that.query('sat', function(sat){ callback(sat); @@ -1207,7 +547,7 @@ FHEMAccessory.prototype = { }, perms: ["pw","pr","ev"], format: "int", - initialValue: 100, + initialValue: 100, supportEvents: true, supportBonjour: false, manfDescription: "Adjust the Saturation of the Light", @@ -1215,435 +555,69 @@ FHEMAccessory.prototype = { designedMaxValue: 100, designedMinStep: 1, unit: "%" - }); - - if( !this.mappings.pct ) - cTypes.push({ - cType: types.BRIGHTNESS_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.name+'-bri', that); - }, - onUpdate: function(value) { that.command('B-rgb', value); }, - onRead: function(callback) { - that.query('bri', function(bri){ - callback(bri); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust Brightness of the Light", - designedMinValue: 0, - designedMaxValue: this.pctMax, - designedMinStep: 1, - unit: "%" - }); + }) } - if( this.mappings.sat ) { + if( match = this.PossibleSets.match(/\bvolume\b/) ) { cTypes.push({ - cType: types.SATURATION_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.sat.informId, that); - }, - onUpdate: function(value) { that.command('sat', value); }, + cType: types.OUTPIUTVOLUME_CTYPE, + onUpdate: function(value) { that.command('volume', value); }, onRead: function(callback) { - that.query(that.mappings.sat.reading, function(sat){ - callback(sat); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: FHEM_cached[that.mappings.sat.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust the Saturation of the Light", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - } - - if( this.mappings.volume ) { - cTypes.push({ - cType: types.OUTPUTVOLUME_CTYPE, - onUpdate: function(value) { that.delayed('volume', value); }, - onRegister: function(characteristic) { - if( !that.mappings.volume.nocache ) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.volume.informId, that); - } - }, - onRead: function(callback) { - that.query(that.mappings.volume.reading, function(volume){ - callback(volume); + that.query('volume', function(vol){ + callback(vol); }); }, perms: ["pw","pr","ev"], format: "int", initialValue: 10, - //initialValue: FHEM_cached[that.mappings.volume.informId], supportEvents: true, supportBonjour: false, - manfDescription: "Adjust the Volume of this device", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1 - //unit: "%" - }); - } - - if( this.mappings.blind ) { - cTypes.push({ - cType: types.WINDOW_COVERING_TARGET_POSITION_CTYPE, - onUpdate: function(value) { that.delayed('targetPosition', value, 1500); }, - //onRegister: function(characteristic) { - // characteristic.eventEnabled = true; - // FHEM_subscribe(characteristic, that.mappings.blind.informId, that); - //}, - onRead: function(callback) { - that.query(that.mappings.blind.reading, function(pct){ - callback(pct); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: FHEM_cached[that.mappings.blind.informId], - supportEvents: false, - supportBonjour: false, - manfDescription: "Target Blind Position", + manfDescription: "Adjust the Volume of the device", designedMinValue: 0, designedMaxValue: 100, designedMinStep: 1, unit: "%" - }); - cTypes.push({ - cType: types.WINDOW_COVERING_CURRENT_POSITION_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.blind.informId, that); - }, - onRead: function(callback) { - that.query(that.mappings.blind.reading, function(pos){ - callback(pos); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: FHEM_cached[that.mappings.blind.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Current Blind Position", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - - cTypes.push({ - cType: types.WINDOW_COVERING_OPERATION_STATE_CTYPE, - onRegister: function(characteristic) { - if( that.mappings.motor ) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.motor.informId, that); - } - }, - onRead: function(callback) { - if( that.mappings.motor ) - that.query(that.mappings.motor.reading, function(state){ - callback(state); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: that.mappings.motor?FHEM_cached[that.mappings.motor.informId]:2, - supportEvents: false, - supportBonjour: false, - manfDescription: "Position State", - designedMinValue: 0, - designedMaxValue: 2, - designedMinStep: 1, - }); + }) } - if( this.mappings.window ) { - cTypes.push({ - cType: types.WINDOW_COVERING_TARGET_POSITION_CTYPE, - onUpdate: function(value) { that.delayed('targetPosition', value, 1500); }, - onRead: function(callback) { - that.query(that.mappings.window.reading, function(level){ - callback(level); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 50, - supportEvents: false, - supportBonjour: false, - manfDescription: "Target Window Position", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - cTypes.push({ - cType: types.WINDOW_COVERING_CURRENT_POSITION_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.name+'-state', that); - FHEM_subscribe(characteristic, that.mappings.window.informId, that); - }, - onRead: function(callback) { - that.query(that.mappings.window.reading, function(pos){ - callback(pos); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: FHEM_cached[that.mappings.window.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Current Window Position", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - - cTypes.push({ - cType: types.WINDOW_COVERING_OPERATION_STATE_CTYPE, - onRegister: function(characteristic) { - if( that.mappings.direction ) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.direction.informId, that); - } - }, - onRead: function(callback) { - if( that.mappings.direction ) - that.query(that.mappings.direction.reading, function(direction){ - callback(direction); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: that.mappings.direction?FHEM_cached[that.mappings.direction.informId]:2, - supportEvents: false, - supportBonjour: false, - manfDescription: "Position State", - designedMinValue: 0, - designedMaxValue: 2, - designedMinStep: 1, - }); - } - - if( this.mappings.garage ) { - cTypes.push({ - onUpdate: function(value) { - that.command( 'set', value == 0 ? that.mappings.garage.cmdOpen : that.mappings.garage.cmdClose ); - }, - cType: types.TARGET_DOORSTATE_CTYPE, - onRead: function(callback) { - callback(1); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 1, - supportEvents: false, - supportBonjour: false, - manfDescription: "Target GarageDoor Position", - designedMinValue: 0, - designedMaxValue: 1, - designedMinStep: 1, - designedMaxLength: 1 - }); - cTypes.push({ - cType: types.CURRENT_DOOR_STATE_CTYPE, - onRead: function(callback) { - callback(4); - }, - perms: ["pr","ev"], - format: "int", - initialValue: 4, - supportEvents: true, - supportBonjour: false, - manfDescription: "Current GarageDoor State", - designedMinValue: 0, - designedMaxValue: 4, - designedMinStep: 1, - designedMaxLength: 1 - }); - - cTypes.push({ - cType: types.OBSTRUCTION_DETECTED_CTYPE, - onRead: function(callback) { - callback(false); - }, - perms: ["pr","ev"], - format: "bool", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Obstruction Detected", - designedMaxLength: 1 - }); - } - - //FIXME: parse range and set designedMinValue & designedMaxValue & designedMinStep - if( this.mappings.thermostat ) { - cTypes.push({ - cType: types.TARGET_TEMPERATURE_CTYPE, - onUpdate: function(value) { that.delayed('targetTemperature', value, 1500); }, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.thermostat.informId, that); - }, - onRead: function(callback) { - that.query(that.mappings.thermostat.reading, function(temperature){ - callback(temperature); - }); - }, - perms: ["pw","pr","ev"], - format: "float", - initialValue: FHEM_cached[that.mappings.thermostat.informId], - supportEvents: false, - supportBonjour: false, - manfDescription: "Target Temperature", - designedMinValue: 5.0, - designedMaxValue: 30.0, - //designedMinStep: 0.5, - unit: "celsius" - }); - cTypes.push({ - cType: types.CURRENTHEATINGCOOLING_CTYPE, - perms: ["pr","ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Current Mode", - designedMaxLength: 1, - designedMinValue: 0, - designedMaxValue: 2, - designedMinStep: 1, - }); - cTypes.push({ - cType: types.TARGETHEATINGCOOLING_CTYPE, - onUpdate: function(value) { that.command('targetMode', value); }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Target Mode", - designedMinValue: 0, - designedMaxValue: 3, - designedMinStep: 1, - }); - - cTypes.push({ - cType: types.TEMPERATURE_UNITS_CTYPE, - perms: ["pr","ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Unit", - }); - } - - if( this.mappings.contact ) { - cTypes.push({ - cType: types.CONTACT_SENSOR_STATE_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.contact.informId, that); - }, - onRead: function(callback) { - that.query(that.mappings.contact.reading, function(state){ - callback(state); - }); - }, - perms: ["pr","ev"], - format: "bool", - initialValue: FHEM_cached[that.mappings.contact.informId], - supportEvents: false, - supportBonjour: false, - manfDescription: "Contact State", - designedMaxLength: 1 - }); - } - - if( this.mappings.occupancy ) { - cTypes.push({ - cType: types.OCCUPANCY_DETECTED_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.occupancy.informId, that); - }, - onRead: function(callback) { - that.query(that.mappings.occupancy.reading, function(state){ - callback(state); - }); - }, - perms: ["pr","ev"], - format: "bool", - initialValue: FHEM_cached[that.mappings.occupancy.informId], - supportEvents: false, - supportBonjour: false, - manfDescription: "Occupancy State", - designedMaxLength: 1 - }); - } - - if( this.mappings.temperature ) { + if( this.hasTemperature ) { cTypes.push({ cType: types.CURRENT_TEMPERATURE_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.temperature.informId, that); - }, + //onUpdate: function(value) { console.log("Change:",value); execute("Thermostat", "Current Temperature", value); }, onRead: function(callback) { - that.query(that.mappings.temperature.reading, function(temperature){ - callback(temperature); + that.query('temperature', function(temperature){ + callback(parseFloat(temperature)); }); }, perms: ["pr","ev"], format: "float", - initialValue: FHEM_cached[that.mappings.temperature.informId], + initialValue: 20, supportEvents: true, supportBonjour: false, manfDescription: "Current Temperature", unit: "celsius" - }); + }) } - if( this.mappings.humidity ) { + if( this.hasHumidity ) { cTypes.push({ cType: types.CURRENT_RELATIVE_HUMIDITY_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.humidity.informId, that); - }, + //onUpdate: function(value) { console.log("Change:",value); execute("Thermostat", "Current Temperature", value); }, onRead: function(callback) { - that.query(that.mappings.humidity.reading, function(humidity){ - callback(humidity); + that.query('humidity', function(humidity){ + callback(parseInt(humidity)); }); }, perms: ["pr","ev"], format: "int", - initialValue: FHEM_cached[that.mappings.humidity.informId], + initialValue: 50, designedMinValue: 0, designedMaxValue: 100, supportEvents: true, supportBonjour: false, manfDescription: "Current Humidity", unit: "%" - }); + }) } @@ -1651,30 +625,16 @@ FHEMAccessory.prototype = { }, sType: function() { - if( match = this.PossibleSets.match(/[\^ ]volume\b/) ) { - return types.SPEAKER_STYPE; - } else if( this.isSwitch ) { - return types.SWITCH_STYPE; - } else if( this.mappings.garage ) { - return types.GARAGE_DOOR_OPENER_STYPE; - } else if( this.mappings.window ) { - return types.WINDOW_STYPE; - } else if( this.mappings.blind ) { - return types.WINDOW_COVERING_STYPE; - } else if( this.mappings.thermostat ) { - return types.THERMOSTAT_STYPE; - } else if( this.mappings.contact ) { - return types.CONTACT_SENSOR_STYPE; - } else if( this.mappings.occupancy ) { - return types.OCCUPANCY_SENSOR_STYPE; - } else if( this.isLight || this.mappings.pct || this.mappings.hue || this.mappings.rgb ) { - return types.LIGHTBULB_STYPE; - } else if( this.mappings.temperature ) { - return types.TEMPERATURE_SENSOR_STYPE; - } else if( this.mappings.humidity ) { - return types.HUMIDITY_SENSOR_STYPE; + if( match = this.PossibleSets.match(/\bvolume\b/) ) { + return types.SPEAKER_STYPE + } else if( this.hasTemperature ) { + return types.TEMPERATURE_SENSOR_STYPE + } else if( this.hasHumidity ) { + return types.HUMIDITY_SENSOR_STYPE + } else if( this.hasPct || this.hasHue || this.hasRGB ) { + return types.LIGHTBULB_STYPE } else { - return types.SWITCH_STYPE; + return types.SWITCH_STYPE } }, @@ -1682,7 +642,7 @@ FHEMAccessory.prototype = { var that = this; var services = [{ sType: types.ACCESSORY_INFORMATION_STYPE, - characteristics: this.informationCharacteristics(that), + characteristics: this.informationCharacteristics(), }, { sType: this.sType(), @@ -1695,63 +655,3 @@ FHEMAccessory.prototype = { //module.exports.accessory = FHEMAccessory; module.exports.platform = FHEMPlatform; - - - -//http server for debugging -var http = require('http'); - -const FHEMdebug_PORT=8080; - -function FHEMdebug_handleRequest(request, response){ - //console.log( request ); - - if( request.url == "/cached" ) { - response.write( "home

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

" ); - response.end( "cached: " + util.inspect(FHEM_cached).replace(/\n/g, '
') ); - - } else if( request.url == "/subscriptions" ) { - response.write( "home

" ); - response.end( "subscriptions: " + util.inspect(FHEM_subscriptions, {depth: 4}).replace(/\n/g, '
') ); - - } else if( request.url == "/persist" ) { - response.write( "home

" ); - var unique = {}; - Object.keys(FHEM_subscriptions).forEach(function(key) { - var characteristic = FHEM_subscriptions[key].characteristic; - var info = characteristic.accessoryController.tcpServer.accessoryInfo; - if( unique[info.displayName] ) - return; - unique[info.displayName] = info.username; - - var accessory = FHEM_subscriptions[key].accessory; - - //var cmd = '{$defs{'+ accessory.device +'}->{homekitID} = "'+info.username+'" if(defined($defs{'+ accessory.device +'}));;}'; - //accessory.execute( cmd ); - } ); - - var keys = Object.keys(unique); - keys.sort(); - for( i = 0; i < keys.length; i++ ) { - var k = keys[i]; - response.write( k +': '+ unique[k] +'
' ); - } - response.end( "" ); - - } else - response.end( "cached
persist
subscriptions" ); -} - -var FHEMdebug_server = http.createServer( FHEMdebug_handleRequest ); - -FHEMdebug_server.on('error', function (e) { - console.log("Server error: " + e); -}); - -//Lets start our server -FHEMdebug_server.listen(FHEMdebug_PORT, function(){ - console.log("Server listening on: http://:%s", FHEMdebug_PORT); -}); - From ca941bd3508e1f229389dfc426001e97ba8c286a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=20Schr=C3=B6ter?= Date: Sat, 12 Sep 2015 20:21:37 +0200 Subject: [PATCH 33/71] now for real... --- platforms/FHEM.js | 1649 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 1408 insertions(+), 241 deletions(-) diff --git a/platforms/FHEM.js b/platforms/FHEM.js index a62e4b6..01b9b21 100644 --- a/platforms/FHEM.js +++ b/platforms/FHEM.js @@ -3,26 +3,228 @@ // Remember to add platform to config.json. Example: // "platforms": [ // { -// "platform": "FHEM", -// "name": "FHEM", -// "server": "127.0.0.1", -// "port": "8083", -// "filter": "room=xyz" +// 'platform': "FHEM", +// 'name': "FHEM", +// 'server': "127.0.0.1", +// 'port': 8083, +// 'ssl': true, +// 'auth': {'user': "fhem", 'pass': "fhempassword"}, +// '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 types = require("HAP-NodeJS/accessories/types.js"); -var request = require("request"); + +var types = require('HAP-NodeJS/accessories/types.js'); var util = require('util'); + +// subscriptions to fhem longpoll evens +var FHEM_subscriptions = {}; +function +FHEM_subscribe(characteristic, inform_id, accessory) { + FHEM_subscriptions[inform_id] = { 'characteristic': characteristic, 'accessory': accessory }; +} + +// cached readings from longpoll & query +var FHEM_cached = {}; +//var FHEM_internal = {}; +function +FHEM_update(inform_id, value, no_update) { + var subscription = FHEM_subscriptions[inform_id]; + if( subscription != undefined ) { + if( value == undefined + || FHEM_cached[inform_id] === value ) + return; + + //if( FHEM_internal['.'+subscription.accessory.device+'-homekitID'] == undefined ) { + // var info = subscription.characteristic.accessoryController.tcpServer.accessoryInfo; + + // if( info.username ) { + // var accessory = subscription.accessory; + // var cmd = '{$defs{'+ accessory.device +'}->{homekitID} = "'+info.username+'" if(defined($defs{'+ accessory.device +'}));;}'; + // //accessory.execute( cmd ); + + // FHEM_internal['.'+accessory.device+'-homekitID'] = info.username; + // } + //} + + FHEM_cached[inform_id] = value; + //FHEM_cached[inform_id] = { 'value': value, 'timestamp': Date.now() }; + console.log(" caching: " + inform_id + ": " + value + " as " + typeof(value) ); + + if( !no_update ) + subscription.characteristic.setValue(value, undefined, 'fromFhem'); + //subscription.characteristic.updateValue(value, null); + } +} + + +var FHEM_lastEventTime; +var FHEM_longpoll_running = false; +//FIXME: add filter +function FHEM_startLongpoll(connection) { + if( FHEM_longpoll_running ) + return; + FHEM_longpoll_running = true; + + var filter = ".*"; + var since = "null"; + if( FHEM_lastEventTime ) + since = FHEM_lastEventTime/1000; + var query = "/fhem.pl?XHR=1"+ + "&inform=type=status;filter="+filter+";since="+since+";fmt=JSON"+ + "×tamp="+Date.now() + + var url = encodeURI( connection.base_url + query ); + console.log( 'starting longpoll: ' + url ); + + var FHEM_longpollOffset = 0; + var input = ""; + connection.request.get( { url: url } ).on( 'data', function(data) { +//console.log( 'data: '+ data ); + if( !data ) + return; + + input += data; + var lastEventTime = Date.now(); + for(;;) { + var nOff = input.indexOf('\n', FHEM_longpollOffset); + if(nOff < 0) + break; + var l = input.substr(FHEM_longpollOffset, nOff-FHEM_longpollOffset); + FHEM_longpollOffset = nOff+1; +//console.log( "Rcvd: "+(l.length>132 ? l.substring(0,132)+"...("+l.length+")":l) ); + if(!l.length) + continue; + + var d; + if( l.substr(0,1) == '[' ) + d = JSON.parse(l); + else + d = l.split("<<", 3); + + //console.log(d); + + if(d.length != 3) + continue; + if(d[0].match(/-ts$/)) + continue; + +//console.log( "Rcvd: "+(l.length>132 ? l.substring(0,132)+"...("+l.length+")":l) ); + + 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; + var accessory = subscription.accessory; + + var value = d[1]; + if( value.match( /^set-/ ) ) + continue; + if( value.match( /^set_/ ) ) + continue; + + var match = d[0].match(/([^-]*)-(.*)/); + var device = match[1]; + var reading = match[2]; + if( reading == undefined ) + continue; + + if( reading == 'state') { + if( accessory.mappings.window ) { + var level = 50; + if( match = value.match(/^(\d+)/ ) ) + level = parseInt( match[1] ); + else if( value == 'locked' ) + level = 0; + + FHEM_update( accessory.mappings.window.informId, level ); + continue; + + } else if( accessory.mappings.lock ) { + var lock = 0; + if( value.match( /^locked/ ) ) + lock = 1; + + if( value.match( /uncertain/ ) ) + level = 4; + + FHEM_update( accessory.mappings.lock.informId, lock ); + continue; + + } else if( match = value.match(/dim(\d+)%/ ) ) { + var pct = parseInt( match[1] ); + + FHEM_update( device+'-pct', pct ); + } + + } else if(accessory.mappings.rgb && reading == accessory.mappings.rgb.reading) { + var hsv = FHEM_rgb2hsv(value); + 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( device+'-hue', hue ); + FHEM_update( device+'-sat', sat ); + FHEM_update( device+'-bri', bri ); + continue; + } + + value = accessory.reading2homekit(reading, value); + FHEM_update( device+'-'+reading, value ); + + } else { + } + + } + + input = input.substr(FHEM_longpollOffset); + FHEM_longpollOffset = 0; + + } ).on( 'end', function() { + console.log( "longpoll ended" ); + + FHEM_longpoll_running = false; + setTimeout( function(){FHEM_startLongpoll(connection)}, 2000 ); + + } ).on( 'error', function(err) { + console.log( "longpoll error: " + err ); + + FHEM_longpoll_running = false; + setTimeout( function(){FHEM_startLongpoll(connection)}, 5000 ); + + } ); +} + + function FHEMPlatform(log, config) { this.log = log; - this.server = config["server"]; - this.port = config["port"]; - this.filter = config["filter"]; + this.server = config['server']; + this.port = config['port']; + this.filter = config['filter']; + + var base_url; + if( config['ssl'] ) + base_url = 'https://'; + else + base_url = 'http://'; + base_url += this.server + ':' + this.port; + + var request = require('request'); + var auth = config['auth']; + if( auth ) { + if( auth.sendImmediately == undefined ) + auth.sendImmediately = false; + + request = request.defaults( { 'auth': auth, 'rejectUnauthorized': false } ); + } + + this.connection = { 'base_url': base_url, 'request': request }; + + FHEM_startLongpoll( this.connection ); } function @@ -91,7 +293,7 @@ FHEM_hsv2rgb(h,s,v) { } function -FHEM_rgb2h(r,g,b){ +FHEM_rgb2hsv(r,g,b){ if( r == undefined ) return; @@ -117,17 +319,15 @@ FHEM_rgb2h(r,g,b){ h = ( 60 * ( ( r - g ) / c ) + 240 ) / 360; } - return h; - if( M == 0 ) { s = 0; } else { s = c / M; } - v = M; + v = M/255; - return h; + return [h,s,v]; } @@ -135,16 +335,23 @@ FHEMPlatform.prototype = { accessories: function(callback) { this.log("Fetching FHEM switchable devices..."); - var that = this; var foundAccessories = []; - var cmd = 'jsonlist'; + // mechanism to ensure callback is only executed once all requests complete + var asyncCalls = 0; + function callbackLater() { if (--asyncCalls == 0) callback(foundAccessories); } + + var cmd = 'jsonlist2'; if( this.filter ) cmd += " " + this.filter; - var url = encodeURI("http://" + this.server + ":" + this.port + "/fhem?cmd=" + cmd + "&XHR=1"); + var url = encodeURI( this.connection.base_url + "/fhem?cmd=" + cmd + "&XHR=1"); this.log( 'fetching: ' + url ); - request.get( { url: url, json: true }, + + 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' ); @@ -152,37 +359,64 @@ FHEMPlatform.prototype = { 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'); - } else if( s.PossibleSets.match(/\bon\b/) - && s.PossibleSets.match(/\boff\b/) ) { - accessory = new FHEMAccessory(that.log, that.server, that.port, s); - foundAccessories.push(accessory); + } else if( s.Internals.TYPE == 'structure' ) { + that.log( s.Internals.NAME + ' is a structure'); - } else if( s.PossibleSets.match(/\bvolume\b/) ) { + } else if( s.Attributes.genericDisplayType + || s.Attributes.genericDeviceType ) { + accessory = new FHEMAccessory(that.log, that.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.server, that.port, s); - foundAccessories.push(accessory); + accessory = new FHEMAccessory(that.log, that.connection, s); + + } else if( s.Attributes.subType == 'thermostat' + || s.Attributes.subType == 'blindActuator' + || s.Attributes.subType == 'threeStateSensor' ) { + accessory = new FHEMAccessory(that.log, that.connection, s); + + } else if( s.Attributes.model == 'HM-SEC-WIN' ) { + accessory = new FHEMAccessory(that.log, that.connection, s); + + } else if( s.Attributes.model == 'HM-SEC-KEY' ) { + accessory = new FHEMAccessory(that.log, that.connection, s); + + } else if( s.Internals.TYPE == 'PRESENCE' ) { + accessory = new FHEMAccessory(that.log, that.connection, s); } else if( s.Readings.temperature ) { - accessory = new FHEMAccessory(that.log, that.server, that.port, s); - foundAccessories.push(accessory); + accessory = new FHEMAccessory(that.log, that.connection, s); } else if( s.Readings.humidity ) { - accessory = new FHEMAccessory(that.log, that.server, that.port, s); - foundAccessories.push(accessory); + accessory = new FHEMAccessory(that.log, that.connection, s); } else { - that.log( s.Internals.NAME + ' is not switchable'); + that.log( 'ignoring ' + s.Internals.NAME ); } + + if( accessory && Object.getOwnPropertyNames(accessory).length ) + foundAccessories.push(accessory); + }); } - callback(foundAccessories); + + //callback(foundAccessories); + callbackLater(); } else { - that.log("There was a problem connecting to FHEM."); + that.log("There was a problem connecting to FHEM (1)."); + if( response ) + that.log( " " + response.statusCode + ": " + response.statusMessage ); } @@ -191,217 +425,598 @@ FHEMPlatform.prototype = { } function -FHEMAccessory(log, server, port, s) { +FHEMAccessory(log, connection, s) { //log( 'sets: ' + s.PossibleSets ); //log("got json: " + util.inspect(s) ); //log("got json: " + util.inspect(s.Internals) ); + if( !(this instanceof FHEMAccessory) ) + return new FHEMAccessory(log, connection, s); + + if( s.Attributes.disable == 1 ) { + that.log( s.Internals.NAME + ' is disabled'); + return null; + + } else if( s.Internals.TYPE == 'structure' ) { + that.log( s.Internals.NAME + ' is a structure'); + return null; + + } + + + this.mappings = {}; + var match; - if( match = s.PossibleSets.match(/\bpct\b/) ) { - s.hasPct = true; + if( match = s.PossibleSets.match(/[\^ ]pct\b/) ) { + this.mappings.pct = { reading: 'pct', cmd: 'pct' }; + } else if( match = s.PossibleSets.match(/[\^ ]dim\d+%/) ) { + s.hasDim = true; s.pctMax = 100; } - if( match = s.PossibleSets.match(/\bhue[^\b\s]*(,(\d*)?)+\b/) ) { - s.hasHue = true; - s.hueMax = 360; + if( match = s.PossibleSets.match(/[\^ ]hue[^\b\s]*(,(\d+)?)+\b/) ) { + s.isLight = true; + var max = 360; if( match[2] != undefined ) - s.hueMax = match[2]; + max = match[2]; + this.mappings.hue = { reading: 'hue', cmd: 'hue', min: 0, max: max }; } - if( match = s.PossibleSets.match(/\bsat[^\b\s]*(,(\d*)?)+\b/) ) { - s.hasSat = true; - s.satMax = 100; + if( match = s.PossibleSets.match(/[\^ ]sat[^\b\s]*(,(\d+)?)+\b/) ) { + s.isLight = true; + var max = 100; if( match[2] != undefined ) - s.satMax = match[2]; + max = match[2]; + this.mappings.sat = { reading: 'sat', cmd: 'sat', min: 0, max: max }; + } + + 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/) ) { + s.isLight = true; + this.mappings.rgb = { reading: 'RGB', cmd: 'RGB' }; + } + + if( s.Readings['measured-temp'] ) + this.mappings.temperature = { reading: 'measured-temp' }; + else if( s.Readings.temperature ) + this.mappings.temperature = { reading: 'temperature' }; + + if( s.Readings.volume ) + this.mappings.volume = { reading: 'volume', cmd: 'volume' }; + else if( s.Readings.Volume ) { + this.mappings.volume = { reading: 'Volume', cmd: 'Volume', nocache: true }; + if( s.Attributes.generateVolumeEvent == 1 ) + delete this.mappings.volume.nocache; } - if( s.PossibleSets.match(/\brgb\b/) ) { - s.hasRGB = true; - } - if( s.Readings.temperature ) - s.hasTemperature = true; if( s.Readings.humidity ) - s.hasHumidity = true; + this.mappings.humidity = { reading: 'humidity' }; - if( s.hasHue ) - log( s.Internals.NAME + ' has hue [0-' + s.hueMax +']'); - else if( s.hasRGB ) - log( s.Internals.NAME + ' has RGB'); - else if( s.hasPct ) - log( s.Internals.NAME + ' is dimable [' + s.pctMax +']'); - else if( s.hasTemperature ) - log( s.Internals.NAME + ' has temperature' ); + if( s.Readings.motor ) + this.mappings.motor = { reading: 'motor' }; + + if( s.Readings.direction ) + this.mappings.direction = { reading: 'direction' }; + + + var genericType = s.Attributes.genericDeviceType; + if( !genericType ) + genericType = s.Attributes.genericDisplayType; + + if( genericType == 'switch' ) + s.isSwitch = true; + + else if( genericType == 'garage' ) + this.mappings.garage = { cmdOpen: 'on', cmdClose: 'off' }; + + else if( genericType == 'light' ) + s.isLight = true; + + else if( genericType == 'blind' + || s.Attributes.subType == 'blindActuator' ) { + delete this.mappings.pct; + this.mappings.blind = { reading: 'pct', cmd: 'pct' }; + + } else if( genericType == 'window' + || 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 == 'thermostat' + || s.Attributes.subType == 'thermostat' ) { + s.isThermostat = true; + + } else if( s.Internals.TYPE == 'CUL_FHTTK' ) { + this.mappings.contact = { reading: 'Window' }; + + } else if( s.Attributes.subType == 'threeStateSensor' ) { + this.mappings.contact = { reading: 'contact' }; + + } else if( s.Internals.TYPE == 'PRESENCE' ) + this.mappings.occupancy = { reading: 'state' }; + + else if( s.Attributes.model == 'fs20di' ) + s.isLight = true; + + if( s.PossibleSets.match(/[\^ ]desired-temp\b/) ) + this.mappings.thermostat = { reading: 'desired-temp', cmd: 'desired-temp' }; + else if( s.PossibleSets.match(/[\^ ]desiredTemperature\b/) ) + this.mappings.thermostat = { reading: 'desiredTemperature', cmd: 'desiredTemperature' }; + 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/) ) + this.mappings.onOff = { reading: 'state', cmdOn: 'on', cmdOff: 'off' }; + + var event_map = s.Attributes.eventMap; + if( event_map ) { + var parts = event_map.split( ' ' ); + for( var p = 0; p < parts.length; p++ ) { + var map = parts[p].split( ':' ); + if( map[1] == 'on' + || map[1] == 'off' ) { + if( !this.event_map ) + this.event_map = {} + this.event_map[map[0]] = map[1]; + } + } + } + + if( this.mappings.door ) + log( s.Internals.NAME + ' is door' ); + else if( this.mappings.garage ) + log( s.Internals.NAME + ' is garage' ); + else if( this.mappings.lock ) + log( s.Internals.NAME + ' is lock ['+ this.mappings.lock.reading +']' ); + else if( this.mappings.window ) + log( s.Internals.NAME + ' is window' ); + 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 +']' ); + else if( this.mappings.contact ) + log( s.Internals.NAME + ' is contactsensor [' + this.mappings.contact.reading +']' ); + else if( this.mappings.occupancy ) + log( s.Internals.NAME + ' is occupancysensor' ); + else if( this.mappings.rgb ) + log( s.Internals.NAME + ' has RGB [0-' + this.mappings.rgb.reading +']'); + else if( this.mappings.pct ) + log( s.Internals.NAME + ' is dimable ['+ this.mappings.pct.reading +']' ); + else if( s.hasDim ) + log( s.Internals.NAME + ' is dimable [0-'+ s.pctMax +']' ); + else if( s.isLight ) + log( s.Internals.NAME + ' is light' ); + else if( this.mappings.onOff || s.isSwitch ) + log( s.Internals.NAME + ' is switchable' ); else - log( s.Internals.NAME + ' is switchable'); + return {}; - if( s.hasHumidity ) - log( s.Internals.NAME + ' has humidity' ); + + if( this.mappings.onOff ) + log( s.Internals.NAME + ' has onOff [' + this.mappings.onOff + ']' ); + if( this.mappings.hue ) + log( s.Internals.NAME + ' has hue [0-' + this.mappings.hue.max +']' ); + if( this.mappings.sat ) + log( s.Internals.NAME + ' has sat [0-' + this.mappings.sat.max +']' ); + 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.motor ) + log( s.Internals.NAME + ' has motor' ); + if( this.mappings.direction ) + log( s.Internals.NAME + ' has direction' ); + +log(s); // device info - this.name = s.Attributes.alias ? s.Attributes.alias : s.Internals.NAME; this.name = s.Internals.NAME; + this.alias = s.Attributes.alias ? s.Attributes.alias : s.Internals.NAME; this.device = s.Internals.NAME; this.type = s.Internals.TYPE; - this.model = s.Attributes.model ? s.Attributes.model : s.Internals.model; + this.model = s.Readings.model ? s.Readings.model.Value + : (s.Attributes.model ? s.Attributes.model + : ( s.Internals.model ? s.Internals.model : '' ) ); this.PossibleSets = s.PossibleSets; - if( this.type == 'CUL_HM' ) + if( this.type == 'CUL_HM' ) { + this.serial = 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; + else if( this.type == 'FS20' ) + this.serial = s.Internals.DEF; + else if( this.type == 'IT' ) this.serial = s.Internals.DEF; else if( this.type == 'HUEDevice' ) this.serial = s.Internals.uniqueid; else if( this.type == 'SONOSPLAYER' ) this.serial = s.Internals.UDN; - this.hasPct = s.hasPct; + this.hasDim = s.hasDim; this.pctMax = s.pctMax; - this.hasHue = s.hasHue; - this.hueMax = s.hueMax; - this.hasSat = s.hasSat; - this.satMax = s.satMax; - this.hasRGB = s.hasRGB; - this.hasTemperature = s.hasTemperature; - this.hasHumidity = s.hasHumidity; + this.isLight = s.isLight; + this.isSwitch = s.isSwitch; -log( util.inspect(s.Readings) ); +//log( util.inspect(s.Readings) ); - this.log = log; - this.server = server; - this.port = port; + 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; + if( s.Readings[reading] && s.Readings[reading].Value ) { + var value = s.Readings[reading].Value; + value = that.reading2homekit(reading, value); + + if( value != undefined ) { + var inform_id = that.device +'-'+ reading; + that.mappings[key].informId = inform_id; + + if( !that.mappings[key].nocache ) + FHEM_cached[inform_id] = value; + } + } + } ); + + this.log = log; + this.connection = connection; + + this.onRegister = function(accessory) { +console.log( ">>>>>>>>>>>>:" + util.inspect(accessory) ); + }; } +FHEM_dim_values = [ 'dim06%', 'dim12%', 'dim18%', 'dim25%', 'dim31%', 'dim37%', 'dim43%', 'dim50%', 'dim56%', 'dim62%', 'dim68%', 'dim75%', 'dim81%', 'dim87%', 'dim93%' ]; + FHEMAccessory.prototype = { + reading2homekit: function(reading,value) { + if( reading == 'hue' ) { + 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); + + } else if( reading == 'pct' ) { + value = parseInt( value ); + + } else if(reading == 'direction') { + if( value.match(/^up/)) + value = 1; + else if( value.match(/^down/)) + value = 0; + else + value = 2; + + } else if(reading == 'motor') { + if( value.match(/^opening/)) + value = 1; + else if( value.match(/^closing/)) + value = 0; + else + value = 2; + + } else if( reading == 'transportState' ) { + if( value == 'PLAYING' ) + value = 1; + else + value = 0; + + } else if( reading == 'volume' + || reading == 'Volume' ) { + value = parseInt( value ); + + } else if( reading == 'contact' ) { + if( value.match( /^closed/ ) ) + value = 1; + else + value = 0; + + } else if( reading == 'Window' ) { + if( value.match( /^Closed/ ) ) + value = 1; + else + value = 0; + + } else if( reading == 'lock' ) { + if( value.match( /^locked/ ) ) + value = 1; + else + value = 0; + + if( value.match( /uncertain/ ) ) + value = 4; + + } else if( reading == 'temperature' + || reading == 'measured-temp' + || reading == 'desired-temp' + || reading == 'desiredTemperature' ) { + value = parseFloat( value ); + + } else if( reading == 'humidity' ) { + value = parseInt( value ); + + } else if( reading == 'state' ) { + if( value.match(/^set-/ ) ) + return undefined; + if( value.match(/^set_/ ) ) + return undefined; + + if( this.event_map != undefined ) { + var mapped = this.event_map[value]; + if( mapped != undefined ) + value = mapped; + } + + if( value == 'off' ) + value = 0; + else if( value == 'absent' ) + value = 0; + else if( value == '000000' ) + value = 0; + else if( value.match( /^[A-D]0$/ ) ) //FIXME: is handled by event_map now + value = 0; + else + value = 1; + + } + + return(value); + }, + + delayed: function(c,value,delay) { + var timer = this.delayed[c]; + if( timer ) { + //this.log(this.name + " removing old command " + c); + clearTimeout( timer ); + } + + 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 ); + }, + command: function(c,value) { this.log(this.name + " sending command " + c + " with value " + value); - if( c == 'on' ) { - if( this.PossibleSets.match(/\bplay\b/i) ) - url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " play&XHR=1"; - else if( this.PossibleSets.match(/\bon\b/) ) - url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " on&XHR=1"; + if( c == 'identify' ) { + if( this.type == 'HUEDevice' ) + cmd = "set " + this.device + "alert select"; + else + cmd = "set " + this.device + " toggle; sleep 1; set "+ this.device + " toggle"; + + } else if( c == 'set' ) { + cmd = "set " + this.device + " " + value; + + } else if( c == 'volume' ) { + cmd = "set " + this.device + " volume " + value; + + } else if( c == 'pct' ) { + cmd = "set " + this.device + " pct " + value; + + } else if( c == 'dim' ) { + //if( value < 3 ) + // cmd = "set " + this.device + " off"; + //else + if( value > 97 ) + cmd = "set " + this.device + " on"; + else + cmd = "set " + this.device + " " + FHEM_dim_values[Math.round(value/6.25)]; + + } else if( c == 'H-rgb' || c == 'S-rgb' || c == 'B-rgb' ) { + var h = FHEM_cached[this.device + '-hue' ] / 360; + var s = FHEM_cached[this.device + '-sat' ] / 100; + var v = FHEM_cached[this.device + '-bri' ] / 100; + //this.log( this.name + ' cached : [' + h + ',' + s + ',' + v + ']' ); + if( h == undefined ) h = 0.0; + if( s == undefined ) s = 1.0; + if( v == undefined ) v = 1.0; + //this.log( this.name + ' old : [' + h + ',' + s + ',' + v + ']' ); + + if( c == 'H-rgb' ) { + FHEM_update(this.device + '-hue', value, false ); + h = value / 360; + } else if( c == 'S-rgb' ) { + FHEM_update(this.device + '-sat', value, false ); + s = value / 100; + } else if( c == 'B-rgb' ) { + FHEM_update(this.device + '-bri', value, false ); + v = value / 100; + } + //this.log( this.name + ' new : [' + h + ',' + s + ',' + v + ']' ); + + value = FHEM_hsv2rgb( h, s, v ); + //this.log( this.name + ' rgb : [' + value + ']' ); + cmd = "set " + this.device + " " + this.mappings.rgb.cmd + " " + value; + + } else if( c == 'hue' ) { + value = Math.round(value * this.mappings.hue.max / 360); + cmd = "set " + this.device + " hue " + value; + + } else if( c == 'sat' ) { + value = value / 100 * this.mappings.sat.max; + cmd = "set " + this.device + " sat " + value; + + } else if( c == 'targetTemperature' ) { + cmd = "set " + this.device + " " + this.mappings.thermostat.cmd + " " + value; + + } else if( c == 'targetPosition' ) { + if( this.mappings.window ) { + if( value == 0 ) + value = 'lock'; + + cmd = "set " + this.device + " " + this.mappings.window.cmd + " " + value; + + } else if( this.mappings.blind ) + cmd = "set " + this.device + " " + this.mappings.blind.cmd + " " + value; + else this.log(this.name + " Unhandled command! cmd=" + c + ", value=" + value); - } else if( c == 'off' ) { - if( this.PossibleSets.match(/\bpause\b/i) ) - url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " pause&XHR=1"; - else if( this.PossibleSets.match(/\boff\b/) ) - url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " off&XHR=1"; - else - this.log(this.device + " Unhandled command! cmd=" + c + ", value=" + value); - - } else if( c == 'pct' ) { - url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " pct " + value + "&XHR=1"; - - } else if( c == 'hue' ) { - if( !this.hasHue ) { - value = FHEM_hsv2rgb( value/360.0, this.sat?this.sat/100.0:1.0, this.pct?this.pct/100.0:1.0 ); - url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " rgb " + value + "&XHR=1"; - - } else { - value = Math.round(value * this.hueMax / 360); - url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " hue " + value + "&XHR=1"; - } - - } else if( c == 'sat' ) { - value = value / 100 * this.satMax; - url = "http://" + this.server + ":" + this.port + "/fhem?cmd=set " + this.device + " sat " + value + "&XHR=1"; - - } else if( value != undefined ) { + } else { this.log(this.name + " Unhandled command! cmd=" + c + ", value=" + value); + return; } - var that = this; - request.put( { url: encodeURI(url) }, - function(err, response) { - if( err ) { - that.log("There was a problem sending command " + c + " to" + that.name); - that.log(url); - } else { - that.log(that.name + " sent command " + c); - that.log(url); - } - } ); + this.execute(cmd); }, - query: function(reading, callback) { - this.log("query: " + reading); - - var rgb_to_hue = false; - if( reading == 'hue' && !this.hasHue && this.hasRGB ) { - reading = 'rgb'; - rgb_to_hue = true; - - } else if( reading == 'state' - && this.type == 'SONOSPLAYER' ) { - reading = 'transportState'; - - } - - if( reading == 'rgb' - && this.type == 'SWAP_0000002200000003' ) { - reading = '0B-RGBlevel'; - - } - - var cmd = '{ReadingsVal("'+this.device+'","'+reading+'","")}'; - var url = encodeURI("http://" + this.server + ":" + this.port + "/fhem?cmd=" + cmd + "&XHR=1"); - this.log( ' querying: ' + url ); + execute: function(cmd,callback) { + var url = encodeURI( this.connection.base_url + "/fhem?cmd=" + cmd + "&XHR=1"); + this.log( ' executing: ' + url ); var that = this; - request.get( { url: url }, + this.connection.request.get( { url: url, gzip: true }, function(err, response, result) { if( !err && response.statusCode == 200 ) { - result = result.replace(/[\r\n]/g, ""); - that.log(" result: " + result); - - if( rgb_to_hue ) { - result = FHEM_rgb2h(result) * 360; - that.hue = result; - } else if( reading == 'hue' ) { - result = Math.round(result * 360 / that.hueMax); - that.hue = result; - } else if( reading == 'sat' ) { - result = Math.round(result * 100 / that.satMax); - that.sat = result; - } else if( reading == 'pct' ) { - that.pct = result; - } else if( reading == 'transportState' ) { - if( result == 'PLAYING' ) - result = 1; - else - result = 0; - that.state = result; - } else if( reading == 'state' ) { - if( result == 'off' ) - result = 0; - else if( result == 'on' ) - result = 1; - else if( result == '000000' ) - result = 0; - else - result = 1; - - that.state = result; - } - that.log(" mapped: " + result); - - callback(result); + if( callback ) + callback( result ); } else { - that.log("There was a problem connecting to FHEM."); + 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); + } ); }, - informationCharacteristics: function() { + query: function(reading, callback) { + this.log("query: " + this.name + "-" + reading); + + var result = FHEM_cached[this.device + '-' + reading]; + if( result != undefined ) { + this.log(" cached: " + result); + if( callback != undefined ) + callback( result ); + return( result ); + } else + this.log(" not cached" ); + + var query_reading = reading; + if( reading == 'hue' && !this.mappings.hue && this.mappings.rgb ) { + query_reading = this.mappings.rgb.reading; + + } else if( reading == 'sat' && !this.mappings.sat && this.mappings.rgb ) { + query_reading = this.mappings.rgb.reading; + + } else if( reading == 'bri' && !this.mappings.pct && this.mappings.rgb ) { + query_reading = this.mappings.rgb.reading; + + } else if( reading == 'pct' && !this.mappings.pct && this.hasDim ) { + query_reading = 'state'; + + } else if( reading == 'level' && this.mappings.window ) { + query_reading = 'state'; + + } else if( reading == 'lock' && this.mappings.lock ) { + 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); + + if( value == undefined ) + return value; + + if( reading != query_reading ) { + if( reading == 'pct' + && query_reading == 'state') { + + if( match = value.match(/dim(\d+)%/ ) ) + value = parseInt( match[1] ); + else if( value == 'off' ) + value = 0; + else + value = 100; + + } else if( reading == 'level' + && query_reading == 'state') { + + if( match = value.match(/^(\d+)/ ) ) + value = parseInt( match[1] ); + else if( value == 'locked' ) + value = 0; + else + value = 50; + + } else if( reading == 'lock' + && query_reading == 'state') { + + if( value.match( /^locked/ ) ) + value = 1; + else + value = 0; + + if( value.match( /uncertain/ ) ) + value = 4; + + } else if(reading == 'hue' && query_reading == that.mappings.rgb) { + //FHEM_update( that.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 ); + + value = parseInt( FHEM_rgb2hsv(value)[1] * 100 ); + + } else if(reading == 'bri' && query_reading == that.mappings.rgb) { + //FHEM_update( that.device+'-'+query_reading, value ); + + value = parseInt( FHEM_rgb2hsv(value)[2] * 100 ); + + } + } else { + value = that.reading2homekit(reading, value); + } + + that.log(" mapped: " + value); + FHEM_update( that.device + '-' + reading, value, true ); + + if( value == undefined ) + return; + if( callback != undefined ) + callback(value); + return(value); + + } ); + }, + + informationCharacteristics: function(that) { return [ { cType: types.NAME_CTYPE, onUpdate: null, perms: ["pr"], format: "string", - initialValue: this.name, + initialValue: this.alias, supportEvents: false, supportBonjour: false, manfDescription: "Name of the accessory", @@ -431,14 +1046,19 @@ FHEMAccessory.prototype = { onUpdate: null, perms: ["pr"], format: "string", - initialValue: this.serial ? this.serial : "A1S2NASF88EW", + initialValue: this.serial ? this.serial : "", supportEvents: false, supportBonjour: false, manfDescription: "SN", designedMaxLength: 255 },{ cType: types.IDENTIFY_CTYPE, - onUpdate: null, + onUpdate: function(value, context) { + if( context === 'fromFhem' ) + return; + if( that.mappings.onOff ) + that.command( 'identify' ); + }, perms: ["pw"], format: "bool", initialValue: false, @@ -446,8 +1066,7 @@ FHEMAccessory.prototype = { supportBonjour: false, manfDescription: "Identify Accessory", designedMaxLength: 1 - } - ] + }]; }, controlCharacteristics: function(that) { @@ -456,46 +1075,79 @@ FHEMAccessory.prototype = { onUpdate: null, perms: ["pr"], format: "string", - initialValue: this.name, + initialValue: this.alias, supportEvents: true, supportBonjour: false, manfDescription: "Name of service", designedMaxLength: 255 }] - if( this.name != undefined - && !this.hasTemperature ) { + if( this.mappings.onOff ) { cTypes.push({ cType: types.POWER_STATE_CTYPE, - onRegister: function(assignedCharacteristic) { -//that.log("onRegister: " + util.inspect(assignedCharacteristic) ); + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.mappings.onOff.informId, that); }, - onUpdate: function(value) { - if( value == 0 ) { - that.command("off") - } else { - that.command("on") - } + onUpdate: function(value, context) { + if( context === 'fromFhem' ) + return; + that.command( 'set', value == 0 ? that.mappings.onOff.cmdOff : that.mappings.onOff.cmdOn ); }, onRead: function(callback) { - that.query('state', function(powerState){ - callback(powerState); - }); + that.query( that.mappings.onOff.reading, function(state){ callback(state) } ); }, perms: ["pw","pr","ev"], format: "bool", - initialValue: 0, + initialValue: FHEM_cached[that.mappings.onOff.informId], supportEvents: true, supportBonjour: false, manfDescription: "Change the power state", designedMaxLength: 1 - }) + }); } - if( this.hasPct == true ) { + if( this.mappings.pct ) { cTypes.push({ cType: types.BRIGHTNESS_CTYPE, - onUpdate: function(value) { that.command('pct', value); }, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.mappings.pct.informId, that); + }, + onUpdate: function(value, context) { + if( context === 'fromFhem' ) + return; + that.command('pct', value); + }, + onRead: function(callback) { + that.query(that.mappings.pct.reading, function(pct){ + callback(pct); + }); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: FHEM_cached[that.mappings.pct.informId], + supportEvents: true, + supportBonjour: false, + manfDescription: "Adjust Brightness of the Light", + designedMinValue: 0, + designedMaxValue: 100, + designedMinStep: 1, + unit: "%" + }); + } else if( this.hasDim ) { + cTypes.push({ + cType: types.BRIGHTNESS_CTYPE, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + // state is alreadi subscribed from POWER_STATE_CTYPE + FHEM_subscribe(characteristic, that.name+'-pct', that); + }, + onUpdate: function(value, context) { + if( context === 'fromFhem' ) + return; + that.delayed('dim', value); + }, onRead: function(callback) { that.query('pct', function(pct){ callback(pct); @@ -504,6 +1156,7 @@ FHEMAccessory.prototype = { perms: ["pw","pr","ev"], format: "int", initialValue: 0, + //initialValue: FHEM_cached[that.mappings.dim.informId], supportEvents: true, supportBonjour: false, manfDescription: "Adjust Brightness of the Light", @@ -511,13 +1164,50 @@ FHEMAccessory.prototype = { designedMaxValue: this.pctMax, designedMinStep: 1, unit: "%" - }) + }); } - if( this.hasHue == true || this.hasRGB == true ) { + if( that.mappings.hue ) { cTypes.push({ cType: types.HUE_CTYPE, - onUpdate: function(value) { that.command('hue', value); }, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.mappings.hue.informId, that); + }, + onUpdate: function(value, context) { + if( context === 'fromFhem' ) + return; + that.command('hue', value); + }, + onRead: function(callback) { + that.query(that.mappings.hue.reading, function(hue){ + callback(hue); + }); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: FHEM_cached[that.mappings.hue.informId], + supportEvents: true, + supportBonjour: false, + manfDescription: "Adjust the Hue of the Light", + designedMinValue: 0, + designedMaxValue: 360, + designedMinStep: 1, + unit: "arcdegrees" + }); + } else if( this.mappings.rgb ) { + cTypes.push({ + cType: types.HUE_CTYPE, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.name+'-hue', that); + FHEM_subscribe(characteristic, that.mappings.rgb.informId, that); + }, + onUpdate: function(value, context) { + if( context === 'fromFhem' ) + return; + that.command('H-rgb', value); + }, onRead: function(callback) { that.query('hue', function(hue){ callback(hue); @@ -533,13 +1223,20 @@ FHEMAccessory.prototype = { designedMaxValue: 360, designedMinStep: 1, unit: "arcdegrees" - }) - } + }); - if( this.hasSat == true ) { + if( !this.mappings.sat ) cTypes.push({ cType: types.SATURATION_CTYPE, - onUpdate: function(value) { that.command('sat', value); }, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.name+'-sat', that); + }, + onUpdate: function(value, context) { + if( context === 'fromFhem' ) + return; + that.command('S-rgb', value); + }, onRead: function(callback) { that.query('sat', function(sat){ callback(sat); @@ -547,7 +1244,7 @@ FHEMAccessory.prototype = { }, perms: ["pw","pr","ev"], format: "int", - initialValue: 100, + initialValue: 100, supportEvents: true, supportBonjour: false, manfDescription: "Adjust the Saturation of the Light", @@ -555,69 +1252,465 @@ FHEMAccessory.prototype = { designedMaxValue: 100, designedMinStep: 1, unit: "%" - }) + }); + + if( !this.mappings.pct ) + cTypes.push({ + cType: types.BRIGHTNESS_CTYPE, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.name+'-bri', that); + }, + onUpdate: function(value, context) { + if( context === 'fromFhem' ) + return; + that.command('B-rgb', value); + }, + onRead: function(callback) { + that.query('bri', function(bri){ + callback(bri); + }); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Adjust Brightness of the Light", + designedMinValue: 0, + designedMaxValue: this.pctMax, + designedMinStep: 1, + unit: "%" + }); } - if( match = this.PossibleSets.match(/\bvolume\b/) ) { + if( this.mappings.sat ) { cTypes.push({ - cType: types.OUTPIUTVOLUME_CTYPE, - onUpdate: function(value) { that.command('volume', value); }, + cType: types.SATURATION_CTYPE, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.mappings.sat.informId, that); + }, + onUpdate: function(value, context) { + if( context === 'fromFhem' ) + return; + that.command('sat', value); + }, onRead: function(callback) { - that.query('volume', function(vol){ - callback(vol); + that.query(that.mappings.sat.reading, function(sat){ + callback(sat); + }); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: FHEM_cached[that.mappings.sat.informId], + supportEvents: true, + supportBonjour: false, + manfDescription: "Adjust the Saturation of the Light", + designedMinValue: 0, + designedMaxValue: 100, + designedMinStep: 1, + unit: "%" + }); + } + + if( this.mappings.volume ) { + cTypes.push({ + cType: types.OUTPUTVOLUME_CTYPE, + onUpdate: function(value, context) { + if( context === 'fromFhem' ) + return; + that.delayed('volume', value); + }, + onRegister: function(characteristic) { + if( !that.mappings.volume.nocache ) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.mappings.volume.informId, that); + } + }, + onRead: function(callback) { + that.query(that.mappings.volume.reading, function(volume){ + callback(volume); }); }, perms: ["pw","pr","ev"], format: "int", initialValue: 10, + //initialValue: FHEM_cached[that.mappings.volume.informId], supportEvents: true, supportBonjour: false, - manfDescription: "Adjust the Volume of the device", + manfDescription: "Adjust the Volume of this device", + designedMinValue: 0, + designedMaxValue: 100, + designedMinStep: 1 + //unit: "%" + }); + } + + if( this.mappings.blind ) { + cTypes.push({ + cType: types.WINDOW_COVERING_TARGET_POSITION_CTYPE, + onUpdate: function(value, context) { + if( context === 'fromFhem' ) + return; + that.delayed('targetPosition', value, 1500); + }, + //onRegister: function(characteristic) { + // characteristic.eventEnabled = true; + // FHEM_subscribe(characteristic, that.mappings.blind.informId, that); + //}, + onRead: function(callback) { + that.query(that.mappings.blind.reading, function(pct){ + callback(pct); + }); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: FHEM_cached[that.mappings.blind.informId], + supportEvents: false, + supportBonjour: false, + manfDescription: "Target Blind Position", designedMinValue: 0, designedMaxValue: 100, designedMinStep: 1, unit: "%" - }) + }); + cTypes.push({ + cType: types.WINDOW_COVERING_CURRENT_POSITION_CTYPE, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.mappings.blind.informId, that); + }, + onRead: function(callback) { + that.query(that.mappings.blind.reading, function(pos){ + callback(pos); + }); + }, + perms: ["pr","ev"], + format: "int", + initialValue: FHEM_cached[that.mappings.blind.informId], + supportEvents: true, + supportBonjour: false, + manfDescription: "Current Blind Position", + designedMinValue: 0, + designedMaxValue: 100, + designedMinStep: 1, + unit: "%" + }); + + cTypes.push({ + cType: types.WINDOW_COVERING_OPERATION_STATE_CTYPE, + onRegister: function(characteristic) { + if( that.mappings.motor ) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.mappings.motor.informId, that); + } + }, + onRead: function(callback) { + if( that.mappings.motor ) + that.query(that.mappings.motor.reading, function(state){ + callback(state); + }); + }, + perms: ["pr","ev"], + format: "int", + initialValue: that.mappings.motor?FHEM_cached[that.mappings.motor.informId]:2, + supportEvents: false, + supportBonjour: false, + manfDescription: "Position State", + designedMinValue: 0, + designedMaxValue: 2, + designedMinStep: 1, + }); } - if( this.hasTemperature ) { + if( this.mappings.window ) { + cTypes.push({ + cType: types.WINDOW_COVERING_TARGET_POSITION_CTYPE, + onUpdate: function(value, context) { + if( context === 'fromFhem' ) + return; + that.delayed('targetPosition', value, 1500); + }, + onRead: function(callback) { + that.query(that.mappings.window.reading, function(level){ + callback(level); + }); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 50, + supportEvents: false, + supportBonjour: false, + manfDescription: "Target Window Position", + designedMinValue: 0, + designedMaxValue: 100, + designedMinStep: 1, + unit: "%" + }); + cTypes.push({ + cType: types.WINDOW_COVERING_CURRENT_POSITION_CTYPE, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.name+'-state', that); + FHEM_subscribe(characteristic, that.mappings.window.informId, that); + }, + onRead: function(callback) { + that.query(that.mappings.window.reading, function(pos){ + callback(pos); + }); + }, + perms: ["pr","ev"], + format: "int", + initialValue: FHEM_cached[that.mappings.window.informId], + supportEvents: true, + supportBonjour: false, + manfDescription: "Current Window Position", + designedMinValue: 0, + designedMaxValue: 100, + designedMinStep: 1, + unit: "%" + }); + + cTypes.push({ + cType: types.WINDOW_COVERING_OPERATION_STATE_CTYPE, + onRegister: function(characteristic) { + if( that.mappings.direction ) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.mappings.direction.informId, that); + } + }, + onRead: function(callback) { + if( that.mappings.direction ) + that.query(that.mappings.direction.reading, function(direction){ + callback(direction); + }); + }, + perms: ["pr","ev"], + format: "int", + initialValue: that.mappings.direction?FHEM_cached[that.mappings.direction.informId]:2, + supportEvents: false, + supportBonjour: false, + manfDescription: "Position State", + designedMinValue: 0, + designedMaxValue: 2, + designedMinStep: 1, + }); + } + + if( this.mappings.garage ) { + cTypes.push({ + onUpdate: function(value, context) { + if( context === 'fromFhem' ) + return; + that.command( 'set', value == 0 ? that.mappings.garage.cmdOpen : that.mappings.garage.cmdClose ); + }, + cType: types.TARGET_DOORSTATE_CTYPE, + onRead: function(callback) { + callback(1); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 1, + supportEvents: false, + supportBonjour: false, + manfDescription: "Target GarageDoor Position", + designedMinValue: 0, + designedMaxValue: 1, + designedMinStep: 1, + designedMaxLength: 1 + }); + cTypes.push({ + cType: types.CURRENT_DOOR_STATE_CTYPE, + onRead: function(callback) { + callback(4); + }, + perms: ["pr","ev"], + format: "int", + initialValue: 4, + supportEvents: true, + supportBonjour: false, + manfDescription: "Current GarageDoor State", + designedMinValue: 0, + designedMaxValue: 4, + designedMinStep: 1, + designedMaxLength: 1 + }); + + cTypes.push({ + cType: types.OBSTRUCTION_DETECTED_CTYPE, + onRead: function(callback) { + callback(false); + }, + perms: ["pr","ev"], + format: "bool", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Obstruction Detected", + designedMaxLength: 1 + }); + } + + //FIXME: parse range and set designedMinValue & designedMaxValue & designedMinStep + if( this.mappings.thermostat ) { + cTypes.push({ + cType: types.TARGET_TEMPERATURE_CTYPE, + onUpdate: function(value, context) { + if( context === 'fromFhem' ) + return; + that.delayed('targetTemperature', value, 1500); + }, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.mappings.thermostat.informId, that); + }, + onRead: function(callback) { + that.query(that.mappings.thermostat.reading, function(temperature){ + callback(temperature); + }); + }, + perms: ["pw","pr","ev"], + format: "float", + initialValue: FHEM_cached[that.mappings.thermostat.informId], + supportEvents: false, + supportBonjour: false, + manfDescription: "Target Temperature", + designedMinValue: 5.0, + designedMaxValue: 30.0, + //designedMinStep: 0.5, + unit: "celsius" + }); + cTypes.push({ + cType: types.CURRENTHEATINGCOOLING_CTYPE, + perms: ["pr","ev"], + format: "int", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Current Mode", + designedMaxLength: 1, + designedMinValue: 0, + designedMaxValue: 2, + designedMinStep: 1, + }); + cTypes.push({ + cType: types.TARGETHEATINGCOOLING_CTYPE, + onUpdate: function(value, context) { + if( context === 'fromFhem' ) + return; + that.command('targetMode', value); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Target Mode", + designedMinValue: 0, + designedMaxValue: 3, + designedMinStep: 1, + }); + + cTypes.push({ + cType: types.TEMPERATURE_UNITS_CTYPE, + perms: ["pr","ev"], + format: "int", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Unit", + }); + } + + if( this.mappings.contact ) { + cTypes.push({ + cType: types.CONTACT_SENSOR_STATE_CTYPE, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.mappings.contact.informId, that); + }, + onRead: function(callback) { + that.query(that.mappings.contact.reading, function(state){ + callback(state); + }); + }, + perms: ["pr","ev"], + format: "bool", + initialValue: FHEM_cached[that.mappings.contact.informId], + supportEvents: false, + supportBonjour: false, + manfDescription: "Contact State", + designedMaxLength: 1 + }); + } + + if( this.mappings.occupancy ) { + cTypes.push({ + cType: types.OCCUPANCY_DETECTED_CTYPE, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.mappings.occupancy.informId, that); + }, + onRead: function(callback) { + that.query(that.mappings.occupancy.reading, function(state){ + callback(state); + }); + }, + perms: ["pr","ev"], + format: "bool", + initialValue: FHEM_cached[that.mappings.occupancy.informId], + supportEvents: false, + supportBonjour: false, + manfDescription: "Occupancy State", + designedMaxLength: 1 + }); + } + + if( this.mappings.temperature ) { cTypes.push({ cType: types.CURRENT_TEMPERATURE_CTYPE, - //onUpdate: function(value) { console.log("Change:",value); execute("Thermostat", "Current Temperature", value); }, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.mappings.temperature.informId, that); + }, onRead: function(callback) { - that.query('temperature', function(temperature){ - callback(parseFloat(temperature)); + that.query(that.mappings.temperature.reading, function(temperature){ + callback(temperature); }); }, perms: ["pr","ev"], format: "float", - initialValue: 20, + initialValue: FHEM_cached[that.mappings.temperature.informId], supportEvents: true, supportBonjour: false, manfDescription: "Current Temperature", unit: "celsius" - }) + }); } - if( this.hasHumidity ) { + if( this.mappings.humidity ) { cTypes.push({ cType: types.CURRENT_RELATIVE_HUMIDITY_CTYPE, - //onUpdate: function(value) { console.log("Change:",value); execute("Thermostat", "Current Temperature", value); }, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.mappings.humidity.informId, that); + }, onRead: function(callback) { - that.query('humidity', function(humidity){ - callback(parseInt(humidity)); + that.query(that.mappings.humidity.reading, function(humidity){ + callback(humidity); }); }, perms: ["pr","ev"], format: "int", - initialValue: 50, + initialValue: FHEM_cached[that.mappings.humidity.informId], designedMinValue: 0, designedMaxValue: 100, supportEvents: true, supportBonjour: false, manfDescription: "Current Humidity", unit: "%" - }) + }); } @@ -625,16 +1718,30 @@ FHEMAccessory.prototype = { }, sType: function() { - if( match = this.PossibleSets.match(/\bvolume\b/) ) { - return types.SPEAKER_STYPE - } else if( this.hasTemperature ) { - return types.TEMPERATURE_SENSOR_STYPE - } else if( this.hasHumidity ) { - return types.HUMIDITY_SENSOR_STYPE - } else if( this.hasPct || this.hasHue || this.hasRGB ) { - return types.LIGHTBULB_STYPE + if( match = this.PossibleSets.match(/[\^ ]volume\b/) ) { + return types.SPEAKER_STYPE; + } else if( this.isSwitch ) { + return types.SWITCH_STYPE; + } else if( this.mappings.garage ) { + return types.GARAGE_DOOR_OPENER_STYPE; + } else if( this.mappings.window ) { + return types.WINDOW_STYPE; + } else if( this.mappings.blind ) { + return types.WINDOW_COVERING_STYPE; + } else if( this.mappings.thermostat ) { + return types.THERMOSTAT_STYPE; + } else if( this.mappings.contact ) { + return types.CONTACT_SENSOR_STYPE; + } else if( this.mappings.occupancy ) { + return types.OCCUPANCY_SENSOR_STYPE; + } else if( this.isLight || this.mappings.pct || this.mappings.hue || this.mappings.rgb ) { + return types.LIGHTBULB_STYPE; + } else if( this.mappings.temperature ) { + return types.TEMPERATURE_SENSOR_STYPE; + } else if( this.mappings.humidity ) { + return types.HUMIDITY_SENSOR_STYPE; } else { - return types.SWITCH_STYPE + return types.SWITCH_STYPE; } }, @@ -642,7 +1749,7 @@ FHEMAccessory.prototype = { var that = this; var services = [{ sType: types.ACCESSORY_INFORMATION_STYPE, - characteristics: this.informationCharacteristics(), + characteristics: this.informationCharacteristics(that), }, { sType: this.sType(), @@ -655,3 +1762,63 @@ FHEMAccessory.prototype = { //module.exports.accessory = FHEMAccessory; module.exports.platform = FHEMPlatform; + + + +//http server for debugging +var http = require('http'); + +const FHEMdebug_PORT=8080; + +function FHEMdebug_handleRequest(request, response){ + //console.log( request ); + + if( request.url == "/cached" ) { + response.write( "home

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

" ); + response.end( "cached: " + util.inspect(FHEM_cached).replace(/\n/g, '
') ); + + } else if( request.url == "/subscriptions" ) { + response.write( "home

" ); + response.end( "subscriptions: " + util.inspect(FHEM_subscriptions, {depth: 4}).replace(/\n/g, '
') ); + + } else if( request.url == "/persist" ) { + response.write( "home

" ); + var unique = {}; + Object.keys(FHEM_subscriptions).forEach(function(key) { + var characteristic = FHEM_subscriptions[key].characteristic; + var info = characteristic.accessoryController.tcpServer.accessoryInfo; + if( unique[info.displayName] ) + return; + unique[info.displayName] = info.username; + + var accessory = FHEM_subscriptions[key].accessory; + + //var cmd = '{$defs{'+ accessory.device +'}->{homekitID} = "'+info.username+'" if(defined($defs{'+ accessory.device +'}));;}'; + //accessory.execute( cmd ); + } ); + + var keys = Object.keys(unique); + keys.sort(); + for( i = 0; i < keys.length; i++ ) { + var k = keys[i]; + response.write( k +': '+ unique[k] +'
' ); + } + response.end( "" ); + + } else + response.end( "cached
persist
subscriptions" ); +} + +var FHEMdebug_server = http.createServer( FHEMdebug_handleRequest ); + +FHEMdebug_server.on('error', function (e) { + console.log("Server error: " + e); +}); + +//Lets start our server +FHEMdebug_server.listen(FHEMdebug_PORT, function(){ + console.log("Server listening on: http://:%s", FHEMdebug_PORT); +}); + From 5358ef94a8631fff3449f0898b5ba35a989b9955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=20Schr=C3=B6ter?= Date: Sun, 13 Sep 2015 09:14:13 +0200 Subject: [PATCH 34/71] catch undefined values in reading2homekit --- platforms/FHEM.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/platforms/FHEM.js b/platforms/FHEM.js index 01b9b21..fba5d01 100644 --- a/platforms/FHEM.js +++ b/platforms/FHEM.js @@ -687,6 +687,9 @@ FHEM_dim_values = [ 'dim06%', 'dim12%', 'dim18%', 'dim25%', 'dim31%', 'dim37%', FHEMAccessory.prototype = { reading2homekit: function(reading,value) { + if( value == undefined ) + return undefined; + if( reading == 'hue' ) { value = Math.round(value * 360 / this.mappings.hue ? this.mappings.hue.max : 360); @@ -735,14 +738,13 @@ FHEMAccessory.prototype = { value = 0; } else if( reading == 'lock' ) { - if( value.match( /^locked/ ) ) + if( value.match( /uncertain/ ) ) + value = 4; + else if( value.match( /^locked/ ) ) value = 1; else value = 0; - if( value.match( /uncertain/ ) ) - value = 4; - } else if( reading == 'temperature' || reading == 'measured-temp' || reading == 'desired-temp' From 428186eb8624d506d6c3cb46cc086155aee6ef75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=20Schr=C3=B6ter?= Date: Sun, 13 Sep 2015 10:24:28 +0200 Subject: [PATCH 35/71] fixed debug messages --- platforms/FHEM.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platforms/FHEM.js b/platforms/FHEM.js index fba5d01..d9f2815 100644 --- a/platforms/FHEM.js +++ b/platforms/FHEM.js @@ -604,7 +604,7 @@ FHEMAccessory(log, connection, s) { if( this.mappings.onOff ) - log( s.Internals.NAME + ' has onOff [' + 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 +']' ); if( this.mappings.sat ) @@ -618,7 +618,7 @@ FHEMAccessory(log, connection, s) { if( this.mappings.direction ) log( s.Internals.NAME + ' has direction' ); -log(s); +//log( util.inspect(s) ); // device info this.name = s.Internals.NAME; From 6c34301c138f93521670f35541d2ea64922b0dc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=20Schr=C3=B6ter?= Date: Sun, 13 Sep 2015 12:21:50 +0200 Subject: [PATCH 36/71] sonos volume fix --- platforms/FHEM.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/platforms/FHEM.js b/platforms/FHEM.js index d9f2815..cd4a60c 100644 --- a/platforms/FHEM.js +++ b/platforms/FHEM.js @@ -1318,7 +1318,7 @@ FHEMAccessory.prototype = { if( this.mappings.volume ) { cTypes.push({ - cType: types.OUTPUTVOLUME_CTYPE, + cType: '00000027-0000-1000-8000-0026BB765291', // FIXME!!! onUpdate: function(value, context) { if( context === 'fromFhem' ) return; @@ -1781,11 +1781,11 @@ function FHEMdebug_handleRequest(request, response){ response.write( "FHEM_lastEventTime: "+ new Date(FHEM_lastEventTime) +"

" ); response.end( "cached: " + util.inspect(FHEM_cached).replace(/\n/g, '
') ); - } else if( request.url == "/subscriptions" ) { + } else if( request.url == "/xxsubscriptions" ) { response.write( "home

" ); response.end( "subscriptions: " + util.inspect(FHEM_subscriptions, {depth: 4}).replace(/\n/g, '
') ); - } else if( request.url == "/persist" ) { + } else if( request.url == "/xxpersist" ) { response.write( "home

" ); var unique = {}; Object.keys(FHEM_subscriptions).forEach(function(key) { From 1f4ae9e87558cc9a83782369a5ca08b33f279f5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=20Schr=C3=B6ter?= Date: Sun, 13 Sep 2015 13:30:04 +0200 Subject: [PATCH 37/71] removed speaker stype --- platforms/FHEM.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/platforms/FHEM.js b/platforms/FHEM.js index cd4a60c..d9a561c 100644 --- a/platforms/FHEM.js +++ b/platforms/FHEM.js @@ -1720,9 +1720,7 @@ FHEMAccessory.prototype = { }, sType: function() { - if( match = this.PossibleSets.match(/[\^ ]volume\b/) ) { - return types.SPEAKER_STYPE; - } else if( this.isSwitch ) { + if( this.isSwitch ) { return types.SWITCH_STYPE; } else if( this.mappings.garage ) { return types.GARAGE_DOOR_OPENER_STYPE; @@ -1781,7 +1779,7 @@ function FHEMdebug_handleRequest(request, response){ response.write( "FHEM_lastEventTime: "+ new Date(FHEM_lastEventTime) +"

" ); response.end( "cached: " + util.inspect(FHEM_cached).replace(/\n/g, '
') ); - } else if( request.url == "/xxsubscriptions" ) { + } else if( request.url == "/subscriptions" ) { response.write( "home

" ); response.end( "subscriptions: " + util.inspect(FHEM_subscriptions, {depth: 4}).replace(/\n/g, '
') ); From 45ae56cf12a5a3e5abaa0f48f0360e384577929d Mon Sep 17 00:00:00 2001 From: jmtatsch Date: Sun, 13 Sep 2015 14:53:35 +0200 Subject: [PATCH 38/71] Fix 406 error for lifxjs by git+https --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 05db017..875ea30 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "harmonyhubjs-client": "^1.1.4", "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", "lifx-api": "^1.0.1", - "lifx": "https://github.com/magicmonkey/lifxjs.git", + "lifx": "git+https://github.com/magicmonkey/lifxjs.git", "mdns": "^2.2.4", "node-hue-api": "^1.0.5", "node-icontrol": "^0.1.4", From b1d0ef57ac5653db8560e52d2a1c2ad28da10d02 Mon Sep 17 00:00:00 2001 From: Alessandro Zummo Date: Mon, 14 Sep 2015 01:15:56 +0200 Subject: [PATCH 39/71] add mpd client accessory --- accessories/mpdclient.js | 89 ++++++++++++++++++++++++++++++++++++++++ config-sample.json | 7 ++++ package.json | 1 + 3 files changed, 97 insertions(+) create mode 100644 accessories/mpdclient.js diff --git a/accessories/mpdclient.js b/accessories/mpdclient.js new file mode 100644 index 0000000..8fee5eb --- /dev/null +++ b/accessories/mpdclient.js @@ -0,0 +1,89 @@ +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; +var request = require("request"); +var komponist = require('komponist') + +module.exports = { + accessory: MpdClient +} + +function MpdClient(log, config) { + this.log = log; + this.host = config["host"] || 'localhost'; + this.port = config["port"] || 6600; +} + +MpdClient.prototype = { + + setPowerState: function(powerOn, callback) { + + var log = this.log; + + komponist.createConnection(this.port, this.host, function(error, client) { + + if (error) { + return callback(error); + } + + if (powerOn) { + client.play(function(error) { + log("start playing"); + client.destroy(); + callback(error); + }); + } else { + client.stop(function(error) { + log("stop playing"); + client.destroy(); + callback(error); + }); + } + + }); + }, + + getPowerState: function(callback) { + + komponist.createConnection(this.port, this.host, function(error, client) { + + if (error) { + return callback(error); + } + + client.status(function(error, status) { + + client.destroy(); + + if (status['state'] == 'play') { + callback(error, 1); + } else { + callback(error, 0); + } + }); + + }); + }, + + identify: function(callback) { + this.log("Identify requested!"); + callback(); // success + }, + + getServices: function() { + + var informationService = new Service.AccessoryInformation(); + + informationService + .setCharacteristic(Characteristic.Manufacturer, "MPD") + .setCharacteristic(Characteristic.Model, "MPD Client") + .setCharacteristic(Characteristic.SerialNumber, "81536334"); + + var switchService = new Service.Switch(); + + switchService.getCharacteristic(Characteristic.On) + .on('get', this.getPowerState.bind(this)) + .on('set', this.setPowerState.bind(this)); + + return [informationService, switchService]; + } +}; diff --git a/config-sample.json b/config-sample.json index afb2893..6286591 100644 --- a/config-sample.json +++ b/config-sample.json @@ -183,6 +183,13 @@ "description": "Control the Hyperion TV backlight server. https://github.com/tvdzwan/hyperion", "host": "localhost", "port": "19444" + }, + { + "accessory": "mpdclient", + "name" : "mpd", + "host" : "localhost", + "port" : 6600, + "description": "Allows some control of an MPD server" } ] } diff --git a/package.json b/package.json index 05db017..1233014 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "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" } From f5cc6cf6fb8128736506b8749ef69e66dd96f3d5 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Sun, 13 Sep 2015 22:24:39 -0400 Subject: [PATCH 40/71] add home assistant shim --- platforms/HomeAssistant.js | 301 +++++++++++++++++++++++++++++++++++++ 1 file changed, 301 insertions(+) create mode 100644 platforms/HomeAssistant.js diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js new file mode 100644 index 0000000..f2b5cd1 --- /dev/null +++ b/platforms/HomeAssistant.js @@ -0,0 +1,301 @@ +// Home Assistant +// +// Current Support: lights +// +// This is a shim to publish lights maintained by Home Assistant. +// Home Assistant is an open-source home automation platform. +// URL: http://home-assistant.io +// GitHub: https://github.com/balloob/home-assistant +// +// Remember to add platform to config.json. Example: +// "platforms": [ +// { +// "platform": "HomeAssistant", +// "name": "HomeAssistant", +// "host": "http://192.168.1.50:8123", +// "password": "xxx" +// } +// ] +// +// 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 url = require('url') +var request = require("request"); + +function HomeAssistantPlatform(log, config){ + + // auth info + this.host = config["host"]; + this.password = config["password"]; + + this.log = log; +} + +HomeAssistantPlatform.prototype = { + _request: function(method, path, options, callback) { + var self = this + var requestURL = this.host + '/api' + path + options = options || {} + options.query = options.query || {} + + var reqOpts = { + url: url.parse(requestURL), + method: method || 'GET', + qs: options.query, + body: JSON.stringify(options.body), + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'x-ha-access': this.password + } + } + + request(reqOpts, function onResponse(error, response, body) { + if (error) { + callback(error, response) + return + } + + if (response.statusCode === 401) { + callback(new Error('You are not authenticated'), response) + return + } + + json = JSON.parse(body) + callback(error, response, json) + }) + + }, + accessories: function(callback) { + this.log("Fetching HomeAssistant devices."); + + var that = this; + var foundAccessories = []; + var lightsRE = /^light\./i + + + this._request('GET', '/states', {}, function(error, response, data){ + + for (var i = 0; i < data.length; i++) { + entity = data[i] + + if (entity.entity_id.match(lightsRE)) { + accessory = new HomeAssistantAccessory(that.log, entity, that) + foundAccessories.push(accessory) + } + } + + callback(foundAccessories) + }) + + } +} + +function HomeAssistantAccessory(log, data, client) { + // device info + this.data = data + this.entity_id = data.entity_id + this.name = data.attributes.friendly_name + + this.client = client + this.log = log; +} + +HomeAssistantAccessory.prototype = { + _callService: function(service, service_data, callback){ + var options = {} + options.body = service_data + + this.client._request('POST', '/services/light/' + service, options, function(error, response, data){ + if (error) { + callback(null) + }else{ + callback(data) + } + }) + }, + _fetchState: function(callback){ + this.client._request('GET', '/states/' + this.entity_id, {}, function(error, response, data){ + if (error) { + callback(null) + }else{ + callback(data) + } + }) + }, + getPowerState: function(callback){ + this.log("fetching power state for: " + this.name); + this._fetchState(function(data){ + if (data) { + powerState = data.state == 'on' + callback(powerState) + }else{ + callback(null) + } + }) + }, + getBrightness: function(callback){ + this.log("fetching brightness for: " + this.name); + that = this + this._fetchState(function(data){ + if (data && data.attributes) { + that.log('returned brightness: ' + data.attributes.brightness) + brightness = ((data.attributes.brightness || 0) / 255)*100 + callback(brightness) + }else{ + callback(null) + } + }) + }, + setPowerState: function(powerOn) { + var that = this; + var service_data = {} + service_data.entity_id = this.entity_id + + if (powerOn) { + this.log("Setting power state on the '"+this.name+"' to on"); + + this._callService('turn_on', service_data, function(data){ + if (data) { + that.log("Successfully set power state on the '"+that.name+"' to on"); + } + }) + }else{ + this.log("Setting power state on the '"+this.name+"' to off"); + + this._callService('turn_off', service_data, function(data){ + if (data) { + that.log("Successfully set power state on the '"+that.name+"' to off"); + } + }) + } + }, + setBrightness: function(level) { + var that = this; + var service_data = {} + service_data.entity_id = this.entity_id + + service_data.brightness = 255*(level/100.0) + + this.log("Setting brightness on the '"+this.name+"' to " + level); + + this._callService('turn_on', service_data, function(data){ + if (data) { + that.log("Successfully set brightness on the '"+that.name+"' to " + level); + } + }) + }, + 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: "HomeAssistant", + supportEvents: false, + supportBonjour: false, + manfDescription: "Manufacturer", + designedMaxLength: 255 + },{ + cType: types.MODEL_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "Rev-1", + supportEvents: false, + supportBonjour: false, + manfDescription: "Model", + designedMaxLength: 255 + },{ + cType: types.SERIAL_NUMBER_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: "A1S2NASF88EW", + supportEvents: false, + supportBonjour: false, + manfDescription: "SN", + designedMaxLength: 255 + },{ + cType: types.IDENTIFY_CTYPE, + onUpdate: null, + perms: ["pw"], + format: "bool", + initialValue: false, + supportEvents: false, + supportBonjour: false, + manfDescription: "Identify Accessory", + designedMaxLength: 1 + }] + },{ + sType: types.LIGHTBULB_STYPE, + characteristics: [{ + cType: types.NAME_CTYPE, + onUpdate: null, + perms: ["pr"], + format: "string", + initialValue: this.name, + supportEvents: true, + supportBonjour: false, + manfDescription: "Name of service", + designedMaxLength: 255 + },{ + cType: types.POWER_STATE_CTYPE, + onUpdate: function(value) { + that.setPowerState(value); + }, + onRead: function(callback) { + that.getPowerState(function(powerState){ + callback(powerState); + }); + }, + perms: ["pw","pr","ev"], + format: "bool", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Change the power state of the Bulb", + designedMaxLength: 1 + },{ + cType: types.BRIGHTNESS_CTYPE, + onUpdate: function(value) { + that.setBrightness(value); + }, + onRead: function(callback) { + that.getBrightness(function(level){ + callback(level); + }); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 0, + supportEvents: true, + supportBonjour: false, + manfDescription: "Adjust Brightness of Light", + designedMinValue: 0, + designedMaxValue: 255, + designedMinStep: 1, + unit: "%" + }] + }]; + } + +} + +module.exports.accessory = HomeAssistantAccessory; +module.exports.platform = HomeAssistantPlatform; From ec550d1638d17326723aa687e7551b1010596396 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Sun, 13 Sep 2015 22:24:54 -0400 Subject: [PATCH 41/71] add sample config for home assistant --- config-sample.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config-sample.json b/config-sample.json index afb2893..58a1f4b 100644 --- a/config-sample.json +++ b/config-sample.json @@ -89,6 +89,12 @@ "delay": 30, "repeat": 3, "zones":["Kitchen Lamp","Bedroom Lamp","Living Room Lamp","Hallway Lamp"] + }, + { + "platform": "HomeAssistant", + "name": "HomeAssistant", + "host": "http://192.168.1.10:8123", + "password": "XXXXX" } ], From a6d61cc93afab8b86cde6c60534881e7f450c9dc Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Sun, 13 Sep 2015 22:30:47 -0400 Subject: [PATCH 42/71] add link to HA in readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f11dd75..641e32f 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ Since Siri supports devices added through HomeKit, this means that with Homebrid * _Siri, turn off the Speakers._ ([Sonos](http://www.sonos.com)) * _Siri, turn on the Dehumidifier._ ([WeMo](http://www.belkin.com/us/Products/home-automation/c/wemo-home-automation/)) * _Siri, turn on Away Mode._ ([Xfinity Home](http://www.comcast.com/home-security.html)) - * _Siri, turn on the living room lights._ ([Wink](http://www.wink.com), [SmartThings](http://www.smartthings.com), [X10](http://github.com/edc1591/rest-mochad), [Philips Hue](http://meethue.com), [LimitlessLED/MiLight/Easybulb](http://www.limitlessled.com/), [LIFx](http://www.lifx.com/)) + * _Siri, turn on the living room lights._ ([Wink](http://www.wink.com), [SmartThings](http://www.smartthings.com), [X10](http://github.com/edc1591/rest-mochad), [Philips Hue](http://meethue.com), [Home Assistant](http://home-assistant.io) [LimitlessLED/MiLight/Easybulb](http://www.limitlessled.com/), [LIFx](http://www.lifx.com/)) * _Siri, set the movie scene._ ([Logitech Harmony](http://myharmony.com/)) If you would like to support any other devices, please write a shim and create a pull request and I'd be happy to add it to this official list. From b39b33726d2ad1fb6ad6459b7c7d1e2013b4f41b Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Mon, 14 Sep 2015 05:43:11 +0200 Subject: [PATCH 43/71] Make TargetHeatingCoolingState writable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apparently, if TargetHeatingCoolingState is not writable, you can’t add a thermostat to a scene. This fix makes it writable from HomeKit, but it still always remains set to HEAT. --- platforms/ZWayServer.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/platforms/ZWayServer.js b/platforms/ZWayServer.js index 322b9fb..5b79c8e 100644 --- a/platforms/ZWayServer.js +++ b/platforms/ZWayServer.js @@ -458,7 +458,12 @@ ZWayServerAccessory.prototype = { debug("Getting value for " + vdev.metrics.title + ", characteristic \"" + cx.displayName + "\"..."); callback(false, Characteristic.TargetHeatingCoolingState.HEAT); }); - cx.writable = false; + // 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); + }.bind(this)); return cx; } From 167a983068000b9f26cab30f726285777fe04b1f Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 00:14:02 -0400 Subject: [PATCH 44/71] handle missing friendly name --- platforms/HomeAssistant.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index f2b5cd1..ce7fc3d 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -97,7 +97,11 @@ function HomeAssistantAccessory(log, data, client) { // device info this.data = data this.entity_id = data.entity_id - this.name = data.attributes.friendly_name + if (data.attributes && data.attributes.friendly_name) { + this.name = data.attributes.friendly_name + }else{ + this.name = data.entity_id.split('.').pop().replace(/_/g, ' ') + } this.client = client this.log = log; From 544124fbabdc803ab5e245f4d2c8a52fca2a1c94 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 00:20:28 -0400 Subject: [PATCH 45/71] clean it up and get on that modern tip --- platforms/HomeAssistant.js | 116 ++++--------------------------------- 1 file changed, 11 insertions(+), 105 deletions(-) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index ce7fc3d..727cc77 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -192,111 +192,17 @@ HomeAssistantAccessory.prototype = { }) }, 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: "HomeAssistant", - supportEvents: false, - supportBonjour: false, - manfDescription: "Manufacturer", - designedMaxLength: 255 - },{ - cType: types.MODEL_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Rev-1", - supportEvents: false, - supportBonjour: false, - manfDescription: "Model", - designedMaxLength: 255 - },{ - cType: types.SERIAL_NUMBER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "A1S2NASF88EW", - supportEvents: false, - supportBonjour: false, - manfDescription: "SN", - designedMaxLength: 255 - },{ - cType: types.IDENTIFY_CTYPE, - onUpdate: null, - perms: ["pw"], - format: "bool", - initialValue: false, - supportEvents: false, - supportBonjour: false, - manfDescription: "Identify Accessory", - designedMaxLength: 1 - }] - },{ - sType: types.LIGHTBULB_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.name, - supportEvents: true, - supportBonjour: false, - manfDescription: "Name of service", - designedMaxLength: 255 - },{ - cType: types.POWER_STATE_CTYPE, - onUpdate: function(value) { - that.setPowerState(value); - }, - onRead: function(callback) { - that.getPowerState(function(powerState){ - callback(powerState); - }); - }, - perms: ["pw","pr","ev"], - format: "bool", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Change the power state of the Bulb", - designedMaxLength: 1 - },{ - cType: types.BRIGHTNESS_CTYPE, - onUpdate: function(value) { - that.setBrightness(value); - }, - onRead: function(callback) { - that.getBrightness(function(level){ - callback(level); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust Brightness of Light", - designedMinValue: 0, - designedMaxValue: 255, - designedMinStep: 1, - unit: "%" - }] - }]; + var lightbulbService = new Service.Lightbulb(); + + lightbulbService + .getCharacteristic(Characteristic.On) + .on('set', this.setPowerState.bind(this)); + + lightbulbService + .addCharacteristic(new Characteristic.Brightness()) + .on('set', this.setBrightness.bind(this)); + + return [lightbulbService]; } } From 025bca7a4357c1f516305fe77fc96e30090198e4 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 01:16:34 -0400 Subject: [PATCH 46/71] factor things out of the accessory and make it a Light --- platforms/HomeAssistant.js | 91 ++++++++++++++++++++++---------------- 1 file changed, 52 insertions(+), 39 deletions(-) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index 727cc77..a081adf 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -20,7 +20,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 types = require("HAP-NodeJS/accessories/types.js"); +var Service = require("HAP-NodeJS").Service; +var Characteristic = require("HAP-NodeJS").Characteristic; var url = require('url') var request = require("request"); @@ -68,6 +69,27 @@ HomeAssistantPlatform.prototype = { }) }, + fetchState: function(entity_id, callback){ + this._request('GET', '/states/' + entity_id, {}, function(error, response, data){ + if (error) { + callback(null) + }else{ + callback(data) + } + }) + }, + callService: function(domain, service, service_data, callback){ + var options = {} + options.body = service_data + + this._request('POST', '/services/' + domain + '/' + service, options, function(error, response, data){ + if (error) { + callback(null) + }else{ + callback(data) + } + }) + }, accessories: function(callback) { this.log("Fetching HomeAssistant devices."); @@ -82,7 +104,7 @@ HomeAssistantPlatform.prototype = { entity = data[i] if (entity.entity_id.match(lightsRE)) { - accessory = new HomeAssistantAccessory(that.log, entity, that) + accessory = new HomeAssistantLight(that.log, entity, that) foundAccessories.push(accessory) } } @@ -93,8 +115,9 @@ HomeAssistantPlatform.prototype = { } } -function HomeAssistantAccessory(log, data, client) { +function HomeAssistantLight(log, data, client) { // device info + this.domain = "light" this.data = data this.entity_id = data.entity_id if (data.attributes && data.attributes.friendly_name) { @@ -107,43 +130,22 @@ function HomeAssistantAccessory(log, data, client) { this.log = log; } -HomeAssistantAccessory.prototype = { - _callService: function(service, service_data, callback){ - var options = {} - options.body = service_data - - this.client._request('POST', '/services/light/' + service, options, function(error, response, data){ - if (error) { - callback(null) - }else{ - callback(data) - } - }) - }, - _fetchState: function(callback){ - this.client._request('GET', '/states/' + this.entity_id, {}, function(error, response, data){ - if (error) { - callback(null) - }else{ - callback(data) - } - }) - }, +HomeAssistantLight.prototype = { getPowerState: function(callback){ this.log("fetching power state for: " + this.name); - this._fetchState(function(data){ + this.client.fetchState(this.entity_id, function(data){ if (data) { powerState = data.state == 'on' callback(powerState) }else{ callback(null) } - }) + }.bind(this)) }, getBrightness: function(callback){ this.log("fetching brightness for: " + this.name); that = this - this._fetchState(function(data){ + this.client.fetchState(this.entity_id, function(data){ if (data && data.attributes) { that.log('returned brightness: ' + data.attributes.brightness) brightness = ((data.attributes.brightness || 0) / 255)*100 @@ -151,9 +153,9 @@ HomeAssistantAccessory.prototype = { }else{ callback(null) } - }) + }.bind(this)) }, - setPowerState: function(powerOn) { + setPowerState: function(powerOn, callback) { var that = this; var service_data = {} service_data.entity_id = this.entity_id @@ -161,22 +163,28 @@ HomeAssistantAccessory.prototype = { if (powerOn) { this.log("Setting power state on the '"+this.name+"' to on"); - this._callService('turn_on', service_data, function(data){ + this.client.callService(this.domain, 'turn_on', service_data, function(data){ if (data) { that.log("Successfully set power state on the '"+that.name+"' to on"); + callback() + }else{ + callback(new Error('Can not communicate with Home Assistant.')) } - }) + }.bind(this)) }else{ this.log("Setting power state on the '"+this.name+"' to off"); - this._callService('turn_off', service_data, function(data){ + this.client.callService(this.domain, 'turn_off', service_data, function(data){ if (data) { that.log("Successfully set power state on the '"+that.name+"' to off"); + callback() + }else{ + callback(new Error('Can not communicate with Home Assistant.')) } - }) + }.bind(this)) } }, - setBrightness: function(level) { + setBrightness: function(level, callback) { var that = this; var service_data = {} service_data.entity_id = this.entity_id @@ -185,21 +193,26 @@ HomeAssistantAccessory.prototype = { this.log("Setting brightness on the '"+this.name+"' to " + level); - this._callService('turn_on', service_data, function(data){ + this.client.callService(this.domain, 'turn_on', service_data, function(data){ if (data) { that.log("Successfully set brightness on the '"+that.name+"' to " + level); + callback() + }else{ + callback(new Error('Can not communicate with Home Assistant.')) } - }) + }.bind(this)) }, getServices: function() { var lightbulbService = new Service.Lightbulb(); lightbulbService .getCharacteristic(Characteristic.On) + .on('get', this.getPowerState.bind(this)) .on('set', this.setPowerState.bind(this)); lightbulbService - .addCharacteristic(new Characteristic.Brightness()) + .addCharacteristic(Characteristic.Brightness) + .on('get', this.getBrightness.bind(this)) .on('set', this.setBrightness.bind(this)); return [lightbulbService]; @@ -207,5 +220,5 @@ HomeAssistantAccessory.prototype = { } -module.exports.accessory = HomeAssistantAccessory; +module.exports.accessory = HomeAssistantLight; module.exports.platform = HomeAssistantPlatform; From 488456c1081d688e55220337baaf8570ae9369c2 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 01:30:55 -0400 Subject: [PATCH 47/71] ohhhhhhh the callback signature --- platforms/HomeAssistant.js | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index a081adf..f08ddfb 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -25,6 +25,8 @@ var Characteristic = require("HAP-NodeJS").Characteristic; var url = require('url') var request = require("request"); +var communicationError = new Error('Can not communicate with Home Assistant.') + function HomeAssistantPlatform(log, config){ // auth info @@ -133,25 +135,25 @@ function HomeAssistantLight(log, data, client) { HomeAssistantLight.prototype = { getPowerState: function(callback){ this.log("fetching power state for: " + this.name); + this.client.fetchState(this.entity_id, function(data){ if (data) { powerState = data.state == 'on' - callback(powerState) + callback(null, powerState) }else{ - callback(null) + callback(communicationError) } }.bind(this)) }, getBrightness: function(callback){ this.log("fetching brightness for: " + this.name); - that = this + this.client.fetchState(this.entity_id, function(data){ if (data && data.attributes) { - that.log('returned brightness: ' + data.attributes.brightness) brightness = ((data.attributes.brightness || 0) / 255)*100 - callback(brightness) + callback(null, brightness) }else{ - callback(null) + callback(communicationError) } }.bind(this)) }, @@ -168,7 +170,7 @@ HomeAssistantLight.prototype = { that.log("Successfully set power state on the '"+that.name+"' to on"); callback() }else{ - callback(new Error('Can not communicate with Home Assistant.')) + callback(communicationError) } }.bind(this)) }else{ @@ -179,7 +181,7 @@ HomeAssistantLight.prototype = { that.log("Successfully set power state on the '"+that.name+"' to off"); callback() }else{ - callback(new Error('Can not communicate with Home Assistant.')) + callback(communicationError) } }.bind(this)) } @@ -198,7 +200,7 @@ HomeAssistantLight.prototype = { that.log("Successfully set brightness on the '"+that.name+"' to " + level); callback() }else{ - callback(new Error('Can not communicate with Home Assistant.')) + callback(communicationError) } }.bind(this)) }, From 69d948e0fae82393a409427370d991b3bb941d7c Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 01:40:03 -0400 Subject: [PATCH 48/71] add switch --- platforms/HomeAssistant.js | 71 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index f08ddfb..6440728 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -222,5 +222,76 @@ HomeAssistantLight.prototype = { } +function HomeAssistantSwitch(log, data, client) { + // device info + this.domain = "switch" + this.data = data + this.entity_id = data.entity_id + if (data.attributes && data.attributes.friendly_name) { + this.name = data.attributes.friendly_name + }else{ + this.name = data.entity_id.split('.').pop().replace(/_/g, ' ') + } + + this.client = client + this.log = log; +} + +HomeAssistantSwitch.prototype = { + getPowerState: function(callback){ + this.log("fetching power state for: " + this.name); + + this.client.fetchState(this.entity_id, function(data){ + if (data) { + powerState = data.state == 'on' + callback(null, powerState) + }else{ + callback(communicationError) + } + }.bind(this)) + }, + setPowerState: function(powerOn, callback) { + var that = this; + var service_data = {} + service_data.entity_id = this.entity_id + + if (powerOn) { + this.log("Setting power state on the '"+this.name+"' to on"); + + this.client.callService(this.domain, 'turn_on', service_data, function(data){ + if (data) { + that.log("Successfully set power state on the '"+that.name+"' to on"); + callback() + }else{ + callback(communicationError) + } + }.bind(this)) + }else{ + this.log("Setting power state on the '"+this.name+"' to off"); + + this.client.callService(this.domain, 'turn_off', service_data, function(data){ + if (data) { + that.log("Successfully set power state on the '"+that.name+"' to off"); + callback() + }else{ + callback(communicationError) + } + }.bind(this)) + } + }, + getServices: function() { + var switchService = new Service.Switch(); + + switchService + .getCharacteristic(Characteristic.On) + .on('get', this.getPowerState.bind(this)) + .on('set', this.setPowerState.bind(this)); + + return [switchService]; + } + +} + module.exports.accessory = HomeAssistantLight; +module.exports.accessory = HomeAssistantSwitch; module.exports.platform = HomeAssistantPlatform; From c1e3d45fa1755c838545ba8db409a4f6f1f18a81 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 01:40:09 -0400 Subject: [PATCH 49/71] scan in switches --- platforms/HomeAssistant.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index 6440728..d9984c8 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -98,15 +98,22 @@ HomeAssistantPlatform.prototype = { var that = this; var foundAccessories = []; var lightsRE = /^light\./i + var switchRE = /^switch\./i this._request('GET', '/states', {}, function(error, response, data){ for (var i = 0; i < data.length; i++) { entity = data[i] + var accessory = null if (entity.entity_id.match(lightsRE)) { accessory = new HomeAssistantLight(that.log, entity, that) + }else if (entity.entity_id.match(switchRE)){ + accessory = new HomeAssistantSwitch(that.log, entity, that) + } + + if (accessory) { foundAccessories.push(accessory) } } From 13347d1879df1c9ba7adcb769f204cce914410e5 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Mon, 14 Sep 2015 07:43:29 -0700 Subject: [PATCH 50/71] Upgrade Lockitron accessory to new API --- accessories/Lockitron.js | 244 ++++++++++----------------------------- 1 file changed, 60 insertions(+), 184 deletions(-) diff --git a/accessories/Lockitron.js b/accessories/Lockitron.js index b5fd023..130a6bc 100644 --- a/accessories/Lockitron.js +++ b/accessories/Lockitron.js @@ -1,196 +1,72 @@ -var types = require("HAP-NodeJS/accessories/types.js"); +var Service = require('HAP-NodeJS').Service; +var Characteristic = require('HAP-NodeJS').Characteristic; var request = require("request"); +module.exports = { + accessory: LockitronAccessory +} + function LockitronAccessory(log, config) { this.log = log; - this.name = config["name"]; - this.lockID = config["lock_id"]; this.accessToken = config["api_token"]; + this.lockID = config["lock_id"]; } -LockitronAccessory.prototype = { - getState: function(callback) { - this.log("Getting current state..."); - - var that = this; - - var query = { - access_token: this.accessToken - }; - - request.get({ - url: "https://api.lockitron.com/v2/locks/"+this.lockID, - qs: query - }, function(err, response, body) { - - if (!err && response.statusCode == 200) { - var json = JSON.parse(body); - var state = json.state; // "lock" or "unlock" - var locked = state == "lock" - callback(locked); - } - else { - that.log("Error getting state (status code "+response.statusCode+"): " + err) - callback(undefined); - } - }); - }, +LockitronAccessory.prototype.getState = function(callback) { + this.log("Getting current state..."); - setState: function(state) { - this.log("Set state to " + state); + request.get({ + url: "https://api.lockitron.com/v2/locks/"+this.lockID, + qs: { access_token: this.accessToken } + }, function(err, response, body) { + + if (!err && response.statusCode == 200) { + var json = JSON.parse(body); + var state = json.state; // "lock" or "unlock" + this.log("Lock state is %s", state); + var locked = state == "lock" + callback(null, locked); // success + } + else { + this.log("Error getting state (status code %s): %s", response.statusCode, err); + callback(err); + } + }.bind(this)); +} + +LockitronAccessory.prototype.setState = function(state, callback) { + var lockitronState = (state == 1) ? "lock" : "unlock"; - var lockitronState = (state == 1) ? "lock" : "unlock"; - var that = this; + this.log("Set state to %s", lockitronState); - var query = { - access_token: this.accessToken, - state: lockitronState - }; + request.put({ + url: "https://api.lockitron.com/v2/locks/"+this.lockID, + qs: { access_token: this.accessToken, state: lockitronState } + }, function(err, response, body) { - request.put({ - url: "https://api.lockitron.com/v2/locks/"+this.lockID, - qs: query - }, function(err, response, body) { + if (!err && response.statusCode == 200) { + this.log("State change complete."); + callback(null); // success + } + else { + this.log("Error '%s' setting lock state. Response: %s", err.message, body); + callback(err); + } + }.bind(this)); +}, - if (!err && response.statusCode == 200) { - that.log("State change complete."); - } - else { - that.log("Error '"+err+"' setting lock 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: "Apigee", - supportEvents: false, - supportBonjour: false, - manfDescription: "Manufacturer", - designedMaxLength: 255 - },{ - cType: types.MODEL_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Rev-2", - 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.LOCK_MECHANISM_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Lock Mechanism", - supportEvents: false, - supportBonjour: false, - manfDescription: "Name of service", - designedMaxLength: 255 - },{ - cType: types.CURRENT_LOCK_MECHANISM_STATE_CTYPE, - onRead: function(callback) { that.getState(callback); }, - onUpdate: function(value) { that.log("Update current state to " + value); }, - perms: ["pr","ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "BlaBla", - designedMinValue: 0, - designedMaxValue: 3, - designedMinStep: 1, - designedMaxLength: 1 - },{ - cType: types.TARGET_LOCK_MECHANISM_STATE_CTYPE, - onUpdate: function(value) { that.setState(value); }, - perms: ["pr","pw","ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "BlaBla", - designedMinValue: 0, - designedMaxValue: 1, - designedMinStep: 1, - designedMaxLength: 1 - }] - },{ - sType: types.LOCK_MANAGEMENT_STYPE, - characteristics: [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "Lock Management", - supportEvents: false, - supportBonjour: false, - manfDescription: "Name of service", - designedMaxLength: 255 - },{ - cType: types.LOCK_MANAGEMENT_CONTROL_POINT_CTYPE, - onUpdate: function(value) { that.log("Update control point to " + value); }, - perms: ["pw"], - format: "data", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "BlaBla", - designedMaxLength: 255 - },{ - cType: types.VERSION_CTYPE, - onUpdate: function(value) { that.log("Update version to " + value); }, - perms: ["pr"], - format: "string", - initialValue: "1.0", - supportEvents: false, - supportBonjour: false, - manfDescription: "BlaBla", - designedMaxLength: 255 - }] - }]; - } -}; - -module.exports.accessory = LockitronAccessory; \ No newline at end of file +LockitronAccessory.prototype.getServices = function() { + + var service = new Service.LockMechanism(this.name); + + service + .getCharacteristic(Characteristic.LockCurrentState) + .on('get', this.getState.bind(this)); + + service + .getCharacteristic(Characteristic.LockTargetState) + .on('get', this.getState.bind(this)) + .on('set', this.setState.bind(this)); + + return [service]; +} From db3f32c57792634a61c17291a6ddc70a944dbae4 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Mon, 14 Sep 2015 07:45:29 -0700 Subject: [PATCH 51/71] Fix name from config --- accessories/Lockitron.js | 1 + 1 file changed, 1 insertion(+) diff --git a/accessories/Lockitron.js b/accessories/Lockitron.js index 130a6bc..32d900a 100644 --- a/accessories/Lockitron.js +++ b/accessories/Lockitron.js @@ -8,6 +8,7 @@ module.exports = { function LockitronAccessory(log, config) { this.log = log; + this.name = config["name"]; this.accessToken = config["api_token"]; this.lockID = config["lock_id"]; } From bb39f5f73e1dfec74c0fabf6d9611051b17c26d9 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Mon, 14 Sep 2015 07:47:21 -0700 Subject: [PATCH 52/71] [Lockitron] err could be null --- accessories/Lockitron.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accessories/Lockitron.js b/accessories/Lockitron.js index 32d900a..7e7db38 100644 --- a/accessories/Lockitron.js +++ b/accessories/Lockitron.js @@ -50,7 +50,7 @@ LockitronAccessory.prototype.setState = function(state, callback) { callback(null); // success } else { - this.log("Error '%s' setting lock state. Response: %s", err.message, body); + this.log("Error '%s' setting lock state. Response: %s", err, body); callback(err); } }.bind(this)); From adbe116a5ab0c7159b6f5dcd662ea4c48f948ae4 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Mon, 14 Sep 2015 07:48:42 -0700 Subject: [PATCH 53/71] [Lockitron] Create an Error if necessary --- accessories/Lockitron.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accessories/Lockitron.js b/accessories/Lockitron.js index 7e7db38..04414af 100644 --- a/accessories/Lockitron.js +++ b/accessories/Lockitron.js @@ -51,7 +51,7 @@ LockitronAccessory.prototype.setState = function(state, callback) { } else { this.log("Error '%s' setting lock state. Response: %s", err, body); - callback(err); + callback(err || new Error("Error setting lock state.")); } }.bind(this)); }, From c88d01c9a9ebcd7fda45c2615339e6e41e84f0af Mon Sep 17 00:00:00 2001 From: Luke Redpath Date: Mon, 14 Sep 2015 19:29:02 +0100 Subject: [PATCH 54/71] Updated CHANGELOG with my previous changes --- platforms/Domoticz.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/platforms/Domoticz.js b/platforms/Domoticz.js index de5b466..2042094 100644 --- a/platforms/Domoticz.js +++ b/platforms/Domoticz.js @@ -8,6 +8,9 @@ // - Added support for Scenes // - Sorting device names // +// 22 July 2015 [lukeredpath] +// - Added SSL and basic auth support +// // 26 August 2015 [EddyK69] // - Added parameter in config.json: 'loadscenes' for enabling/disabling loading scenes // - Fixed issue with dimmer-range; was 0-100, should be 0-16 @@ -362,4 +365,4 @@ DomoticzAccessory.prototype = { }; module.exports.accessory = DomoticzAccessory; -module.exports.platform = DomoticzPlatform; \ No newline at end of file +module.exports.platform = DomoticzPlatform; From 7f14df04342b32e94bfe710323671456bf754dfa Mon Sep 17 00:00:00 2001 From: Luke Redpath Date: Mon, 14 Sep 2015 19:40:04 +0100 Subject: [PATCH 55/71] Honor the MaxDimLevel property of accessories. LightwaveRF lights could not be dimmed properly as they require a dim level of betwene 0-32. The Domoticz documentation incorrectly states this should be 0-16. Other devices may use different values which is why the MaxDimLevel property should be used rather than hardcoded value. Also refactored the code slightly. --- platforms/Domoticz.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/platforms/Domoticz.js b/platforms/Domoticz.js index 2042094..8930011 100644 --- a/platforms/Domoticz.js +++ b/platforms/Domoticz.js @@ -20,6 +20,10 @@ // - Fixed issue that 'on-off'-type lights would not react on Siri 'Switch on/off light'; On/Off types are now handled as Lights instead of Switches // (Cannot determine if 'on/off'-type device is a Light or a Switch :( ) // +// 14 September 2015 [lukeredpath] +// - Fixed incorrect dimmer range for LightwaveRF lights (0-32 required, MaxDimLevel should be honored) +// +// // Domoticz JSON API required // https://www.domoticz.com/wiki/Domoticz_API/JSON_URL's#Lights_and_switches // @@ -186,9 +190,7 @@ DomoticzAccessory.prototype = { url = this.platform.urlForQuery("type=command¶m=setcolbrightnessvalue&idx=" + this.idx + "&hue=" + value + "&brightness=100" + "&iswhite=false"); } else if (c == "setLevel") { - //Range should be 0-16 instead of 0-100 - //See http://www.domoticz.com/wiki/Domoticz_API/JSON_URL%27s#Set_a_dimmable_light_to_a_certain_level - value = Math.round((value / 100) * 16) + value = this.dimmerLevelForValue(value) url = this.platform.urlForQuery("type=command¶m=switchlight&idx=" + this.idx + "&switchcmd=Set%20Level&level=" + value); } else if (value != undefined) { @@ -211,11 +213,19 @@ DomoticzAccessory.prototype = { that.log("There was a problem sending command " + c + " to" + that.name); that.log(url); } else { - that.log(that.name + " sent command " + c); + that.log(that.name + " sent command " + c + " (value: " + value + ")"); } }) }, + // translates the HomeKit dim level as a percentage to whatever scale the device requires + dimmerLevelForValue: function(value) { + if (this.MaxDimLevel == 100) { + return value; + } + return Math.round((value / 100.0) * this.MaxDimLevel) + }, + informationCharacteristics: function() { return [ { From 9f39b49dd59bf350791d7bb2259b6e88f6bd51c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=20Schr=C3=B6ter?= Date: Mon, 14 Sep 2015 22:10:14 +0200 Subject: [PATCH 56/71] complete switch to new api basic device (light,hue,swap,thermometer,thermostat) are working still problems with custom characteristic volume --- platforms/FHEM.js | 1354 ++++++++++++++++++--------------------------- 1 file changed, 542 insertions(+), 812 deletions(-) diff --git a/platforms/FHEM.js b/platforms/FHEM.js index d9a561c..6c7d45a 100644 --- a/platforms/FHEM.js +++ b/platforms/FHEM.js @@ -16,7 +16,11 @@ // 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 types = require('HAP-NodeJS/accessories/types.js'); + var util = require('util'); @@ -38,25 +42,12 @@ FHEM_update(inform_id, value, no_update) { || FHEM_cached[inform_id] === value ) return; - //if( FHEM_internal['.'+subscription.accessory.device+'-homekitID'] == undefined ) { - // var info = subscription.characteristic.accessoryController.tcpServer.accessoryInfo; - - // if( info.username ) { - // var accessory = subscription.accessory; - // var cmd = '{$defs{'+ accessory.device +'}->{homekitID} = "'+info.username+'" if(defined($defs{'+ accessory.device +'}));;}'; - // //accessory.execute( cmd ); - - // FHEM_internal['.'+accessory.device+'-homekitID'] = info.username; - // } - //} - FHEM_cached[inform_id] = value; //FHEM_cached[inform_id] = { 'value': value, 'timestamp': Date.now() }; console.log(" caching: " + inform_id + ": " + value + " as " + typeof(value) ); if( !no_update ) subscription.characteristic.setValue(value, undefined, 'fromFhem'); - //subscription.characteristic.updateValue(value, null); } } @@ -123,8 +114,6 @@ function FHEM_startLongpoll(connection) { var value = d[1]; if( value.match( /^set-/ ) ) continue; - if( value.match( /^set_/ ) ) - continue; var match = d[0].match(/([^-]*)-(.*)/); var device = match[1]; @@ -144,12 +133,12 @@ function FHEM_startLongpoll(connection) { continue; } else if( accessory.mappings.lock ) { - var lock = 0; + var lock = Characteristic.LockCurrentState.UNSECURED; if( value.match( /^locked/ ) ) - lock = 1; + lock = Characteristic.LockCurrentState.SECURED; if( value.match( /uncertain/ ) ) - level = 4; + level = Characteristic.LockCurrentState.UNKNOWN; FHEM_update( accessory.mappings.lock.informId, lock ); continue; @@ -399,6 +388,9 @@ FHEMPlatform.prototype = { } else if( s.Readings.humidity ) { accessory = new FHEMAccessory(that.log, that.connection, s); + } else if( s.Readings.voc ) { + accessory = new FHEMAccessory(that.log, that.connection, s); + } else { that.log( 'ignoring ' + s.Internals.NAME ); @@ -494,6 +486,9 @@ FHEMAccessory(log, connection, s) { if( s.Readings.humidity ) this.mappings.humidity = { reading: 'humidity' }; + if( s.Readings.voc ) + this.mappings.airquality = { reading: 'voc' }; + if( s.Readings.motor ) this.mappings.motor = { reading: 'motor' }; @@ -586,11 +581,11 @@ FHEMAccessory(log, connection, s) { else if( this.mappings.thermostat ) log( s.Internals.NAME + ' is thermostat ['+ this.mappings.thermostat.reading +']' ); else if( this.mappings.contact ) - log( s.Internals.NAME + ' is contactsensor [' + this.mappings.contact.reading +']' ); + log( s.Internals.NAME + ' is contact sensor [' + this.mappings.contact.reading +']' ); else if( this.mappings.occupancy ) - log( s.Internals.NAME + ' is occupancysensor' ); + log( s.Internals.NAME + ' is occupancy sensor' ); else if( this.mappings.rgb ) - log( s.Internals.NAME + ' has RGB [0-' + this.mappings.rgb.reading +']'); + log( s.Internals.NAME + ' has RGB [' + this.mappings.rgb.reading +']'); else if( this.mappings.pct ) log( s.Internals.NAME + ' is dimable ['+ this.mappings.pct.reading +']' ); else if( s.hasDim ) @@ -599,12 +594,12 @@ FHEMAccessory(log, connection, s) { log( s.Internals.NAME + ' is light' ); else if( this.mappings.onOff || s.isSwitch ) log( s.Internals.NAME + ' is switchable' ); - else + else if( !this.mappings ) return {}; if( this.mappings.onOff ) - log( s.Internals.NAME + ' has onOff [' + this.mappings.onOff.reading + ':' + this.mappings.onOff.cmdOn +',' + this.mappings.onOff.cmdOff + ']' ); + 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 +']' ); if( this.mappings.sat ) @@ -613,6 +608,8 @@ FHEMAccessory(log, connection, s) { 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.airquality ) + log( s.Internals.NAME + ' has voc ['+ this.mappings.airquality.reading +']' ); if( this.mappings.motor ) log( s.Internals.NAME + ' has motor' ); if( this.mappings.direction ) @@ -677,10 +674,6 @@ FHEMAccessory(log, connection, s) { this.log = log; this.connection = connection; - - this.onRegister = function(accessory) { -console.log( ">>>>>>>>>>>>:" + util.inspect(accessory) ); - }; } FHEM_dim_values = [ 'dim06%', 'dim12%', 'dim18%', 'dim25%', 'dim31%', 'dim37%', 'dim43%', 'dim50%', 'dim56%', 'dim62%', 'dim68%', 'dim75%', 'dim81%', 'dim87%', 'dim93%' ]; @@ -699,21 +692,21 @@ FHEMAccessory.prototype = { } else if( reading == 'pct' ) { value = parseInt( value ); - } else if(reading == 'direction') { - if( value.match(/^up/)) - value = 1; - else if( value.match(/^down/)) - value = 0; - else - value = 2; - } else if(reading == 'motor') { - if( value.match(/^opening/)) - value = 1; - else if( value.match(/^closing/)) - value = 0; + if( value.match(/^up/)) + value = Characteristic.PositionState.INCREASING; + else if( value.match(/^down/)) + value = Characteristic.PositionState.DECREASING; else - value = 2; + value = Characteristic.PositionState.STOPPED; + + } else if(reading == 'direction') { + if( value.match(/^opening/)) + value = PositionState.INCREASING; + else if( value.match(/^closing/)) + value = Characteristic.PositionState.DECREASING; + else + value = Characteristic.PositionState.STOPPED; } else if( reading == 'transportState' ) { if( value == 'PLAYING' ) @@ -727,23 +720,23 @@ FHEMAccessory.prototype = { } else if( reading == 'contact' ) { if( value.match( /^closed/ ) ) - value = 1; + value = Characteristic.ContactSensorState.CONTACT_DETECTED; else - value = 0; + value = Characteristic.ContactSensorState.CONTACT_NOT_DETECTED; } else if( reading == 'Window' ) { if( value.match( /^Closed/ ) ) - value = 1; + value = Characteristic.ContactSensorState.CONTACT_DETECTED; else - value = 0; + value = Characteristic.ContactSensorState.CONTACT_NOT_DETECTED; } else if( reading == 'lock' ) { if( value.match( /uncertain/ ) ) - value = 4; + value = Characteristic.LockCurrentState.UNKNOWN; else if( value.match( /^locked/ ) ) - value = 1; + value = Characteristic.LockCurrentState.SECURED; else - value = 0; + value = Characteristic.LockCurrentState.UNSECURED; } else if( reading == 'temperature' || reading == 'measured-temp' @@ -754,11 +747,24 @@ FHEMAccessory.prototype = { } else if( reading == 'humidity' ) { value = parseInt( value ); + } else if( reading == 'voc' ) { + value = parseInt( value ); + if( value > 1500 ) + Characteristic.AirQuality.POOR; + else if( value > 1000 ) + Characteristic.AirQuality.INFERIOR; + else if( value > 800 ) + Characteristic.AirQuality.FAIR; + else if( value > 600 ) + Characteristic.AirQuality.GOOD; + else if( value > 0 ) + Characteristic.AirQuality.EXCELLENT; + else + Characteristic.AirQuality.UNKNOWN; + } else if( reading == 'state' ) { if( value.match(/^set-/ ) ) return undefined; - if( value.match(/^set_/ ) ) - return undefined; if( this.event_map != undefined ) { var mapped = this.event_map[value]; @@ -768,8 +774,10 @@ FHEMAccessory.prototype = { if( value == 'off' ) value = 0; + else if( value == 'present' ) + value = Characteristic.OccupancyDetected.OCCUPANCY_DETECTED; else if( value == 'absent' ) - value = 0; + value = Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED; else if( value == '000000' ) value = 0; else if( value.match( /^[A-D]0$/ ) ) //FIXME: is handled by event_map now @@ -971,13 +979,12 @@ FHEMAccessory.prototype = { } else if( reading == 'lock' && query_reading == 'state') { - if( value.match( /^locked/ ) ) - value = 1; - else - value = 0; - if( value.match( /uncertain/ ) ) - value = 4; + value = Characteristic.LockCurrentState.UNKNOWN; + else if( value.match( /^locked/ ) ) + value = Characteristic.LockCurrentState.SECURED; + else + value = Characteristic.LockCurrentState.UNSECURED; } else if(reading == 'hue' && query_reading == that.mappings.rgb) { //FHEM_update( that.device+'-'+query_reading, value ); @@ -1011,753 +1018,500 @@ FHEMAccessory.prototype = { } ); }, - informationCharacteristics: function(that) { - return [ - { - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.alias, - supportEvents: false, - supportBonjour: false, - manfDescription: "Name of the accessory", - designedMaxLength: 255 - },{ - cType: types.MANUFACTURER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: "FHEM:"+this.type, - supportEvents: false, - supportBonjour: false, - manfDescription: "Manufacturer", - designedMaxLength: 255 - },{ - cType: types.MODEL_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.model ? this.model : '', - supportEvents: false, - supportBonjour: false, - manfDescription: "Model", - designedMaxLength: 255 - },{ - cType: types.SERIAL_NUMBER_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.serial ? this.serial : "", - supportEvents: false, - supportBonjour: false, - manfDescription: "SN", - designedMaxLength: 255 - },{ - cType: types.IDENTIFY_CTYPE, - onUpdate: function(value, context) { - if( context === 'fromFhem' ) - return; - if( that.mappings.onOff ) - that.command( 'identify' ); - }, - perms: ["pw"], - format: "bool", - initialValue: false, - supportEvents: false, - supportBonjour: false, - manfDescription: "Identify Accessory", - designedMaxLength: 1 - }]; - }, + createDeviceService: function() { + var name = this.alias + 'xxx'; - controlCharacteristics: function(that) { - cTypes = [{ - cType: types.NAME_CTYPE, - onUpdate: null, - perms: ["pr"], - format: "string", - initialValue: this.alias, - supportEvents: true, - supportBonjour: false, - manfDescription: "Name of service", - designedMaxLength: 255 - }] - - if( this.mappings.onOff ) { - cTypes.push({ - cType: types.POWER_STATE_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.onOff.informId, that); - }, - onUpdate: function(value, context) { - if( context === 'fromFhem' ) - return; - that.command( 'set', value == 0 ? that.mappings.onOff.cmdOff : that.mappings.onOff.cmdOn ); - }, - onRead: function(callback) { - that.query( that.mappings.onOff.reading, function(state){ callback(state) } ); - }, - perms: ["pw","pr","ev"], - format: "bool", - initialValue: FHEM_cached[that.mappings.onOff.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Change the power state", - designedMaxLength: 1 - }); - } - - if( this.mappings.pct ) { - cTypes.push({ - cType: types.BRIGHTNESS_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.pct.informId, that); - }, - onUpdate: function(value, context) { - if( context === 'fromFhem' ) - return; - that.command('pct', value); - }, - onRead: function(callback) { - that.query(that.mappings.pct.reading, function(pct){ - callback(pct); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: FHEM_cached[that.mappings.pct.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust Brightness of the Light", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - } else if( this.hasDim ) { - cTypes.push({ - cType: types.BRIGHTNESS_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - // state is alreadi subscribed from POWER_STATE_CTYPE - FHEM_subscribe(characteristic, that.name+'-pct', that); - }, - onUpdate: function(value, context) { - if( context === 'fromFhem' ) - return; - that.delayed('dim', value); - }, - onRead: function(callback) { - that.query('pct', function(pct){ - callback(pct); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - //initialValue: FHEM_cached[that.mappings.dim.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust Brightness of the Light", - designedMinValue: 0, - designedMaxValue: this.pctMax, - designedMinStep: 1, - unit: "%" - }); - } - - if( that.mappings.hue ) { - cTypes.push({ - cType: types.HUE_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.hue.informId, that); - }, - onUpdate: function(value, context) { - if( context === 'fromFhem' ) - return; - that.command('hue', value); - }, - onRead: function(callback) { - that.query(that.mappings.hue.reading, function(hue){ - callback(hue); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: FHEM_cached[that.mappings.hue.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust the Hue of the Light", - designedMinValue: 0, - designedMaxValue: 360, - designedMinStep: 1, - unit: "arcdegrees" - }); - } else if( this.mappings.rgb ) { - cTypes.push({ - cType: types.HUE_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.name+'-hue', that); - FHEM_subscribe(characteristic, that.mappings.rgb.informId, that); - }, - onUpdate: function(value, context) { - if( context === 'fromFhem' ) - return; - that.command('H-rgb', value); - }, - onRead: function(callback) { - that.query('hue', function(hue){ - callback(hue); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust the Hue of the Light", - designedMinValue: 0, - designedMaxValue: 360, - designedMinStep: 1, - unit: "arcdegrees" - }); - - if( !this.mappings.sat ) - cTypes.push({ - cType: types.SATURATION_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.name+'-sat', that); - }, - onUpdate: function(value, context) { - if( context === 'fromFhem' ) - return; - that.command('S-rgb', value); - }, - onRead: function(callback) { - that.query('sat', function(sat){ - callback(sat); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 100, - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust the Saturation of the Light", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - - if( !this.mappings.pct ) - cTypes.push({ - cType: types.BRIGHTNESS_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.name+'-bri', that); - }, - onUpdate: function(value, context) { - if( context === 'fromFhem' ) - return; - that.command('B-rgb', value); - }, - onRead: function(callback) { - that.query('bri', function(bri){ - callback(bri); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust Brightness of the Light", - designedMinValue: 0, - designedMaxValue: this.pctMax, - designedMinStep: 1, - unit: "%" - }); - } - - if( this.mappings.sat ) { - cTypes.push({ - cType: types.SATURATION_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.sat.informId, that); - }, - onUpdate: function(value, context) { - if( context === 'fromFhem' ) - return; - that.command('sat', value); - }, - onRead: function(callback) { - that.query(that.mappings.sat.reading, function(sat){ - callback(sat); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: FHEM_cached[that.mappings.sat.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust the Saturation of the Light", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - } - - if( this.mappings.volume ) { - cTypes.push({ - cType: '00000027-0000-1000-8000-0026BB765291', // FIXME!!! - onUpdate: function(value, context) { - if( context === 'fromFhem' ) - return; - that.delayed('volume', value); - }, - onRegister: function(characteristic) { - if( !that.mappings.volume.nocache ) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.volume.informId, that); - } - }, - onRead: function(callback) { - that.query(that.mappings.volume.reading, function(volume){ - callback(volume); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 10, - //initialValue: FHEM_cached[that.mappings.volume.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Adjust the Volume of this device", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1 - //unit: "%" - }); - } - - if( this.mappings.blind ) { - cTypes.push({ - cType: types.WINDOW_COVERING_TARGET_POSITION_CTYPE, - onUpdate: function(value, context) { - if( context === 'fromFhem' ) - return; - that.delayed('targetPosition', value, 1500); - }, - //onRegister: function(characteristic) { - // characteristic.eventEnabled = true; - // FHEM_subscribe(characteristic, that.mappings.blind.informId, that); - //}, - onRead: function(callback) { - that.query(that.mappings.blind.reading, function(pct){ - callback(pct); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: FHEM_cached[that.mappings.blind.informId], - supportEvents: false, - supportBonjour: false, - manfDescription: "Target Blind Position", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - cTypes.push({ - cType: types.WINDOW_COVERING_CURRENT_POSITION_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.blind.informId, that); - }, - onRead: function(callback) { - that.query(that.mappings.blind.reading, function(pos){ - callback(pos); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: FHEM_cached[that.mappings.blind.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Current Blind Position", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - - cTypes.push({ - cType: types.WINDOW_COVERING_OPERATION_STATE_CTYPE, - onRegister: function(characteristic) { - if( that.mappings.motor ) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.motor.informId, that); - } - }, - onRead: function(callback) { - if( that.mappings.motor ) - that.query(that.mappings.motor.reading, function(state){ - callback(state); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: that.mappings.motor?FHEM_cached[that.mappings.motor.informId]:2, - supportEvents: false, - supportBonjour: false, - manfDescription: "Position State", - designedMinValue: 0, - designedMaxValue: 2, - designedMinStep: 1, - }); - } - - if( this.mappings.window ) { - cTypes.push({ - cType: types.WINDOW_COVERING_TARGET_POSITION_CTYPE, - onUpdate: function(value, context) { - if( context === 'fromFhem' ) - return; - that.delayed('targetPosition', value, 1500); - }, - onRead: function(callback) { - that.query(that.mappings.window.reading, function(level){ - callback(level); - }); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 50, - supportEvents: false, - supportBonjour: false, - manfDescription: "Target Window Position", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - cTypes.push({ - cType: types.WINDOW_COVERING_CURRENT_POSITION_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.name+'-state', that); - FHEM_subscribe(characteristic, that.mappings.window.informId, that); - }, - onRead: function(callback) { - that.query(that.mappings.window.reading, function(pos){ - callback(pos); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: FHEM_cached[that.mappings.window.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Current Window Position", - designedMinValue: 0, - designedMaxValue: 100, - designedMinStep: 1, - unit: "%" - }); - - cTypes.push({ - cType: types.WINDOW_COVERING_OPERATION_STATE_CTYPE, - onRegister: function(characteristic) { - if( that.mappings.direction ) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.direction.informId, that); - } - }, - onRead: function(callback) { - if( that.mappings.direction ) - that.query(that.mappings.direction.reading, function(direction){ - callback(direction); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: that.mappings.direction?FHEM_cached[that.mappings.direction.informId]:2, - supportEvents: false, - supportBonjour: false, - manfDescription: "Position State", - designedMinValue: 0, - designedMaxValue: 2, - designedMinStep: 1, - }); - } - - if( this.mappings.garage ) { - cTypes.push({ - onUpdate: function(value, context) { - if( context === 'fromFhem' ) - return; - that.command( 'set', value == 0 ? that.mappings.garage.cmdOpen : that.mappings.garage.cmdClose ); - }, - cType: types.TARGET_DOORSTATE_CTYPE, - onRead: function(callback) { - callback(1); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 1, - supportEvents: false, - supportBonjour: false, - manfDescription: "Target GarageDoor Position", - designedMinValue: 0, - designedMaxValue: 1, - designedMinStep: 1, - designedMaxLength: 1 - }); - cTypes.push({ - cType: types.CURRENT_DOOR_STATE_CTYPE, - onRead: function(callback) { - callback(4); - }, - perms: ["pr","ev"], - format: "int", - initialValue: 4, - supportEvents: true, - supportBonjour: false, - manfDescription: "Current GarageDoor State", - designedMinValue: 0, - designedMaxValue: 4, - designedMinStep: 1, - designedMaxLength: 1 - }); - - cTypes.push({ - cType: types.OBSTRUCTION_DETECTED_CTYPE, - onRead: function(callback) { - callback(false); - }, - perms: ["pr","ev"], - format: "bool", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Obstruction Detected", - designedMaxLength: 1 - }); - } - - //FIXME: parse range and set designedMinValue & designedMaxValue & designedMinStep - if( this.mappings.thermostat ) { - cTypes.push({ - cType: types.TARGET_TEMPERATURE_CTYPE, - onUpdate: function(value, context) { - if( context === 'fromFhem' ) - return; - that.delayed('targetTemperature', value, 1500); - }, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.thermostat.informId, that); - }, - onRead: function(callback) { - that.query(that.mappings.thermostat.reading, function(temperature){ - callback(temperature); - }); - }, - perms: ["pw","pr","ev"], - format: "float", - initialValue: FHEM_cached[that.mappings.thermostat.informId], - supportEvents: false, - supportBonjour: false, - manfDescription: "Target Temperature", - designedMinValue: 5.0, - designedMaxValue: 30.0, - //designedMinStep: 0.5, - unit: "celsius" - }); - cTypes.push({ - cType: types.CURRENTHEATINGCOOLING_CTYPE, - perms: ["pr","ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Current Mode", - designedMaxLength: 1, - designedMinValue: 0, - designedMaxValue: 2, - designedMinStep: 1, - }); - cTypes.push({ - cType: types.TARGETHEATINGCOOLING_CTYPE, - onUpdate: function(value, context) { - if( context === 'fromFhem' ) - return; - that.command('targetMode', value); - }, - perms: ["pw","pr","ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Target Mode", - designedMinValue: 0, - designedMaxValue: 3, - designedMinStep: 1, - }); - - cTypes.push({ - cType: types.TEMPERATURE_UNITS_CTYPE, - perms: ["pr","ev"], - format: "int", - initialValue: 0, - supportEvents: false, - supportBonjour: false, - manfDescription: "Unit", - }); - } - - if( this.mappings.contact ) { - cTypes.push({ - cType: types.CONTACT_SENSOR_STATE_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.contact.informId, that); - }, - onRead: function(callback) { - that.query(that.mappings.contact.reading, function(state){ - callback(state); - }); - }, - perms: ["pr","ev"], - format: "bool", - initialValue: FHEM_cached[that.mappings.contact.informId], - supportEvents: false, - supportBonjour: false, - manfDescription: "Contact State", - designedMaxLength: 1 - }); - } - - if( this.mappings.occupancy ) { - cTypes.push({ - cType: types.OCCUPANCY_DETECTED_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.occupancy.informId, that); - }, - onRead: function(callback) { - that.query(that.mappings.occupancy.reading, function(state){ - callback(state); - }); - }, - perms: ["pr","ev"], - format: "bool", - initialValue: FHEM_cached[that.mappings.occupancy.informId], - supportEvents: false, - supportBonjour: false, - manfDescription: "Occupancy State", - designedMaxLength: 1 - }); - } - - if( this.mappings.temperature ) { - cTypes.push({ - cType: types.CURRENT_TEMPERATURE_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.temperature.informId, that); - }, - onRead: function(callback) { - that.query(that.mappings.temperature.reading, function(temperature){ - callback(temperature); - }); - }, - perms: ["pr","ev"], - format: "float", - initialValue: FHEM_cached[that.mappings.temperature.informId], - supportEvents: true, - supportBonjour: false, - manfDescription: "Current Temperature", - unit: "celsius" - }); - } - - if( this.mappings.humidity ) { - cTypes.push({ - cType: types.CURRENT_RELATIVE_HUMIDITY_CTYPE, - onRegister: function(characteristic) { - characteristic.eventEnabled = true; - FHEM_subscribe(characteristic, that.mappings.humidity.informId, that); - }, - onRead: function(callback) { - that.query(that.mappings.humidity.reading, function(humidity){ - callback(humidity); - }); - }, - perms: ["pr","ev"], - format: "int", - initialValue: FHEM_cached[that.mappings.humidity.informId], - designedMinValue: 0, - designedMaxValue: 100, - supportEvents: true, - supportBonjour: false, - manfDescription: "Current Humidity", - unit: "%" - }); - - } - - return cTypes; - }, - - sType: function() { if( this.isSwitch ) { - return types.SWITCH_STYPE; + this.log(" switch service for " + this.name) + return new Service.Switch(name); } else if( this.mappings.garage ) { - return types.GARAGE_DOOR_OPENER_STYPE; + this.log(" garage door opener service for " + this.name) + return new Service.GarageDoorOpener(name); } else if( this.mappings.window ) { - return types.WINDOW_STYPE; + this.log(" window service for " + this.name) + return new Service.Window(name); } else if( this.mappings.blind ) { - return types.WINDOW_COVERING_STYPE; + this.log(" window covering service for " + this.name) + return new Service.WindowCovering(name); } else if( this.mappings.thermostat ) { - return types.THERMOSTAT_STYPE; + this.log(" thermostat service for " + this.name) + return new Service.Thermostat(name); } else if( this.mappings.contact ) { - return types.CONTACT_SENSOR_STYPE; + this.log(" contact sensor service for " + this.name) + return new Service.ContactSensorState(name); } else if( this.mappings.occupancy ) { - return types.OCCUPANCY_SENSOR_STYPE; + this.log(" occupancy sensor service for " + this.name) + return new Service.OccupancySensor(name); } else if( this.isLight || this.mappings.pct || this.mappings.hue || this.mappings.rgb ) { - return types.LIGHTBULB_STYPE; + this.log(" lightbulb service for " + this.name) + return new Service.Lightbulb(name); } else if( this.mappings.temperature ) { - return types.TEMPERATURE_SENSOR_STYPE; + this.log(" temperature sensor service for " + this.name) + return new Service.TemperatureSensor(name); } else if( this.mappings.humidity ) { - return types.HUMIDITY_SENSOR_STYPE; - } else { - return types.SWITCH_STYPE; + this.log(" humidity sensor service for " + this.name) + return new Service.HumiditySensor(name); + } else if( this.mappings.airquality ) { + this.log(" humidity sensor service for " + this.name) + return new Service.AirQualitySensor(name); } + + this.log(" switch service for " + this.name) + return new Service.Switch(name); + }, + + identify: function(callback) { + this.log('['+this.name+'] identify requested!'); + callback(); }, getServices: function() { + this.log("creating services for " + this.name) + + this.log(" information service for " + this.name) + var informationService = new Service.AccessoryInformation(); + + informationService + .setCharacteristic(Characteristic.Manufacturer, "FHEM:"+this.type) + .setCharacteristic(Characteristic.Model, "FHEM:"+this.model ? this.model : '') + .setCharacteristic(Characteristic.SerialNumber, this.serial ? this.serial : ''); + + var controlService = this.createDeviceService(); + var that = this; - var services = [{ - sType: types.ACCESSORY_INFORMATION_STYPE, - characteristics: this.informationCharacteristics(that), - }, - { - sType: this.sType(), - characteristics: this.controlCharacteristics(that) - }]; - this.log("Loaded services for " + this.name) - return services; + if( this.mappings.onOff ) { + this.log(" power 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]; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.command( 'set', value == 0 ? that.mappings.onOff.cmdOff : that.mappings.onOff.cmdOn ); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query( that.mappings.onOff.reading, function(state){ callback(undefined, state); } ); + }.bind(this) ); + } + + if( this.mappings.pct ) { + this.log(" brightness characteristic for " + this.name) + + 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]; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.command('pct', value); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query(that.mappings.pct.reading, function(pct){ callback(undefined,pct); }); + }.bind(this) ); + + } else if( this.hasDim ) { + this.log(" fake brightness characteristic for " + this.name) + + var characteristic = controlService.addCharacteristic(Characteristic.Brightness); + + FHEM_subscribe(characteristic, that.name+'-pct', that); + characteristic.value = 0; + characteristic.maximumValue = this.pctMax; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.delayed('dim', value); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query('pct', function(pct){ callback(undefined,pct); }); + }.bind(this) ); + + } + + if( that.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]; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.command('hue', value); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query(that.mappings.hue.reading, function(hue){ callback(undefined,hue); }); + }.bind(this) ); + + } else if( this.mappings.rgb ) { + this.log(" fake hue characteristic for " + this.name) + + var characteristic = controlService.addCharacteristic(Characteristic.Hue); + + FHEM_subscribe(characteristic, that.name+'-hue', that); + FHEM_subscribe(characteristic, that.mappings.rgb.informId, that); + characteristic.value = 0; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.command('H-rgb', value); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query('hue', function(hue){ callback(undefined,hue); }); + }.bind(this) ); + + if( !this.mappings.sat ) { + this.log(" fake saturation characteristic for " + this.name) + + var characteristic = controlService.addCharacteristic(Characteristic.Saturation); + + FHEM_subscribe(characteristic, that.name+'-sat', that); + characteristic.value = 100; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.command('S-rgb', value); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query('sat', function(sat){ callback(undefined,sat); }); + }.bind(this) ); + } + + if( !this.mappings.pct ) { + this.log(" fake brightness characteristic for " + this.name) + + var characteristic = controlService.addCharacteristic(Characteristic.Brightness); + + FHEM_subscribe(characteristic, that.name+'-bri', that); + characteristic.value = 0; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.command('B-rgb', value); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query('bri', function(bri){ callback(undefined,bri); }); + }.bind(this) ); + } + + } + + if( this.mappings.sat ) { + this.log(" saturation characteristic for " + this.name) + + 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]; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.command('sat', value); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query(that.mappings.sat.reading, function(sat){ callback(undefined,sat); }); + }.bind(this) ); + } + + if( this.mappings.volume ) { + this.log(" custom volume characteristic for " + this.name) + + var characteristic = new Characteristic('Adjust the Volume of this device', '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]; + } else { + characteristic.value = 10; + } + + characteristic.format = 'int'; + //characteristic.unit = 'percentage'; + characteristic.maximumValue = 100; + characteristic.minimumValue = 0; + characteristic.stepValue = 1; + characteristic.readable = true; + characteristic.writable = true; + characteristic.supportsEventNotification = true; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.delayed('volume', value); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query(that.mappings.volume.reading, function(volume){ callback(undefined,volume); }); + }.bind(this) ); + + } + + if( this.mappings.blind ) { + this.log(" current position characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.CurrentPosition); + + FHEM_subscribe(characteristic, that.mappings.blind.informId, that); + characteristic.value = FHEM_cached[that.mappings.blind.informId]; + + characteristic + .on('get', function(callback) { + that.query(that.mappings.blind.reading, function(pos){ callback(undefined,pos); }); + }.bind(this) ); + + + this.log(" target position characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.TargetPosition); + + characteristic.value = FHEM_cached[that.mappings.blind.informId]; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.delayed('targetPosition', value, 1500); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query(that.mappings.blind.reading, function(pos){ callback(undefined,pos); }); + }.bind(this) ); + + + this.log(" position state characteristic for " + this.name) + + 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; + + characteristic + .on('get', function(callback) { + if( that.mappings.motor ) + that.query(that.mappings.motor.reading, function(state){ callback(undefined,state); }); + }.bind(this) ); + + } + + if( this.mappings.window ) { + this.log(" current position characteristic for " + this.name) + + 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]; + + characteristic + .on('get', function(callback) { + that.query(that.mappings.window.reading, function(pos){ callback(undefined,pos); }); + }.bind(this) ); + + + this.log(" target position characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.TargetPosition); + + characteristic.value = FHEM_cached[that.mappings.window.informId]; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.delayed('targetPosition', value, 1500); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query(that.mappings.window.reading, function(level){ callback(undefined,level); }); + }.bind(this) ); + + + this.log(" position state characteristic for " + this.name) + + 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; + + characteristic + .on('get', function(callback) { + if( that.mappings.direction ) + that.query(that.mappings.direction.reading, function(direction){ callback(undefined,direction); }); + }.bind(this) ); + + } + + if( this.mappings.garage ) { + this.log(" current door state characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.CurrentDoorState); + + characteristic.value = Characteristic.CurrentDoorState.STOPPED; + + characteristic + .on('get', function(callback) { + callback(undefined, Characteristic.CurrentDoorState.STOPPED); + }.bind(this) ); + + + this.log(" target door state characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.TargetDoorState); + + characteristic.value = 1; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.command( 'set', value == 0 ? that.mappings.garage.cmdOpen : that.mappings.garage.cmdClose ); + callback(); + }.bind(this) ) + .on('get', function(callback) { + callback(undefined,0); + }.bind(this) ); + + + this.log(" obstruction detected characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.ObstructionDetected); + + //FHEM_subscribe(characteristic, that.mappings.direction.informId, that); + characteristic.value = 0; + + characteristic + .on('get', function(callback) { + callback(undefined,1); + }.bind(this) ); + + } + + if( this.mappings.temperature ) { + this.log(" temperature characteristic for " + this.name) + + 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]; + + characteristic + .on('get', function(callback) { + that.query(that.mappings.temperature.reading, function(temperature){ callback(undefined,temperature); }); + }.bind(this) ); + + } + + if( this.mappings.humidity ) { + this.log(" humidity characteristic for " + this.name) + + 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]; + + characteristic + .on('get', function(callback) { + that.query(that.mappings.humidity.reading, function(humidity){ callback(undefined,humidity); }); + }.bind(this) ); + + } + + if( this.mappings.airquality ) { + this.log(" air quality characteristic for " + this.name) + + 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]; + + characteristic + .on('get', function(callback) { + that.query(that.mappings.airquality.reading, function(airquality){ callback(undefined,airquality); }); + }.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]; + + characteristic + .on('set', function(value, callback, context) { + if( context !== 'fromFhem' ) + that.delayed('targetTemperature', value, 1500); + callback(); + }.bind(this) ) + .on('get', function(callback) { + that.query(that.mappings.thermostat.reading, function(temperature){ callback(undefined,temperature); }); + }.bind(this) ); + + } + + if( this.mappings.contact ) { + this.log(" contact sensor characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.ContactSensorState); + + FHEM_subscribe(characteristic, that.mappings.contact.informId, that); + characteristic.value = FHEM_cached[that.mappings.contact.informId]; + + characteristic + .on('get', function(callback) { + that.query(that.mappings.contact.reading, function(state){ callback(undefined,state); }); + }.bind(this) ); + + } + + if( this.mappings.occupancy ) { + this.log(" occupancy detected characteristic for " + this.name) + + var characteristic = controlService.getCharacteristic(Characteristic.OccupancyDetected); + + FHEM_subscribe(characteristic, that.mappings.occupancy.informId, that); + characteristic.value = FHEM_cached[that.mappings.occupancy.informId]; + + characteristic + .on('get', function(callback) { + that.query(that.mappings.occupancy.reading, function(state){ callback(undefined,state); }); + }.bind(this) ); + + } + + return [informationService, controlService]; } + }; //module.exports.accessory = FHEMAccessory; @@ -1768,7 +1522,7 @@ module.exports.platform = FHEMPlatform; //http server for debugging var http = require('http'); -const FHEMdebug_PORT=8080; +const FHEMdebug_PORT=8081; function FHEMdebug_handleRequest(request, response){ //console.log( request ); @@ -1783,32 +1537,8 @@ function FHEMdebug_handleRequest(request, response){ response.write( "home

" ); response.end( "subscriptions: " + util.inspect(FHEM_subscriptions, {depth: 4}).replace(/\n/g, '
') ); - } else if( request.url == "/xxpersist" ) { - response.write( "home

" ); - var unique = {}; - Object.keys(FHEM_subscriptions).forEach(function(key) { - var characteristic = FHEM_subscriptions[key].characteristic; - var info = characteristic.accessoryController.tcpServer.accessoryInfo; - if( unique[info.displayName] ) - return; - unique[info.displayName] = info.username; - - var accessory = FHEM_subscriptions[key].accessory; - - //var cmd = '{$defs{'+ accessory.device +'}->{homekitID} = "'+info.username+'" if(defined($defs{'+ accessory.device +'}));;}'; - //accessory.execute( cmd ); - } ); - - var keys = Object.keys(unique); - keys.sort(); - for( i = 0; i < keys.length; i++ ) { - var k = keys[i]; - response.write( k +': '+ unique[k] +'
' ); - } - response.end( "" ); - } else - response.end( "cached
persist
subscriptions" ); + response.end( "cached
subscriptions" ); } var FHEMdebug_server = http.createServer( FHEMdebug_handleRequest ); From f4160c2d01246eeaa98b3a7baad721d899d96c18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=20Schr=C3=B6ter?= Date: Mon, 14 Sep 2015 22:43:37 +0200 Subject: [PATCH 57/71] added identify --- platforms/FHEM.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/platforms/FHEM.js b/platforms/FHEM.js index 6c7d45a..b81342a 100644 --- a/platforms/FHEM.js +++ b/platforms/FHEM.js @@ -1062,6 +1062,9 @@ FHEMAccessory.prototype = { identify: function(callback) { this.log('['+this.name+'] identify requested!'); + if( match = this.PossibleSets.match(/[\^ ]toggle\b/) ) { + this.command( 'identify' ); + } callback(); }, From e894b1a1a1314c3005d9f8235e9f833f05325c8e Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 20:14:27 -0400 Subject: [PATCH 58/71] support media players to be addressable as lights --- platforms/HomeAssistant.js | 143 +++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index d9984c8..22a3d72 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -99,6 +99,7 @@ HomeAssistantPlatform.prototype = { var foundAccessories = []; var lightsRE = /^light\./i var switchRE = /^switch\./i + var mediaPlayerRE = /^media_player\./i this._request('GET', '/states', {}, function(error, response, data){ @@ -111,6 +112,8 @@ HomeAssistantPlatform.prototype = { accessory = new HomeAssistantLight(that.log, entity, that) }else if (entity.entity_id.match(switchRE)){ accessory = new HomeAssistantSwitch(that.log, entity, that) + }else if (entity.entity_id.match(mediaPlayerRE) && entity.attributes && entity.attributes.supported_media_commands){ + accessory = new HomeAssistantMediaPlayer(that.log, entity, that) } if (accessory) { @@ -229,6 +232,145 @@ HomeAssistantLight.prototype = { } +function HomeAssistantMediaPlayer(log, data, client) { + var SUPPORT_PAUSE = 1 + var SUPPORT_SEEK = 2 + var SUPPORT_VOLUME_SET = 4 + var SUPPORT_VOLUME_MUTE = 8 + var SUPPORT_PREVIOUS_TRACK = 16 + var SUPPORT_NEXT_TRACK = 32 + var SUPPORT_YOUTUBE = 64 + var SUPPORT_TURN_ON = 128 + var SUPPORT_TURN_OFF = 256 + + // device info + this.domain = "media_player" + this.data = data + this.entity_id = data.entity_id + this.supportsVolume = false + this.supportedMediaCommands = data.attributes.supported_media_commands + + if (data.attributes && data.attributes.friendly_name) { + this.name = data.attributes.friendly_name + }else{ + this.name = data.entity_id.split('.').pop().replace(/_/g, ' ') + } + + if ((this.supportedMediaCommands | SUPPORT_PAUSE) == this.supportedMediaCommands) { + this.onState = "playing" + this.offState = "paused" + this.onService = "media_play" + this.offService = "media_pause" + }else if ((this.supportedMediaCommands | SUPPORT_TURN_ON) == this.supportedMediaCommands && (this.supportedMediaCommands | SUPPORT_TURN_OFF) == this.supportedMediaCommands) { + this.onState = "on" + this.offState = "off" + this.onService = "turn_on" + this.offService = "turn_off" + } + + if ((this.supportedMediaCommands | SUPPORT_VOLUME_SET) == this.supportedMediaCommands) { + this.supportsVolume = true + } + + this.client = client + this.log = log; +} + +HomeAssistantMediaPlayer.prototype = { + getPowerState: function(callback){ + this.log("fetching power state for: " + this.name); + + this.client.fetchState(this.entity_id, function(data){ + if (data) { + powerState = data.state == this.onState + callback(null, powerState) + }else{ + callback(communicationError) + } + }.bind(this)) + }, + getVolume: function(callback){ + this.log("fetching volume for: " + this.name); + that = this + this.client.fetchState(this.entity_id, function(data){ + if (data && data.attributes) { + that.log(JSON.stringify(data.attributes)) + level = data.attributes.volume_level ? data.attributes.volume_level*100 : 0 + callback(null, level) + }else{ + callback(communicationError) + } + }.bind(this)) + }, + setPowerState: function(powerOn, callback) { + var that = this; + var service_data = {} + service_data.entity_id = this.entity_id + + if (powerOn) { + this.log("Setting power state on the '"+this.name+"' to on"); + + this.client.callService(this.domain, this.onService, service_data, function(data){ + if (data) { + that.log("Successfully set power state on the '"+that.name+"' to on"); + callback() + }else{ + callback(communicationError) + } + }.bind(this)) + }else{ + this.log("Setting power state on the '"+this.name+"' to off"); + + this.client.callService(this.domain, this.offService, service_data, function(data){ + if (data) { + that.log("Successfully set power state on the '"+that.name+"' to off"); + callback() + }else{ + callback(communicationError) + } + }.bind(this)) + } + }, + setVolume: function(level, callback) { + var that = this; + var service_data = {} + service_data.entity_id = this.entity_id + + service_data.volume_level = level/100.0 + + this.log("Setting volume on the '"+this.name+"' to " + level); + + this.client.callService(this.domain, 'volume_set', service_data, function(data){ + if (data) { + that.log("Successfully set volume on the '"+that.name+"' to " + level); + callback() + }else{ + callback(communicationError) + } + }.bind(this)) + }, + getServices: function() { + var lightbulbService = new Service.Lightbulb(); + + lightbulbService + .getCharacteristic(Characteristic.On) + .on('get', this.getPowerState.bind(this)) + .on('set', this.setPowerState.bind(this)); + + + if (this.supportsVolume) { + lightbulbService + .addCharacteristic(Characteristic.Brightness) + .on('get', this.getVolume.bind(this)) + .on('set', this.setVolume.bind(this)); + } + + return [lightbulbService]; + } + +} + + function HomeAssistantSwitch(log, data, client) { // device info this.domain = "switch" @@ -300,5 +442,6 @@ HomeAssistantSwitch.prototype = { } module.exports.accessory = HomeAssistantLight; +module.exports.accessory = HomeAssistantMediaPlayer; module.exports.accessory = HomeAssistantSwitch; module.exports.platform = HomeAssistantPlatform; From 1df32fca3d0d9cdc8d852865119d91e37b97c218 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 20:20:26 -0400 Subject: [PATCH 59/71] add some docs --- platforms/HomeAssistant.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index 22a3d72..075d90c 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -7,6 +7,29 @@ // URL: http://home-assistant.io // GitHub: https://github.com/balloob/home-assistant // +// HA accessories supported: Lights, Switches, Media Players. +// +// Media Player Support +// +// Media players on your Home Assistant will be added to your HomeKit as a light. +// While this seems like a hack at first, it's actually quite useful. You can +// turn them on, off, and set their volume (as a function of brightness). +// +// There are some rules to know about how on/off treats your media player. If +// your media player supports play/pause, then turning them on and off via +// HomeKit will play and pause them. If they do not support play/pause but then +// support on/off they will be turned on and then off. +// +// HomeKit does not have a characteristic of Volume or a Speaker type. So we are +// using the light device type here. So to turn your speaker up and down, you +// will need to use the same language you use to set the brighness of a light. +// You can play around with language to see what fits best. +// +// Examples +// +// Dim the Kitchen Speaker to 40% +// Set the brightness of the Kitchen Speaker to 40% +// // Remember to add platform to config.json. Example: // "platforms": [ // { From be589d4fb50a2e09aed8aa2b8b4a6cc18b26f611 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Mon, 14 Sep 2015 20:46:51 -0400 Subject: [PATCH 60/71] you can do this too --- platforms/HomeAssistant.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index 075d90c..6088b94 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -27,7 +27,8 @@ // // Examples // -// Dim the Kitchen Speaker to 40% +// Dim the Kitchen Speaker to 40% - sets volume to 40% +// Dim the the Kitchen Speaker 10% - lowers the volume by 10% // Set the brightness of the Kitchen Speaker to 40% // // Remember to add platform to config.json. Example: From 13d1ed75cf889b8d5e4ace9ab9c1ea2a3da76000 Mon Sep 17 00:00:00 2001 From: S'pht'Kr Date: Tue, 15 Sep 2015 06:55:53 +0200 Subject: [PATCH 61/71] File-based motion or contact sensor This module creates a motion sensor accessory based on watching a directory or file. --- accessories/FileSensor.js | 76 +++++++++++++++++++++++++++++++++++++++ config-sample.json | 9 +++++ package.json | 1 + 3 files changed, 86 insertions(+) create mode 100644 accessories/FileSensor.js diff --git a/accessories/FileSensor.js b/accessories/FileSensor.js new file mode 100644 index 0000000..e377dc6 --- /dev/null +++ b/accessories/FileSensor.js @@ -0,0 +1,76 @@ +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"); + +module.exports = { + accessory: FileSensorAccessory +} + +function FileSensorAccessory(log, config) { + this.log = log; + + // url info + this.name = config["name"]; + this.path = config["path"]; + this.window_seconds = config["window_seconds"] || 5; + this.sensor_type = config["sensor_type"] || "m"; + this.inverse = config["inverse"] || false; + + if(config["sn"]){ + this.sn = config["sn"]; + } else { + var shasum = crypto.createHash('sha1'); + shasum.update(this.path); + this.sn = shasum.digest('base64'); + debug('Computed SN ' + this.sn); + } +} + +FileSensorAccessory.prototype = { + + 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.Name, this.name) + .setCharacteristic(Characteristic.Manufacturer, "Homebridge") + .setCharacteristic(Characteristic.Model, "File Sensor") + .setCharacteristic(Characteristic.SerialNumber, this.sn); + + var service, changeAction; + if(this.sensor_type === "c"){ + service = new Service.ContactSensor(); + changeAction = function(newState){ + service.getCharacteristic(Characteristic.ContactSensorState) + .setValue(newState ? Characteristic.ContactSensorState.CONTACT_DETECTED : Characteristic.ContactSensorState.CONTACT_NOT_DETECTED); + }; + } else { + service = new Service.MotionSensor(); + changeAction = function(newState){ + service.getCharacteristic(Characteristic.MotionDetected) + .setValue(newState); + }; + } + + var changeHandler = function(path, stats){ + var d = new Date(); + if(d.getTime() - stats.mtime.getTime() <= (this.window_seconds * 1000)){ + var newState = this.inverse ? false : true; + changeAction(newState); + if(this.timer !== undefined) clearTimeout(this.timer); + this.timer = setTimeout(function(){changeAction(!newState);}, this.window_seconds * 1000); + } + }.bind(this); + + var watcher = chokidar.watch(this.path, {alwaysStat: true}); + watcher.on('add', changeHandler); + watcher.on('change', changeHandler); + + return [informationService, service]; + } +}; diff --git a/config-sample.json b/config-sample.json index 48c1db4..86782e6 100644 --- a/config-sample.json +++ b/config-sample.json @@ -196,6 +196,15 @@ "host" : "localhost", "port" : 6600, "description": "Allows some control of an MPD server" + }, + { + "accessory": "FileSensor", + "name": "File Time Motion Sensor", + "path": "/tmp/CameraDump/", + "window_seconds": 5, + "sensor_type": "m", + "inverse": false } + ] } diff --git a/package.json b/package.json index 49294ef..f83cf26 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "dependencies": { "ad2usb": "git+https://github.com/alistairg/node-ad2usb.git#local", "carwingsjs": "0.0.x", + "chokidar": "^1.0.5", "color": "0.10.x", "eibd": "^0.3.1", "elkington": "kevinohara80/elkington", From d6b3fc766793dc42299a4c6e5df73d8da14ae7b4 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Tue, 15 Sep 2015 10:58:25 -0700 Subject: [PATCH 62/71] Bump HAP-NodeJS with fixes for Node 4.0.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f83cf26..35c8f39 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "color": "0.10.x", "eibd": "^0.3.1", "elkington": "kevinohara80/elkington", - "hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#6bf0f9eaaa2d87db8d1768114c61f4acbb095c41", + "hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#98ef550c8d6fd961741673d4b695a74dd0126eba", "harmonyhubjs-client": "^1.1.4", "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", "lifx-api": "^1.0.1", From 8ceaccfca43c864b05d8a2f21977d4f65c52c8c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=20Schr=C3=B6ter?= Date: Tue, 15 Sep 2015 22:03:17 +0200 Subject: [PATCH 63/71] cleanups, fixes and more new api stuff --- platforms/FHEM.js | 79 +++++++++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/platforms/FHEM.js b/platforms/FHEM.js index b81342a..03f3ee1 100644 --- a/platforms/FHEM.js +++ b/platforms/FHEM.js @@ -551,8 +551,11 @@ FHEMAccessory(log, connection, s) { 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/) ) + && s.PossibleSets.match(/[\^ ]off\b/) ) { this.mappings.onOff = { reading: 'state', cmdOn: 'on', cmdOff: 'off' }; + if( !s.Readings.state ) + delete this.mappings.onOff.reading; + } var event_map = s.Attributes.eventMap; if( event_map ) { @@ -912,14 +915,21 @@ FHEMAccessory.prototype = { }, query: function(reading, callback) { + if( reading == undefined ) { + if( callback != undefined ) + callback( 1 ); + return; + } + this.log("query: " + this.name + "-" + reading); var result = FHEM_cached[this.device + '-' + reading]; if( result != undefined ) { this.log(" cached: " + result); if( callback != undefined ) - callback( result ); - return( result ); + callback( undefined, result ); + return result; + } else this.log(" not cached" ); @@ -1009,11 +1019,14 @@ FHEMAccessory.prototype = { that.log(" mapped: " + value); FHEM_update( that.device + '-' + reading, value, true ); - if( value == undefined ) - return; - if( callback != undefined ) - callback(value); - return(value); + if( callback != undefined ) { + if( value == undefined ) + callback(1); + else + callback(undefined, value); + } + + return value ; } ); }, @@ -1038,7 +1051,7 @@ FHEMAccessory.prototype = { return new Service.Thermostat(name); } else if( this.mappings.contact ) { this.log(" contact sensor service for " + this.name) - return new Service.ContactSensorState(name); + return new Service.ContactSensor(name); } else if( this.mappings.occupancy ) { this.log(" occupancy sensor service for " + this.name) return new Service.OccupancySensor(name); @@ -1098,7 +1111,7 @@ FHEMAccessory.prototype = { callback(); }.bind(this) ) .on('get', function(callback) { - that.query( that.mappings.onOff.reading, function(state){ callback(undefined, state); } ); + that.query(that.mappings.onOff.reading, callback); }.bind(this) ); } @@ -1118,7 +1131,7 @@ FHEMAccessory.prototype = { callback(); }.bind(this) ) .on('get', function(callback) { - that.query(that.mappings.pct.reading, function(pct){ callback(undefined,pct); }); + that.query(that.mappings.pct.reading, callback); }.bind(this) ); } else if( this.hasDim ) { @@ -1137,7 +1150,7 @@ FHEMAccessory.prototype = { callback(); }.bind(this) ) .on('get', function(callback) { - that.query('pct', function(pct){ callback(undefined,pct); }); + that.query('pct', callback); }.bind(this) ); } @@ -1158,7 +1171,7 @@ FHEMAccessory.prototype = { callback(); }.bind(this) ) .on('get', function(callback) { - that.query(that.mappings.hue.reading, function(hue){ callback(undefined,hue); }); + that.query(that.mappings.hue.reading, callback); }.bind(this) ); } else if( this.mappings.rgb ) { @@ -1177,7 +1190,7 @@ FHEMAccessory.prototype = { callback(); }.bind(this) ) .on('get', function(callback) { - that.query('hue', function(hue){ callback(undefined,hue); }); + that.query('hue', callback); }.bind(this) ); if( !this.mappings.sat ) { @@ -1195,7 +1208,7 @@ FHEMAccessory.prototype = { callback(); }.bind(this) ) .on('get', function(callback) { - that.query('sat', function(sat){ callback(undefined,sat); }); + that.query('sat', callback); }.bind(this) ); } @@ -1214,7 +1227,7 @@ FHEMAccessory.prototype = { callback(); }.bind(this) ) .on('get', function(callback) { - that.query('bri', function(bri){ callback(undefined,bri); }); + that.query('bri', callback); }.bind(this) ); } @@ -1236,14 +1249,14 @@ FHEMAccessory.prototype = { callback(); }.bind(this) ) .on('get', function(callback) { - that.query(that.mappings.sat.reading, function(sat){ callback(undefined,sat); }); + that.query(that.mappings.sat.reading, callback); }.bind(this) ); } if( this.mappings.volume ) { this.log(" custom volume characteristic for " + this.name) - var characteristic = new Characteristic('Adjust the Volume of this device', '00000027-0000-1000-8000-0026BB765291'); // FIXME!!! + var characteristic = new Characteristic('Volume', '00000027-0000-1000-8000-0026BB765291'); // FIXME!!! controlService.addCharacteristic(characteristic); if( !that.mappings.volume.nocache ) { @@ -1253,8 +1266,8 @@ FHEMAccessory.prototype = { characteristic.value = 10; } - characteristic.format = 'int'; - //characteristic.unit = 'percentage'; + characteristic.format = 'uint8'; + characteristic.unit = 'percentage'; characteristic.maximumValue = 100; characteristic.minimumValue = 0; characteristic.stepValue = 1; @@ -1269,7 +1282,7 @@ FHEMAccessory.prototype = { callback(); }.bind(this) ) .on('get', function(callback) { - that.query(that.mappings.volume.reading, function(volume){ callback(undefined,volume); }); + that.query(that.mappings.volume.reading, callback); }.bind(this) ); } @@ -1284,7 +1297,7 @@ FHEMAccessory.prototype = { characteristic .on('get', function(callback) { - that.query(that.mappings.blind.reading, function(pos){ callback(undefined,pos); }); + that.query(that.mappings.blind.reading, callback); }.bind(this) ); @@ -1301,7 +1314,7 @@ FHEMAccessory.prototype = { callback(); }.bind(this) ) .on('get', function(callback) { - that.query(that.mappings.blind.reading, function(pos){ callback(undefined,pos); }); + that.query(that.mappings.blind.reading, callback); }.bind(this) ); @@ -1316,7 +1329,7 @@ FHEMAccessory.prototype = { characteristic .on('get', function(callback) { if( that.mappings.motor ) - that.query(that.mappings.motor.reading, function(state){ callback(undefined,state); }); + that.query(that.mappings.motor.reading, callback); }.bind(this) ); } @@ -1332,7 +1345,7 @@ FHEMAccessory.prototype = { characteristic .on('get', function(callback) { - that.query(that.mappings.window.reading, function(pos){ callback(undefined,pos); }); + that.query(that.mappings.window.reading, callback); }.bind(this) ); @@ -1349,7 +1362,7 @@ FHEMAccessory.prototype = { callback(); }.bind(this) ) .on('get', function(callback) { - that.query(that.mappings.window.reading, function(level){ callback(undefined,level); }); + that.query(that.mappings.window.reading, callback); }.bind(this) ); @@ -1364,7 +1377,7 @@ FHEMAccessory.prototype = { characteristic .on('get', function(callback) { if( that.mappings.direction ) - that.query(that.mappings.direction.reading, function(direction){ callback(undefined,direction); }); + that.query(that.mappings.direction.reading, callback); }.bind(this) ); } @@ -1424,7 +1437,7 @@ FHEMAccessory.prototype = { characteristic .on('get', function(callback) { - that.query(that.mappings.temperature.reading, function(temperature){ callback(undefined,temperature); }); + that.query(that.mappings.temperature.reading, callback); }.bind(this) ); } @@ -1440,7 +1453,7 @@ FHEMAccessory.prototype = { characteristic .on('get', function(callback) { - that.query(that.mappings.humidity.reading, function(humidity){ callback(undefined,humidity); }); + that.query(that.mappings.humidity.reading, callback); }.bind(this) ); } @@ -1456,7 +1469,7 @@ FHEMAccessory.prototype = { characteristic .on('get', function(callback) { - that.query(that.mappings.airquality.reading, function(airquality){ callback(undefined,airquality); }); + that.query(that.mappings.airquality.reading, callback); }.bind(this) ); } @@ -1477,7 +1490,7 @@ FHEMAccessory.prototype = { callback(); }.bind(this) ) .on('get', function(callback) { - that.query(that.mappings.thermostat.reading, function(temperature){ callback(undefined,temperature); }); + that.query(that.mappings.thermostat.reading, callback); }.bind(this) ); } @@ -1492,7 +1505,7 @@ FHEMAccessory.prototype = { characteristic .on('get', function(callback) { - that.query(that.mappings.contact.reading, function(state){ callback(undefined,state); }); + that.query(that.mappings.contact.reading, callback); }.bind(this) ); } @@ -1507,7 +1520,7 @@ FHEMAccessory.prototype = { characteristic .on('get', function(callback) { - that.query(that.mappings.occupancy.reading, function(state){ callback(undefined,state); }); + that.query(that.mappings.occupancy.reading, callback); }.bind(this) ); } From f3e08b0a151760830982141e41b88703d15c4ad5 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Wed, 16 Sep 2015 03:12:56 -0400 Subject: [PATCH 64/71] add device types from Home Assistant based on the supported types declared in the config Instead of matching with a regex, we use a config of supported device types to attempt to load in. This allows the user to whitelist just the ones they want to add. --- platforms/HomeAssistant.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index 6088b94..4c12a40 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -37,7 +37,8 @@ // "platform": "HomeAssistant", // "name": "HomeAssistant", // "host": "http://192.168.1.50:8123", -// "password": "xxx" +// "password": "xxx", +// "supported_types": ["light", "switch", "media_player", "scene"] // } // ] // @@ -56,6 +57,7 @@ function HomeAssistantPlatform(log, config){ // auth info this.host = config["host"]; this.password = config["password"]; + this.supportedTypes = config["supported_types"]; this.log = log; } @@ -121,22 +123,24 @@ HomeAssistantPlatform.prototype = { var that = this; var foundAccessories = []; - var lightsRE = /^light\./i - var switchRE = /^switch\./i - var mediaPlayerRE = /^media_player\./i - this._request('GET', '/states', {}, function(error, response, data){ for (var i = 0; i < data.length; i++) { entity = data[i] + entity_type = entity.entity_id.split('.')[0] + + if (that.supportedTypes.indexOf(entity_type) == -1) { + continue; + } + var accessory = null - if (entity.entity_id.match(lightsRE)) { + if (entity_type == 'light') { accessory = new HomeAssistantLight(that.log, entity, that) - }else if (entity.entity_id.match(switchRE)){ + }else if (entity_type == 'switch'){ accessory = new HomeAssistantSwitch(that.log, entity, that) - }else if (entity.entity_id.match(mediaPlayerRE) && entity.attributes && entity.attributes.supported_media_commands){ + }else if (entity_type == 'media_player' && entity.attributes && entity.attributes.supported_media_commands){ accessory = new HomeAssistantMediaPlayer(that.log, entity, that) } From 7f753f79f69f38fa84a9b1f1c4a43bfe3b022792 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Wed, 16 Sep 2015 03:14:09 -0400 Subject: [PATCH 65/71] HA switch takes a type argument This lets you pass the domain to the switch to let it be other types of objects, like say, a scene. --- platforms/HomeAssistant.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index 4c12a40..3929b3e 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -399,9 +399,9 @@ HomeAssistantMediaPlayer.prototype = { } -function HomeAssistantSwitch(log, data, client) { +function HomeAssistantSwitch(log, data, client, type) { // device info - this.domain = "switch" + this.domain = type || "switch" this.data = data this.entity_id = data.entity_id if (data.attributes && data.attributes.friendly_name) { From eb6c881d2893c46c42581cda311d0b296ab5950e Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Wed, 16 Sep 2015 03:14:22 -0400 Subject: [PATCH 66/71] support loading scenes from HA --- platforms/HomeAssistant.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index 3929b3e..a4fd992 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -140,6 +140,8 @@ HomeAssistantPlatform.prototype = { accessory = new HomeAssistantLight(that.log, entity, that) }else if (entity_type == 'switch'){ accessory = new HomeAssistantSwitch(that.log, entity, that) + }else if (entity_type == 'scene'){ + accessory = new HomeAssistantSwitch(that.log, entity, that, 'scene') }else if (entity_type == 'media_player' && entity.attributes && entity.attributes.supported_media_commands){ accessory = new HomeAssistantMediaPlayer(that.log, entity, that) } From ec8b5566183296cbe1de06be7b23f821101d3b3a Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Wed, 16 Sep 2015 03:15:37 -0400 Subject: [PATCH 67/71] return informationServices for HA devices --- platforms/HomeAssistant.js | 49 ++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index a4fd992..5c50b26 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -246,6 +246,12 @@ HomeAssistantLight.prototype = { }, getServices: function() { var lightbulbService = new Service.Lightbulb(); + var informationService = new Service.AccessoryInformation(); + + informationService + .setCharacteristic(Characteristic.Manufacturer, "Home Assistant") + .setCharacteristic(Characteristic.Model, "Light") + .setCharacteristic(Characteristic.SerialNumber, "xxx"); lightbulbService .getCharacteristic(Characteristic.On) @@ -257,7 +263,7 @@ HomeAssistantLight.prototype = { .on('get', this.getBrightness.bind(this)) .on('set', this.setBrightness.bind(this)); - return [lightbulbService]; + return [informationService, lightbulbService]; } } @@ -381,6 +387,12 @@ HomeAssistantMediaPlayer.prototype = { }, getServices: function() { var lightbulbService = new Service.Lightbulb(); + var informationService = new Service.AccessoryInformation(); + + informationService + .setCharacteristic(Characteristic.Manufacturer, "Home Assistant") + .setCharacteristic(Characteristic.Model, "Media Player") + .setCharacteristic(Characteristic.SerialNumber, "xxx"); lightbulbService .getCharacteristic(Characteristic.On) @@ -395,7 +407,7 @@ HomeAssistantMediaPlayer.prototype = { .on('set', this.setVolume.bind(this)); } - return [lightbulbService]; + return [informationService, lightbulbService]; } } @@ -460,13 +472,36 @@ HomeAssistantSwitch.prototype = { }, getServices: function() { var switchService = new Service.Switch(); + var informationService = new Service.AccessoryInformation(); + var model; - switchService - .getCharacteristic(Characteristic.On) - .on('get', this.getPowerState.bind(this)) - .on('set', this.setPowerState.bind(this)); + switch (this.domain) { + case "scene": + model = "Scene" + break; + default: + model = "Switch" - return [switchService]; + } + + informationService + .setCharacteristic(Characteristic.Manufacturer, "Home Assistant") + .setCharacteristic(Characteristic.Model, model) + .setCharacteristic(Characteristic.SerialNumber, "xxx"); + + if (this.domain == 'switch') { + switchService + .getCharacteristic(Characteristic.On) + .on('get', this.getPowerState.bind(this)) + .on('set', this.setPowerState.bind(this)); + + }else{ + switchService + .getCharacteristic(Characteristic.On) + .on('set', this.setPowerState.bind(this)); + } + + return [informationService, switchService]; } } From 0f89a6ae366c0c3808ce2f49adf1230ac2e220b0 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Wed, 16 Sep 2015 03:15:58 -0400 Subject: [PATCH 68/71] add new supported_types key for HA to sample config --- config-sample.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config-sample.json b/config-sample.json index 86782e6..49fc923 100644 --- a/config-sample.json +++ b/config-sample.json @@ -94,7 +94,8 @@ "platform": "HomeAssistant", "name": "HomeAssistant", "host": "http://192.168.1.10:8123", - "password": "XXXXX" + "password": "XXXXX", + "supported_types": ["light", "switch", "media_player", "scene"] } ], From 651cdfa786c011cb9ecbd3e213b536bdf36f6548 Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Wed, 16 Sep 2015 03:16:49 -0400 Subject: [PATCH 69/71] close it --- platforms/HomeAssistant.js | 1 - 1 file changed, 1 deletion(-) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index 5c50b26..23a4799 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -481,7 +481,6 @@ HomeAssistantSwitch.prototype = { break; default: model = "Switch" - } informationService From 773eb8fd0e878d2f1907883d98628564fb89f5af Mon Sep 17 00:00:00 2001 From: Jon Maddox Date: Wed, 16 Sep 2015 03:21:24 -0400 Subject: [PATCH 70/71] some docs --- platforms/HomeAssistant.js | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/platforms/HomeAssistant.js b/platforms/HomeAssistant.js index 23a4799..5a23ab5 100644 --- a/platforms/HomeAssistant.js +++ b/platforms/HomeAssistant.js @@ -7,7 +7,27 @@ // URL: http://home-assistant.io // GitHub: https://github.com/balloob/home-assistant // -// HA accessories supported: Lights, Switches, Media Players. +// HA accessories supported: Lights, Switches, Media Players, Scenes. +// +// Optional Devices - Edit the supported_types key in the config to pick which +// of the 4 types you would like to expose to HomeKit from +// Home Assistant. light, switch, media_player, scene. +// +// +// Scene Support +// +// You can optionally import your Home Assistant scenes. These will appear to +// HomeKit as switches. You can simply say "turn on party time". In some cases +// scenes names are already rerved in HomeKit...like "Good Morning" and +// "Good Night". You will be able to just say "Good Morning" or "Good Night" to +// have these triggered. +// +// You might want to play with the wording to figure out what ends up working well +// for your scene names. It's also important to not populate any actual HomeKit +// scenes with the same names, as Siri will pick these instead of your Home +// Assistant scenes. +// +// // // Media Player Support // @@ -25,6 +45,8 @@ // will need to use the same language you use to set the brighness of a light. // You can play around with language to see what fits best. // +// +// // Examples // // Dim the Kitchen Speaker to 40% - sets volume to 40% From 2c118e964987d863f4e8f1b3cb2924077678da54 Mon Sep 17 00:00:00 2001 From: Nick Farina Date: Wed, 16 Sep 2015 07:45:44 -0700 Subject: [PATCH 71/71] Pull in latest HAP fixes --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 35c8f39..d0eb41c 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "color": "0.10.x", "eibd": "^0.3.1", "elkington": "kevinohara80/elkington", - "hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#98ef550c8d6fd961741673d4b695a74dd0126eba", + "hap-nodejs": "git+https://github.com/KhaosT/HAP-NodeJS#0030b35856e04ee2b42f0d05839feaa5c44cbd1f", "harmonyhubjs-client": "^1.1.4", "harmonyhubjs-discover": "git+https://github.com/swissmanu/harmonyhubjs-discover.git", "lifx-api": "^1.0.1",