iobroker.fritzbox
Version:
Verarbeitet die Callmontiorinformationen der AVM Fritzbox.
1,208 lines (1,092 loc) • 70.4 kB
JavaScript
/**
*
* ioBroker fritzbox Adapter
*
* (c) 2015 Michael Herwig <ruhr@digheim.de>
*
* MIT License
*
*
* Callmonitor an der Fritzbox einschalten:
* Wenn man beim Telefon #96*5* eintippt, wird der TCP-Port 1012 geoffnet.
* Mit #96*4* wird dieser wieder geschlossen.
*
* Format der Fritzbox-Meldungen:
*
* Ausgehende Anrufe: datum;CALL; CallID;Nebenstelle;callingNumber;calledNumber;lineType;
* Eingehende Anrufe: datum;RING; CallID;callingNumber;calledNumber;lineTpe;
* Zustandegekommene Verbindung: datum;CONNECT; CallID;Nebenstelle;connectedNumber;
* Ende der Verbindung: datum;DISCONNECT;CallID;dauerInSekunden;
*
*/
// TODO Wie können auf der Adminseite die Werte vor Änderung (onChange) kontrolliert und ggf. markiert werden?
// TODO Leistungsmerkmale: (2) Fritzbox Telefonbuch importieren und verarbeiten
// TODO Anruferlisten persistent realisieren (nicht die erstellten Tabellen, sonder die die Arrays mit den Listeneinträgen (historyListAllHtml, historyListAllTxt, historyListAllJson, historyListMissedHtml)
// -> ggf. Redesign. Nicht verschiedene Listen, sondern eine JSON und die Funktionen so ändern, dass makeList auf diese eine JSON aufbaut
'use strict'; // verlangt saubereren Code (nach ECMAScript 5) - EMPFEHLUNG: beim Programmieren rein, im fertigen Programm raus
// require() ist eine Funktion von node.js, um weitere Module nachzuladen
const utils = require('@iobroker/adapter-core'); // Get common adapter utils
const xml2js = require('xml2js'); // node-Modul um xml Strukturen in JSON umzuwandeln. Eine Beschreibung: http://www.flyacts.com/blog/nodejs-xml-daten-verarbeiten/
// Bei der Entwicklung ins iobroker.frittbox Verzeichnis installieren. Als Admin, d.h. beim Mac mit sudo:
// sudo npm install xml2js --save
// mit dem --save wird es ins packet.json geschrieben
// vorbereitet, um das Telefonbuch der Fritzbox zu verarbeiten (Export im XML Format)
const net = require('net'); // node-Modul für die tcp/ip Kommunikation
// ist schon in node.js enthalten
// Beschreibung: https://nodejs.org/api/net.html
const tr = require('tr-064'); // node-Modul für die Kommunikation via TR-064 mit der FritzBox
// Beschreibung zu TR-064: http://avm.de/service/schnittstellen/
const https = require('https');
const request = require('request');
const { existsSync, mkdirSync, readdir, unlink, createWriteStream } = require('fs');
const path = require('path');
const adapter = utils.Adapter('fritzbox');
var call = [];
// Werte werden über die Adminseite gesetzt
var // adapter.config.fritzboxAddress // ip-Adresse der Fritzbox
configCC = '', // eigene Landesvorwahl ohne führende 0
configAC = '', // eigene Ortsvorwahl ohne führende 0
configUnknownNumber = '', // "### ? ###"wie soll eine unbekannte/unterdrückte externe Rufnummer angezeigt werden
configNumberLength = 15, // (df=15) max. Stellen externer Rufnummern, die in den Tabellen HTML und TXT angezeigt werden
configExternalLink = true, // externe Rufnummer als wählbaren Link (tel:)
configShowHeadline = true, // Anruferliste mit überschrift?
showHistoryAllTableTxt = true,
showHistoryAllTableHTML = true,
showHistoryAllTableJSON = true,
showCallmonitor = true,
showMissedTableHTML = true,
showMissedTableJSON = true;
// restlichen Config Werte
var configDeltaTimeOKSec = 5, // Abweichung in Sekunden zwischen der Systemzeit von ioBroker und der Meldungszeit aus der Fritzbox, die noch als OK gilt
configHistoryAllLines = 12, // Anzahl der Einträge in der Anruferliste gesamt
configHistoryMissedLines = 10; // Anzahl der Einträge in der Anruferliste verpasste Anrufe
var ringLastNumber = '',
ringLastNumberTel = '',
ringLastMissedNumber = '',
ringLastMissedNumberTel = '',
// ringActualNumber = "",
// ring = false,
callLastNumber = '',
callLastNumberTel = '',
// connectNumber = "",
missedCount = 0,
historyListAllHtml = [],
historyListAllTxt = [],
historyListAllJson = [],
historyListMissedJson = [],
historyListMissedHtml = [];
var objMissedCount = 0;
var headlineDate = 'Tag Zeit ', // mit utf-8 nbsp
headlineExtnumber = 'ext. Rufnr.',
headlineDirection = 'g<>k',
headlineExtension = 'Nbst',
headlineOwnnumber = 'Eigenes Amt',
headlineLine = 'Ltg.',
headlineDuration = ' Dauer', // mit utf-8 nbsp
nbsp = ' ', // utf-8 nbsp
headlineTableAllTxt = '',
headlineTableAllHTML = '',
headlineTableMissedHTML = '';
// Liste der aktiven RINGs, CALLs, CONNECTs
var listRing = [],
listCall = [],
listConnect = [],
listAll = [];
// für den 1-Sekundentimer für den Callmonitor
var intervalRunningCall = null;
// für die TR-064-Abfragen
var intervalTR046 = null;
var wlanState = null;
var connecting = null;
var socketBox = null;
var instanceDir = null;
adapter.on('message', function (_obj) {
// if (obj) processMessage(obj);
// processMessages();
adapter.log.debug('adapter.on-message: << MESSAGE >>');
});
// hier startet der Adapter
adapter.on('ready', function () {
adapter.log.debug('adapter.on-ready: << READY >>');
main();
});
// hier wird der Adapter beendet
adapter.on('unload', function (callback) {
adapter.log && adapter.log.debug('adapter.on-unload: << UNLOAD >>');
clearRealtimeVars();
if (connecting) {
clearInterval(connecting);
connecting = null;
}
if (intervalRunningCall) {
clearInterval(intervalRunningCall);
intervalRunningCall = null;
}
if (intervalTR046) {
clearInterval(intervalTR046);
intervalTR046 = null;
}
if (socketBox) {
try {
socketBox.close();
} catch {
// ignroe
}
}
callback && callback();
});
// is called if a subscribed state changes
adapter.on('stateChange', function (id, state) {
// adapter.log.debug('adapter.on-stateChange: << stateChange >>');
if (!state) {
return;
}
if (id === `${adapter.namespace}.calls.missedCount`) {
// New value of
// adapter.log.debug("state.val: " + state.val);
if (state.val == 0 || state.val == '0 ') {
adapter.setState('calls.missedDateReset', dateNow(), true);
adapter.log.debug(`missed calls: counter zurückgesetzt ${dateNow()}`);
}
} else if (id === `${adapter.namespace}.wlan.enabled` && !state.ack) {
adapter.log.debug(`${id}=${state.val}`);
if (state.val != wlanState && intervalTR046 && adapter.config.enableWlan) {
adapter.log.info(`Changing WLAN to ${state.val}`);
setWlanEnabled(
adapter.config.fritzboxAddress,
adapter.config.fritzboxUser,
adapter.config.fritzboxPassword,
state.val,
);
}
}
});
function fritzboxDateEpoch(date) {
date = date || '';
var year = `20${date.substring(6, 8)}`, // Achtung: ab dem Jahr 2100 erzeugt die Funktion ein falsches Datum ;-)
month = parseInt(date.substring(3, 5)) - 1,
day = date.substring(0, 2),
hour = date.substring(9, 11),
minute = date.substring(12, 14),
second = date.substring(15, 17),
time = new Date(year, month, day, hour, minute, second);
return Date.parse(time); // fritzbox message date in epoch
}
function dateEpochNow() {
var now = new Date();
now = Date.parse(now); // aktuelle Zeit in epoch
return now;
}
function dateNow() {
var date = new Date(dateEpochNow()),
year = date.getFullYear(),
month = date.getMonth() + 1,
day = date.getDate(),
hour = date.getHours(),
minute = date.getMinutes(),
second = date.getSeconds();
if (month.toString().length === 1) {
month = `0${month}`;
}
if (day.toString().length === 1) {
day = `0${day}`;
}
if (hour.toString().length === 1) {
hour = `0${hour}`;
}
if (minute.toString().length === 1) {
minute = `0${minute}`;
}
if (second.toString().length === 1) {
second = `0${second}`;
}
return `${day}.${month}.${year} ${hour}:${minute}`;
}
function fill(n, str) {
// liefere Anzahl n nbsp in utf-8, wenn str nicht angegeben oder n-mal str
var space = '';
for (var i = 0; i < n; ++i) {
space += !str ? ' ' : str; // als utf-8 Code (Mac: alt+Leerzeichen) TODO: wie kann das nbsp-Leerzeichen in Wbstorm sichtbar gemacht werden
}
return space;
}
function dynamicSort(property) {
var sortOrder = 1;
if (property[0] === '-') {
sortOrder = -1;
property = property.substr(1);
}
return function (a, b) {
var result = a[property] < b[property] ? -1 : a[property] > b[property] ? 1 : 0;
return result * sortOrder;
};
}
function numberFormat(number, length, align) {
// bringt die Rufnummer auf die konfigurierte Länge (zu kurz: mit utf-8 Non-breaking-space auffüllen // zu lang: kürzen und als letze "Ziffer" ein x ("x" nur, wenn es sich um Zahlen handelt).
var onlyNumbers = /\D+/g;
if (!number.match(onlyNumbers)) {
// wenn der String numbers nur Ziffern enthält und diese in der Anzahl zu lang sind, kürzen und die letzte Ziffer gegen ein "x" ersetzen
if (number.length > length) {
number = `${number.substring(0, length - 1)}x`;
}
}
number = number.substring(0, length); // sollte die externen numbern insgesamt länger sein, werden diese auf die max. gewünschte Länge gekürzt
if (align !== 'r') {
number = number + fill(length - number.length); // richtet linksbündig aus und füllt nbsp; auf
} else {
number = fill(length - number.length) + number; // wenn allign = "r" wird rechtsbündig ausgerichtet
}
var i = 0;
while (number.search(' ') >= 0) {
number = number.replace(' ', ' '); // alle normalen Leerzeichen gegen utf-8 non breaking spaces ersetzen
if (!i) {
adapter.log.debug(`Leerzeichen gegen utf-8 non breaking space ersetzt: <${number}>`);
}
i++;
if (i > 40) {
adapter.log.warn(`function numberFormat: zu langer Sting: ${number}`);
break;
}
}
return number;
}
function e164(extrnr) {
// wandelt eine externe Rufnummer in das e164 Format, z.B. rufnummer 4711321 wird zu +492114711321 (in Abhängigkeit des konfigurierten CC und AC)
extrnr = extrnr.toString().replace(/\D+/g, ''); // nur Ziffern behalten, Rest kürzen
if (extrnr === configUnknownNumber) {
return extrnr;
}
if (extrnr.length > 0) {
var i;
var extrnr0 = 0;
for (i = 0; i < extrnr.length; i++) {
if (extrnr.substr(i, i + 1) != '0') {
break;
}
extrnr0++;
}
extrnr = extrnr.slice(extrnr0, extrnr.length);
if (extrnr0 == 0) {
extrnr = `+${configCC}${configAC}${extrnr}`;
}
if (extrnr0 == 1) {
extrnr = `+${configCC}${extrnr}`;
}
if (extrnr0 == 2) {
extrnr = `+${extrnr}`;
}
if (extrnr0 < 0 && extrnr0 > 2) {
adapter.log.error('fritzbox - ungültige externe Rufnummer, Anzahl führender Nullen prüfen');
}
validateE164(extrnr);
}
return extrnr;
}
function telLink(e164, formattedNumber) {
// erstellt aus einer e164 Rufnummer einen tel: link. Wird als 2. Parameter eine formatierte Rufnummer übergeben, wird diese dargestellt.
var link = '';
if (!formattedNumber) {
link = `<a style="text-decoration: none;" href="tel:${e164}">${e164}</a>`;
} else {
link = `<a style="text-decoration: none;" href="tel:${e164}">${formattedNumber}</a>`;
}
return link;
}
function validateE164(e164) {
// testet auf eine gültige internationale Rufnummer mit + am Anfang
var regex = /^\+(?:[0-9] ?){6,14}[0-9]$/;
if (regex.test(e164)) {
// Valid international phone number
return true;
}
// Invalid international phone number
adapter.log.warn(`${e164} is not a vail int. phone number. Please check your configuration (Country + Area Code).`);
return false;
}
function fritzboxDateToTableDate(fritzboxDate) {
var month = fritzboxDate.substring(3, 5);
var day = fritzboxDate.substring(0, 2);
var hour = fritzboxDate.substring(9, 11);
var minute = fritzboxDate.substring(12, 14);
var date = `${day}.${month}.${nbsp}${hour}:${minute}${nbsp}${nbsp}`; // space = non-breaking-space in utf-8
return date;
}
function durationForm(duration) {
// Dauer in Sekunden formatiert zu einem 7-stelligen String:
// " -" = 0 Sek.
// " 5" = einstellige Sekunde
// " 27" = zweistellige Sekunden
// " 1:41" = einstellige Minuten
// " 59:32" = zweistellige Minuten
// "8:21:44" = mehr als eine Stunde, weniger als 10h
// " >10 h" = mehr als 10h
if (duration === '') {
duration = fill(7);
return duration;
}
var durationMin = Math.floor(parseInt(duration) / 60);
var durationSek = parseInt(duration) % 60;
var durationStd = Math.floor(durationMin / 60);
durationMin %= 60;
if (durationStd < 1) {
if (durationMin < 1) {
duration = durationSek;
} else {
duration = `${durationMin}:${fill(2 - durationSek.toString().length, '0')}${durationSek}`;
}
} else {
duration = `${durationStd}:${fill(2 - durationMin.toString().length, '0')}${durationMin}:${fill(2 - durationSek.toString().length, 0)}${durationSek}`;
}
duration = duration.toString();
if (duration == '0') {
duration = '-';
}
if (duration.length > 7) {
duration = '> 10h';
}
duration = fill(7 - duration.toString().length) + duration; // auf 7-Stellen auffüllen
return duration;
}
function getEventsList(anruferliste) {
var text = '';
for (var i = 0; i < anruferliste.length; i++) {
text += `${text ? '<br>\n' : ''}${anruferliste[i]}`;
// text += anruferliste[i] + (text ? '<br>\n' : '');
}
return text;
}
function makeList(list, line, headline, howManyLines, showHeadline) {
var lines = 0;
if (showHeadline === true) {
list.shift(); // erste Element im Array löschen (Kopfzeile)
list.unshift(headline, line); // (Kopfzeile und aktueller Eintrag an Stelle 1 und 2 hinzufügen)
lines = 1;
} else {
list.unshift(line);
}
if (list.length > howManyLines + lines) {
list.length = howManyLines + lines;
}
return getEventsList(list);
}
function callmonitor(list) {
// Liste aktueller Anrufe (RING, CONNECT und CALL)
var txt = '';
for (var i = 0; i < list.length; i++) {
txt += `${fritzboxDateToTableDate(call[list[i].id].dateStart)}${nbsp}${call[list[i].id].externalNumberForm}`;
if (call[list[i].id].type === 'CONNECT') {
txt += durationForm(call[list[i].id].durationSecs2);
}
if (call[list[i].id].type === 'RING') {
txt += durationForm(call[list[i].id].durationRingSecs);
}
if (i < list.length - 1) {
txt += txt ? '<br>\n' : '';
}
}
return txt;
}
function callmonitorAll(list) {
var txt = '';
for (var i = 0; i < list.length; i++) {
var id = list[i].id;
var intNr = fill(4 - call[id].extensionLine.length) + call[id].extensionLine;
if (call[id].callSymbol == ' -> ') {
intNr = '<span style="color:red ">RING</span>';
}
txt += `${fritzboxDateToTableDate(call[id].dateStart)}${nbsp}${call[id].externalNumberForm}`;
txt += `${call[id].callSymbolColor}${nbsp}`;
txt += intNr;
if (call[id].type === 'CONNECT') {
txt += `${nbsp}<span style="color:green"><b>${durationForm(call[id].durationSecs2)}</b></span>`;
}
if (call[id].type === 'RING') {
txt += `${nbsp}<span style="color:red ">${durationForm(call[id].durationRingSecs)}</span>`;
}
if (i < list.length - 1) {
txt += txt ? '<br>\n' : '';
}
}
return txt;
}
function headlineMissedHTML() {
var headlineHistoryMissedHTML = `<b>${headlineDate}${headlineExtnumber}${nbsp}${headlineOwnnumber}</b>`;
return headlineHistoryMissedHTML;
}
function headlineAllTxt() {
// Überschrift formatieren
headlineDate = numberFormat(headlineDate, 14);
headlineExtnumber = numberFormat(headlineExtnumber, configNumberLength);
headlineDirection = numberFormat(headlineDirection, 4);
headlineExtension = numberFormat(headlineExtension, 5, 'r'); // 5 Zeichen lang, rechtsbündig
headlineOwnnumber = numberFormat(headlineOwnnumber, configNumberLength);
headlineLine = numberFormat(headlineLine, 4);
headlineDuration = numberFormat(headlineDuration, 7, 'r'); // 7 Zeichen lang, rechtsbündig
// Überschrift Anruferliste gesamt (txt)
var headlineHistoryAllTxt =
headlineDate +
headlineExtnumber +
nbsp +
headlineDirection +
headlineExtension +
nbsp +
headlineOwnnumber +
nbsp +
headlineLine +
nbsp +
headlineDuration;
return headlineHistoryAllTxt;
}
function headlineAllHTML() {
// Überschrift Anruferliste gesamt (html)
var headlineHistoryAllHTML = `<b>${headlineAllTxt()}</b>`;
return headlineHistoryAllHTML;
}
function clearRealtimeVars() {
// da die Daten nach Start des Adapters oder neuer IP-Verbindung zur Fritzbox nicht konsitent sind, werden die Callmonitore gelöscht
// Callmonitore löschen
if (showCallmonitor) {
adapter.setState('callmonitor.ring', '', true);
adapter.setState('callmonitor.call', '', true);
adapter.setState('callmonitor.connect', '', true);
adapter.setState('callmonitor.all', '', true);
}
//Realtime Daten löschen
adapter.setState('calls.ring', false, true);
adapter.setState('calls.ringActualNumber', '', true);
adapter.setState('calls.ringActualNumbers', '', true);
adapter.setState('calls.connectNumber', '', true);
adapter.setState('calls.connectNumbers', '', true);
adapter.setState('calls.counterActualCalls.ringCount', 0, true);
adapter.setState('calls.counterActualCalls.callCount', 0, true);
adapter.setState('calls.counterActualCalls.connectCount', 0, true);
adapter.setState('calls.counterActualCalls.allActiveCount', 0, true);
// Liste der aktiven RINGs, CALLs, CONNECTs
listRing = [];
listCall = [];
listConnect = [];
listAll = [];
}
function adapterSetOnChange(object, value) {
// ioBroker Objekte schreiben aber nur, wenn der Wert geändert wurde
adapter.getState(object, function (err, state) {
if (!err && state) {
if (state.val !== value || !state.ack) {
adapter.setState(object, value, true);
}
}
});
}
function adapterSetOnUndefined(object, value) {
adapter.getState(object, function (err, state) {
if (!err && state) {
var newValue = state.val || value;
if (state.val == newValue) {
return;
} // vorhandene Werte nicht noch einmal schreiben
adapter.setState(object, newValue, true);
// adapter.log.info("Objekt: " + object + " Wert: " + state.val + " -> " + newValue + " INIT: " +value);
} else {
adapter.setState(object, value, true);
}
});
}
function initVars() {
// alte Zustände übernehmen oder bei Erstinitalisierung sinnvolle Werte
adapterSetOnUndefined('calls.missedCount', 0);
adapterSetOnUndefined('calls.missedDateReset', dateNow());
adapterSetOnUndefined('calls.ringLastNumber', '');
adapterSetOnUndefined('calls.ringLastMissedNumber', '');
adapterSetOnUndefined('calls.callLastNumber', '');
adapterSetOnUndefined('telLinks.ringLastNumberTel', '');
adapterSetOnUndefined('telLinks.ringLastMissedNumberTel', '');
adapterSetOnUndefined('telLinks.callLastNumberTel', '');
adapterSetOnUndefined('system.deltaTime', 0);
adapterSetOnUndefined('system.deltaTimeOK', true);
// config aus der Adapter Admin-Webseite übernehmen
configCC = adapter.config.cc; // "49", // eigene Landesvorwahl ohne führende 0
configAC = adapter.config.ac; // "211", // eigene Ortsvorwahl ohne führende 0
configUnknownNumber = numberFormat(adapter.config.unknownNumber, adapter.config.unknownNumber.length); // Leerzeichen gegen utf-8 nbsp ersetzen
configNumberLength = adapter.config.numberLength;
if (configNumberLength < 4) {
adapter.log.warn(`Rufnummernlänge zu klein gewählt, geändert von ${configNumberLength} auf 4`);
configNumberLength = 4;
}
if (configNumberLength > 30) {
adapter.log.warn(`Rufnummernlänge zu groß gewählt, geändert von ${configNumberLength} auf 30`);
configNumberLength = 30;
}
configExternalLink = adapter.config.externalLink;
configShowHeadline = adapter.config.showHeadline;
showHistoryAllTableTxt = adapter.config.showHistoryAllTableTxt;
showHistoryAllTableHTML = adapter.config.showHistoryAllTableHTML;
showHistoryAllTableJSON = adapter.config.showHistoryAllTableJSON;
showCallmonitor = adapter.config.showCallmonitor;
showMissedTableHTML = adapter.config.showMissedTableHTML;
showMissedTableJSON = adapter.config.showMissedTableJSON;
// vorhandene Werte aus den ioBroker Objekte des Fritzbox Adapters rauslesen und in die globalen Variablen übernehmen
adapter.getState('calls.missedCount', function (err, state) {
if (!err && state) {
objMissedCount = state.val;
missedCount = state.val;
}
});
if (adapter.config.showHeadline) {
headlineTableAllTxt = headlineAllTxt();
headlineTableAllHTML = headlineAllHTML();
headlineTableMissedHTML = headlineMissedHTML();
} else {
headlineTableAllTxt = '';
headlineTableAllHTML = '';
headlineTableMissedHTML = '';
}
if (!showHistoryAllTableTxt) {
adapter.setState('history.allTableTxt', 'deactivated', true);
} else {
adapter.setState('history.allTableTxt', headlineTableAllTxt, true);
}
if (!showHistoryAllTableJSON) {
adapter.setState('history.allTableJSON', 'deactivated', true);
}
if (!showHistoryAllTableHTML) {
adapter.setState('history.allTableHTML', 'deactivated', true);
} else {
adapter.setState('history.allTableHTML', headlineTableAllHTML, true);
}
if (!showMissedTableHTML) {
adapter.setState('history.missedTableHTML', 'deactivated', true);
} else {
adapter.setState('history.missedTableHTML', headlineTableMissedHTML, true);
}
if (!showMissedTableJSON) {
adapter.setState('history.missedTableJSON', 'deactivated', true);
}
if (!showCallmonitor) {
adapter.setState('callmonitor.connect', 'deactivated', true);
adapter.setState('callmonitor.ring', 'deactivated', true);
adapter.setState('callmonitor.call', 'deactivated', true);
adapter.setState('callmonitor.all', 'deactivated', true);
} else {
adapter.setState('callmonitor.connect', '', true);
adapter.setState('callmonitor.ring', '', true);
adapter.setState('callmonitor.call', '', true);
adapter.setState('callmonitor.all', '', true);
}
adapter.setState('wlan.enabled', false, true);
}
function parseData(message) {
adapter.log.info(`data from ${adapter.config.fritzboxAddress}: ${message}`);
message = message.toString('utf8');
adapter.setState('message', message, true);
var obj = message.split(';'); // speichert die Felder der Meldung in ein Array mit dem Namen "obj"
var id = obj[2];
call[id] = call[id] || {};
var ringCount = 0,
callCount = 0,
connectCount = 0,
allActiveCount = 0,
ring = null,
ringActualNumber = '';
var ringActualNumbers = [],
connectNumbers = [],
connectNumber = '';
var cssGreen = '<span style="color:green">',
cssRed = '<span style="color:red ">',
cssBlack = '<span style="color:black">',
cssColor = cssGreen,
cssEnd = '</span>';
// ########### Auswertung und Aufbau der Anruferinformationen aus der Fritzbox ###########
// für alle Anruftypen identisch
call[id].date = obj[0]; // 01.07.15 12:34:56 - dd.mm.yy hh:mm:ss
call[id].dateEpoch = fritzboxDateEpoch(obj[0]); // fritzbox time
call[id].dateEpochNow = dateEpochNow(); // system time
call[id].deltaTime = (call[id].dateEpochNow - call[id].dateEpoch) / 1000; // delta Time beetween system and fritzbox
call[id].deltaTimeOK = call[id].deltaTime < configDeltaTimeOKSec;
call[id].type = obj[1]; // CALL (outgoing), RING (incoming), CONNECT (start), DISCONNECT (end)
call[id].id = obj[2]; // call identifier as integer
// Outgoing call
if (call[id].type === 'CALL') {
call[id].extensionLine = obj[3]; // used extension line
call[id].ownNumber = obj[4]; // calling number - used own number
call[id].externalNumber = obj[5]; // called number
call[id].lineType = obj[6]; // line type
call[id].durationSecs = null; // duration of the connect in sec
call[id].durationForm = null; // duration of the connect in sec
call[id].durationSecs2 = ''; // duration of the connect in sec, count during connect
call[id].durationRingSecs = ''; // duration ringing time in sec
call[id].connect = false;
call[id].direction = 'out';
call[id].dateStartEpoch = fritzboxDateEpoch(obj[0]);
call[id].dateConnEpoch = null;
call[id].dateEndEpoch = null;
call[id].dateStart = obj[0];
call[id].dateConn = null;
call[id].dateEnd = null;
call[id].callSymbol = ' <- '; // utf-8 nbsp
call[id].callSymbolColor = cssBlack + call[id].callSymbol + cssEnd;
} else if (call[id].type === 'RING') {
// Incoming (RING)
// Incoming (RING)
call[id].extensionLine = ''; // used extension line
call[id].ownNumber = obj[4]; // called number - used own number
call[id].externalNumber = obj[3]; // calling number
call[id].lineType = obj[5]; // line type
call[id].durationSecs = null; // duration of the connect in sec
call[id].durationForm = null;
call[id].durationSecs2 = ''; // duration of the connect in sec, count during connect
call[id].durationRingSecs = 0; // duration of ringing time in sec
call[id].connect = false;
call[id].direction = 'in';
call[id].dateStartEpoch = fritzboxDateEpoch(obj[0]);
call[id].dateConnEpoch = null;
call[id].dateEndEpoch = null;
call[id].dateStart = obj[0];
call[id].dateConn = null;
call[id].dateEnd = null;
call[id].callSymbol = ' -> '; // utf-8 nbsp
call[id].callSymbolColor = cssBlack + call[id].callSymbol + cssEnd;
} else if (call[id].type === 'CONNECT') {
// Start of connect
// Start of connect
call[id].extensionLine = obj[3]; // used extension line
call[id].ownNumber = call[id].ownNumber || '????????'; // used own number
call[id].externalNumber = obj[4]; // connected number
call[id].lineType = call[id].lineType || '????'; // line type
call[id].durationSecs = null; // duration of the connect in sec
call[id].durationForm = null;
call[id].durationSecs2 = 0; // duration of the connect in sec, count during connect
call[id].durationRingSecs = call[id].durationRingSecs || ''; // duration of ringing time in sec
call[id].connect = true;
call[id].direction = call[id].direction || '?';
call[id].dateStartEpoch = call[id].dateStartEpoch || null;
call[id].dateConnEpoch = fritzboxDateEpoch(obj[0]);
call[id].dateEndEpoch = null;
call[id].dateStart = call[id].dateStart || obj[0];
call[id].dateConn = obj[0];
call[id].dateEnd = null;
if (call[id].direction === 'in') {
call[id].callSymbol = ' ->>';
} else {
call[id].callSymbol = '<<- '; // utf-8 nbsp
}
call[id].callSymbolColor = `${cssGreen}<b>${call[id].callSymbol}</b>${cssEnd}`;
} else if (call[id].type === 'DISCONNECT') {
// End of call
// End of call
call[id].extensionLine = call[id].extensionLine || ''; // used extension line
call[id].ownNumber = call[id].ownNumber || '????????'; // used own number
call[id].externalNumber = call[id].externalNumber || '????????'; // connected number
call[id].lineType = call[id].lineType || '????'; // line type
call[id].durationSecs = obj[3]; // call duration in seconds
call[id].durationForm = durationForm(obj[3]);
call[id].durationSecs2 = call[id].durationSecs; // duration of the connect in sec
call[id].durationRingSecs = call[id].durationRingSecs || ''; // duration of ringing time in sec
//call[id].connect = call[id].connect || null;
call[id].direction = call[id].direction || '?';
call[id].dateStartEpoch = call[id].dateStartEpoch || fritzboxDateEpoch(obj[0]);
call[id].dateConnEpoch = call[id].dateConnEpoch || fritzboxDateEpoch(obj[0]);
call[id].dateEndEpoch = fritzboxDateEpoch(obj[0]);
call[id].dateStart = call[id].dateStart || obj[0];
call[id].dateConn = call[id].dateConn || obj[0];
call[id].dateEnd = obj[0];
if (call[id].connect === false) {
cssColor = cssRed;
if (call[id].direction === 'in') {
call[id].callSymbol = ' ->X';
} else {
call[id].callSymbol = 'X<- '; // utf-8 nbsp
}
}
call[id].callSymbol = call[id].callSymbol || '????';
if (call[id].callSymbol === '????') {
cssColor = cssBlack;
}
call[id].callSymbolColor = `${cssColor}<b>${call[id].callSymbol}</b>${cssEnd}`;
} else {
adapter.log.error(`adapter fritzBox unknown event type ${call[id].type}`);
return;
}
// für alle Anruftypen identisch
call[id].unknownNumber = false;
if (
call[id].externalNumber === '' ||
call[id].externalNumber === null ||
call[id].externalNumber === configUnknownNumber ||
call[id].externalNumber === '????????'
) {
if (call[id].externalNumber !== '????????') {
call[id].externalNumber = configUnknownNumber;
}
call[id].unknownNumber = true;
}
call[id].ownNumberForm = numberFormat(call[id].ownNumber, configNumberLength);
call[id].externalNumberForm = numberFormat(call[id].externalNumber, configNumberLength);
call[id].ownNumberE164 = e164(call[id].ownNumber);
call[id].externalE164 = e164(call[id].externalNumber);
if (call[id].unknownNumber) {
call[id].externalTelLink = call[id].externalNumberForm;
call[id].externalTelLinkCenter = call[id].externalNumber;
} else {
call[id].externalTelLink = telLink(call[id].externalE164, call[id].externalNumberForm);
call[id].externalTelLinkCenter = telLink(call[id].externalE164, call[id].externalNumber);
}
/*
adapter.log.debug("fritzBox event (date: " + call[id].date +
", dateEpoch: " + call[id].dateEpoch +
", type: " + call[id].type +
", ID: " + call[id].id +
", exLine: " + call[id].extensionLine +
", extern: " + call[id].externalNumber +
", own: " + call[id].ownNumber +
", duration: " + call[id].durationSecs
);
adapter.log.debug("fritzBox event (delta time: " + call[id].deltaTime +
", delta time ok: " + call[id].deltaTimeOK +
", symbol: " + call[id].callSymbol +
", ext E164: " + call[id].externalE164 +
", ext Tel Link: " + call[id].externalTelLink
);
*/
// Listen aktuelle Gesprächszustände nach jeder Fritzbox Message, neuaufbauen und damit aktualisieren)
listRing = [];
listCall = [];
listConnect = [];
listAll = [];
// Schleife, Anzahl der Call IDs im Array: Zählt die jeweiligen Zustände, ein Array je Zustand
for (var i = 0; i < call.length; i++) {
if (call[i] != null) {
// Call Status = RING
if (call[i].type === 'RING') {
listRing[ringCount] = listRing[ringCount] || {};
listRing[ringCount].id = call[i].id;
listRing[ringCount].dateStartEpoch = call[i].dateStartEpoch;
ringCount++;
}
// Call Status = CALL
if (call[i].type === 'CALL') {
listCall[callCount] = listCall[callCount] || {};
listCall[callCount].id = call[i].id;
listCall[callCount].dateStartEpoch = call[i].dateStartEpoch;
callCount++;
}
// Call Status = CONNECT
if (call[i].type === 'CONNECT') {
listConnect[connectCount] = listConnect[connectCount] || {};
listConnect[connectCount].id = call[i].id;
listConnect[connectCount].dateStartEpoch = call[i].dateStartEpoch;
connectCount++;
}
// Alle Status (RING, CALL, CONNECT)
if (call[i].type !== 'DISCONNECT') {
listAll[allActiveCount] = listAll[allActiveCount] || {};
listAll[allActiveCount].id = call[i].id;
listAll[allActiveCount].dateStartEpoch = call[i].dateStartEpoch;
allActiveCount++;
}
}
}
adapter.log.debug(
`ringCount: ${ringCount}, callCount: ${callCount}, connectCount: ${connectCount}, allActiveCount: ${allActiveCount}`,
);
// sortiert die ermittelten Listen nach der tatsächlichen Meldungszeit (Systemzeit)
listRing.sort(dynamicSort('-dateStartEpoch')); // Liste sortieren: jüngster Eintrag oben
listCall.sort(dynamicSort('-dateStartEpoch')); // Liste sortieren: jüngster Eintrag oben
listConnect.sort(dynamicSort('-dateStartEpoch')); // Liste sortieren: jüngster Eintrag oben
listAll.sort(dynamicSort('-dateStartEpoch')); // Liste sortieren: jüngster Eintrag oben
// aktuellste Anrufernummer wird als aktueller und neuester Anrufer (Ring) gespeichert (es kann noch mehr aktive RINGs geben, diese werden in "ringActualNumbers" gespeichert)
if (listRing[0] != null) {
ringActualNumber = call[listRing[0].id].externalNumber;
ring = true;
}
// kein aktiver Ring, ringAktuell löschen && ring = false (kein Ring aktiv)
if (ringCount < 1) {
ringActualNumber = '';
ring = false;
}
if (listConnect[0] != null) {
connectNumber = call[listConnect[0].id].externalNumber;
}
objMissedCount = missedCount; // den alten errechneten Wert der verpassten Anrufe sichern
// aktuellen Wert Anzahl verpasste Anrufe (missedCount aus den ioBroker Objekten auslesen)
adapter.getState('calls.missedCount', function (err, state) {
if (!err && state) {
missedCount = state.val;
}
});
if (call[id].type === 'DISCONNECT') {
if (call[id].direction === 'in') {
ringLastNumber = call[id].externalNumber; // letzter Anrufer
adapter.setState('calls.ringLastNumber', ringLastNumber, true);
ringLastNumberTel = call[id].externalTelLinkCenter; // letzter Anrufer als wählbarer Link
adapter.setState('calls.telLinks.ringLastNumberTel', ringLastNumberTel, true);
if (!call[id].connect) {
// letzter Anrufer, der verpasst wurde
ringLastMissedNumber = call[id].externalNumber;
adapter.setState('calls.ringLastMissedNumber', ringLastMissedNumber, true);
ringLastMissedNumberTel = call[id].externalTelLinkCenter;
adapter.setState('calls.telLinks.ringLastMissedNumberTel', ringLastMissedNumberTel, true);
}
// letzter verpasste Anruf wird hochgezählt, Zähler verpasste Anrufe (bis max. 999)
// ioBroker Datenpunkt kann beschrieben und über ioBrkoer zurückgesetzt werden
// das Datum, zu dem der Zähler auf 0 gesett wird, wird in calls.missedDateReset gespeichert.
if (!call[id].connect) {
++missedCount;
if (missedCount > 999) {
missedCount = 999;
}
}
} else if (call[id].direction === 'out') {
callLastNumber = call[id].externalNumber;
adapter.setState('calls.callLastNumber', callLastNumber, true);
callLastNumberTel = call[id].externalTelLinkCenter;
adapter.setState('calls.telLinks.callLastNumberTel', callLastNumberTel, true);
} else {
adapter.log.warn('Adapter starts during call. Some values are unknown.');
}
} // END DISCONNECT
// aktuelle Rufnummeren der gerade laufenden Anrufe (RING)
for (i = 0; i < listRing.length; i++) {
ringActualNumbers.push(call[listRing[0].id].externalNumber);
}
// aktuelle Rufnummern der gerade laufenden Gespräche (CONNECT)
for (i = 0; i < listConnect.length; i++) {
connectNumbers.push(call[listConnect[i].id].externalNumber);
}
// Daten, die bei jeder Meldung aktualisiert werden
adapter.setState('system.deltaTime', call[id].deltaTime, true);
adapter.setState('system.deltaTimeOK', call[id].deltaTimeOK, true);
if (!call[id].deltaTimeOK) {
adapter.log.warn(`delta time between system and fritzbox: ${call[id].deltaTime} sec`);
}
//Auf Änderungen im ioBroker Objekt reagieren und die lokale Variable auch ändern.
if (objMissedCount !== missedCount) {
// Zähler nur schreiben, wenn er sich verändert hat (wg. Traffic Überwachung des Objekts auf Änderung)
adapter.setState('calls.missedCount', missedCount, true);
}
// adapter.log.debug ("objMissedCount: " + objMissedCount + " missedCount: " + missedCount);
//Realtime Daten
adapterSetOnChange('calls.ring', ring);
adapterSetOnChange('calls.ringActualNumber', ringActualNumber);
adapterSetOnChange('calls.ringActualNumbers', ringActualNumbers.join());
adapterSetOnChange('calls.connectNumber', connectNumber);
adapterSetOnChange('calls.connectNumbers', connectNumbers.join());
adapterSetOnChange('calls.counterActualCalls.ringCount', ringCount);
adapterSetOnChange('calls.counterActualCalls.callCount', callCount);
adapterSetOnChange('calls.counterActualCalls.connectCount', connectCount);
adapterSetOnChange('calls.counterActualCalls.allActiveCount', allActiveCount);
// History / Anruferlisten
if (call[id].type === 'DISCONNECT') {
// Datensatz formatieren
var extensionLine = numberFormat(call[id].extensionLine, 5, 'r');
// die externe Rufnummer, je nach Konfiguration als Link (tel:) oder Text
var externalNumber = call[id].externalTelLink;
if (!configExternalLink) {
externalNumber = call[id].externalNumberForm;
}
// Einzelne Datenzeile Anruferliste gesamt (txt)
var lineHistoryAllTxt =
fritzboxDateToTableDate(call[id].date) +
call[id].externalNumberForm +
nbsp +
call[id].callSymbol +
extensionLine +
nbsp +
call[id].ownNumberForm +
nbsp +
call[id].lineType +
nbsp +
call[id].durationForm;
// Einzelne Datenzeile Anruferliste gesamt (html)
var lineHistoryAllHtml =
fritzboxDateToTableDate(call[id].date) +
// call[id].externalTelLink + nbsp +
externalNumber +
nbsp +
call[id].callSymbolColor +
extensionLine +
nbsp +
call[id].ownNumberForm +
nbsp +
call[id].lineType +
nbsp +
call[id].durationForm;
// Datensatz html verpasste Anrufe erstellen && html & json schreiben
if (!call[id].connect) {
if (call[id].direction === 'in') {
// Datensatz verpasste Anrufe
var lineHistoryMissedHtml =
fritzboxDateToTableDate(call[id].date) + externalNumber + nbsp + call[id].ownNumberForm + nbsp;
// aktuelle Call-ID als JSON für verpasste Anrufe wegschreiben
adapter.setState('cdr.missedJSON', JSON.stringify(call[id]), true);
adapter.setState('cdr.missedHTML', lineHistoryMissedHtml, true);
}
}
if (showMissedTableHTML) {
// Anruferliste verpasste Anrufe erstellen
if (!call[id].connect) {
if (call[id].direction === 'in') {
var historyListMissedHtmlStr = makeList(
historyListMissedHtml,
lineHistoryMissedHtml,
headlineTableMissedHTML,
configHistoryMissedLines,
configShowHeadline,
);
adapter.setState('history.missedTableHTML', historyListMissedHtmlStr, true);
}
}
}
if (showMissedTableJSON) {
if (!call[id].connect) {
var lineMissedJson = {
date: call[id].date,
externalNumber: call[id].externalNumber,
callSymbolColor: call[id].callSymbolColor,
extensionLine: call[id].extensionLine,
ownNumber: call[id].ownNumber,
lineType: call[id].lineType,
durationForm: call[id].durationForm,
};
// Anruferliste als JSON auf max Anzahl configHistoryAllLine beschränken
historyListMissedJson.unshift(lineMissedJson);
if (historyListMissedJson.length > configHistoryMissedLines) {
historyListMissedJson.length = configHistoryMissedLines;
}
adapter.setState('history.missedTableJSON', JSON.stringify(historyListMissedJson), true);
}
}
if (showHistoryAllTableHTML) {
// Tabelle html erstellen
var historyListAllHtmlStr = makeList(
historyListAllHtml,
lineHistoryAllHtml,
headlineTableAllHTML,
configHistoryAllLines,
configShowHeadline,
);
adapter.setState('history.allTableHTML', historyListAllHtmlStr, true);
}
if (showHistoryAllTableTxt) {
// Tabelle txt erstellen
var historyListAllTxtStr = makeList(
historyListAllTxt,
lineHistoryAllTxt,
headlineTableAllTxt,
configHistoryAllLines,
configShowHeadline,
);
adapter.setState('history.allTableTxt', historyListAllTxtStr, true);
}
if (showHistoryAllTableJSON) {
var lineHistoryAllJson = {
date: call[id].date,
externalNumber: call[id].externalNumber,
callSymbolColor: call[id].callSymbolColor,
extensionLine: call[id].extensionLine,
ownNumber: call[id].ownNumber,
lineType: call[id].lineType,
durationForm: call[id].durationForm,
};
// Anruferliste als JSON auf max Anzahl configHistoryAllLine beschränken
// historyListAllJson.unshift(call[id]);
historyListAllJson.unshift(lineHistoryAllJson);
if (historyListAllJson.length > configHistoryAllLines) {
historyListAllJson.length = configHistoryAllLines;
}
adapter.setState('history.allTableJSON', JSON.stringify(historyListAllJson), true);
}
// adapter.log.debug("historyListAllJson Obj " + JSON.stringify(historyListAllJson[0]) ); // erste Zeile der JSON ANruferliste im Log
// adapter.log.debug("history all JSON Items: " + historyListAllJson.length ); // Anzahl der JSON Elemente in der Anruferliste
// aktuellen Datensatz des letzten Anrufs als JSON speicherern (für History und die weiteren Verarbeitung in Javascript, usw.)
adapter.setState('cdr.json', JSON.stringify(call[id]), true);
adapter.setState('cdr.html', lineHistoryAllHtml, true);
adapter.setState('cdr.txt', lineHistoryAllTxt, true);
// try to get phonebook
if (call[id].connect && call[id].direction === 'in' && adapter.config.enableTAM) {
getTAM(adapter.config.fritzboxAddress, adapter.config.fritzboxUser, adapter.config.fritzboxPassword);
}
} // End DSICONNECT
if (showCallmonitor) {
// alle drei Callmonitorausgaben mit Sekundenintervall werden auf einmal aktualisiert, auch, wenn ein Anrufzustand keinen aktiven Zähler hat
// wird aus Performancegründen eine Optimierung benötigt, muss es für CONNECT und RING eigenen Intervalle geben
// (weniger Traffic und Speicherplatz) # Hinweis: der Callmonitor kann auch in der Config (webadmin) deaktiviert werden
if (allActiveCount && !intervalRunningCall) {
// Do it every second
intervalRunningCall = setInterval(function () {
// adapter.log.debug("###### callmonitor aktiv ######");
for (var i = 0; i < listConnect.length; i++) {
call[listConnect[i].id].durationSecs2 =
dateEpochNow() / 1000 - call[listConnect[i].id].dateEpochNow / 1000;
}
for (i = 0; i < listRing.length; i++) {
call[listRing[i].id].durationRingSecs =
dateEpochNow() / 1000 - call[listRing[i].id].dateEpochNow / 1000;
}
adapterSetOnChange('callmonitor.connect', callmonitor(listConnect));
adapterSetOnChange('callmonitor.ring', callmonitor(listRing));
adapterSetOnChange('callmonitor.all', callmonitorAll(listAll));
// adapter.setState('callmonitor.connect', callmonitor(listConnect), true);
// adapter.setState('callmonitor.ring', callmonitor(listRing), true);
// adapter.setState('callmonitor.all', callmonitorAll(listAll), true);
}, 1000);
} else if (!allActiveCount && intervalRunningCall) {
// Stop interval
clearInterval(intervalRunningCall);
intervalRunningCall = null;
}
adapterSetOnChange('callmonitor.call', callmonitor(listCall));
// adapter.setState('callmonitor.call', callmonitor(listCall), true);
// wenn der Interval beendet wird, müssen die letzet Anrufe im Callmonitor auch bereinigt werden
// die Callmonitorlisten werden zusätzlich zum Intervall mit jeder Fritzbox-Meldung aktualisiert
// nur im else-Zweig im Intervall ist dies nicht ausreichend, wg. der Asynchonität
adapterSetOnChange('callmonitor.connect', callmonitor(listConnect));
adapterSetOnChange('callmonitor.ring', callmonitor(listRing));
adapterSetOnChange('callmonitor.all', callmonitorAll(listAll));
}
}
function handleWLANConfiguration(config) {
//console.log(config);
adapter.log.debug(`WLAN: ${config['NewEnable']}`);
wlanState = config['NewEnable'] == 1;
adapterSetOnChange('wlan.enabled', wlanState);
}
function connectToTR064(host, user, password, callback) {
var tr064 = new tr.TR064();
tr064.initTR064Device(host, 49000, function (err, device) {
if (!err && device) {
device.startEncryptedCommunication(function (err, sslDev) {
if (!err) {
sslDev.login(user, password);
callback(sslDev);
} else {
adapter.log.warn(`TR-064 error: ${err}`);
}
});
} else {
adapter.log.warn(`TR-064 error: ${err}`);
}
});
}
function getWlanConfig(host, user, password, callback) {
connectToTR064(host, user, password, function (sslDev) {
var wlanConfig = sslDev.services['urn:dslforum-org:service:WLANConfiguration:1'];
adapter.log.debug('TR-064: calling GetInfo()');
wlanConfig.actions.GetInfo(function (err, result) {
if (!err) {
adapter.log.debug('TR-064: got result from GetInfo()');
callback(result);
} else {
adapter.log.warn(`TR-064 error: ${err}`);
}
});
});
}
function setWlanEnabled(host, user, password, enabled) {
connectToTR064(host, user, password, function (sslDev) {
var wlanConfig = sslDev.services['urn:dslforum-org:service:WLANConfiguration:1'];
adapter.log.debug(`TR-064: calling SetEnable(${enabled})`);
wlanConfig.actions.SetEnable({ NewEnable: enabled ? 1 : 0 }, function (err, _result) {
if (!err) {
adapter.log.debug('TR-064: got result from SetEnable()');
} else {
adapter.log.warn(`TR-064 error: ${err}`);