From 0fb6fc736e06fd2fae7fe2e3b9cc9a2b59e0812b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andre=20Schr=C3=B6ter?= Date: Mon, 27 Jul 2015 09:38:53 +0200 Subject: [PATCH] added FHEM.js platform shim for the FHEM home automation system. supports switches, lightbulbs (dimmers and rgb/colored lights), homematic window blinds, max and homematic thermostats, homematic door&window sensors, PRESENCE and sonos (including volume) the shim is bidirectional and uses longpoll to push fhem events back to homekit. fhem events are cached so homekit querys for known values will not cause a roundtrip to fhem more: http://forum.fhem.de/index.php/topic,32652.msg314166.html#msg314166 --- platforms/FHEM.js | 1321 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1321 insertions(+) create mode 100644 platforms/FHEM.js diff --git a/platforms/FHEM.js b/platforms/FHEM.js new file mode 100644 index 0000000..9035b8f --- /dev/null +++ b/platforms/FHEM.js @@ -0,0 +1,1321 @@ +// FHEM Platform Shim for 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" +// } +// ], +// +// 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 util = require('util'); + + +// cached readings from longpoll & query +var FHEM_cached = {}; + + +// subscriptions to fhem longpoll evens +var FHEM_subscriptions = {}; +function +FHEM_subscribe(characteristic, inform_id, accessory) { + FHEM_subscriptions[inform_id] = { 'characteristic': characteristic, 'accessory': accessory }; + //FHEM_subscriptions[inform_id] = characteristic; +} +function +FHEM_update(inform_id, value, no_update) { + var subscription = FHEM_subscriptions[inform_id]; + if( subscription != undefined ) { + FHEM_cached[inform_id] = value; + console.log(" caching: " + inform_id + ": " + value + " as " + typeof(value) ); + + if( !no_update ) + subscription.characteristic.updateValue(value, null); + } +} + + +var FHEM_longpoll_running = false; +//FIXME: add reconnect, force reconnect on xxx bytes received, add filter, add since +function FHEM_startLongpoll(connection) { + if( FHEM_longpoll_running ) + return; + FHEM_longpoll_running = true; + + var filter = ".*"; + var since = "null"; + var query = "/fhem.pl?XHR=1"+ + "&inform=type=status;filter="+filter+";since="+since+";fmt=JSON"+ + "×tamp="+new Date().getTime(); + + 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; + 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) ); + + //FIXME: create reading2value + //FIXME: redirect device reading to homekit reading: rgb->hue,... + var subscription = FHEM_subscriptions[d[0]]; + if( subscription != undefined ) { +//console.log( "Rcvd: "+(l.length>132 ? l.substring(0,132)+"...("+l.length+")":l) ); + var accessory = subscription.accessory; + var value = d[1]; + + if(d[0].match(/-state$/)) { + if( match = d[1].match(/dim(\d*)%/ ) ) { + var pct = parseInt( match[1] ); + + FHEM_update( d[0].replace( '-state', '-pct' ), pct ); + } + + value = 1; + if( d[1] == 'off' ) + value = 0; + else if( d[1] == '000000' ) + value = 0; + else if( d[1] == 'absent' ) + value = 0; + else if( d[1] == 'A0' ) + value = 0; + + value = parseInt(value); + + } else if(d[0].match(/-motor$/)) { + value = 2; + if( d[1].match(/^opening/)) + value = 1; + else if( d[1].match(/^up/)) + value = 1; + else if( d[1].match(/^closing/)) + value = 0; + else if( d[1].match(/^down/)) + value = 0; + + value = parseInt(value); + + } else if(d[0].match(/-transportState$/)) { + value = 0; + if( d[1] == 'PLAYING' ) + value = 1; + + value = parseInt(value); + + } else if(d[0].match(/-Volume$/)) { + value = parseInt( d[1] ); + + } else if(d[0].match(/-contact$/)) { + value = 0; + if( d[1].match( /^closed/ ) ) + value = 1; + + value = parseInt(value); + + } else if(d[0].match(/-pct$/)) { + value = parseInt( d[1] ); + + } else if(d[0].match(/-hue$/)) { + value = Math.round(d[1] * 360 / accessory.hueMax); + + } else if(d[0].match(/-sat$/)) { + result = Math.round(d[1] * 100 / accessory.satMax); + + } else if(d[0].match(/-rgb$/)) { + var hue = parseInt( FHEM_rgb2h(d[1]) * 360 ); + + FHEM_update( d[0].replace( '-rgb', '-hue' ), hue ); + + } else if(d[0].match(/-temperature$/) + || d[0].match(/-measured-temp$/) + || d[0].match(/-desired-temp$/) + || d[0].match(/-desiredTemperature$/) ) { + value = parseFloat( d[1] ); + + } else if(d[0].match(/-humidity$/)) { + value = parseInt( d[1] ); + + } + + FHEM_update( d[0], value ); + } + + } + + input = input.substr(FHEM_longpollOffset); + FHEM_longpollOffset = 0; + + } ).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; +console.log( "auth: "+ auth ); + + 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_rgb2h(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; + } + + return h; + + if( M == 0 ) { + s = 0; + } else { + s = c / M; + } + + v = M; + + return h; +} + + +FHEMPlatform.prototype = { + accessories: function(callback) { + this.log("Fetching FHEM switchable devices..."); + + var 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 ); + + var that = this; + this.connection.request.get( { url: url, json: true, gzip: true }, + function(err, response, json) { + if( !err && response.statusCode == 200 ) { + that.log( 'got: ' + json['totalResultsReturned'] + ' results' ); +//that.log("got json: " + util.inspect(json) ); + if( json['totalResultsReturned'] ) { + var sArray=FHEM_sortByKey(json['Results'],"Name"); + sArray.map(function(s) { + 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.PossibleSets.match(/\bon\b/) + && s.PossibleSets.match(/\boff\b/) ) { + accessory = new FHEMAccessory(that.log, that.connection, s); + foundAccessories.push(accessory); + + } else if( s.PossibleSets.match(/\bvolume\b/) ) { + that.log( s.Internals.NAME + ' has volume'); + accessory = new FHEMAccessory(that.log, that.connection, s); + foundAccessories.push(accessory); + + } else if( s.Attributes.genericDisplayType + || s.Attributes.genericDeviceType ) { + accessory = new FHEMAccessory(that.log, that.connection, s); + foundAccessories.push(accessory); + + } else if( s.Attributes.subType == 'thermostat' + || s.Attributes.subType == 'blindActuator' + || s.Attributes.subType == 'threeStateSensor' ) { + accessory = new FHEMAccessory(that.log, that.connection, s); + foundAccessories.push(accessory); + + } else if( s.Internals.TYPE == 'PRESENCE' ) { + accessory = new FHEMAccessory(that.log, that.connection, s); + foundAccessories.push(accessory); + + + } else if( s.Readings.temperature ) { + accessory = new FHEMAccessory(that.log, that.connection, s); + foundAccessories.push(accessory); + + } else if( s.Readings.humidity ) { + accessory = new FHEMAccessory(that.log, that.connection, s); + foundAccessories.push(accessory); + + } else { + that.log( 'ignoring ' + s.Internals.NAME ); + + } + }); + } + callback(foundAccessories); + + } else { + that.log("There was a problem connecting to FHEM (1)."); + if( response ) + that.log( " " + response.statusCode + ": " + response.statusMessage ); + + } + + }); + } +} + +function +FHEMAccessory(log, connection, s) { +//log( 'sets: ' + s.PossibleSets ); +//log("got json: " + util.inspect(s) ); +//log("got json: " + util.inspect(s.Internals) ); + + //FIXME: replace hasPct(true/false) by hasBri(reading) + var match; + if( match = s.PossibleSets.match(/\bpct\b/) ) { + s.hasPct = true; + s.pctMax = 100; + } else if( match = s.PossibleSets.match(/\bdim\d*%/) ) { + s.hasDim = true; + s.pctMax = 100; + } + if( match = s.PossibleSets.match(/\bhue[^\b\s]*(,(\d*)?)+\b/) ) { + s.isLight = true; + s.hasHue = true; + s.hueMax = 360; + if( match[2] != undefined ) + s.hueMax = match[2]; + } + if( match = s.PossibleSets.match(/\bsat[^\b\s]*(,(\d*)?)+\b/) ) { + s.isLight = true; + s.hasSat = true; + s.satMax = 100; + if( match[2] != undefined ) + s.satMax = match[2]; + } + if( s.PossibleSets.match(/\brgb\b/) ) { + s.isLight = true; + s.hasRGB = true; + } + + if( s.Readings['measured-temp'] ) + s.hasTemperature = "measured-temp"; + else if( s.Readings.temperature ) + s.hasTemperature = "temperature"; + + if( s.Readings.humidity ) + s.hasHumidity = true; + + if( s.Readings.motor ) + s.hasMotor = 'motor'; + + + var genericType = s.Attributes.genericDeviceType; + if( !genericType ) + genericType = s.Attributes.genericDisplayType; + + if( genericType == 'light' ) + s.isLight = true; + else if( genericType == 'blind' ) { + s.hasPct = false; + s.isBlind = 'pct'; + } else if( genericType == 'thermostat' ) + s.isThermostat = true; + else if( s.Attributes.subType == 'thermostat' ) + s.isThermostat = true; + else if( s.Attributes.subType == 'blindActuator' ) { + s.hasPct = false; + s.isBlind = 'pct'; + } else if( s.Attributes.subType == 'threeStateSensor' ) { + s.isContactSensor = true; + if( s.Attributes.model == 'HM-SEC-RHS' ) + s.isWindow = true; + } else if( s.Internals.TYPE == 'PRESENCE' ) + s.isOccupancySensor = true; + //else if( s.PossibleSets.match(/\bon\b/) + // && s.PossibleSets.match(/\boff\b/) ) + // s.isSwitch = true; + + if( s.PossibleSets.match(/\bdesired-temp\b/) ) + s.isThermostat = 'desired-temp'; + else if( s.PossibleSets.match(/\bdesiredTemperature\b/) ) + s.isThermostat = 'desiredTemperature'; + + 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 [0-'+ s.pctMax +']' ); + else if( s.hasDim ) + log( s.Internals.NAME + ' is dimable [0-'+ s.pctMax +']' ); + else if( s.isThermostat ) + log( s.Internals.NAME + ' is thermostat ['+ s.isThermostat +']' ); + else if( s.isContactSensor ) + log( s.Internals.NAME + ' is contactsensor' ); + else if( s.isOccupancySensor ) + log( s.Internals.NAME + ' is occupancysensor' ); + else if( s.isBlind ) + log( s.Internals.NAME + ' is blind ['+ s.isBlind +']' ); + else if( s.isLight ) + log( s.Internals.NAME + ' is light' ); + else + log( s.Internals.NAME + ' is switchable' ); + + if( s.hasTemperature ) + log( s.Internals.NAME + ' has temperature ['+ s.hasTemperature +']' ); + if( s.hasHumidity ) + log( s.Internals.NAME + ' has humidity' ); + if( s.hasMotor ) + log( s.Internals.NAME + ' has motor' ); + + //FIXME: create redirectReading() / redirectSet(): on/off -> play/pause + + // 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.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( 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.hasMotor = s.hasMotor; + + this.isLight = s.isLight; + this.isBlind = s.isBlind; + this.isThermostat = s.isThermostat; + this.isContactSensor = s.isContactSensor; + this.isOccupancySensor = s.isOccupancySensor; + this.isWindow = s.isWindow; + +//log( util.inspect(s.Readings) ); + + 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 = { + 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 = this.connection.base_url + "/fhem?cmd=set " + this.device + " play&XHR=1"; + else if( this.PossibleSets.match(/\bon\b/) ) + url = this.connection.base_url + "/fhem?cmd=set " + this.device + " on&XHR=1"; + else + this.log(this.name + " Unhandled command! cmd=" + c + ", value=" + value); + + } else if( c == 'off' ) { + if( this.PossibleSets.match(/\bpause\b/i) ) + url = this.connection.base_url + "/fhem?cmd=set " + this.device + " pause&XHR=1"; + else if( this.PossibleSets.match(/\boff\b/) ) + url = this.connection.base_url + "/fhem?cmd=set " + this.device + " off&XHR=1"; + else + this.log(this.device + " Unhandled command! cmd=" + c + ", value=" + value); + + } else if( c == 'volume' ) { + url = this.connection.base_url + "/fhem?cmd=set " + this.device + " volume " + value + "&XHR=1"; + + } else if( c == 'pct' ) { + url = this.connection.base_url + "/fhem?cmd=set " + this.device + " pct " + value + "&XHR=1"; + + } else if( c == 'dim' ) { + //if( value < 3 ) + // url = this.connection.base_url + "/fhem?cmd=set " + this.device + " off&XHR=1"; + //else + if( value > 97 ) + url = this.connection.base_url + "/fhem?cmd=set " + this.device + " on&XHR=1"; + else + url = this.connection.base_url + "/fhem?cmd=set " + this.device + " " + FHEM_dim_values[Math.round(value/6.25)] + "&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 = this.connection.base_url + "/fhem?cmd=set " + this.device + " rgb " + value + "&XHR=1"; + + } else { + value = Math.round(value * this.hueMax / 360); + url = this.connection.base_url + "/fhem?cmd=set " + this.device + " hue " + value + "&XHR=1"; + } + + } else if( c == 'sat' ) { + value = value / 100 * this.satMax; + url = this.connection.base_url + "/fhem?cmd=set " + this.device + " sat " + value + "&XHR=1"; + + } else if( c == 'targetTemperature' ) { + url = this.connection.base_url + "/fhem?cmd=set " + this.device + " " + this.isThermostat + " " + value + "&XHR=1"; + + } else if( c == 'targetPosition' ) { + url = this.connection.base_url + "/fhem?cmd=set " + this.device + " " + this.isBlind + " " + value + "&XHR=1"; + + } else if( value != undefined ) { + this.log(this.name + " Unhandled command! cmd=" + c + ", value=" + value); + return; + + } + + var that = this; + this.connection.request.put( { url: encodeURI(url), gzip: true }, + function(err, response) { + if( err ) { + that.log("There was a problem sending command " + c + " to" + that.name); + that.log(url); + if( response ) + that.log( " " + response.statusCode + ": " + response.statusMessage ); + + } else { + that.log(that.name + " sent command " + c); + that.log(url); + + } + + } ); + }, + + query: function(reading, callback) { + this.log("query: " + reading); + + var orig_reading = reading; + + //FIXME: create redirectReading() + var rgb_to_hue = false; + if( reading == 'hue' && !this.hasHue && this.hasRGB ) { + reading = 'rgb'; + rgb_to_hue = true; + + } else if( reading == 'pct' && !this.hasPct ) { + reading = 'state'; + + } + + if( reading == 'rgb' + && this.type == 'SWAP_0000002200000003' ) { + reading = '0B-RGBlevel'; + + } + + var result = FHEM_cached[this.device + '-' + orig_reading]; + if( result != undefined ) { + this.log(" cached: " + result); + if( callback != undefined ) + callback(result); + return(result); + } else + this.log(" not cached" ); + + var cmd = '{ReadingsVal("'+this.device+'","'+reading+'","")}'; + var url = encodeURI( this.connection.base_url + "/fhem?cmd=" + cmd + "&XHR=1"); + this.log( ' querying: ' + url ); + + var that = this; + that.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); + + //FIXME: create reading2value + if( rgb_to_hue ) { + result = parseInt( FHEM_rgb2h(result) * 360 ); + + } else if( reading == 'hue' ) { + result = Math.round(result * 360 / that.hueMax); + + } else if( reading == 'sat' ) { + result = Math.round(result * 100 / that.satMax); + + } else if( reading == 'pct' ) { + result = parseInt( result ); + + } else if(reading.match(/-motor$/)) { + if( result.match(/^opening/)) + result = 1; + else if( result.match(/^up/)) + result = 1; + else if( result.match(/^closing/)) + result = 0; + else if( result.match(/^down/)) + result = 0; + else + result = 2; + + result = parseInt(result); + + } else if( reading == 'transportState' ) { + if( result == 'PLAYING' ) + result = 1; + else + result = 0; + + result = parseInt(result); + + } else if( reading == 'Volume' ) { + result = parseInt( result ); + + } else if( reading == 'contact' ) { + if( result.match( /^closed/ ) ) + result = 1; + else + result = 0; + + result = parseInt(result); + + } else if( reading == 'temperature' + || reading == 'measured-temp' + || reading == 'desired-temp' + || reading == 'desiredTemperature' ) { + result = parseFloat( result ); + + } else if( reading == 'humidity' ) { + result = parseInt( result ); + + } else if( reading == 'state' ) { + if( orig_reading == 'pct' ) { + if( match = result.match(/dim(\d*)%/ ) ) { + result = match[1]; + } else if( result == 'off' ) + result = 0; + else + result = 100; + + } else if( result == 'off' ) + result = 0; + else if( result == 'absent' ) + result = 0; + else if( result == '000000' ) + result = 0; + else if( result == 'A0' ) + result = 0; + else + result = 1; + + result = parseInt( result ); + + } + that.log(" mapped: " + result); + + if( !rgb_to_hue && reading != 'transportState' && reading != '0B-RGBlevel' ) + FHEM_update( that.device + '-' + orig_reading, result, true ); + + if( callback != undefined ) + callback(result); + return(result); + + } else { + that.log("There was a problem connecting to FHEM (2)."); + if( response ) + that.log( " " + response.statusCode + ": " + response.statusMessage ); + + } + + } ); + }, + + informationCharacteristics: function() { + 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 : "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.alias, + supportEvents: true, + supportBonjour: false, + manfDescription: "Name of service", + designedMaxLength: 255 + }] + + if( this.name != undefined + && !this.hasTemperature + && !this.hasHumidity + && !this.isBlind + && !this.isThermostat + && !this.isContactSensor + && !this.isOccupancySensor ) { + cTypes.push({ + cType: types.POWER_STATE_CTYPE, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + if( that.type == 'SONOSPLAYER' ) + FHEM_subscribe(characteristic, that.name+'-transportState', that); + else + FHEM_subscribe(characteristic, that.name+'-state', that); + }, + onUpdate: function(value) { + that.command( value == 0 ? 'off' : 'on' ); + }, + onRead: function(callback) { + that.query( that.type == 'SONOSPLAYER' ? 'transportState' : 'state', function(state){ callback(state) } ); + }, + perms: ["pw","pr","ev"], + format: "bool", + initialValue: 0, + //initialValue: that.query( that.type == 'SONOSPLAYER' ? 'transportState' : 'state' ), + supportEvents: true, + supportBonjour: false, + manfDescription: "Change the power state", + designedMaxLength: 1 + }); + } + + if( this.hasPct ) { + cTypes.push({ + cType: types.BRIGHTNESS_CTYPE, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.name+'-pct', that); + }, + onUpdate: function(value) { that.command('pct', value); }, + onRead: function(callback) { + that.query('pct', function(pct){ + callback(pct); + }); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 0, + //initialValue: that.query( 'pct' ), + supportEvents: true, + supportBonjour: false, + manfDescription: "Adjust Brightness of the Light", + designedMinValue: 0, + designedMaxValue: this.pctMax, + designedMinStep: 1, + unit: "%" + }); + } else if( this.hasDim ) { + cTypes.push({ + cType: types.BRIGHTNESS_CTYPE, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.name+'-pct', that); + }, + onUpdate: function(value) { that.delayed('dim', value); }, + onRead: function(callback) { + that.query('pct', function(pct){ + callback(pct); + }); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 0, + //initialValue: that.query( 'state' ), + supportEvents: true, + supportBonjour: false, + manfDescription: "Adjust Brightness of the Light", + designedMinValue: 0, + designedMaxValue: this.pctMax, + designedMinStep: 1, + unit: "%" + }); + } + + if( this.hasHue == true || this.hasRGB == true ) { + cTypes.push({ + cType: types.HUE_CTYPE, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.name+'-hue', that); + }, + onUpdate: function(value) { that.command('hue', 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.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('sat', 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: "%" + }); + } + + //FIXME: parse range and set designedMinValue & designedMaxValue & designedMinStep + if( match = this.PossibleSets.match(/\bVolume\b/) ) { + cTypes.push({ + cType: types.OUTPUTVOLUME_CTYPE, + onUpdate: function(value) { that.delayed('volume', value); }, + onRegister: function(characteristic) { + //characteristic.eventEnabled = true; + //FHEM_subscribe(characteristic, that.name+'-Volume', that); + }, + onRead: function(callback) { + that.query('Volume', function(vol){ + callback(vol); + }); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 10, + supportEvents: true, + supportBonjour: false, + manfDescription: "Adjust the Volume of this device", + designedMinValue: 0, + designedMaxValue: 100, + designedMinStep: 1 + //unit: "%" + }); + } + + //FIXME: parse range and set designedMinValue & designedMaxValue & designedMinStep + if( this.isBlind ) { + 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.name+'-'+that.isBlind, that); + //}, + onRead: function(callback) { + that.query(that.isBlind, function(pct){ + callback(pct); + }); + }, + perms: ["pw","pr","ev"], + format: "int", + initialValue: 0, + //initialValue: that.query( that.isBlind ), + 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.name+'-'+that.isBlind, that); + }, + onRead: function(callback) { + that.query(that.isBlind, function(pos){ + callback(pos); + }); + }, + perms: ["pr","ev"], + format: "int", + initialValue: 0, + //initialValue: that.query( that.isBlind ), + 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.hasMotor ) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.name+'-'+that.hasMotor, that); + } + }, + onRead: function(callback) { + if( that.hasMotor ) + that.query(that.hasMotor, function(state){ + callback(state); + }); + }, + perms: ["pr","ev"], + format: "int", + initialValue: 2, + supportEvents: false, + supportBonjour: false, + manfDescription: "Position State", + designedMinValue: 0, + designedMaxValue: 2, + designedMinStep: 1, + }); + } + + //FIXME: parse range and set designedMinValue & designedMaxValue & designedMinStep + if( this.isThermostat ) { + 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.name+'-'+that.isThermostat, that); + }, + onRead: function(callback) { + that.query(that.isThermostat, function(temperature){ + callback(temperature); + }); + }, + perms: ["pw","pr","ev"], + format: "float", + initialValue: 20, + 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.isWindow ) { + cTypes.push({ + cType: types.CONTACT_SENSOR_STATE_CTYPE, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.name+'-contact', that); + }, + onRead: function(callback) { + that.query('contact', function(state){ + callback(state); + }); + }, + perms: ["pr","ev"], + format: "bool", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Contact State", + designedMaxLength: 1 + }); + } else if( this.isContactSensor ) { + cTypes.push({ + cType: types.CONTACT_SENSOR_STATE_CTYPE, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.name+'-contact', that); + }, + onRead: function(callback) { + that.query('contact', function(state){ + callback(state); + }); + }, + perms: ["pr","ev"], + format: "bool", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Contact State", + designedMaxLength: 1 + }); + } + + if( this.isOccupancySensor ) { + cTypes.push({ + cType: types.OCCUPANCY_DETECTED_CTYPE, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.name+'-state', that); + }, + onRead: function(callback) { + that.query('state', function(state){ + callback(state); + }); + }, + perms: ["pr","ev"], + format: "bool", + initialValue: 0, + supportEvents: false, + supportBonjour: false, + manfDescription: "Occupancy State", + designedMaxLength: 1 + }); + } + + if( this.hasTemperature ) { + cTypes.push({ + cType: types.CURRENT_TEMPERATURE_CTYPE, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.name+'-'+that.hasTemperature, that); + }, + onRead: function(callback) { + that.query(that.hasTemperature, function(temperature){ + callback(temperature); + }); + }, + perms: ["pr","ev"], + format: "float", + initialValue: 20, + supportEvents: true, + supportBonjour: false, + manfDescription: "Current Temperature", + unit: "celsius" + }); + } + + if( this.hasHumidity ) { + cTypes.push({ + cType: types.CURRENT_RELATIVE_HUMIDITY_CTYPE, + onRegister: function(characteristic) { + characteristic.eventEnabled = true; + FHEM_subscribe(characteristic, that.name+'-humidity', that); + }, + onRead: function(callback) { + that.query('humidity', function(humidity){ + callback(humidity); + }); + }, + perms: ["pr","ev"], + format: "int", + initialValue: 50, + designedMinValue: 0, + designedMaxValue: 100, + supportEvents: true, + supportBonjour: false, + manfDescription: "Current Humidity", + unit: "%" + }); + + } + + return cTypes; + }, + + sType: function() { + if( match = this.PossibleSets.match(/\bvolume\b/) ) { + return types.SPEAKER_STYPE; + } else if( this.isBlind ) { + return types.WINDOW_COVERING_STYPE; + } else if( this.isThermostat ) { + return types.THERMOSTAT_STYPE; + } else if( this.isWindow ) { + return types.CONTACT_SENSOR_STYPE; + } else if( this.isContactSensor ) { + return types.CONTACT_SENSOR_STYPE; + } else if( this.isOccupancySensor ) { + return types.OCCUPANCY_SENSOR_STYPE; + } else if( this.isLight || this.hasPct || this.hasHue || this.hasRGB ) { + return types.LIGHTBULB_STYPE; + } else if( this.hasTemperature ) { + return types.TEMPERATURE_SENSOR_STYPE; + } else if( this.hasHumidity ) { + return types.HUMIDITY_SENSOR_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 = FHEMAccessory; +module.exports.platform = FHEMPlatform;