iobroker.fritzdect
Version:
ioBroker fritzbox DECT Adapter
1,418 lines (1,381 loc) • 141 kB
JavaScript
'use strict';
/*
* Created with @iobroker/create-adapter v1.31.0
*/
// The adapter-core module gives you access to the core ioBroker functions
// you need to create an adapter
const utils = require('@iobroker/adapter-core');
// Load your modules here, e.g.:
// const fs = require("fs");
const Fritz = require('fritzdect-aha-nodejs').Fritz;
/*
let Fritz;
(async () => {
let fb = await import('fritzdect-aha-nodejs');
Fritz = fb.Fritz;
})().catch((err) => console.error(err));
*/
const parser = require('./lib/xml2json.js');
let polling;
/* errorcodes hkr
0: kein Fehler
1: Keine Adaptierung möglich. Gerät korrekt am Heizkörper montiert?
2: Ventilhub zu kurz oder Batterieleistung zu schwach. Ventilstößel per Hand mehrmals öfnen und schließen oder neue Batterien einsetzen.
3: Keine Ventilbewegung möglich. Ventilstößel frei?
4: Die Installation wird gerade vorbereitet.
5: Der Heizkörperregler ist im Installationsmodus und kann auf das Heizungsventil montiert werden.
6: Der Heizkörperregler passt sich nun an den Hub des Heizungsventils an.
*/
/* errorcodes blind
alert state
Beim Rollladen als Bitmaske auszuwerten.
0000 0000 - Es liegt kein Fehler vor.
0000 0001 - Hindernisalarm, der Rollladen wird gestoppt und ein kleines Stück in entgegengesetzte Richtung bewegt.
0000 0010 - Temperaturalarm, Motor überhitzt.
*/
/*
functionbitmask
Bit 0: HAN-FUN Gerät
Bit 2: Licht/Lampe
Bit 4: Alarm-Sensor
Bit 5: AVM Button
Bit 6: AVM Heizkörperregler
Bit 7: AVM Energie Messgerät
Bit 8: Temperatursensor
Bit 9: AVM Schaltsteckdose
Bit 10: AVM DECT Repeater
Bit 11: AVM Mikrofon
Bit 13: HAN-FUN-Unit
Bit 15: an-/ausschaltbares Gerät/Steckdose/Lampe/Aktor
Bit 16: Gerät mit einstellbarem Dimm-, Höhen- bzw. Niveau-Level Bit 17: Lampe mit einstellbarer Farbe/Farbtemperatur
Bit 18: Rollladen(Blind) - hoch, runter, stop und level 0% bis 100 % Bit 20: Luftfeuchtigkeitssensor
Die Bits 5,6,7,9,10 und 11 werden nur von FRITZ!-Geräten verwendet und nicht von HANFUN- oder Zigbee-Geräten.
*/
/* HANFUN unittypes
256 = SIMPLE_ON_OFF_SWITCHABLE
257 = SIMPLE_ON_OFF_SWITCH
262 = AC_OUTLET
263 = AC_OUTLET_SIMPLE_POWER_METERING
264 = SIMPLE_LIGHT
265 = DIMMABLE_LIGHT
266 = DIMMER_SWITCH
273 = SIMPLE_BUTTON
277 = COLOR_BULB
278 = DIMMABLE_COLOR_BULB
281 = BLIND
282 = LAMELLAR
512 = SIMPLE_DETECTOR
513 = DOOR_OPEN_CLOSE_DETECTOR
514 = WINDOW_OPEN_CLOSE_DETECTOR
515 = MOTION_DETECTOR
518 = FLOOD_DETECTOR
519 = GLAS_BREAK_DETECTOR
520 = VIBRATION_DETECTOR
640 = SIREN
*/
/* HANFUN interfaces
256 = ALERT
277 = KEEP_ALIVE
512 = ON_OFF
513 = LEVEL_CTRL
514 = COLOR_CTRL
516 = OPEN_CLOSE ? detected with blinds, different alert -> status bits?
517 = OPEN_CLOSE_CONFIG ? detected with blinds
768 = ?
772 = SIMPLE_BUTTON
1024 = SUOTA-Update
*/
/* modes of DECT500 supported/current_mode
0 = nothing, because OFF or not present
1 = HueSaturation-Mode
2 =
3 =
4 = Colortemperature-Mode
5 =
*/
const settings = {
Username: '',
Password: '',
Url: '',
options: {},
intervall: 300,
boosttime: 5,
windowtime: 5,
tsolldefault: 23,
exclude_templates: false,
exclude_routines: false
};
class Fritzdect extends utils.Adapter {
/**
* @param {Partial<utils.AdapterOptions>} [options={}]
*/
constructor(options) {
super({
...options,
name: 'fritzdect'
});
this.on('ready', this.onReady.bind(this));
this.on('stateChange', this.onStateChange.bind(this));
// this.on('objectChange', this.onObjectChange.bind(this));
this.on('message', this.onMessage.bind(this));
this.on('unload', this.onUnload.bind(this));
this.systemConfig = {};
this.fritz = null;
this.boosttime = 5;
this.windowtime = 5;
this.tsolldefault = 23;
this.updatePromise = null;
}
/**
* Is called when databases are connected and adapter received configuration.
*/
async onReady() {
// Initialize your adapter here
try {
// Load user settings
settings.Username = this.config.fritz_user;
settings.Password = this.config.fritz_pw;
settings.Url = this.config.fritz_ip;
//settings.options = this.config.fritz_options;
settings.intervall = this.config.fritz_interval;
settings.boosttime = this.boosttime = this.config.fritz_boosttime;
settings.windowtime = this.windowtime = this.config.fritz_windowtime;
settings.tsolldefault = this.tsolldefault = this.config.fritz_tsolldefault;
settings.fritz_writeonhyst = this.fritz_writeonhyst = this.config.fritz_writeonhyst;
settings.exclude_templates = this.exclude_templates = this.config.fritz_exclude_templates;
settings.exclude_routines = this.exclude_routines = this.config.fritz_exclude_routines;
settings.exclude_stats = this.exclude_stats = this.config.fritz_exclude_stats;
// The adapters config (in the instance object everything under the attribute "native") is accessible via
// this.config:
this.log.info('fritzdect entered ready');
const sysConf = await this.getForeignObjectAsync('system.config');
if (sysConf && sysConf.common) {
this.systemConfig = sysConf.common;
} else {
throw `ioBroker system configuration not found.`;
}
// jsonUI should transfer PW decrypted
if (settings.Username !== '' && settings.Password !== '') {
this.getForeignObject('system.config', async (err) => {
// Adapter is alive, make API call
// Make a call to fritzboxAPI and get a list devices/groups and templates
this.fritz = new Fritz(
settings.Username,
settings.Password,
settings.Url || '',
settings.options || {}
);
this.log.info('fritzdect uses USER: ' + settings.Username);
try {
const login = await this.fritz.login_SID().catch((e) => this.errorHandlerApi(e));
if (login) {
this.log.info('checking user permissions');
const resp = await this.fritz.check_SID().catch((e) => this.errorHandlerApi(e));
// wird zu try/catch error
if (resp) {
this.log.debug('raw perm =>' + JSON.stringify(resp));
try {
let rights = '';
if (resp.rights.indexOf('ights') == -1) {
rights = parser.xml2json(''.concat('<Rights>', resp.rights, '</Rights>'));
} else {
rights = parser.xml2json(resp.rights);
}
this.log.info('the rights are : ' + JSON.stringify(rights));
} catch (error) {
this.log.error('error in permission xml2json ' + error);
}
}
this.log.info('start creating global values ');
await this.createGlobal();
this.log.info('finished creating global values');
this.log.info('start creating devices/groups');
await this.createDevices(this.fritz).catch((e) => this.errorHandlerAdapter(e));
this.log.info('finished creating devices/groups (if any)');
const templinfo = settings.exclude_templates ? 'not used ' : 'used';
this.log.info('templates are ' + templinfo + '(' + settings.exclude_templates + ')');
if (!settings.exclude_templates) {
this.log.info('start creating templates ');
await this.createTemplates(this.fritz).catch((e) => this.errorHandlerAdapter(e));
this.log.info('finished creating templates (if any) ');
}
const routineinfo = settings.exclude_routines ? 'not used ' : 'used';
this.log.info('routines are ' + routineinfo + '(' + settings.exclude_routines + ')');
if (!settings.exclude_routines) {
this.log.info('start creating routines ');
await this.createRoutines(this.fritz).catch((e) => this.errorHandlerAdapter(e));
this.log.info('finished creating routines (if any) ');
}
this.log.info('start initial updating devices/groups');
await this.updateDevices(this.fritz).catch((e) => this.errorHandlerAdapter(e));
this.log.info('finished initial updating devices/groups');
if (!polling && settings.intervall > 0) {
this.log.info(
'going over to cyclic polling, messages to poll activity only in debug-mode '
);
polling = setInterval(async () => {
// poll fritzbox
try {
this.log.debug('polling! fritzdect is alive with ' + settings.intervall + ' s');
this.update();
} catch (e) {
this.log.warn(`[Polling] <== ${e}`);
}
}, (settings.intervall || 300) * 1000);
}
} else {
this.log.error('login not possible, check user and permissions');
}
} catch (error) {
//from login
this.log.warn(
'catched error in onReady (most likely no connection to FB or wrong credentials)' + error
);
}
if (err) {
this.log.error('error getting system.config ' + err);
}
});
} else {
this.log.error(
'*** Adapter running, but doing nothing, credentials missing in Adaptper Settings !!! ***'
);
}
// in this template all states changes inside the adapters namespace are subscribed
this.subscribeStates('*');
} catch (error) {
this.log.error('[asyncOnReady()]' + error);
return;
}
}
/**
* Is called when adapter shuts down - callback has to be called under any circumstances!
* @param {() => void} callback
*/
async onUnload(callback) {
try {
// Here you must clear all timeouts or intervals that may still be active
// clearTimeout(timeout1);
// clearTimeout(timeout2);
// ...
// clearInterval(interval1);
if (polling) clearInterval(polling);
// await this.fritz.logout_SID().catch((e) => this.errorHandlerApi(e));
this.log.info('cleaned everything up...');
callback();
} catch (e) {
this.log.error(e);
callback();
}
}
// If you need to react to object changes, uncomment the following block and the corresponding line in the constructor.
// You also need to subscribe to the objects with `this.subscribeObjects`, similar to `this.subscribeStates`.
// /**
// * Is called if a subscribed object changes
// * @param {string} id
// * @param {ioBroker.Object | null | undefined} obj
// */
// onObjectChange(id, obj) {
// if (obj) {
// // The object was changed
// this.log.info(`object ${id} changed: ${JSON.stringify(obj)}`);
// } else {
// // The object was deleted
// this.log.info(`object ${id} deleted`);
// }
// }
/**
* Is called if a subscribed state changes
* @param {string} id
* @param {ioBroker.State | null | undefined} state
*/
async onStateChange(id, state) {
if (state) {
// The state was changed
this.log.debug(`onStateChange => state ${id} changed: ${state.val} (ack = ${state.ack})`);
if (!this.fritz) {
this.fritz = new Fritz(
settings.Username,
settings.Password,
settings.moreParam || '',
settings.strictSsl || true
);
try {
const login = await this.fritz.login_SID();
if (login) {
this.log.debug('login in stateChange success');
} else {
this.log.error('login not possible, check user and permissions');
}
} catch (error) {
this.errorHandlerApi(error);
}
}
//const fritz = new Fritz(settings.Username, settings.Password, settings.moreParam || '', settings.strictSsl || true);
// you can use the ack flag to detect if it is status (true) or command (false)
if (state && !state.ack && state.val !== null && id !== null) {
this.log.debug('ack is not set! -> command');
//hier noch eine Abfrage ob das Gerät present=false hat und Fehlermeldung das man Nichterreichbares Gerät bedienen wiil
const tmp = id.split('.');
const dp = tmp.pop();
const idx = tmp.pop(); //is the name after fritzdect.x.
// devices or groups
if (idx && idx !== null) {
if (idx.startsWith('DECT_')) {
// braucht man nicht wenn kein toggle in devices vorkommt
id = idx.replace(/DECT_/g, ''); //Thermostat
this.log.info('DECT ID: ' + id + ' identified for command (' + dp + ') : ' + state.val);
if (dp === 'tsoll') {
if (state.val < 8) {
//kann gelöscht werden, wenn Temperaturvorwahl nicht zur Moduswahl benutzt werden soll
await this.setStateAsync('DECT_' + id + '.hkrmode', { val: 1, ack: false }); //damit das Ventil auch regelt
await this.fritz
.setTempTarget(id, 'off')
.then(() => {
this.log.debug('Switched Mode' + id + ' to closed');
})
.catch((e) => this.errorHandlerApi(e));
} else if (state.val > 28) {
//kann gelöscht werden, wenn Temperaturvorwahl nicht zur Moduswahl benutzt werden soll
await this.setStateAsync('DECT_' + id + '.hkrmode', { val: 2, ack: false }); //damit das Ventil auch regelt (false= Befehl und nochmaliger Einsprung )
await this.fritz
.setTempTarget(id, 'on')
.then(() => {
this.log.debug('Switched Mode' + id + ' to opened permanently');
})
.catch((e) => this.errorHandlerApi(e));
} else {
await this.setStateAsync('DECT_' + id + '.hkrmode', { val: 0, ack: false }); //damit das Ventil auch regelt
await this.fritz
.setTempTarget(id, state.val)
.then(() => {
this.log.debug('Set target temp ' + id + state.val + ' °C');
this.setStateAsync('DECT_' + id + '.lasttarget', {
val: state.val,
ack: true
}); //iobroker Tempwahl wird zum letzten Wert gespeichert
this.setStateAsync('DECT_' + id + '.tsoll', {
val: state.val,
ack: true
}); //iobroker Tempwahl wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
}
} else if (dp === 'hkrmode') {
if (state.val === 0) {
const targettemp = await this.getStateAsync('DECT_' + id + '.tsoll').catch((e) => {
this.log.warn('problem getting the tsoll status ' + e);
});
// oder hier die Verwendung von lasttarget
if (targettemp && targettemp.val !== null) {
if (targettemp.val) {
let setTemp = targettemp.val;
if (setTemp < 8) {
await this.setStateAsync('DECT_' + id + '.tsoll', { val: 8, ack: true });
setTemp = 8;
} else if (setTemp > 28) {
await this.setStateAsync('DECT_' + id + '.tsoll', { val: 28, ack: true });
setTemp = 28;
}
await this.fritz
.setTempTarget(id, setTemp)
.then(() => {
this.log.debug('Set target temp ' + id + ' ' + setTemp + ' °C');
this.setStateAsync('DECT_' + id + '.tsoll', {
val: setTemp,
ack: true
}); //iobroker Tempwahl wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
this.setStateAsync('DECT_' + id + '.operationmode', {
val: 'Auto',
ack: true
}); //iobroker setzen des operationmode, da API Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
} else {
this.log.error('no data in targettemp for setting mode');
}
} else {
throw { error: ' targettemp is NULL ' };
}
} else if (state.val === 1) {
await this.fritz
.setTempTarget(id, 'off')
.then(() => {
this.log.debug('Switched Mode' + id + ' to closed.');
this.setStateAsync('DECT_' + id + '.operationmode', {
val: 'Off',
ack: true
}); //iobroker setzen des operationmode, da API Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
} else if (state.val === 2) {
await this.fritz
.setTempTarget(id, 'on')
.then(() => {
this.log.debug('Switched Mode' + id + ' to opened permanently');
this.setStateAsync('DECT_' + id + '.operationmode', {
val: 'On',
ack: true
}); //iobroker setzen des operationmode, da API Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
}
}
//no need to check the state.val, it is a button
if (dp === 'setmodeauto') {
//zurücksetzen wegen toggle/button click
await this.setStateAsync('DECT_' + id + '.setmodeauto', {
val: false,
ack: true
});
const targettemp = await this.getStateAsync('DECT_' + id + '.tsoll').catch((e) => {
this.log.warn('problem getting the tsoll status ' + e);
});
// oder hier die Verwendung von lasttarget
if (targettemp && targettemp.val !== null) {
if (targettemp.val) {
let setTemp = targettemp.val;
if (setTemp < 8) {
await this.setStateAsync('DECT_' + id + '.tsoll', { val: 8, ack: true });
setTemp = 8;
} else if (setTemp > 28) {
await this.setStateAsync('DECT_' + id + '.tsoll', { val: 28, ack: true });
setTemp = 28;
}
this.fritz
.setTempTarget(id, setTemp)
.then(() => {
this.log.debug('Set target temp ' + id + ' ' + setTemp + ' °C');
this.setStateAsync('DECT_' + id + '.tsoll', {
val: setTemp,
ack: true
}); //iobroker Tempwahl wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
this.setStateAsync('DECT_' + id + '.operationmode', {
val: 'Auto',
ack: true
}); //iobroker setzen des operationmode, da API Aufruf erfolgreich
this.setStateAsync('DECT_' + id + '.hkrmode', {
val: 0,
ack: true
}); //iobroker setzen des hkrmode, da API Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
} else {
this.log.error('no data in targettemp for setting mode');
}
} else {
throw { error: ' targettemp is NULL ' };
}
}
if (dp === 'setmodeoff') {
//zurücksetzen wegen toggle/button click
await this.setStateAsync('DECT_' + id + '.setmodeoff', {
val: false,
ack: true
});
await this.fritz
.setTempTarget(id, 'off')
.then(() => {
this.log.debug('Switched Mode' + id + ' to closed.');
this.setStateAsync('DECT_' + id + '.operationmode', {
val: 'Off',
ack: true
}); //iobroker setzen des operationmode, da API Aufruf erfolgreich
this.setStateAsync('DECT_' + id + '.hkrmode', {
val: 1,
ack: true
}); //iobroker setzen des hkrmode, da API Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
}
if (dp === 'setmodeon') {
//zurücksetzen wegen toggle/button click
await this.setStateAsync('DECT_' + id + '.setmodeon', {
val: false,
ack: true
});
await this.fritz
.setTempTarget(id, 'on')
.then(() => {
this.log.debug('Switched Mode' + id + ' to opened permanently');
this.setStateAsync('DECT_' + id + '.operationmode', {
val: 'On',
ack: true
}); //iobroker setzen des operationmode, da API Aufruf erfolgreich
this.setStateAsync('DECT_' + id + '.hkrmode', {
val: 2,
ack: true
}); //iobroker setzen des hkrmode, da API Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
}
if (dp == 'boostactivetime') {
this.log.debug(
'Nothing to send external, but the boost active time was defined for ' +
state.val +
' min'
);
}
if (dp == 'boostactive') {
if (
state.val === 0 ||
state.val === '0' ||
state.val === 'false' ||
state.val === false ||
state.val === 'off' ||
state.val === 'OFF'
) {
this.fritz
.setHkrBoost(id, 0)
.then(() => {
this.log.debug('Reset thermostat boost ' + id + ' to ' + state.val);
this.setStateAsync('DECT_' + id + '.boostactive', {
val: state.val,
ack: true
}); //iobroker State-Bedienung wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
//kein pauschales Setzen des Operationmode, da unbekannt wohin es dann geht
const convTime = new Date(0);
this.setStateAsync('DECT_' + id + '.boostactiveendtime', {
val: String(convTime),
ack: true
});
})
.catch((e) => this.errorHandlerApi(e));
} else if (
state.val === 1 ||
state.val === '1' ||
state.val === 'true' ||
state.val === true ||
state.val === 'on' ||
state.val === 'ON'
) {
const minutes = await this.getStateAsync(
'DECT_' + id + '.boostactivetime'
).catch((error) => {
this.log.warn('DECT_' + +id + '.boostactivetime did not get state -> ' + error);
});
if (minutes && minutes.val !== null) {
let activetime = minutes.val;
const jetzt = +new Date();
if (minutes.val > 1440) {
activetime = 1440;
}
const ende = Math.floor(jetzt / 1000 + Number(activetime) * 60); //time for fritzbox is in seconds
this.log.debug(' unix returned ' + ende + ' real ' + new Date(ende * 1000));
this.fritz
.setHkrBoost(id, ende)
.then((body) => {
const endtime = new Date(Math.floor(body * 1000));
this.log.debug('window ' + body + ' reading to ' + endtime);
this.log.debug(
'Set thermostat boost ' +
id +
' to ' +
state.val +
' until calculated ' +
ende +
' ' +
new Date(ende * 1000)
);
this.setStateAsync('DECT_' + id + '.boostactive', {
val: state.val,
ack: true
}); //iobroker State-Bedienung wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
this.setStateAsync('DECT_' + id + '.boostactiveendtime', {
val: String(endtime),
ack: true
}); //iobroker State-Bedienung wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
this.setStateAsync('DECT_' + id + '.operationmode', {
val: 'Boost',
ack: true
}); //iobroker setzen des operationmode, da API Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
} else {
throw { error: 'minutes were NULL' };
}
}
}
if (dp == 'windowopenactivetime') {
this.log.debug(
'Nothing to send external, but the window open active time was defined for ' +
state.val +
' min'
);
}
if (dp == 'windowopenactiv') {
if (
state.val === 0 ||
state.val === '0' ||
state.val === 'false' ||
state.val === false ||
state.val === 'off' ||
state.val === 'OFF'
) {
this.fritz
.setWindowOpen(id, 0)
.then(() => {
this.log.debug('Reset thermostat windowopen ' + id + ' to ' + state.val);
this.setStateAsync('DECT_' + id + '.windowopenactiv', {
val: state.val,
ack: true
}); //iobroker State-Bedienung wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
//keine Nachführung operationmode, da unbekannt wohin es geht
const convTime = new Date(0);
this.setStateAsync('DECT_' + id + '.windowopenactiveendtime', {
val: String(convTime),
ack: true
});
})
.catch((e) => this.errorHandlerApi(e));
} else if (
state.val === 1 ||
state.val === '1' ||
state.val === 'true' ||
state.val === true ||
state.val === 'on' ||
state.val === 'ON'
) {
const minutes = await this.getStateAsync(
'DECT_' + id + '.windowopenactivetime'
).catch((error) => {
this.log.warn(
'DECT_' + +id + '.windowopenactivetime did not get state -> ' + error
);
});
if (minutes && minutes.val !== null) {
let activetime = minutes.val;
const jetzt = +new Date();
if (minutes.val > 1440) {
activetime = 1440;
}
const ende = Math.floor(jetzt / 1000 + Number(activetime) * 60); //time for fritzbox is in seconds
this.log.debug(' unix ' + ende + ' real ' + new Date(ende * 1000));
this.fritz
.setWindowOpen(id, ende)
.then((body) => {
const endtime = new Date(Math.floor(body * 1000));
this.log.debug('window ' + body + ' reading to ' + endtime);
this.log.debug(
'Set thermostat windowopen ' +
id +
' to ' +
state.val +
' until calculated ' +
ende +
' ' +
new Date(ende * 1000)
);
this.setStateAsync('DECT_' + id + '.windowopenactiv', {
val: state.val,
ack: true
}); //iobroker State-Bedienung wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
this.setStateAsync('DECT_' + id + '.windowopenactiveendtime', {
val: String(endtime),
ack: true
}); //iobroker State-Bedienung wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
this.setStateAsync('DECT_' + id + '.operationmode', {
val: 'WindowOpen',
ack: true
}); //iobroker setzen des operationmode, da API Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
} else {
throw { error: 'minutes were NULL' };
}
}
}
// setswitch reicht scheinbar nicht bei simpleonoff, hier müsste irgendwie unterschieden werden ob DECT200 switch/state oder simpleonoff/state
if (dp == 'state') {
if (
state.val === 0 ||
state.val === '0' ||
state.val === 'false' ||
state.val === false ||
state.val === 'off' ||
state.val === 'OFF'
) {
const switchtyp = await this.getStateAsync(
'DECT_' + id + '.switchtype'
).catch((error) => {
this.log.warn('DECT_' + +id + '.switchtype did not get state -> ' + error);
});
if (switchtyp && switchtyp.val !== null) {
if (switchtyp.val === 'switch') {
this.fritz
.setSwitchOff(id)
.then(() => {
this.log.debug('Turned switch ' + id + ' off');
this.setStateAsync('DECT_' + id + '.state', {
val: false,
ack: true
}); //iobroker State-Bedienung wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
} else {
this.fritz
.setSimpleOff(id)
.then(() => {
this.log.debug('Turned switch ' + id + ' off');
this.setStateAsync('DECT_' + id + '.state', {
val: false,
ack: true
}); //iobroker State-Bedienung wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
}
} else {
throw { error: 'could not determine the type of switch (switch/simpleonoff)' };
}
} else if (
state.val === 1 ||
state.val === '1' ||
state.val === 'true' ||
state.val === true ||
state.val === 'on' ||
state.val === 'ON'
) {
const switchtyp = await this.getStateAsync(
'DECT_' + id + '.switchtype'
).catch((error) => {
this.log.warn('DECT_' + +id + '.switchtype did not get state -> ' + error);
});
if (switchtyp && switchtyp.val !== null) {
if (switchtyp.val === 'switch') {
this.fritz
.setSwitchOn(id)
.then(() => {
this.log.debug('Turned switch ' + id + ' on');
this.setStateAsync('DECT_' + id + '.state', {
val: true,
ack: true
}); //iobroker State-Bedienung wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
} else {
this.fritz
.setSimpleOn(id)
.then(() => {
this.log.debug('Turned switch ' + id + ' on');
this.setStateAsync('DECT_' + id + '.state', {
val: true,
ack: true
}); //iobroker State-Bedienung wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
}
} else {
throw { error: 'could not determine the type of switch (switch/simpleonoff)' };
}
}
}
if (dp == 'blindsclose') {
this.fritz
.setBlind(id, 'close')
.then(async () => {
this.log.debug('Started blind ' + id + ' to close');
await this.setStateAsync('DECT_' + id + '.blindsclose', { val: false, ack: true }); //iobroker State-Bedienung wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
}
if (dp == 'blindsopen') {
this.fritz
.setBlind(id, 'open')
.then(async () => {
this.log.debug('Started blind ' + id + ' to open');
await this.setStateAsync('DECT_' + id + '.blindsopen', { val: false, ack: true }); //iobroker State-Bedienung wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
}
if (dp == 'blindsstop') {
this.fritz
.setBlind(id, 'stop')
.then(() => {
this.log.debug('Set blind ' + id + ' to stop');
this.setStateAsync('DECT_' + id + '.blindsstop', { val: false, ack: true }); //iobroker State-Bedienung wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
}
if (dp == 'level') {
this.fritz
.setLevel(id, state.val)
.then(() => {
this.log.debug('Set level' + id + ' to ' + state.val);
this.setStateAsync('DECT_' + id + '.level', { val: state.val, ack: true }); //iobroker State-Bedienung wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
}
if (dp == 'levelpercentage') {
this.fritz
.setLevel(id, Math.floor(Number(state.val) / 100 * 255))
.then(() => {
//level is in 0...255
this.log.debug('Set level %' + id + ' to ' + state.val);
this.setStateAsync('DECT_' + id + '.levelpercentage', {
val: state.val,
ack: true
}); //iobroker State-Bedienung wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
}
if (dp == 'hue') {
const saturation = await this.getStateAsync('DECT_' + id + '.saturation').catch((error) => {
this.log.warn('DECT_' + +id + '.saturation did not get state -> ' + error);
});
if (saturation && saturation.val !== null) {
// oder hier die Verwendung von lasttarget
const setSaturation = saturation.val;
if (setSaturation == '') {
this.log.error(
'No saturation value exists when setting hue, please set saturation to a value '
);
} else {
this.fritz
.setColor(id, setSaturation, state.val)
.then(() => {
this.log.debug(
'Set lamp color hue ' +
id +
' to ' +
state.val +
' and saturation of ' +
setSaturation
);
this.setStateAsync('DECT_' + id + '.hue', {
val: state.val,
ack: true
}); //iobroker State-Bedienung wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
}
} else {
throw { error: 'minutes were NULL' };
}
}
if (dp == 'saturation') {
const hue = await this.getStateAsync('DECT_' + id + '.hue').catch((error) => {
this.log.warn('DECT_' + +id + '.hue did not get state -> ' + error);
});
if (hue && hue.val !== null) {
const setHue = hue.val;
if (setHue == '') {
this.log.error(
'No hue value exists when setting saturation, please set hue to a value '
);
} else {
this.fritz
.setColor(id, state.val, setHue)
.then(() => {
this.log.debug(
'Set lamp color saturation ' +
id +
' to ' +
state.val +
' and hue of ' +
setHue
);
this.setStateAsync('DECT_' + id + '.saturation', {
val: state.val,
ack: true
}); //iobroker State-Bedienung wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
}
} else {
throw { error: 'hue were NULL' };
}
}
if (dp == 'temperature') {
this.fritz
.setColorTemperature(id, state.val)
.then(() => {
this.log.debug('Set lamp color temperature ' + id + ' to ' + state.val);
this.setStateAsync('DECT_' + id + '.temperature', {
val: state.val,
ack: true
}); //iobroker State-Bedienung wird nochmal als Status geschrieben, da API-Aufruf erfolgreich
})
.catch((e) => this.errorHandlerApi(e));
}
} else if (idx.startsWith('template_')) {
//must be fritzbox template
id = idx.replace(/template_/g, ''); //template
this.log.info('Template ID: ' + id + ' identified for command (' + dp + ') : ' + state.val);
if (dp == 'toggle') {
if (
state.val === 1 ||
state.val === '1' ||
state.val === 'true' ||
state.val === true ||
state.val === 'on' ||
state.val === 'ON'
) {
this.fritz
.applyTemplate(id)
.then((sid) => {
this.log.debug('cmd Toggle to template ' + id + ' on');
this.log.debug('response ' + sid);
this.setStateAsync('template.lasttemplate', { val: sid, ack: true }); //when successfull toggle, the API returns the id of the template
})
.catch((e) => this.errorHandlerApi(e));
}
}
} else if (idx.startsWith('routine_')) {
//must be fritzbox routine
id = idx.replace(/routine_/g, ''); //routine
this.log.info('Routine ID: ' + id + ' identified for command (' + dp + ') : ' + state.val);
if (dp == 'active') {
if (
state.val === 1 ||
state.val === '1' ||
state.val === 'true' ||
state.val === true ||
state.val === 'on' ||
state.val === 'ON'
) {
state.val = true;
}
this.fritz
.setTriggerActive(id, state.val)
.then((sid) => {
this.log.debug('cmd Active to template ' + id + ' to ' + state.val);
this.log.debug('response ' + sid);
})
.catch((e) => this.errorHandlerApi(e));
}
}
}
} //from if state&ack
} else {
// The state was deleted
this.log.info(`state ${id} deleted`);
}
}
// If you need to accept messages in your adapter, uncomment the following block and the corresponding line in the constructor.
// /**
// * Some message was sent to this instance over message box. Used by email, pushover, text2speech, ...
// * Using this method requires "common.messagebox" property to be set to true in io-package.json
// * @param {ioBroker.Message} obj
// */
async onMessage(obj) {
let wait = false;
this.log.debug('messagebox received ' + JSON.stringify(obj));
try {
if (typeof obj === 'object' && obj.message) {
// if (obj) {
if (obj.command === 'test') {
// e.g. send email or pushover or whatever
this.log.debug('msg with obj.command for test received');
// Send response in callback if required
if (obj.callback)
this.sendTo(
obj.from,
obj.command,
'Message received (sendTo works). This is not an indication that FB is reachable!',
obj.callback
);
}
} else if (obj) {
//my own messages for detectiung are without a message
let result = '';
if (!this.fritz) {
this.fritz = new Fritz(
settings.Username,
settings.Password,
settings.moreParam || '',
settings.strictSsl || true
);
try {
const login = await this.fritz.login_SID();
if (login) {
this.log.debug('login in stateChange success');
} else {
this.log.error('login not possible, check user and permissions');
}
} catch (error) {
this.errorHandlerApi(error);
}
}
// const fritz = new Fritz(settings.Username, settings.Password, settings.moreParam || '', settings.strictSsl || true);
let statfeedback = {};
switch (obj.command) {
case 'update':
try {
await this.update();
if (obj.callback) {
this.sendTo(obj.from, obj.command, { result: true }, obj.callback);
}
} catch (error) {
this.log.warn('unable to get manual updates' + error);
if (obj.callback) {
this.sendTo(
obj.from,
obj.command,
{ result: false, error: 'unable to get manual updates' + error },
obj.callback
);
}
}
wait = true;
break;
case 'devices':
try {
let xml = await this.fritz.getDeviceListInfos();
this.log.debug('devices' + xml);
result = xml;
if (obj.callback) {
this.sendTo(obj.from, obj.command, { error: result }, obj.callback);
}
} catch (error) {
this.log.warn('error calling in msgbox devices' + error);
if (obj.callback) {
this.sendTo(
obj.from,
obj.command,
{ error: 'unable to get devices' + error },
obj.callback
);
}
}
wait = true;
break;
case 'groups':
//eigentlich jetzt mit devices zusammen, da xml nicht geteilt wird
try {
let xml = await this.fritz.getDeviceListInfos();
this.log.debug('groups' + xml);
result = xml;
if (obj.callback) {
this.sendTo(obj.from, obj.command, { error: result }, obj.callback);
}
} catch (error) {
this.log.warn('error calling in msgbox groups' + error);
if (obj.callback) {
this.sendTo(
obj.from,
obj.command,
{ error: 'unable to get groups' + error },
obj.callback
);
}
}
wait = true;
break;
case 'templates':
try {
let xml = await this.fritz.getTemplateListInfos();
this.log.debug('templates' + xml);
result = xml;
if (obj.callback) {
this.sendTo(obj.from, obj.command, { error: result }, obj.callback);
}
} catch (error) {
this.log.warn('error calling in msgbox templates' + error);
if (obj.callback) {
this.sendTo(
obj.from,
obj.command,
{ error: 'unable to get templates' + error },
obj.callback
);
}
}
wait = true;
break;
case 'trigger':
try {
let xml = await this.fritz.getTriggerListInfos();
this.log.debug('trigger' + xml);
result = xml;
if (obj.callback) {
this.sendTo(obj.from, obj.command, { error: result }, obj.callback);
}
} catch (error) {
this.log.warn('error calling in msgbox trigger' + error);
if (obj.callback) {
this.sendTo(
obj.from,
obj.command,
{ error: 'unable to get trigger' + error },
obj.callback
);
}
}
wait = true;
break;
case 'statistic':
try {
const deviceswithstat = await this.getStateAsync('global.statdevices').catch((e) => {
this.log.warn('problem getting statdevices ' + e);
});
if (deviceswithstat && deviceswithstat.val) {
this.log.debug('msg statistics ' + deviceswithstat.val);
let devstat = [].concat([], JSON.parse(String(deviceswithstat.val)));
for (let i = 0; i < devstat.length; i++) {
let stats = await this.fritz.getBasicDeviceStats(devstat[i]).catch((e) => {
this.log.debug('error calling in msgbox');
throw {
msg: 'issue getting statistics',
function: 'onMessage',
error: e
};
});
this.log.debug('processed ' + devstat[i]);
statfeedback[devstat[i]] = stats;
}
}
result = JSON.stringify(statfeedback);
if (obj.callback) {
this.sendTo(obj.from, obj.command, { error: result }, obj.callback);
}
} catch (error) {
this.log.warn('error calling in msgbox statistic' + error);
if (obj.callback) {
this.sendTo(
obj.from,
obj.command,
{ error: 'unable to get statistic' + error },
obj.callback
);
}
}
wait = true;
break;
case 'color':
try {
let xml = await this.fritz.getColorDefaults();
this.log.debug('color' + xml);
result = xml;
if (obj.callback) {
this.sendTo(obj.from, obj.command, { error: result }, obj.callback);
}
} catch (error) {
this.log.warn('error calling in msgbox color' + error);
if (obj.callback) {
this.sendTo(
obj.from,
obj.command,
{ error: 'unable to get color' + error },
obj.callback
);
}
}
wait = true;
break;
case 'rights':
try {
let xml = await this.fritz.getUserPermissions();
this.log.debug('rights' + xml);
result = xml;
if (obj.callback) {
this.sendTo(obj.from, obj.command, { error: result }, obj.callback);
}
} catch (error) {
this.log.warn('error calling in msgbox rights' + error);
if (obj.callback) {
this.sendTo(
obj.from,
obj.command,
{ error: 'unable to get rights' + error },
obj.callback
);
}
}
wait = true;
break;
//idea for other statistics: call of message returns everything (loop over all devices)
default:
this.log.warn('Received unhandled message: ' + obj.command);
break;
}
}
if (!wait && obj.callback) {
this.log.debug('messagebox landed in last evaluation wait=false and callback');
this.sendTo(obj.from, obj.command, obj.message, obj.callback);
}
} catch (e) {
this.log.debug('try/catch messagebox error occured ' + e);
}
}
decryptfc(key, value) {
let result = '';
for (let i = 0; i < value.length; ++i) {
result += String.fromCharCode(key[i % key.length].charCodeAt(0) ^ value.charCodeAt(i));
}
return result;
}
errorHandlerApi(error) {
try {
this.log.error('--------------- error calling the fritzbox -----------');
this.log.error('API msg => ' + error.msg);
this.log.error('API funct => ' + error.function);
if (error == '0000000000000000') {
this.log.error('Did not get session id -> invalid username or password?');
} else if (!error.response) {
this.log.error('no response part in returned error message');
} else if (error.response.statusCode) {
if (error.response.statusCode == 403) {
this.log.error(
'no permission for this call (403), has user all the rights and access to fritzbox?'
);
} else if (error.response.statusCode == 404) {
this.log.error('call to API does not exist! (404)');
} else if (error.response.statusCode == 400) {
this.log.error('bad request (400), ain correct?');
} else if (error.response.statusCode == 500) {
this.log.error('internal fritzbox error (500)');
} else if (error.response.statusCode == 503) {
this.log.error('service unavailable (503)');
} else if (error.response.statusCode == 303) {
this.log.error('unknwon error (303)');
} else {
this.log.error('statuscode not in errorHandlerApi of fritzdect');
}
}
this.log.error('API err => ' + JSON.stringify(error.error));
} catch (e) {
this.log.error('catched error in function errorHandlerApi() ' + e);
}
}
errorHandlerAdapter(error) {
try {
this.log.error('--------------- error calling the fritzbox -----------');
this.log.error('iob err => ' + error);
//this.log.error('iob msg => ' + error.msg);
//this.log.error('iob funct => ' + error.function);
//this.log.error('iob err => ' + error.error);
} catch (e) {
this.log.error('try/catch error in function errorHandlerAdapter' + e);
}
}
/**
* @param {any[]} devicearray
*/
unifyDevicesUnits(devicearray) {
let id = [];
let etsiunit = [];
let etsidelete = [];
for (let i = 0; i < devicearray.length; i++) {
id.push(devicearray[i].id);
//if there is no identifier, then the dataset is useless (issue #598)
if (!devicearray[i].identifier) {
etsidelete.push(i);
}
if (devicearray[i]['etsiunitinfo']) {
//prepare array with etsi units for later merge with etsidevice
etsiunit.push(i);
//setting the role of device
if (Number(devicearray[i].etsiunitinfo.unittype) > 510) {
devicearray[i]['role'] = 'sensor';
} else if (Number(devicearray[i].etsiunitinfo.unittype) > 280) {
devicearray[i]['role'] = 'blinds';
} else if (Number(devicearray[i].etsiunitinfo.unittype) == 273) {
devicearray[i]['role'] = 'sensor';
} else if (Number(devicearray[i].etsiunitinfo.unittype) > 263) {
devicearray[i]['role'] = 'light';
} else if (Number(devicearray[i].etsiunitinfo.unittype) > 255) {
devicearray[i]['role'] = 'switch';
}
} else {
//setting the role of device
if (devicearray[i].switch) {
devicearray[i]['role'] = 'switch';
} else if (devicearray[i].hkr) {
devicearray[i]['role'] = 'thermo.heat';
} else if (devicearray[i].blind) {
devicearray[i]['role'] = 'blinds';
} else if (devicearray[i].colortemperature) {
devicearray[i]['role'] = 'light';
} else if (devicearray[i].levelcontrol) {
devicearray[i]['role'] = 'light';
} else if (devicearray[i].temperature) {
devicearray[i]['role'] = 'thermo';
} else if (devicearray[i].button) {
devicearray[i]['role'] = 'sensor';
} else if (devicearray[i].alert) {
devicearray[i]['role'] = 'sensor';
} else {
devicearray[i]['role'] = 'etsi';
}
//setting the switchtype
if (devicearray[i].switch) {
devicearray[i]['switchtype'] = 'switch';
} else if (devicearray[i].simpleonoff) {
devicearray[i]['switchtype'] = 'simpleonoff';
}
}
}
for (let etsiunitpos of etsiunit) {
//find the matching etsidevice for etsiunit
let etsidevpos = id.indexOf(devicearray[etsiunitpos]['etsiunitinfo']['etsideviceid']);
//prepare array for deletion of etsidevices
if (etsidelete.indexOf(etsidevpos) === -1 && etsidevpos !== -1) {
etsidelete.push(etsidevpos);
//merge etsidevice info into etsiunit
for (let item in devicearray[etsidevpos]) {
if (item !== 'id' && item !== 'identifier' && item !== 'functionbitmask' && item !== 'role') {
devicearray[etsiunitpos][item] = devicearray[etsidevpos][item];
}
}
}
}
etsidelete.sort();
//delete the etsidevices
for (let k = 0; k < etsidelete.length; k++) {
devicearray.splice(etsidelete[k] - k, 1);
}
return devicearray;
}
async update() {
if (!this.updatePromise) {
this.updatePromise = this._update().finally(() => {
this.updatePromise = null;
});
}
return this.updatePromise;
}
async _update() {
await this.updateDevices(this.fritz).catch((e) => this.errorHandlerAdapter(e));
if (!settings.exclude_routines) {
await this.updateRoutines(this.fritz).catch((e) => this.errorHandlerAdapter(e));
}
if (!settings.exclude_stats) {
const deviceswithstat = await this.getStateAsync('global.statdevices').catch((e) => {
this.log.warn('problem getting statdevices ' + e);
});
if (deviceswithstat && deviceswithstat.val) {
this.log.debug('glob state ' + deviceswithstat.val);
let devstat = [].concat([], JSON.parse(String(deviceswithstat.val)));
for (let i = 0; i < devstat.length; i++) {
this.log.debug('updating Stats of device ' + devstat[i]);
await this.updateStats(devstat[i], this.fritz);
}
}
}
}
async updateRoutines(fritz) {
this.log.debug('__________________________');
this.log.debug('updating Routines ');
try {
const routineslistinfos = await fritz.getTriggerListInfos().catch((e) => this.errorHandlerApi(e));
//let typ = '';
//let role = '';
if (routineslistinfos) {
let routines = parser.xml2json(routineslistinfos);
routines = [].concat((routines.triggerlist || {}).trigger || []).map((trigger) => {
return trigger;
});
this.log.debug('__________________________');
this.log.debug('routines\n');
this.log.debug(JSON.stringify(routines));
if (routines.length) {
this.log.debug('update routines ' + routines.length);
await Promise.all(
routines.map(async (routine) => {
let active = routine.active == 0 ? false : true;
let old = await this.getStateAsync(
'routine_' + routine.identifier.replace(/\s/g, '') + '.active'
).catch((error) => {
this.log.warn(
'problem getting routine_' +
routine.identifier.replace(/\s/g, '') +