UNPKG

alexa-fhem

Version:
1,439 lines (1,174 loc) 105 kB
'use strict'; var util = require('util'); var events = require('events'); var version = require('./version'); var WebSocket = null; try { WebSocket = require('ws'); } catch(e) { if( e.code !== 'MODULE_NOT_FOUND' ) throw e; console.error( 'websocket not available, falling back to longpoll' ); } var Characteristic = {}; var CustomUUIDs = { // F H E M h o m e b r i d g e xVolume: '4648454d-0101-686F-6D65-627269646765', Actuation: '4648454d-0201-686F-6D65-627269646765', //ColorTemperature: '4648454d-0301-686F-6D65-627269646765', // see: https://github.com/ebaauw/homebridge-hue/wiki/Characteristics CT: 'E887EF67-509A-552D-A138-3DA215050F46', ColorTemperature: 'A18E5901-CFA1-4D37-A10F-0071CEEEEEBD', Volume: '00001001-0000-1000-8000-135D67EC4377', // used in YamahaAVRPlatform, recognized by EVE // see: https://gist.github.com/gomfunkel/b1a046d729757120907c Voltage: 'E863F10A-079E-48FF-8F27-9C2605A29F52', Current: 'E863F126-079E-48FF-8F27-9C2605A29F52', Power: 'E863F10D-079E-48FF-8F27-9C2605A29F52', Energy: 'E863F10C-079E-48FF-8F27-9C2605A29F52', AirPressure: 'E863F10F-079E-48FF-8F27-9C2605A29F52', }; var use_ssl; var auth; module.exports = { FHEM: FHEM, }; function FHEM(log, config, api) { FHEM.CustomUUIDs = CustomUUIDs; FHEM.FHEM_hsv2rgb = FHEM_hsv2rgb; FHEM.FHEM_rgb2hsv = FHEM_rgb2hsv; events.EventEmitter.call(this); this.log = log; this.config = config; if( api ) { this.api = api; //this.api.on('didFinishLaunching', this.didFinishLaunching.bind(this)); } this.server = config['server']; this.port = config['port']; this.filter = config['filter']; this.jsFunctions = config['jsFunctions']; this.scope = config['scope']; this.proactiveEvents = config['proactiveEvents']; if( this.proactiveEvents === undefined) this.proactiveEvents = false; this.log.info( 'defaults to: ' + (this.proactiveEvents ? 'try to send proactive events' : 'will not send proactive events') ); if( !this.port) this.port = 8083; if( this.jsFunctions !== undefined ) { try { var path = this.jsFunctions; if( path.substr(0,1) !== '/' ) path = User.storagePath()+'/'+this.jsFunctions; this.jsFunctions = require(path); } catch(err) { log.error( ' jsFunctions: ' + err ); delete this.jsFunctions; } } var base_url = 'http://'; if( config.ssl ) { if( typeof config.ssl !== 'boolean' ) { this.log.error( 'config: value for ssl has to be boolean.' ); process.exit(0); } base_url = 'https://'; } else if( use_ssl ) base_url = 'https://'; base_url += this.server + ':' + this.port; if( config.webname ) { base_url += '/'+ config.webname; } else { base_url += '/fhem'; } var request = require('postman-request'); if( config['auth'] ) auth = config['auth']; if( auth ) { if( auth.sendImmediately === undefined ) auth.sendImmediately = false; request = request.defaults( { auth: auth, rejectUnauthorized: false } ); } else request = request.defaults( { rejectUnauthorized: false } ); this.connection = { base_url: base_url, request: request, log: log, fhem: this }; this.connection.longpoll = config['longpoll']; if( WebSocket && this.connection.longpoll === 'websocket' ) FHEM_startWebsocket( this.connection ); else if (this.connection.longpoll !== 'none' || ! this.connection.longpoll) FHEM_startLongpoll( this.connection ); } util.inherits(FHEM, events.EventEmitter); // subscriptions to fhem longpoll evens var FHEM_subscriptions = {}; function FHEM_subscribe(accessory, informId, characteristic, mapping) { if( !FHEM_subscriptions[informId] ) FHEM_subscriptions[informId] = []; FHEM_subscriptions[informId].push( { accessory: accessory, characteristic: characteristic, mapping: mapping } ); } function FHEM_unsubscribe(accessory, informId, characteristic) { var subscriptions = FHEM_subscriptions[informId]; if( subscriptions ) { for( let i = 0; i < subscriptions.length; ++i ) { var subscription = subscriptions[i]; if( subscription.accessory !== accessory ) continue; if( subscription.characteristic !== characteristic ) continue; delete subscriptions[i]; } FHEM_subscriptions[informId] = subscriptions.filter( function(n){ return n !== undefined } ); if( !FHEM_subscriptions[informId].length ) delete FHEM_subscriptions[informId] ; } } function FHEM_isPublished(device) { for( let inform_id in FHEM_subscriptions ) { for( let subscription of FHEM_subscriptions[inform_id] ) { var accessory = subscription.accessory; if( accessory.device === device ) return accessory; } } return null; } var tzoffset = (new Date()).getTimezoneOffset() * 60000; //offset in milliseconds var FHEM_cached = {}; FHEM.prototype.cached = function(informId) { return FHEM_cached[informId]; } function FHEM_update(informId, orig, no_update) { if( orig === undefined || FHEM_cached[informId] === orig ) return; FHEM_cached[informId] = orig; //FHEM_cached[informId] = { orig: orig, timestamp: Date.now() }; var date = new Date(Date.now()-tzoffset).toISOString().replace(/T/, ' ').replace(/\..+/, ''); console.log(' ' + date + ' caching: ' + informId + ': ' + orig ); var subscriptions = FHEM_subscriptions[informId]; if( subscriptions ) subscriptions.forEach( function(subscription) { var mapping = subscription.mapping; if( typeof mapping !== 'object' ) return; mapping.last_update = parseInt( Date.now()/1000 ); var value = FHEM_reading2homekit(mapping, orig); if( value === undefined ) return; //if( !no_update ) //mapping.characteristic.setValue(value, undefined, 'fromFHEM'); } ); } FHEM.prototype.reading2homekit = function(mapping, orig) { return FHEM_reading2homekit( mapping, orig ); } function FHEM_reading2homekit(mapping, orig) { var value = undefined; if( typeof mapping.reading2homekit === 'function' ) { try { value = mapping.reading2homekit(orig); } catch(err) { mapping.log.error( mapping.informId + ' reading2homekit: ' + err ); return undefined; } if( typeof value === 'number' && isNaN(value) ) { mapping.log.error(mapping.informId + ' not a number: ' + orig); return undefined; } } else { value = FHEM_reading2homekit_(mapping, orig); } if( value === undefined ) { if( mapping.default !== undefined ) { orig = 'mapping.default'; value = mapping.default; } else return undefined; } if( 0 && typeof value === 'string' ) { //FIXME: activate this ? if( Characteristic[mapping.characteristic_type] && Characteristic[mapping.characteristic_type][value] !== undefined ) { if( mapping.homekit2name === undefined ) mapping.homekit2name = {}; mapping.homekit2name[Characteristic[mapping.characteristic_type][value]] = value; value = Characteristic[mapping.characteristic_type][value]; } } var defined = undefined; if( mapping.homekit2name !== undefined ) { defined = mapping.homekit2name[value]; if( defined === undefined ) defined = '???'; } mapping.log.info(' caching: ' + (mapping.name?'Custom '+mapping.name:mapping.characteristic_type) + (mapping.subtype?':'+mapping.subtype:'') + ': ' + value + ' (' + 'as '+typeof(value) + (defined?'; means '+defined:'') + '; from \''+orig + '\')'); mapping.cached = value; return value; } function FHEM_reading2homekit_(mapping, orig) { var value = orig; if( value === undefined ) return undefined; var reading = mapping.reading; if( reading == 'temperature' || reading == 'measured' || reading == 'measured-temp' || reading == 'desired-temp' || reading == 'desired' || reading == 'desiredTemperature' ) { if( typeof value === 'string' && value.toLowerCase() == 'on' ) value = 31.0; else if( typeof value === 'string' && value.toLowerCase() == 'off' ) value = 4.0; else value = parseFloat( value ); if( mapping.minValue !== undefined && value < mapping.minValue ) value = mapping.minValue; else if( mapping.maxValue !== undefined && value > mapping.maxValue ) value = mapping.maxValue; if( mapping.minStep ) { if( mapping.minValue ) value -= mapping.minValue; value = parseFloat( (Math.round(value / mapping.minStep) * mapping.minStep).toFixed(1) ); if( mapping.minValue ) value += mapping.minValue; } } else if( reading == 'humidity' ) { value = parseInt( value ); } else if( reading == 'onoff' ) { value = parseInt( value ); } else if( reading == 'reachable' ) { value = parseInt( value ) == true; } else if( reading === 'state' && ( mapping.On && typeof mapping.values !== 'object' && mapping.reading2homekit === undefined && mapping.valueOn === undefined && mapping.valueOff === undefined ) ) { if( value.match(/^set-/ ) ) return undefined; if( value.match(/^set_/ ) ) return undefined; if( mapping.event_map !== undefined ) { var mapped = mapping.event_map[value]; if( mapped !== undefined ) value = mapped; } if( value.toLowerCase() == 'off' ) value = 0; else if( value == '000000' ) value = 0; else if( value.match( /^[A-D]0$/ ) ) value = 0; else value = 1; } else if( mapping.characteristic_type === 'On' && ( mapping.valueOn !== undefined || mapping.valueOff !== undefined ) ) { var mapped = undefined;; if( mapping.valueOn !== undefined ) { var match = mapping.valueOn.match('^/(.*)/$'); if( !match && value == mapping.valueOn ) mapped = 1; else if( match && value.toString().match( match[1] ) ) mapped = 1; else mapped = 0; } if( mapping.valueOff !== undefined ) { var match = mapping.valueOff.match('^/(.*)/$'); if( !match && value == mapping.valueOff ) mapped = 0; else if( match && value.toString().match( match[1] ) ) mapped = 0; else if( mapped === undefined ) mapped = 1; } if( mapping.valueOn === undefined && mapping.valueOff === undefined ) { if( typeof value === 'string' && value.toLowerCase() == 'on' ) mapped = 1; else if( typeof value === 'string' && value.toLowerCase() == 'off' ) mapped = 0; else mapped = parseInt(value)?1:0; } if( mapped !== undefined ) { mapping.log.debug(mapping.informId + ' valueOn/valueOff: value ' + value + ' mapped to ' + mapped); value = mapped; } } else { if( value.match(/^set-/ ) ) return undefined; else if( value.match(/^set_/ ) ) return undefined; var orig = value; var format = undefined; if( typeof mapping.characteristic === 'object' ) format = mapping.characteristic.props.format; else if( typeof mapping.characteristic === 'function' ) { var characteristic = new (Function.prototype.bind.apply(mapping.characteristic, arguments)); format = characteristic.props.format; //delete characteristic; } else if( mapping.format ) { // only for testing ! format = mapping.format; } if( mapping.event_map !== undefined ) { var mapped = mapping.event_map[value]; if( mapped !== undefined ) { mapping.log.debug(mapping.informId + ' eventMap: value ' + value + ' mapped to: ' + mapped); value = mapped; } } if( value !== undefined && mapping.part !== undefined ) { var mapped = value.split(' ')[mapping.part]; if( mapped === undefined ) { mapping.log.error(mapping.informId + ' value ' + value + ' has no part ' + mapping.part); return value; } mapping.log.debug(mapping.informId + ' parts: using part ' + mapping.part + ' of: ' + value + ' results in: ' + mapped); value = mapped; } if( mapping.threshold ) { //if( !format.match( /bool/i ) && mapping.threshold ) { var mapped; if( parseFloat(value) > mapping.threshold ) mapped = 1; else mapped = 0; mapping.log.debug(mapping.informId + ' threshold: value ' + value + ' mapped to ' + mapped); value = mapped; } if( typeof mapping.value2homekit_re === 'object' || typeof mapping.value2homekit === 'object' ) { var mapped = undefined; if( typeof mapping.value2homekit_re === 'object' ) for( let entry of mapping.value2homekit_re ) { if( value.match( entry.re ) ) { mapped = entry.to; break; } } if( mapped === '#' ) mapped = value; if( typeof mapping.value2homekit === 'object' ) if( mapping.value2homekit[value] !== undefined ) mapped = mapping.value2homekit[value]; if( mapped === undefined ) mapped = mapping.default; if( mapped === undefined ) { mapping.log.error(mapping.informId + ' value ' + value + ' not handled in values'); return undefined; } mapping.log.debug(mapping.informId + ' values: value ' + value + ' mapped to ' + mapped); value = mapped; } if( format === undefined ) { if( mapping.factor ) { mapping.log.debug(mapping.informId + ' factor: value ' + value + ' mapped to ' + value * mapping.factor); value *= mapping.factor; } if( mapping.max && mapping.maxValue ) { value = Math.round(value * mapping.maxValue / mapping.max ); mapping.log.debug(mapping.informId + ' value ' + orig + ' scaled to: ' + value); } if( mapping.minValue !== undefined && value < mapping.minValue ) { mapping.log.debug(mapping.informId + ' value ' + value + ' clipped to minValue: ' + mapping.minValue); value = mapping.minValue; } else if( mapping.maxValue !== undefined && value > mapping.maxValue ) { mapping.log.debug(mapping.informId + ' value ' + value + ' clipped to maxValue: ' + mapping.maxValue); value = mapping.maxValue; } if( mapping.minStep ) { if( mapping.minValue ) value -= mapping.minValue; value = parseFloat( (Math.round(value / mapping.minStep) * mapping.minStep).toFixed(1) ); if( mapping.minValue ) value += mapping.minValue; } return value; } //mapping.log.error( format ); if( !format ) { mapping.log.error(mapping.informId + ' empty format' ); } else if( format.match( /bool/i ) ) { var mapped = undefined;; if( mapping.valueOn !== undefined ) { var match = mapping.valueOn.match('^/(.*)/$'); if( !match && value == mapping.valueOn ) mapped = 1; else if( match && value.toString().match( match[1] ) ) mapped = 1; else mapped = 0; } if( mapping.valueOff !== undefined ) { var match = mapping.valueOff.match('^/(.*)/$'); if( !match && value == mapping.valueOff ) mapped = 0; else if( match && value.toString().match( match[1] ) ) mapped = 0; else if( mapped === undefined ) mapped = 1; } if( mapping.valueOn === undefined && mapping.valueOff === undefined ) { if( value.toLowerCase() == 'on' ) mapped = 1; else if( value.toLowerCase() == 'off' ) mapped = 0; else mapped = parseInt(value)?1:0; } if( mapped !== undefined ) { mapping.log.debug(mapping.informId + ' valueOn/valueOff: value ' + value + ' mapped to ' + mapped); value = mapped; } if( mapping.factor ) { mapping.log.debug(mapping.informId + ' factor: value ' + value + ' mapped to ' + value * mapping.factor); value *= mapping.factor; } if( mapping.invert ) { mapping.minValue = 0; mapping.maxValue = 1; } } else if( format.match( /float/i ) ) { var mapped = parseFloat( value ); if( typeof mapped !== 'number' ) { mapping.log.error(mapping.informId + ' is not a number: ' + value); return undefined; } value = mapped; if( mapping.factor ) { mapping.log.debug(mapping.informId + ' factor: value ' + value + ' mapped to ' + value * mapping.factor); value *= mapping.factor; } } else if( format.match(/int/i) ) { var mapped = parseFloat( value ); if( typeof mapped !== 'number' ) { mapping.log.error(mapping.informId + ' not a number: ' + value); return undefined; } value = mapped; if( mapping.factor ) { mapping.log.debug(mapping.informId + ' factor: value ' + value + ' mapped to ' + value * mapping.factor); value *= mapping.factor; } value = parseInt( value + 0.5 ); } else if( format.match( /string/i ) ) { } if( mapping.max && mapping.maxValue ) { value = Math.round(value * mapping.maxValue / mapping.max ); mapping.log.debug(mapping.informId + ' value ' + orig + ' scaled to: ' + value); } if( mapping.minValue !== undefined && value < mapping.minValue ) { mapping.log.debug(mapping.informId + ' value ' + value + ' clipped to minValue: ' + mapping.minValue); value = mapping.minValue; } else if( mapping.maxValue !== undefined && value > mapping.maxValue ) { mapping.log.debug(mapping.informId + ' value ' + value + ' clipped to maxValue: ' + mapping.maxValue); value = mapping.maxValue; } if( mapping.minStep ) { if( mapping.minValue ) value -= mapping.minValue; value = parseFloat( (Math.round(value / mapping.minStep) * mapping.minStep).toFixed(1) ); if( mapping.minValue ) value += mapping.minValue; } if( format && format.match(/int/i) ) value = parseInt( value ); else if( format && format.match(/float/i) ) value = parseFloat( value ); if( typeof value === 'number' ) { var mapped = value; if( isNaN(value) ) { mapping.log.error(mapping.informId + ' not a number: ' + orig); return undefined; } else if( mapping.invert && mapping.minValue !== undefined && mapping.maxValue !== undefined ) { mapped = mapping.maxValue - value + mapping.minValue; } else if( mapping.invert && mapping.maxValue !== undefined ) { mapped = mapping.maxValue - value; } else if( mapping.invert ) { mapped = 100 - value; } if( value !== mapped ) mapping.log.debug(mapping.informId + ' value: ' + value + ' inverted to ' + mapped); value = mapped; } if( format && format.match( /bool/i ) ) value = parseInt(value)?true:false; } return(value); } var FHEM_connections = {}; var FHEM_csrfToken = {}; function FHEM_processEventData(connection, data) { var offset = 0; connection.input += data; try { var lastEventTime = Date.now(); for(;;) { var nOff = connection.input.indexOf('\n', offset); if(nOff < 0) break; var l = connection.input.substr(offset, nOff-offset); offset = 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) == '[' ) { try { d = JSON.parse(l); } catch(err) { connection.log( ' longpoll JSON.parse: ' + err ); continue; } } else d = l.split('<<', 3); //console.log(d); if( connection.fhem.alexa_device && d[0] === connection.fhem.alexa_device.Name ) { if(d.length == 3) { if( d[1] === 'no definition' ) ;//connection.fhem.updateAlexaDevice(); } else if(d.length == 2) { var cmd = d[1].split(' ', 2); if( cmd[0] === 'reload' ) connection.fhem.emit( 'RELOAD', cmd[1] ); else if( cmd[0] === 'customSlotTypes' ) connection.fhem.emit( 'customSlotTypes', cmd[1] ); else if( cmd[0] === 'unregister' ) connection.fhem.emit( 'UNREGISTER SSHPROXY' ); } continue; } if(d[0].match(/-ts$/)) continue; if(d[0].match(/^#FHEMWEB:/)) continue; var match = d[0].match(/([^-]*)-(.*)/); if( !match ) continue; var device = match[1]; var reading = match[2]; //console.log( 'device: ' + device ); //console.log( 'reading: ' + reading ); if( reading === undefined ) continue; var value = d[1]; //console.log( 'value: ' + value ); if( value.match( /^set-/ ) ) continue; if( device === 'global' ) { if( reading === 'DELETEATTR' ) { var parts = d[1].split( ' ' ); device = parts[0]; reading = parts[1]; if( connection.fhem.alexa_device && device === connection.fhem.alexa_device.Name ) connection.fhem.updateAlexaDevice(); else connection.fhem.emit( 'ATTR', device, reading, undefined ); } continue; } if( connection.fhem.alexa_device && device === connection.fhem.alexa_device.Name ) { if( reading === 'a-alexaMapping' || reading === 'a-alexaTypes' || reading === 'a-echoRooms' || reading === 'a-fhemIntents' || reading === 'a-alexaProactiveEvents' || reading.match( /^a-alexa.*Level$/ ) ) { connection.fhem.updateAlexaDevice(); } else if( reading == 'a-alexaFHEM.bearerToken' ) { // ... } continue; } var subscriptions = FHEM_subscriptions[d[0]]; if( subscriptions ) { FHEM_update( d[0], value ); FHEM_connections[connection.base_url].last_event_time = lastEventTime; connection.fhem.emit( 'VALUE CHANGED', device, reading, value ); subscriptions.forEach( function(subscription) { var accessory = subscription.accessory; if(accessory.mappings.colormode) { if( reading == 'xy') { var xy = value.split(','); var rgb = FHEM_xyY2rgb(xy[0], xy[1] , 1); var hsv = FHEM_rgb2hsv(rgb); FHEM_update( device+'-h', hsv[0] ); FHEM_update( device+'-s', hsv[1] ); FHEM_update( device+'-v', hsv[2] ); FHEM_update( device+'-'+reading, value, false ); return; } else if( reading == 'ct') { var rgb = FHEM_ct2rgb(value); var hsv = FHEM_rgb2hsv(rgb); FHEM_update( device+'-h', hsv[0] ); FHEM_update( device+'-s', hsv[1] ); FHEM_update( device+'-v', hsv[2] ); FHEM_update( device+'-'+reading, value, false ); return; } } } ); } else if( d[0].indexOf( '-a-' ) >= 0 ) { var match = d[0].match(/([^-]*)-a-(.*)/); var device = match[1]; var reading = match[2]; connection.fhem.emit( 'ATTR', device, reading, value ); } } } catch(err) { connection.log.error( ' error processing event data: ' + err ); } connection.input = connection.input.substr(offset); } //FIXME: reuse more code between FHEM_startLongpoll and FHEM_startWebsocket function FHEM_startLongpoll(connection) { if( !FHEM_connections[connection.base_url] ) { FHEM_connections[connection.base_url] = {}; FHEM_connections[connection.base_url].connects = 0; FHEM_connections[connection.base_url].disconnects = 0; FHEM_connections[connection.base_url].received_total = 0; } if( FHEM_connections[connection.base_url].connected ) return; FHEM_connections[connection.base_url].connects++; FHEM_connections[connection.base_url].received = 0; FHEM_connections[connection.base_url].connected = true; var filter = '.*'; var since = 'null'; if( FHEM_connections[connection.base_url].last_event_time ) since = FHEM_connections[connection.base_url].last_event_time/1000; var query = '?XHR=1' + '&inform=type=status;addglobal=1;filter='+filter+';since='+since+';fmt=JSON' + '&timestamp='+Date.now(); var url = encodeURI( connection.base_url + query ); connection.input = ''; connection.log( 'trying longpoll to listen for fhem events' ); connection.log( 'starting longpoll: ' + url ); connection.request.get( { url: url } ).on( 'data', function(data) { //console.log( 'data: ' + data ); if( !data ) return; var length = data.length; FHEM_connections[connection.base_url].received += length; FHEM_connections[connection.base_url].received_total += length; FHEM_processEventData( connection, data ); FHEM_connections[connection.base_url].disconnects = 0; } ).on( 'response', function(response) { if( response.headers && response.headers['x-fhem-csrftoken'] ) FHEM_csrfToken[connection.base_url] = response.headers['x-fhem-csrftoken']; else FHEM_csrfToken[connection.base_url] = ''; connection.log.info( 'got csrfToken: '+ FHEM_csrfToken[connection.base_url] ); connection.fhem.checkAndSetGenericDeviceType(); connection.log( 'waiting for events ...' ); connection.fhem.emit( 'CONNECTED' ); } ).on( 'end', function() { FHEM_connections[connection.base_url].connected = false; FHEM_connections[connection.base_url].disconnects++; var timeout = 500 * FHEM_connections[connection.base_url].disconnects - 300; if( timeout > 30000 ) timeout = 30000; connection.log( 'longpoll ended, reconnect in: ' + timeout + 'msec' ); setTimeout( function(){FHEM_startLongpoll(connection)}, timeout ); } ).on( 'error', function(err) { FHEM_connections[connection.base_url].connected = false; FHEM_connections[connection.base_url].disconnects++; var timeout = 5000 * FHEM_connections[connection.base_url].disconnects; if( timeout > 30000 ) timeout = 30000; connection.log( 'longpoll error: ' + err + ', retry in: ' + timeout + 'msec' ); setTimeout( function(){FHEM_startLongpoll(connection)}, timeout ); console.error( '*** FHEM: connection failed: '+ err ); } ); } function FHEM_startWebsocket(connection) { if( !FHEM_connections[connection.base_url] ) { FHEM_connections[connection.base_url] = {}; FHEM_connections[connection.base_url].connects = 0; FHEM_connections[connection.base_url].disconnects = 0; FHEM_connections[connection.base_url].received_total = 0; } if( FHEM_connections[connection.base_url].connected ) return; FHEM_connections[connection.base_url].connects++; FHEM_connections[connection.base_url].received = 0; FHEM_connections[connection.base_url].connected = true; var filter = '.*'; var since = 'null'; if( FHEM_connections[connection.base_url].last_event_time ) since = FHEM_connections[connection.base_url].last_event_time/1000; var query = '?XHR=1' + '&inform=type=status;addglobal=1;filter='+filter+';since='+since+';fmt=JSON' + '&timestamp='+Date.now(); connection.input = ''; connection.log( 'trying websockets to listen for fhem events' ); connection.log( 'opening websocket: ' + connection.base_url.replace('http','ws') + query ); if( !connection.ws ) { connection.ws = new WebSocket(connection.base_url.replace('http','ws') + query ); } //FIXME: add ping connection.ws.on('open', function open() { connection.log( 'websocket opened' ); }); connection.ws.on('message', function incoming(data) { if( !data ) return; var length = data.length; FHEM_connections[connection.base_url].received += length; FHEM_connections[connection.base_url].received_total += length; FHEM_processEventData( connection, data ); FHEM_connections[connection.base_url].disconnects = 0; } ).on( 'upgrade', function(response) { if( response.headers && response.headers['x-fhem-csrftoken'] ) FHEM_csrfToken[connection.base_url] = response.headers['x-fhem-csrftoken']; else FHEM_csrfToken[connection.base_url] = ''; connection.log.info( 'got csrfToken: '+ FHEM_csrfToken[connection.base_url] ); connection.fhem.checkAndSetGenericDeviceType(); connection.log( 'waiting for events ...' ); connection.fhem.emit( 'CONNECTED' ); } ).on( 'close', function() { //FIXME FHEM_connections[connection.base_url].connected = false; FHEM_connections[connection.base_url].disconnects++; var timeout = 500 * FHEM_connections[connection.base_url].disconnects - 300; if( timeout > 30000 ) timeout = 30000; connection.log( 'websocket ended, reconnect in: ' + timeout + 'msec' ); setTimeout( function(){FHEM_startWebsocket(connection)}, timeout ); //setTimeout( function(){FHEM_startLongpoll(connection)}, timeout ); } ).on( 'error', function(err) { //FIXME FHEM_connections[connection.base_url].connected = false; FHEM_connections[connection.base_url].disconnects++; var timeout = 5000 * FHEM_connections[connection.base_url].disconnects; if( timeout > 30000 ) timeout = 30000; connection.log( 'websocket error: ' + err + ', retry in: ' + timeout + 'msec' ); setTimeout( function(){FHEM_startWebsocket(connection)}, timeout ); //setTimeout( function(){FHEM_startLongpoll(connection)}, timeout ); console.error( '*** FHEM: connection failed: '+ err ); } ); } 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)); }); } FHEM.prototype.execute = function(cmd, callback, onerror) { var pre; var post; if( this.alexa_device ) { var pre = '{$defs{"'+ this.alexa_device.Name +'"}->{"active"} = 1;;undef}'; var post = '{$defs{"'+ this.alexa_device.Name +'"}->{"active"} = 0;;undef}'; cmd = pre + ';' + cmd + ';' + post; //var pre = 'setreading '+ this.alexa_device.Name +' active 1'; //var post = 'setreading '+ this.alexa_device.Name +' active 0'; } FHEM_execute(this.log, this.connection, cmd, callback, onerror) }; FHEM.prototype.connect = function(callback, filter) { //this.checkAndSetGenericDeviceType(); if( !filter) filter = this.filter; this.emit( 'DEFINED' ); this.log.info('Fetching FHEM devices...'); this.devices = []; if( FHEM_csrfToken[this.connection.base_url] === undefined ) { setTimeout( function(){this.connection.fhem.connect(callback,filter)}.bind(this), 500 ); return; } var cmd = 'jsonlist2'; if( filter ) cmd += '%20' + encodeURIComponent(filter); if( FHEM_csrfToken[this.connection.base_url] ) cmd += '&fwcsrf='+FHEM_csrfToken[this.connection.base_url]; var url = this.connection.base_url + '?cmd=' + cmd + '&XHR=1'; this.log.info( 'fetching: ' + url ); this.connection.request.get( { url: url, json: true, gzip: true }, function(err, response, json) { if( !err && response.statusCode == 200 && json ) { //console.log("got json: " + util.inspect(json) ); console.log( '*** FHEM: connected' ); this.log.info( 'got: ' + json['totalResultsReturned'] + ' results' ); if( json['totalResultsReturned'] ) { var sArray=FHEM_sortByKey(json['Results'],'Name'); sArray.map( function(s) { //FIXME: change to factory pattern var device = new FHEMDevice(this, s); if( device && device.service_name ) { device.fhem = this.connection.fhem; this.devices.push(device); } else { this.log.info( 'no device created for ' + s.Internals.NAME + ' (' + s.Internals.TYPE + ')' ); return undefined; } }.bind(this) ); } if( callback ) callback(this.devices); } else { this.log.error('There was a problem connecting to FHEM ('+ err +')'); if( json ) this.log.error(' json: '+ json); if( response ) { this.log.error( ' ' + response.statusCode + ': ' + response.statusMessage ); console.error( '*** FHEM: connection failed: '+ response.statusCode +': '+ response.statusMessage ); } else console.error( '*** FHEM: connection failed' ); } }.bind(this) ); } FHEMDevice.prototype.subscribe = function(mapping, characteristic) { if( typeof mapping === 'object' ) { mapping.characteristic = characteristic; if( characteristic ) characteristic.FHEM_mapping = mapping; FHEM_subscribe(this, mapping.informId, characteristic, mapping); } else { FHEM_subscribe(this, mapping, characteristic); } } FHEMDevice.prototype.unsubscribe = function(mapping, characteristic) { if( mapping === undefined ) { for( let characteristic_type in this.mappings ) { var mapping = this.mappings[characteristic_type]; if( characteristic_type === "ModeController"){ for( var key in mapping.instance) FHEM_unsubscribe(this, mapping.instance[key].informId, characteristic); }else{ FHEM_unsubscribe(this, mapping.informId, characteristic); } } } else if( typeof mapping === 'object' ) { mapping.characteristic = characteristic; if( characteristic ) characteristic.FHEM_mapping = mapping; FHEM_unsubscribe(this, mapping.informId, characteristic); } else { FHEM_unsubscribe(this, mapping, characteristic); } } FHEMDevice.prototype.fromHomebridgeMapping = function(homebridgeMapping) { if( !homebridgeMapping ) return; this.log.debug( 'homebridgeMapping: ' + homebridgeMapping ); if( homebridgeMapping.match( /^{.*}$/ ) ) { try { homebridgeMapping = JSON.parse(homebridgeMapping); } catch(err) { this.log.error( ' fromHomebridgeMapping JSON.parse: ' + err ); return; } if( 'ModeController' in homebridgeMapping){ var mcs = {}; if(!Array.isArray(homebridgeMapping.ModeController)){ homebridgeMapping.ModeController = [homebridgeMapping.ModeController]; } homebridgeMapping.ModeController.forEach(function (ModeController){ var name = ModeController.asset.split(":")[0]; var obj = { [name] : ModeController}; Object.assign(mcs, obj); }) var obj = { "instance" : mcs }; homebridgeMapping.ModeController = obj; } //FIXME: handle multiple identical characteristics in this.mappings and in homebridgeMapping ? if( 1 ) this.mappings = homebridgeMapping; else for( let characteristic in homebridgeMapping ) { if( !this.mappings[characteristic] ) this.mappings[characteristic] = {}; for( let attrname in homebridgeMapping[characteristic] ) this.mappings[characteristic][attrname] = homebridgeMapping[characteristic][attrname]; } return; } var seen = {}; var service = undefined; for( var mapping of homebridgeMapping.split(/ |\n/) ) { if( !mapping ) continue; if( mapping.match( /^#/ ) ) continue; if( mapping == 'clear' ) { this.mappings = {}; continue; } var match = mapping.match(/(^.*?)(:|=)(.*)/); if( !match || match.length < 4 || !match[3] ) { this.log.error( ' wrong syntax: ' + mapping ); continue; } var characteristic = match[1]; var params = match[3]; var parts = characteristic.split('#'); if( parts[1] ) service = parts[0]; else if( service !== undefined ) characteristic = service +'#'+ characteristic; var mapping; if( !seen[characteristic] && this.mappings[characteristic] !== undefined ) mapping = this.mappings[characteristic]; else { mapping = {}; if( this.mappings[characteristic] ) { if( this.mappings[characteristic].length == undefined ) this.mappings[characteristic] = [this.mappings[characteristic]]; this.mappings[characteristic].push( mapping ); } else this.mappings[characteristic] = mapping; } seen[characteristic] = true; for( let param of params.split(',') ) { if( param == 'clear' ) { mapping = {}; delete this.mappings[characteristic]; continue; } else if( !this.mappings[characteristic] ) this.mappings[characteristic] = mapping var p = param.split('='); if( p.length == 2 ) if( p[0] == 'values' ) mapping[p[0]] = p[1].split(';'); else if( p[0] == 'valid' ) mapping[p[0]] = p[1].split(';'); else if( p[0] == 'cmds' ) mapping[p[0]] = p[1].split(';'); else if( p[0] == 'actions' ) mapping[p[0]] = p[1].split(';'); else if( p[0] == 'delay' ) { mapping[p[0]] = parseInt(p[1]); if( isNaN(mapping[p[0]]) ) mapping[p[0]] = true; } else if( p[0] === 'minValue' || p[0] === 'maxValue' || p[0] === 'minStep' || p[0] === 'min' || p[0] === 'max' || p[0] === 'default'  ) { mapping[p[0]] = parseFloat(p[1]); if( isNaN(mapping[p[0]]) ) mapping[p[0]] = p[1]; } else mapping[p[0]] = p[1].replace( /\+/g, ' ' ); else if( p.length == 1 ) { if( this.mappings[param] !== undefined ) { try { mapping = Object.assign({}, this.mappings[param]); } catch(err) { console.log(this.mappings[param]); for( let x in this.mappings[param] ) { mapping[x] = this.mappings[param][x] } } this.mappings[characteristic] = mapping; } else if( p === 'invert' ) { mapping[p] = 1; } else { var p = param.split(':'); var reading = p[p.length-1]; var device = p.length > 1 ? p[p.length-2] : undefined; var cmd = p.length > 2 ? p[p.length-3] : undefined; if( reading ) mapping.reading = reading; if( device ) mapping.device = device; if( cmd ) mapping.cmd = cmd; } } else { this.log.error( ' wrong syntax: ' + param ); } } } if( 'ModeController' in this.mappings){ var mcs = {}; if(!Array.isArray(this.mappings.ModeController)){ this.mappings.ModeController = [this.mappings.ModeController]; } this.mappings.ModeController.forEach(function (ModeController){ var name = ModeController.asset.split(":")[0]; var obj = { [name] : ModeController}; Object.assign(mcs, obj); }) var obj = { "instance" : mcs }; this.mappings.ModeController = obj; } } FHEMDevice.prototype.prepare = function(mapping) { if( mapping.default !== undefined ) { if( Characteristic[mapping.characteristic_type] && Characteristic[mapping.characteristic_type][mapping.default] !== undefined ) { if( mapping.homekit2name === undefined ) mapping.homekit2name = {}; mapping.homekit2name[Characteristic[mapping.characteristic_type][mapping.default]] = mapping.default; mapping.default = Characteristic[mapping.characteristic_type][mapping.default]; } this.log.debug( 'default: ' + mapping.default ); } if( typeof mapping.values === 'object' ) { mapping.value2homekit = {}; mapping.value2homekit_re = []; if( mapping.homekit2name === undefined ) mapping.homekit2name = {}; for( let entry of mapping.values ) { var match = entry.match('^([^:]*)(:(.*))?$'); if( !match ) { this.log.error( 'values: format wrong for ' + entry ); continue; } var from = match[1]; var to = match[3] === undefined ? entry : match[3]; to = to.replace( /\+/g, ' ' ); if( Characteristic[mapping.characteristic_type] && Characteristic[mapping.characteristic_type][to] !== undefined ) { mapping.homekit2name[Characteristic[mapping.characteristic_type][to]] = to; to = Characteristic[mapping.characteristic_type][to]; } else if( Characteristic[mapping.characteristic_type] ) { for( let defined in Characteristic[mapping.characteristic_type] ) { if( to == Characteristic[mapping.characteristic_type][defined] ) mapping.homekit2name[to] = defined; } } var match; if( match = from.match('^/(.*)/$') ) mapping.value2homekit_re.push( { re: match[1], to: to} ); else { from = from.replace( /\+/g, ' ' ); mapping.value2homekit[from] = to; } } if(mapping.value2homekit_re && mapping.value2homekit_re.length) this.log.debug( 'value2homekit_re: ' + util.inspect(mapping.value2homekit_re) ); if(mapping.value2homekit && Object.keys(mapping.value2homekit).length) this.log.debug( 'value2homekit: ' + util.inspect(mapping.value2homekit) ); if(mapping.homekit2name ) { if(Object.keys(mapping.homekit2name).length) this.log.debug( 'homekit2name: ' + util.inspect(mapping.homekit2name) ); else delete mapping.homekit2name; } } if( typeof mapping.cmds === 'object' ) { mapping.homekit2cmd = {}; mapping.homekit2cmd_re = []; for( let entry of mapping.cmds ) { var match = entry.match('^([^:]*)(:(.*))?$'); if( !match ) { this.log.error( 'cmds: format wrong for ' + entry ); continue; } var from = match[1]; var to = match[2] !== undefined ? match[3] : match[1]; to = to.replace( /\+/g, ' ' ); if( match = from.match('^/(.*)/$') ) { mapping.homekit2cmd_re.push( { re: match[1], to: to} ); } else { if( Characteristic[mapping.characteristic_type] && Characteristic[mapping.characteristic_type][from] !== undefined ) from = Characteristic[mapping.characteristic_type][from]; else from = from.replace( /\+/g, ' ' ); mapping.homekit2cmd[from] = to; } } if(mapping.homekit2cmd_re && mapping.homekit2cmd_re.length) this.log.debug( 'homekit2cmd_re: ' + util.inspect(mapping.homekit2cmd_re) ); if(mapping.homekit2cmd && Object.keys(mapping.homekit2cmd).length) this.log.debug( 'homekit2cmd: ' + util.inspect(mapping.homekit2cmd) ); } if( typeof mapping.actions === 'object' ) { mapping.action2value = {}; for( let entry of mapping.actions ) { var match = entry.match('^([^:]*)(:(.*))?$'); if( !match ) { this.log.error( 'actions: format wrong for ' + entry ); continue; } var from = match[1]; var to = match[2] !== undefined ? match[3] : match[1]; //to = to.replace( /\+/g, ' ' ); mapping.action2value[from] = to; } if(mapping.action2value && Object.keys(mapping.action2value).length) this.log.debug( 'action2value: ' + util.inspect(mapping.action2value) ); } if( mapping.reading2homekit !== undefined && typeof mapping.reading2homekit !== 'function' ) { if( mapping.reading2homekit.match( /^{.*}$/ ) ) { try { mapping.reading2homekit = new Function( 'mapping', 'orig', mapping.reading2homekit ).bind(null,mapping); } catch(err) { this.log.error( ' reading2homekit: ' + err ); //delete mapping.reading2homekit; } } else if( typeof this.jsFunctions === 'object' ) { if( typeof this.jsFunctions[mapping.reading2homekit] === 'function' ) mapping.reading2homekit = this.jsFunctions[mapping.reading2homekit].bind(null,mapping); else this.log.error( ' reading2homekit: no function named ' + mapping.reading2homekit + ' in ' + util.inspect(this.jsFunctions) ); } if( mapping.reading2homekit !== undefined && typeof mapping.reading2homekit !== 'function' ) { this.log.error( ' reading2homekit disabled.' ); delete mapping.reading2homekit; } } if( mapping.homekit2reading !== undefined && typeof mapping.homekit2reading !== 'function' ) { if( mapping.homekit2reading.match( /^{.*}$/ ) ) { try { mapping.homekit2reading = new Function( 'mapping', 'orig', mapping.homekit2reading ).bind(null,mapping); } catch(err) { this.log.error( ' homekit2reading: ' + err ); //delete mapping.homekit2reading; } } else if( typeof this.jsFunctions === 'object' ) { if( typeof this.jsFunctions[mapping.homekit2reading] === 'function' ) mapping.homekit2reading = this.jsFunctions[mapping.homekit2reading].bind(null,mapping); else this.log.error( ' homekit2reading: no function named ' + mapping.homekit2reading + ' in ' + util.inspect(this.jsFunctions) ); } if( mapping.homekit2reading !== undefined && typeof mapping.homekit2reading !== 'function' ) { this.log.error( ' homekit2reading disabled.' ); delete mapping.reading2homekit; } } } FHEM.prototype.updateAlexaDevice = function() { this.execute( 'jsonlist2 TYPE=alexa', function(result) { try { var d = JSON.parse( result ); if( d.totalResultsReturned === 1 ) { this.alexa_device = d.Results[0]; this.log.info( 'alexa device is '+ this.alexa_device.Name ); this.alexa_device.Attributes.genericDeviceType = 'switch'; this.xxx = new FHEMDevice(this, this.alexa_device); this.xxx.fromHomebridgeMapping( this.alexa_device.Attributes.alexaMapping ); for( let characteristic_type in this.xxx.mappings ) { var mappings = this.xxx.mappings[characteristic_type]; if( characteristic_type === "ModeController") mappings = Object.values(mappings.instance) if( !Array.isArray(mappings) ) mappings = [mappings]; for( let mapping of mappings ) { var device = this.device; if( mapping.device === undefined ) mapping.device = device; else device = mapping.device; if( mapping.reading === undefined && mapping.default === undefined ) mapping.reading = 'state'; //mapping.characteristic = this.characteristicOfName(characteristic_type); mapping.informId = device +'-'+ mapping.reading; mapping.characteristic_type = characteristic_type; mapping.log = this.log; this.xxx.prepare( mapping ); } } this.alexaMapping = this.xxx.mappings; this.alexaTypes = this.alexa_device.Attributes.alexaTypes; this.echoRooms = this.alexa_device.Attributes.echoRooms; this.persons = this.alexa_device.Attributes.persons; this.fhemIntents = this.alexa_device.Attributes.fhemIntents; this.alexaConfirmationLevel = this.alexa_device.Attributes.alexaConfirmationLevel; this.alexaStatusLevel = this.alexa_device.Attributes.alexaStatusLevel; delete this.alexa_device.proactiveEvents if( this.alexa_device.Attributes.alexaProactiveEvents ) this.alexa_device.proactiveEvents = parseInt(this.alexa_device.Attributes.alexaProactiveEvents) == true; delete this.xxx; this.execute( '{$defs{"'+ this.alexa_device.Name +'"}->{"alexa-fhem version"} = "'+ version +'"}' ); this.emit( 'ALEXA DEVICE', this.alexa_device.Name ); } else { delete this.alexa_device; this.emit( 'ALEXA DEVICE' ); this.log.warn( 'no alexa device found. please define it.' ); } } catch(err) { this.log.error( err ); this.log.error( 'failed to parse '+ result ); } }.bind(this) ); }