alexa-fhem
Version:
a fhem skill for amazon alexa
1,439 lines (1,174 loc) • 105 kB
JavaScript
'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'
+ '×tamp='+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'
+ '×tamp='+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) );
}