// FHEM Platform Shim for HomeBridge // current version on https://github.com/justme-1968/homebridge // // 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" // } // ], var Service; try { Service = require("hap-nodejs").Service; } catch(err) { Service = require("HAP-NodeJS").Service; } var Characteristic; try { Characteristic = require("hap-nodejs").Characteristic; } catch(err) { Characteristic = require("HAP-NodeJS").Characteristic; } 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 }; } function FHEM_isPublished(device) { var keys = Object.keys(FHEM_subscriptions); for( var i = 0; i < keys.length; i++ ) { var key = keys[i]; var subscription = FHEM_subscriptions[key]; var accessory = subscription.accessory; if( accessory.device === device ) return true; }; return false; } // cached readings from longpoll & query var FHEM_cached = {}; //var FHEM_internal = {}; 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; FHEM_cached[inform_id] = value; //FHEM_cached[inform_id] = { 'value': value, 'timestamp': Date.now() }; var date = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, ''); console.log(" " + date + " caching: " + inform_id + ": " + value + " as " + typeof(value) ); if( !no_update && subscription.characteristic ) subscription.characteristic.setValue(value, undefined, 'fromFHEM'); } } var FHEM_lastEventTime = {}; var FHEM_longpoll_running = {}; //FIXME: add filter function FHEM_startLongpoll(connection) { if( FHEM_longpoll_running[connection.base_url] ) return; FHEM_longpoll_running[connection.base_url] = true; if( connection.disconnects == undefined ) connection.disconnects = 0; var filter = ".*"; var since = "null"; if( FHEM_lastEventTime[connection.base_url] ) since = FHEM_lastEventTime[connection.base_url]/1000; var query = "/fhem.pl?XHR=1"+ "&inform=type=status;filter="+filter+";since="+since+";fmt=JSON"+ "×tamp="+Date.now() 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[connection.base_url] = lastEventTime; var accessory = subscription.accessory; var value = d[1]; 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 = Characteristic.LockCurrentState.UNSECURED; if( value.match( /^locked/ ) ) lock = Characteristic.LockCurrentState.SECURED; if( value.match( /uncertain/ ) ) level = Characteristic.LockCurrentState.UNKNOWN; 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( reading == 'activity') { FHEM_update( device+'-'+reading, value, true ); Object.keys(FHEM_subscriptions).forEach(function(key) { var parts = key.split( '-', 3 ); if( parts[0] != '#' + device ) return; if( parts[1] != reading ) return; var subscription = FHEM_subscriptions[key]; var accessory = subscription.accessory; var activity = parts[2]; subscription.characteristic.setValue(value==activity?1:0, undefined, 'fromFHEM'); } ); continue; } else if(accessory.mappings.rgb && reading == accessory.mappings.rgb.reading) { var hsv = FHEM_rgb2hsv(value); var hue = parseInt( hsv[0] * 360 ); 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; } else if(accessory.mappings.colormode) { //FIXME: add colormode ct if( reading == 'xy') { var xy = value.split(','); var rgb = FHEM_xyY2rgb(xy[0], xy[1] , 1); var hsv = FHEM_rgb2hsv(rgb); var hue = parseInt( hsv[0] * 360 ); var sat = parseInt( hsv[1] * 100 ); var bri = parseInt( hsv[2] * 100 ); FHEM_update( device+'-hue', hue ); FHEM_update( device+'-sat', sat ); FHEM_update( device+'-bri', bri ); } FHEM_update( device+'-'+reading, value, false ); continue; } value = accessory.reading2homekit(reading, value); FHEM_update( device+'-'+reading, value ); } else { } } input = input.substr(FHEM_longpollOffset); FHEM_longpollOffset = 0; connection.disconnects = 0; } ).on( 'end', function() { FHEM_longpoll_running[connection.base_url] = false; connection.disconnects++; var timeout = 500 * connection.disconnects - 300; if( timeout > 30000 ) timeout = 30000; console.log( "longpoll ended, reconnect in: " + timeout + "msec" ); setTimeout( function(){FHEM_startLongpoll(connection)}, timeout ); } ).on( 'error', function(err) { FHEM_longpoll_running[connection.base_url] = false; connection.disconnects++; var timeout = 5000 * connection.disconnects; if( timeout > 30000 ) timeout = 30000; console.log( "longpoll error: " + err + ", retry in: " + timeout + "msec" ); setTimeout( function(){FHEM_startLongpoll(connection)}, timeout ); } ); } function FHEMPlatform(log, config) { 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 ); } function FHEM_sortByKey(array, key) { return array.sort( function(a, b) { var x = a[key]; var y = b[key]; return ((x < y) ? -1 : ((x > y) ? 1 : 0)); }); } function FHEM_rgb2hex(r,g,b) { if( g == undefined ) return Number(0x1000000 + r[0]*0x10000 + r[1]*0x100 + r[2]).toString(16).substring(1); return Number(0x1000000 + r*0x10000 + g*0x100 + b).toString(16).substring(1); } function FHEM_hsv2rgb(h,s,v) { var r = 0.0; var g = 0.0; var b = 0.0; if( s == 0 ) { r = v; g = v; b = v; } else { var i = Math.floor( h * 6.0 ); var f = ( h * 6.0 ) - i; var p = v * ( 1.0 - s ); var q = v * ( 1.0 - s * f ); var t = v * ( 1.0 - s * ( 1.0 - f ) ); i = i % 6; if( i == 0 ) { r = v; g = t; b = p; } else if( i == 1 ) { r = q; g = v; b = p; } else if( i == 2 ) { r = p; g = v; b = t; } else if( i == 3 ) { r = p; g = q; b = v; } else if( i == 4 ) { r = t; g = p; b = v; } else if( i == 5 ) { r = v; g = p; b = q; } } return FHEM_rgb2hex( Math.round(r*255),Math.round(g*255),Math.round(b*255) ); } function FHEM_ct2rgb(ct) { // calculation from http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code // adjusted by 1000K var temp = (1000000/ct)/100 + 10; var r = 0; var g = 0; var b = 0; r = 255; if( temp > 66 ) r = 329.698727446 * Math.pow(temp - 60, -0.1332047592); if( r < 0 ) r = 0; if( r > 255 ) r = 255; if( temp <= 66 ) g = 99.4708025861 * Math.log(temp) - 161.1195681661; else g = 288.1221695283 * Math.pow(temp - 60, -0.0755148492); if( g < 0 ) g = 0; if( g > 255 ); g = 255; b = 255; if( temp <= 19 ) b = 0; if( temp < 66 ) b = 138.5177312231 * log(temp-10) - 305.0447927307; if( b < 0 ) b = 0; if( b > 255 ) b = 255; return FHEM_rgb2hex( Math.round(r*255),Math.round(g*255),Math.round(b*255) ); } function FHEM_xyY2rgb(x,y,Y) { // calculation from http://www.brucelindbloom.com/index.html var r = 0; var g = 0; var b = 0; if( y > 0 ) { var X = x * Y / y; var Z = (1 - x - y)*Y / y; if( X > 1 || Y > 1 || Z > 1 ) { var f = Math.max(X,Y,Z); X /= f; Y /= f; Z /= f; } r = 0.7982 * X + 0.3389 * Y - 0.1371 * Z; g = -0.5918 * X + 1.5512 * Y + 0.0406 * Z; b = 0.0008 * X + 0.0239 * Y + 0.9753 * Z; if( r > 1 || g > 1 || b > 1 ) { var f = Math.max(r,g,b); r /= f; g /= f; b /= f; } r *= 255; g *= 255; b *= 255; } return FHEM_rgb2hex( Math.round(r*255),Math.round(g*255),Math.round(b*255) ); } function FHEM_rgb2hsv(r,g,b){ if( r == undefined ) return; if( g == undefined ) { var str = r; r = parseInt( str.substr(0,2), 16 ); g = parseInt( str.substr(2,2), 16 ); b = parseInt( str.substr(4,2), 16 ); } var M = Math.max( r, g, b ); var m = Math.min( r, g, b ); var c = M - m; var h, s, v; if( c == 0 ) { h = 0; } else if( M == r ) { h = ( 60 * ( ( g - b ) / c ) % 360 ) / 360; } else if( M == g ) { h = ( 60 * ( ( b - r ) / c ) + 120 ) / 360; } else if( M == b ) { h = ( 60 * ( ( r - g ) / c ) + 240 ) / 360; } if( M == 0 ) { s = 0; } else { s = c / M; } v = M/255; return [h,s,v]; } function FHEM_execute(log,connection,cmd,callback) { var url = encodeURI( connection.base_url + "/fhem?cmd=" + cmd + "&XHR=1"); log( ' executing: ' + url ); connection.request .get( { url: url, gzip: true }, function(err, response, result) { if( !err && response.statusCode == 200 ) { result = result.replace(/[\r\n]/g, ""); if( callback ) callback( result ); } else { log("There was a problem connecting to FHEM ("+ url +")."); if( response ) log( " " + response.statusCode + ": " + response.statusMessage ); } } ) .on( 'error', function(err) { log("There was a problem connecting to FHEM ("+ url +"):"+ err); } ); } FHEMPlatform.prototype = { execute: function(cmd,callback) {FHEM_execute(this.log, this.connection, cmd, callback)}, checkAndSetGenericDeviceType: function() { this.log("Checking genericDeviceType..."); var cmd = '{AttrVal("global","userattr","")}'; this.execute( cmd, function(result) { //if( result == undefined ) //result = ""; if( !result.match(/(^| )genericDeviceType\b/) ) { //FIXME: use addToAttrList var cmd = 'attr global userattr ' + result + ' genericDeviceType:ignore,switch,outlet,light,blind,thermostat,garage,window,lock'; this.execute( cmd, function(result) { console.log( result ); console.log( 'genericDeviceType attribute was not known. please restart homebridge.' ); process.exit(0); } ); } }.bind(this) ); }, accessories: function(callback) { //this.checkAndSetGenericDeviceType(); this.log("Fetching FHEM switchable devices..."); var foundAccessories = []; // 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( this.connection.base_url + "/fhem?cmd=" + cmd + "&XHR=1"); this.log( 'fetching: ' + url ); asyncCalls++; this.connection.request.get( { url: url, json: true, gzip: true }, function(err, response, json) { if( !err && response.statusCode == 200 ) { this.log( 'got: ' + json['totalResultsReturned'] + ' results' ); //this.log("got json: " + util.inspect(json) ); if( json['totalResultsReturned'] ) { var sArray=FHEM_sortByKey(json['Results'],"Name"); sArray.map(function(s) { var accessory; if( FHEM_isPublished(s.Internals.NAME) ) this.log( s.Internals.NAME + ' is already published'); else if( s.Attributes.disable == 1 ) { this.log( s.Internals.NAME + ' is disabled'); } else if( s.Internals.TYPE == 'structure' ) { this.log( 'ignoring structure ' + s.Internals.NAME ); } else if( s.Attributes.genericDisplayType || s.Attributes.genericDeviceType ) { accessory = new FHEMAccessory(this.log, this.connection, s); } else if( s.PossibleSets.match(/(^| )on\b/) && s.PossibleSets.match(/(^| )off\b/) ) { accessory = new FHEMAccessory(this.log, this.connection, s); } else if( s.Attributes.subType == 'thermostat' || s.Attributes.subType == 'blindActuator' || s.Attributes.subType == 'threeStateSensor' ) { accessory = new FHEMAccessory(this.log, this.connection, s); } else if( s.Attributes.model == 'HM-SEC-WIN' ) { accessory = new FHEMAccessory(this.log, this.connection, s); } else if( s.Attributes.model && s.Attributes.model.match(/^HM-SEC-KEY/) ) { accessory = new FHEMAccessory(this.log, this.connection, s); } else if( s.Internals.TYPE == 'PRESENCE' || s.Internals.TYPE == 'ROOMMATE' ) { accessory = new FHEMAccessory(this.log, this.connection, s); } else if( s.Internals.TYPE == 'SONOSPLAYER' ) { accessory = new FHEMAccessory(this.log, this.connection, s); } else if( s.Readings.temperature ) { accessory = new FHEMAccessory(this.log, this.connection, s); } else if( s.Readings.humidity ) { accessory = new FHEMAccessory(this.log, this.connection, s); } else if( s.Readings.voc ) { accessory = new FHEMAccessory(this.log, this.connection, s); } else if( s.Internals.TYPE == 'harmony' ) { accessory = new FHEMAccessory(this.log, this.connection, s); } else { this.log( 'ignoring ' + s.Internals.NAME + ' (' + s.Internals.TYPE + ')' ); } if( accessory && Object.getOwnPropertyNames(accessory).length ) foundAccessories.push(accessory); }.bind(this) ); } callback(foundAccessories); //callbackLater(); } else { this.log("There was a problem connecting to FHEM (1)."); if( response ) this.log( " " + response.statusCode + ": " + response.statusMessage ); } }.bind(this) ); } } function 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 ) { log( s.Internals.NAME + ' is disabled'); return null; } else if( s.Internals.TYPE == 'structure' ) { log( 'ignoring ' + s.Internals.NAME + ' (' + s.Internals.TYPE + ')' ); return null; } var genericType = s.Attributes.genericDeviceType; if( !genericType ) genericType = s.Attributes.genericDisplayType; if( genericType == 'ignore' ) { log( 'ignoring ' + s.Internals.NAME ); return null; } this.mappings = {}; var match; if( match = s.PossibleSets.match(/(^| )pct\b/) ) { 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(/(^| )hue[^\b\s]*(,(\d+)?)+\b/) ) { s.isLight = true; var max = 360; if( match[3] != undefined ) max = match[3]; this.mappings.hue = { reading: 'hue', cmd: 'hue', min: 0, max: max }; } if( match = s.PossibleSets.match(/(^| )sat[^\b\s]*(,(\d+)?)+\b/) ) { s.isLight = true; var max = 100; if( match[3] != undefined ) max = match[3]; this.mappings.sat = { reading: 'sat', cmd: 'sat', min: 0, max: max }; } if( s.Readings.colormode ) this.mappings.colormode = { reading: 'colormode' }; if( s.Readings.xy ) this.mappings.xy = { reading: 'xy' }; //FIXME: add ct/colortemperature if( s.PossibleSets.match(/(^| )rgb\b/) ) { s.isLight = true; this.mappings.rgb = { reading: 'rgb', cmd: 'rgb' }; if( s.Internals.TYPE == 'SWAP_0000002200000003' ) this.mappings.rgb = { reading: '0B-RGBlevel', cmd: 'rgb' }; } else if( s.PossibleSets.match(/(^| )RGB\b/) ) { 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.Readings.humidity ) this.mappings.humidity = { reading: 'humidity' }; if( s.Readings.luminosity ) this.mappings.light = { reading: 'luminosity' }; if( s.Readings.voc ) this.mappings.airquality = { reading: 'voc' }; if( s.Readings.motor ) this.mappings.motor = { reading: 'motor' }; if( s.Readings.battery ) this.mappings.battery = { reading: 'battery' }; if( s.Readings.direction ) this.mappings.direction = { reading: 'direction' }; if( s.Readings['D-firmware'] ) this.mappings.firmware = { reading: 'D-firmware' }; if( genericType == 'switch' ) s.isSwitch = true; else if( genericType == 'outlet' ) s.isOutlet = 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; if( s.PossibleSets.match(/[\^ ]position\b/) ) this.mappings.blind = { reading: 'position', cmd: 'position' }; else this.mappings.blind = { reading: 'pct', cmd: 'pct' }; } else if( genericType == 'window' || s.Attributes.model == 'HM-SEC-WIN' ) this.mappings.window = { reading: 'level', cmd: 'level' }; else if( genericType == 'lock' || ( s.Attributes.model && s.Attributes.model.match(/^HM-SEC-KEY/ ) ) ) { this.mappings.lock = { reading: 'lock', cmdLock: 'lock', cmdUnlock: 'unlock', cmdOpen: 'open' }; if( s.Internals.TYPE == 'dummy' ) this.mappings.lock = { reading: 'lock', cmdLock: 'lock locked', cmdUnlock: 'lock unlocked', cmdOpen: 'open' }; } else if( genericType == 'thermostat' || s.Attributes.subType == 'thermostat' ) s.isThermostat = true; else if( s.Internals.TYPE == 'CUL_FHTTK' ) this.mappings.contact = { reading: 'Window' }; else if( s.Internals.TYPE == 'MAX' && s.Internals.type == 'ShutterContact' ) this.mappings.contact = { reading: 'state' }; else if( s.Attributes.subType == 'threeStateSensor' ) this.mappings.contact = { reading: 'contact' }; else if( s.Internals.TYPE == 'PRESENCE' ) this.mappings.occupancy = { reading: 'state' }; else if( s.Internals.TYPE == 'ROOMMATE' ) this.mappings.occupancy = { reading: 'presence' }; else if( s.Attributes.model == 'fs20di' ) s.isLight = true; //if( s.PossibleSets.match(/(^| )desired-temp\b/) ) { if( match = s.PossibleSets.match(/(^| )desired-temp(:[^\d]*([^\$ ]*))?/) ) { this.mappings.thermostat = { reading: 'desired-temp', cmd: 'desired-temp' }; if( s.Readings.controlMode ) this.mappings.thermostat_mode = { reading: 'controlMode', cmd: 'controlMode' }; if( match[3] ) { var values = match[3].split(','); this.mappings.thermostat.min = parseFloat(values[0]); this.mappings.thermostat.max = parseFloat(values[values.length-1]); this.mappings.thermostat.step = values[1] - values[0]; } //} else if( s.PossibleSets.match(/(^| )desiredTemperature\b/) ) { } else if( match = s.PossibleSets.match(/(^| )desiredTemperature(:[^\d]*([^\$ ]*))?/) ) { this.mappings.thermostat = { reading: 'desiredTemperature', cmd: 'desiredTemperature' }; if( s.Readings.mode ) this.mappings.thermostat_mode = { reading: 'mode', cmd: 'desiredTemperature' }; if( match[3] ) { var values = match[3].split(','); this.mappings.thermostat.min = values[0]; this.mappings.thermostat.max = values[values.length-2]; this.mappings.thermostat.step = values[1] - values[0]; } } else if( s.isThermostat ) { s.isThermostat = false; delete this.mappings.thermostat; log( s.Internals.NAME + ' is NOT a thermostat. set for target temperature missing' ); } if( s.Internals.TYPE == 'SONOSPLAYER' ) //FIXME: use sets [Pp]lay/[Pp]ause/[Ss]top this.mappings.onOff = { reading: 'transportState', cmdOn: 'play', cmdOff: 'pause' }; else if( s.Internals.TYPE == 'harmony' ) { if( s.Internals.id != undefined ) { if( s.Attributes.genericDeviceType ) this.mappings.onOff = { reading: 'power', cmdOn: 'on', cmdOff: 'off' }; else return null; } else this.mappings.onOff = { reading: 'activity', cmdOn: 'activity', cmdOff: 'off' }; } else if( s.PossibleSets.match(/(^| )on\b/) && s.PossibleSets.match(/(^| )off\b/) ) { this.mappings.onOff = { reading: 'state', cmdOn: 'on', cmdOff: 'off' }; if( !s.Readings.state ) delete this.mappings.onOff.reading; } 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 + ';' + this.mappings.thermostat.min + '-' + this.mappings.thermostat.max + ':' + this.mappings.thermostat.step +']' ); else if( this.mappings.contact ) log( s.Internals.NAME + ' is contact sensor [' + this.mappings.contact.reading +']' ); else if( this.mappings.occupancy ) log( s.Internals.NAME + ' is occupancy sensor' ); else if( this.mappings.rgb ) 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 ) log( s.Internals.NAME + ' is dimable [0-'+ s.pctMax +']' ); else if( s.isLight ) log( s.Internals.NAME + ' is light' ); else if( s.isOutlet ) log( s.Internals.NAME + ' is outlet' ); else if( this.mappings.onOff || s.isSwitch ) log( s.Internals.NAME + ' is switchable' ); else if( !this.mappings ) return {}; if( this.mappings.onOff ) log( s.Internals.NAME + ' has onOff [' + this.mappings.onOff.reading + ';' + this.mappings.onOff.cmdOn +',' + this.mappings.onOff.cmdOff + ']' ); if( this.mappings.hue ) log( s.Internals.NAME + ' has hue [' + this.mappings.hue.reading + ';0-' + this.mappings.hue.max +']' ); if( this.mappings.sat ) log( s.Internals.NAME + ' has sat [' + this.mappings.sat.reading + ';0-' + this.mappings.sat.max +']' ); if( this.mappings.colormode ) log( s.Internals.NAME + ' has colormode [' + this.mappings.colormode.reading +']' ); if( this.mappings.xy ) log( s.Internals.NAME + ' has xy [' + this.mappings.xy.reading +']' ); if( this.mappings.thermostat_mode ) log( s.Internals.NAME + ' has thermostat mode ['+ this.mappings.thermostat_mode.reading + ';' + this.mappings.thermostat_mode.cmd +']' ); if( this.mappings.temperature ) log( s.Internals.NAME + ' has temperature ['+ this.mappings.temperature.reading +']' ); if( this.mappings.humidity ) log( s.Internals.NAME + ' has humidity ['+ this.mappings.humidity.reading +']' ); if( this.mappings.light ) log( s.Internals.NAME + ' has light ['+ this.mappings.light.reading +']' ); if( this.mappings.airquality ) log( s.Internals.NAME + ' has voc ['+ this.mappings.airquality.reading +']' ); if( this.mappings.motor ) log( s.Internals.NAME + ' has motor ['+ this.mappings.motor.reading +']' ); if( this.mappings.battery ) log( s.Internals.NAME + ' has battery ['+ this.mappings.battery.reading +']' ); if( this.mappings.direction ) log( s.Internals.NAME + ' has direction ['+ this.mappings.direction.reading +']' ); if( this.mappings.firmware ) log( s.Internals.NAME + ' has firmware ['+ this.mappings.firmware.reading +']' ); if( this.mappings.volume ) log( s.Internals.NAME + ' has volume ['+ this.mappings.volume.reading + ':' + (this.mappings.volume.nocache ? 'not cached' : 'cached' ) +']' ); //log( util.inspect(s) ); // device info 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.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' ) { this.serial = this.type + '.' + s.Internals.DEF; if( s.Attributes.serialNr ) this.serial = s.Attributes.serialNr; else if( s.Readings['D-serialNr'] && s.Readings['D-serialNr'].Value ) this.serial = s.Readings['D-serialNr'].Value; } else if( this.type == 'CUL_WS' ) this.serial = this.type + '.' + s.Internals.DEF; else if( this.type == 'FS20' ) this.serial = this.type + '.' + s.Internals.DEF; else if( this.type == 'IT' ) this.serial = this.type + '.' + s.Internals.DEF; else if( this.type == 'HUEDevice' ) this.serial = s.Internals.uniqueid; else if( this.type == 'SONOSPLAYER' ) this.serial = s.Internals.UDN; else if( this.type == 'EnOcean' ) this.serial = this.type + '.' + s.Internals.DEF; this.uuid_base = this.serial; this.hasDim = s.hasDim; this.pctMax = s.pctMax; this.isLight = s.isLight; this.isSwitch = s.isSwitch; this.isOutlet = s.isOutlet; //log( util.inspect(s.Readings) ); if( this.mappings.blind || this.mappings.door || this.mappings.garage || this.mappings.window || this.mappings.thermostat ) delete this.mappings.onOff; Object.keys(this.mappings).forEach(function(key) { var reading = this.mappings[key].reading; if( s.Readings[reading] && s.Readings[reading].Value ) { var value = s.Readings[reading].Value; value = this.reading2homekit(reading, value); if( value != undefined ) { var inform_id = this.device +'-'+ reading; this.mappings[key].informId = inform_id; if( !this.mappings[key].nocache ) FHEM_cached[inform_id] = value; } } }.bind(this) ); this.log = log; this.connection = connection; } 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( value == undefined ) return undefined; 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 == 'position' ) { value = parseInt( value ); } else if(reading == 'motor') { if( value.match(/^up/)) value = Characteristic.PositionState.INCREASING; else if( value.match(/^down/)) value = Characteristic.PositionState.DECREASING; else value = Characteristic.PositionState.STOPPED; } else if(reading == 'controlMode') { if( value.match(/^auto/)) value = Characteristic.TargetHeatingCoolingState.AUTO; else if( value.match(/^manu/)) value = Characteristic.TargetHeatingCoolingState.HEAT; else value = Characteristic.TargetHeatingCoolingState.OFF; } else if(reading == 'mode') { if( value.match(/^auto/)) value = Characteristic.TargetHeatingCoolingState.AUTO; else value = Characteristic.TargetHeatingCoolingState.HEAT; } else if(reading == 'direction') { if( value.match(/^opening/)) value = PositionState.INCREASING; else if( value.match(/^closing/)) value = Characteristic.PositionState.DECREASING; else value = Characteristic.PositionState.STOPPED; } 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 = Characteristic.ContactSensorState.CONTACT_DETECTED; else value = Characteristic.ContactSensorState.CONTACT_NOT_DETECTED; } else if( reading == 'Window' ) { if( value.match( /^Closed/ ) ) value = Characteristic.ContactSensorState.CONTACT_DETECTED; else value = Characteristic.ContactSensorState.CONTACT_NOT_DETECTED; } else if( reading == 'lock' ) { if( value.match( /uncertain/ ) ) value = Characteristic.LockCurrentState.UNKNOWN; else if( value.match( /^locked/ ) ) value = Characteristic.LockCurrentState.SECURED; else value = Characteristic.LockCurrentState.UNSECURED; } else if( reading == 'temperature' || reading == 'measured-temp' || reading == 'desired-temp' || reading == 'desiredTemperature' ) { value = parseFloat( value ); if( this.mappings.thermostat && reading == this.mappings.thermostat.reading ) { if( value < this.mappings.thermostat.min ) value = this.mappings.thermostat.min; else if( value > this.mappings.thermostat.max ) value = this.mappings.thermostat.min; value = Math.round(value / this.mappings.thermostat.step) * this.mappings.thermostat.step; } } else if( reading == 'humidity' ) { value = parseInt( value ); } else if( reading == 'luminosity' ) { value = parseFloat( value ) / 0.265; } else if( reading == 'voc' ) { value = parseInt( value ); if( value > 1500 ) 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 == 'battery' ) { if( value == 'ok' ) value = Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; else value = Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else if( reading == 'presence' ) { if( value == 'present' ) value = Characteristic.OccupancyDetected.OCCUPANCY_DETECTED; else value = Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED; } else if( reading == 'state' ) { if( value.match(/^set-/ ) ) return undefined; if( this.event_map != undefined ) { var mapped = this.event_map[value]; if( mapped != undefined ) value = mapped; } if( value == 'off' ) value = 0; else if( value == 'opened' ) value = Characteristic.ContactSensorState.CONTACT_NOT_DETECTED; else if( value == 'closed' ) value = Characteristic.ContactSensorState.CONTACT_DETECTED; else if( value == 'present' ) value = Characteristic.OccupancyDetected.OCCUPANCY_DETECTED; else if( value == 'absent' ) value = Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED; else if( value == 'locked' ) value = Characteristic.LockCurrentState.SECURED; else if( value == 'unlocked' ) value = Characteristic.LockCurrentState.UNSECURED; else if( value == '000000' ) value = 0; else if( value.match( /^[A-D]0$/ ) ) //FIXME: not necessary any more. 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); this.delayed[c] = setTimeout( function(){clearTimeout(this.delayed[c]); this.command(c,value);}.bind(this), 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 == 'targetMode' ) { var set = this.mappings.thermostat_mode.cmd; if( value == Characteristic.TargetHeatingCoolingState.OFF ) { value = 'off' if( this.mappings.thermostat_mode.cmd == 'controlMode' ) set = 'desired-temp'; } else if( value == Characteristic.TargetHeatingCoolingState.AUTO ) { value = 'auto' }else { if( this.mappings.thermostat_mode == 'controlMode' ) value = 'manu'; else { value = FHEM_cached[this.mappings.thermostat.informId]; set = 'desired-temp'; } } cmd = "set " + this.device + " " + set + " " + value; } else if( c == 'targetPosition' ) { if( this.mappings.window ) { if( value == 0 ) 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 { this.log(this.name + " Unhandled command! cmd=" + c + ", value=" + value); return; } this.execute(cmd); }, execute: function(cmd,callback) {FHEM_execute(this.log, this.connection, cmd, callback)}, 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( undefined, 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+'","")}'; this.execute( cmd, function(result) { value = result.replace(/[\r\n]/g, ""); this.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( /uncertain/ ) ) value = Characteristic.LockCurrentState.UNKNOWN; else if( value.match( /^locked/ ) ) value = Characteristic.LockCurrentState.SECURED; else value = Characteristic.LockCurrentState.UNSECURED; } else if(reading == 'hue' && query_reading == this.mappings.rgb) { //FHEM_update( this.device+'-'+query_reading, value ); value = parseInt( FHEM_rgb2hsv(value)[0] * 360 ); } else if(reading == 'sat' && query_reading == this.mappings.rgb) { //FHEM_update( this.device+'-'+query_reading, value ); value = parseInt( FHEM_rgb2hsv(value)[1] * 100 ); } else if(reading == 'bri' && query_reading == this.mappings.rgb) { //FHEM_update( this.device+'-'+query_reading, value ); value = parseInt( FHEM_rgb2hsv(value)[2] * 100 ); } } else { value = this.reading2homekit(reading, value); } this.log(" mapped: " + value); FHEM_update( this.device + '-' + reading, value, true ); if( callback != undefined ) { if( value == undefined ) callback(1); else callback(undefined, value); } return value ; }.bind(this) ); }, createDeviceService: function(subtype) { //var name = this.alias + ' (' + this.name + ')'; var name = this.alias; if( subtype ) //name = subtype + ' (' + this.name + ')'; name = subtype + ' (' + this.alias + ')'; if( this.isSwitch ) { this.log(" switch service for " + this.name) return new Service.Switch(name); } else if( this.isOutlet ) { this.log(" outlet service for " + this.name) return new Service.Outlet(name); } else if( this.mappings.garage ) { this.log(" garage door opener service for " + this.name) return new Service.GarageDoorOpener(name); } else if( this.mappings.lock ) { this.log(" lock mechanism service for " + this.name) return new Service.LockMechanism(name); } else if( this.mappings.window ) { this.log(" window service for " + this.name) return new Service.Window(name); } else if( this.mappings.blind ) { this.log(" window covering service for " + this.name) return new Service.WindowCovering(name); } else if( this.mappings.thermostat ) { this.log(" thermostat service for " + this.name) return new Service.Thermostat(name); } else if( this.mappings.contact ) { this.log(" contact sensor service for " + this.name) return new Service.ContactSensor(name); } else if( this.mappings.occupancy ) { 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 ) { this.log(" lightbulb service for " + this.name) return new Service.Lightbulb(name); } else if( this.mappings.temperature ) { this.log(" temperature sensor service for " + this.name) return new Service.TemperatureSensor(name); } else if( this.mappings.humidity ) { this.log(" humidity sensor service for " + this.name) return new Service.HumiditySensor(name); } else if( this.mappings.light ) { this.log(" light sensor service for " + this.name) return new Service.LightSensor(name); } else if( this.mappings.airquality ) { this.log(" air quality sensor service for " + this.name) return new Service.AirQualitySensor(name); } this.log(" switch service for " + this.name + ' (' + subtype + ')' ) return new Service.Switch(name, subtype); }, identify: function(callback) { this.log('['+this.name+'] identify requested!'); if( match = this.PossibleSets.match(/(^| )toggle\b/) ) { this.command( 'identify' ); } callback(); }, getServices: function() { var services = []; this.log("creating services for " + this.name) this.log(" information service for " + this.name) var informationService = new Service.AccessoryInformation(); services.push( informationService ); informationService .setCharacteristic(Characteristic.Manufacturer, "FHEM:"+this.type) .setCharacteristic(Characteristic.Model, "FHEM:"+ (this.model ? this.model : '') ) .setCharacteristic(Characteristic.SerialNumber, this.serial ? this.serial : ''); if( this.mappings.firmware ) { this.log(" firmware revision characteristic for " + this.name) var characteristic = informationService.getCharacteristic(Characteristic.FirmwareRevision) || informationService.addCharacteristic(Characteristic.FirmwareRevision); FHEM_subscribe(characteristic, this.mappings.firmware.informId, this); characteristic.value = FHEM_cached[this.mappings.firmware.informId]; characteristic .on('get', function(callback) { if( this.mappings.firmware ) this.query(this.mappings.firmware.reading, callback); }.bind(this) ); } // FIXME: allow multiple switch characteristics also for other types. check if this.mappings.onOff an array. if( this.type == 'harmony' && this.mappings.onOff.reading == 'activity' ) { FHEM_subscribe(undefined, this.mappings.onOff.informId, this); var match; if( match = this.PossibleSets.match(/(^| )activity:([^\s]*)/) ) { var activities = match[2].split(','); for( var i = 0; i < activities.length; i++ ) { var activity = activities[i]; var controlService = this.createDeviceService(activity); services.push( controlService ); this.log(" on characteristic for " + this.name + ' ' + activity); var characteristic = controlService.getCharacteristic(Characteristic.On); FHEM_subscribe(characteristic, '#' + this.device + '-' + this.mappings.onOff.reading + '-' + activity, this); characteristic.displayName = activity; characteristic.value = (FHEM_cached[this.mappings.onOff.informId]==activity?1:0); characteristic .on('set', function(activity, value, callback, context) { if( context !== 'fromFHEM' ) this.command( 'set', value == 0 ? this.mappings.onOff.cmdOff : this.mappings.onOff.cmdOn + ' ' + activity ); callback(); }.bind(this, activity) ) .on('get', function(activity, callback) { var result = this.query(this.mappings.onOff.reading); callback( undefined, result==activity?1:0 ); }.bind(this, activity) ); } } return services; } if( this.mappings.xy && this.mappings.colormode ) { FHEM_subscribe(undefined, this.mappings.xy.informId, this); FHEM_subscribe(undefined, this.mappings.colormode.informId, this); //FIXME: add colormode ct if( FHEM_cached[this.mappings.colormode.informId] == 'xy' ) { var value = FHEM_cached[this.mappings.xy.informId]; var xy = value.split(','); var rgb = FHEM_xyY2rgb(xy[0], xy[1] , 1); var hsv = FHEM_rgb2hsv(rgb); var hue = parseInt( hsv[0] * 360 ); var sat = parseInt( hsv[1] * 100 ); var bri = parseInt( hsv[2] * 100 ); //FHEM_update( device+'-'+reading, value, false ); FHEM_update( this.device+'-hue', hue ); FHEM_update( this.device+'-sat', sat ); FHEM_update( this.device+'-bri', bri ); } } var controlService = this.createDeviceService(); services.push( controlService ); if( this.mappings.onOff ) { this.log(" on characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.On); FHEM_subscribe(characteristic, this.mappings.onOff.informId, this); if( FHEM_cached[this.mappings.onOff.informId] != undefined ) characteristic.value = FHEM_cached[this.mappings.onOff.informId]; characteristic .on('set', function(value, callback, context) { if( context !== 'fromFHEM' ) this.command( 'set', value == 0 ? this.mappings.onOff.cmdOff : this.mappings.onOff.cmdOn ); callback(); }.bind(this) ) .on('get', function(callback) { this.query(this.mappings.onOff.reading, callback); }.bind(this) ); } if( this.mappings.pct ) { this.log(" brightness characteristic for " + this.name) var characteristic = controlService.addCharacteristic(Characteristic.Brightness); FHEM_subscribe(characteristic, this.mappings.pct.informId, this); if( FHEM_cached[this.mappings.pct.informId] != undefined ) characteristic.value = FHEM_cached[this.mappings.pct.informId]; characteristic .on('set', function(value, callback, context) { if( context !== 'fromFHEM' ) this.command('pct', value); callback(); }.bind(this) ) .on('get', function(callback) { this.query(this.mappings.pct.reading, callback); }.bind(this) ); } else if( this.hasDim ) { this.log(" fake brightness characteristic for " + this.name) var characteristic = controlService.addCharacteristic(Characteristic.Brightness); FHEM_subscribe(characteristic, this.name+'-pct', this); characteristic.value = 0; characteristic.maximumValue = this.pctMax; characteristic .on('set', function(value, callback, context) { if( context !== 'fromFHEM' ) this.delayed('dim', value); callback(); }.bind(this) ) .on('get', function(callback) { this.query('pct', callback); }.bind(this) ); } if( this.mappings.hue ) { this.log(" hue characteristic for " + this.name) var characteristic = controlService.addCharacteristic(Characteristic.Hue); FHEM_subscribe(characteristic, this.mappings.hue.informId, this); if( FHEM_cached[this.mappings.hue.informId] != undefined ) characteristic.value = FHEM_cached[this.mappings.hue.informId]; characteristic .on('set', function(value, callback, context) { if( context !== 'fromFHEM' ) this.command('hue', value); callback(); }.bind(this) ) .on('get', function(callback) { this.query(this.mappings.hue.reading, callback); }.bind(this) ); } else if( this.mappings.rgb ) { this.log(" fake hue characteristic for " + this.name) var characteristic = controlService.addCharacteristic(Characteristic.Hue); FHEM_subscribe(characteristic, this.name+'-hue', this); FHEM_subscribe(characteristic, this.mappings.rgb.informId, this); characteristic.value = 0; characteristic .on('set', function(value, callback, context) { if( context !== 'fromFHEM' ) this.command('H-rgb', value); callback(); }.bind(this) ) .on('get', function(callback) { this.query('hue', callback); }.bind(this) ); if( !this.mappings.sat ) { this.log(" fake saturation characteristic for " + this.name) var characteristic = controlService.addCharacteristic(Characteristic.Saturation); FHEM_subscribe(characteristic, this.name+'-sat', this); characteristic.value = 100; characteristic .on('set', function(value, callback, context) { if( context !== 'fromFHEM' ) this.command('S-rgb', value); callback(); }.bind(this) ) .on('get', function(callback) { this.query('sat', callback); }.bind(this) ); } if( !this.mappings.pct ) { this.log(" fake brightness characteristic for " + this.name) var characteristic = controlService.addCharacteristic(Characteristic.Brightness); FHEM_subscribe(characteristic, this.name+'-bri', this); characteristic.value = 0; characteristic .on('set', function(value, callback, context) { if( context !== 'fromFHEM' ) this.command('B-rgb', value); callback(); }.bind(this) ) .on('get', function(callback) { this.query('bri', callback); }.bind(this) ); } } if( this.mappings.sat ) { this.log(" saturation characteristic for " + this.name) var characteristic = controlService.addCharacteristic(Characteristic.Saturation); FHEM_subscribe(characteristic, this.mappings.sat.informId, this); if( FHEM_cached[this.mappings.sat.informId] != undefined ) characteristic.value = FHEM_cached[this.mappings.sat.informId]; characteristic .on('set', function(value, callback, context) { if( context !== 'fromFHEM' ) this.command('sat', value); callback(); }.bind(this) ) .on('get', function(callback) { this.query(this.mappings.sat.reading, callback); }.bind(this) ); } if( this.mappings.volume ) { this.log(" custom volume characteristic for " + this.name) var characteristic = new Characteristic('Volume', '00000027-0000-1000-8000-0026BB765291'); // FIXME!!! controlService.addCharacteristic(characteristic); if( !this.mappings.volume.nocache ) { FHEM_subscribe(characteristic, this.mappings.volume.informId, this); characteristic.value = FHEM_cached[this.mappings.volume.informId]; } else { characteristic.value = 10; } characteristic.setProps({ format: Characteristic.Formats.UINT8, unit: Characteristic.Units.PERCENTAGE, maxValue: 100, minValue: 0, minStep: 1, perms: [Characteristic.Perms.READ, Characteristic.Perms.WRITE, Characteristic.Perms.NOTIFY] }); characteristic.readable = true; characteristic.writable = true; characteristic.supportsEventNotification = true; characteristic .on('set', function(value, callback, context) { if( context !== 'fromFHEM' ) this.delayed('volume', value); callback(); }.bind(this) ) .on('get', function(callback) { this.query(this.mappings.volume.reading, callback); }.bind(this) ); } if( this.mappings.blind ) { this.log(" current position characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.CurrentPosition); var step = 1; FHEM_subscribe(characteristic, this.mappings.blind.informId, this); characteristic.value = Math.round(FHEM_cached[this.mappings.blind.informId] / step) * step; characteristic .on('get', function(callback) { this.query(this.mappings.blind.reading, callback); }.bind(this) ); this.log(" target position characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.TargetPosition); characteristic.setProps( { minStep: step, } ); characteristic.value = FHEM_cached[this.mappings.blind.informId]; characteristic .on('set', function(value, callback, context) { if( context !== 'fromFHEM' ) this.delayed('targetPosition', value, 1500); callback(); }.bind(this) ) .on('get', function(callback) { this.query(this.mappings.blind.reading, callback); }.bind(this) ); this.log(" position state characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.PositionState); if( this.mappings.motor ) FHEM_subscribe(characteristic, this.mappings.motor.informId, this); characteristic.value = this.mappings.motor?FHEM_cached[this.mappings.motor.informId]:Characteristic.PositionState.STOPPED; characteristic .on('get', function(callback) { if( this.mappings.motor ) this.query(this.mappings.motor.reading, callback); }.bind(this) ); } if( this.mappings.window ) { this.log(" current position characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.CurrentPosition); FHEM_subscribe(characteristic, this.name+'-state', this); FHEM_subscribe(characteristic, this.mappings.window.informId, this); characteristic.value = FHEM_cached[this.mappings.window.informId]; characteristic .on('get', function(callback) { this.query(this.mappings.window.reading, callback); }.bind(this) ); this.log(" target position characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.TargetPosition); characteristic.value = FHEM_cached[this.mappings.window.informId]; characteristic .on('set', function(value, callback, context) { if( context !== 'fromFHEM' ) this.delayed('targetPosition', value, 1500); callback(); }.bind(this) ) .on('get', function(callback) { this.query(this.mappings.window.reading, callback); }.bind(this) ); this.log(" position state characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.PositionState); if( this.mappings.direction ) FHEM_subscribe(characteristic, this.mappings.direction.informId, this); characteristic.value = this.mappings.direction?FHEM_cached[this.mappings.direction.informId]:Characteristic.PositionState.STOPPED; characteristic .on('get', function(callback) { if( this.mappings.direction ) this.query(this.mappings.direction.reading, callback); }.bind(this) ); } if( this.mappings.lock ) { this.log(" lock current state characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.LockCurrentState); //FHEM_subscribe(characteristic, this.name+'-state', this); FHEM_subscribe(characteristic, this.mappings.lock.informId, this); characteristic.value = FHEM_cached[this.mappings.lock.informId]; characteristic .on('get', function(callback) { this.query(this.mappings.lock.reading, callback); }.bind(this) ); this.log(" lock target state characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.LockTargetState); characteristic.value = FHEM_cached[this.mappings.lock.informId]; characteristic .on('set', function(value, callback, context) { if( context !== 'fromFHEM' ) this.command( 'set', value == Characteristic.LockTargetState.UNSECURED ? this.mappings.lock.cmdUnlock : this.mappings.lock.cmdLock ); callback(); }.bind(this) ) .on('get', function(callback) { this.query(this.mappings.lock.reading, callback); }.bind(this) ); if( this.mappings.lock.cmdOpen ) { this.log(" target door state characteristic for " + this.name) var characteristic = controlService.addCharacteristic(Characteristic.TargetDoorState); characteristic.value = Characteristic.TargetDoorState.CLOSED; characteristic .on('set', function(characteristic,value, callback, context) { if( context !== 'fromFHEM' ) { this.command( 'set', this.mappings.lock.cmdOpen ); setTimeout( function(){characteristic.setValue(Characteristic.TargetDoorState.CLOSED, undefined, 'fromFHEM');}, 500 ); } if( callback ) callback(); }.bind(this,characteristic) ) .on('get', function(callback) { callback(undefined,Characteristic.TargetDoorState.CLOSED); }.bind(this) ); } } if( this.mappings.garage ) { 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' ) this.command( 'set', value == 0 ? this.mappings.garage.cmdOpen : this.mappings.garage.cmdClose ); callback(); }.bind(this) ) .on('get', function(callback) { callback(undefined,0); }.bind(this) ); if( 0 ) { this.log(" obstruction detected characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.ObstructionDetected); //FHEM_subscribe(characteristic, this.mappings.direction.informId, this); characteristic.value = 0; characteristic .on('get', function(callback) { callback(undefined,1); }.bind(this) ); } } if( this.mappings.temperature ) { this.log(" temperature characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.CurrentTemperature) || controlService.addCharacteristic(Characteristic.CurrentTemperature); FHEM_subscribe(characteristic, this.mappings.temperature.informId, this); characteristic.value = FHEM_cached[this.mappings.temperature.informId]; characteristic .on('get', function(callback) { this.query(this.mappings.temperature.reading, callback); }.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, this.mappings.humidity.informId, this); characteristic.value = FHEM_cached[this.mappings.humidity.informId]; characteristic .on('get', function(callback) { this.query(this.mappings.humidity.reading, callback); }.bind(this) ); } if( this.mappings.light ) { this.log(" light characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.CurrentAmbientLightLevel) || controlService.addCharacteristic(Characteristic.CurrentAmbientLightLevel); FHEM_subscribe(characteristic, this.mappings.light.informId, this); characteristic.value = FHEM_cached[this.mappings.light.informId]; characteristic .on('get', function(callback) { this.query(this.mappings.light.reading, callback); }.bind(this) ); } if( this.mappings.airquality ) { this.log(" air quality characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.AirQuality) || controlService.addCharacteristic(Characteristic.AirQuality); FHEM_subscribe(characteristic, this.mappings.airquality.informId, this); characteristic.value = FHEM_cached[this.mappings.airquality.informId]; characteristic .on('get', function(callback) { this.query(this.mappings.airquality.reading, callback); }.bind(this) ); } if( this.mappings.battery ) { this.log(" battery status characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.StatusLowBattery) || controlService.addCharacteristic(Characteristic.StatusLowBattery); FHEM_subscribe(characteristic, this.mappings.battery.informId, this); characteristic.value = FHEM_cached[this.mappings.battery.informId]; characteristic .on('get', function(callback) { this.query(this.mappings.battery.reading, callback); }.bind(this) ); } if( this.mappings.thermostat ) { this.log(" target temperature characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.TargetTemperature); FHEM_subscribe(characteristic, this.mappings.thermostat.informId, this); characteristic.value = FHEM_cached[this.mappings.thermostat.informId]; characteristic.setProps( { maxValue: this.mappings.thermostat.max, minValue: this.mappings.thermostat.min, minStep: this.mappings.thermostat.step, } ); characteristic .on('set', function(value, callback, context) { if( context !== 'fromFHEM' ) this.delayed('targetTemperature', value, 1500); callback(); }.bind(this) ) .on('get', function(callback) { this.query(this.mappings.thermostat.reading, callback); }.bind(this) ); if( this.mappings.thermostat_modex ) { this.log(" current mode characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.CurrentHeatingCoolingState); FHEM_subscribe(characteristic, this.mappings.thermostat_mode.informId, this); characteristic.value = FHEM_cached[this.mappings.thermostat_mode.informId]; characteristic .on('get', function(callback) { this.query(this.mappings.thermostat_mode.reading, callback); }.bind(this) ); } if( this.mappings.thermostat_modex ) { this.log(" target mode characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.TargetHeatingCoolingState); FHEM_subscribe(characteristic, this.mappings.thermostat_mode.informId, this); characteristic.value = FHEM_cached[this.mappings.thermostat_mode.informId]; characteristic .on('set', function(value, callback, context) { if( context !== 'fromFHEM' ) this.command('targetMode', value); callback(); }.bind(this) ) .on('get', function(callback) { this.query(this.mappings.thermostat_mode.reading, callback); }.bind(this) ); } } if( this.mappings.contact ) { this.log(" contact sensor characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.ContactSensorState); FHEM_subscribe(characteristic, this.mappings.contact.informId, this); characteristic.value = FHEM_cached[this.mappings.contact.informId]; characteristic .on('get', function(callback) { this.query(this.mappings.contact.reading, callback); }.bind(this) ); if( 1 ) { this.log(" current door state characteristic for " + this.name) var characteristic = controlService.addCharacteristic(Characteristic.CurrentDoorState); characteristic.value = FHEM_cached[this.mappings.contact.informId]==Characteristic.ContactSensorState.CONTACT_DETECTED?Characteristic.CurrentDoorState.CLOSED:Characteristic.CurrentDoorState.OPEN; characteristic .on('get', function(callback) { callback(undefined, FHEM_cached[this.mappings.contact.informId]==Characteristic.ContactSensorState.CONTACT_DETECTED?Characteristic.CurrentDoorState.CLOSED:Characteristic.CurrentDoorState.OPEN); }.bind(this) ); } } if( this.mappings.occupancy ) { this.log(" occupancy detected characteristic for " + this.name) var characteristic = controlService.getCharacteristic(Characteristic.OccupancyDetected); FHEM_subscribe(characteristic, this.mappings.occupancy.informId, this); characteristic.value = FHEM_cached[this.mappings.occupancy.informId]; characteristic .on('get', function(callback) { this.query(this.mappings.occupancy.reading, callback); }.bind(this) ); } return services; } }; //module.exports.accessory = FHEMAccessory; module.exports.platform = FHEMPlatform; //http server for debugging var http = require('http'); const FHEMdebug_PORT=8081; function FHEMdebug_handleRequest(request, response){ //console.log( request ); if( request.url == "/cached" ) { response.write( "home

" ); if( FHEM_lastEventTime ) var keys = Object.keys(FHEM_lastEventTime); for( var i = 0; i < keys.length; i++ ) response.write( "FHEM_lastEventTime " + keys[i] + ": "+ new Date(FHEM_lastEventTime[keys[i]]) +"
" ); response.write( "
" ); response.end( "cached: " + util.inspect(FHEM_cached).replace(/\n/g, '
') ); } else if( request.url == "/subscriptions" ) { response.write( "home

" ); response.end( "subscriptions: " + util.inspect(FHEM_subscriptions, {depth: 4}).replace(/\n/g, '
') ); } else response.end( "cached
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); });