iobroker.device-watcher
Version:
1,451 lines (1,317 loc) • 84.9 kB
JavaScript
'use strict';
const utils = require('@iobroker/adapter-core');
const adapterName = require('./package.json').name.split('.').pop();
const schedule = require('node-schedule');
const arrApart = require('./lib/arrApart.js'); // list of supported adapters
const translations = require('./lib/translations.js');
const tools = require('./lib/tools.js');
const crud = require('./lib/crud.js');
// indicator if the adapter is running (for intervall/shedule)
let isUnloaded = false;
const adapterUpdateListDP = 'admin.*.info.updatesJson';
class DeviceWatcher extends utils.Adapter {
constructor(options) {
super({
...options,
name: adapterName,
useFormatDate: true,
});
// instances and adapters
// raw arrays
this.listInstanceRaw = new Map();
this.adapterUpdatesJsonRaw = [];
this.listErrorInstanceRaw = [];
// user arrays
this.listAllInstances = [];
this.listAllActiveInstances = [];
this.listDeactivatedInstances = [];
this.listAdapterUpdates = [];
this.listErrorInstance = [];
//counts
this.countAllInstances = 0;
this.countAllActiveInstances = 0;
this.countDeactivatedInstances = 0;
this.countAdapterUpdates = 0;
this.countErrorInstance = 0;
// devices
// raw arrays
this.listAllDevicesRaw = new Map();
this.batteryLowPoweredRaw = [];
this.offlineDevicesRaw = [];
this.upgradableDevicesRaw = [];
// arrays
this.listAllDevicesUserRaw = [];
this.listAllDevices = [];
this.offlineDevices = [];
this.linkQualityDevices = [];
this.batteryPowered = [];
this.batteryLowPowered = [];
this.selAdapter = [];
this.adapterSelected = [];
this.upgradableList = [];
// counts
this.offlineDevicesCount = 0;
this.deviceCounter = 0;
this.linkQualityCount = 0;
this.batteryPoweredCount = 0;
this.lowBatteryPoweredCount = 0;
this.upgradableDevicesCount = 0;
// Blacklist
// Instances
this.blacklistInstancesLists = [];
this.blacklistInstancesNotify = [];
// Devices
this.blacklistLists = [];
this.blacklistAdapterLists = [];
this.blacklistNotify = [];
// Timelist instances
this.userTimeInstancesList = new Map();
// Interval timer
this.refreshDataTimeout = null;
// Check if main function is running
this.mainRunning = false;
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));
}
/**
* onReady
*/
async onReady() {
this.log.debug(`Adapter ${adapterName} was started`);
// set user language
if (this.config.userSelectedLanguage === '') {
if (this.language !== undefined && this.language !== null) {
this.config.userSelectedLanguage = this.language;
} else {
this.config.userSelectedLanguage = 'de';
}
}
this.log.debug(`Set language to ${this.config.userSelectedLanguage}`);
this.configCreateInstanceList = this.config.checkAdapterInstances;
this.configListOnlyBattery = this.config.listOnlyBattery;
this.configCreateOwnFolder = this.config.createOwnFolder;
this.configCreateHtmlList = this.config.createHtmlList;
this.configSetAdapter = {
alexa2: this.config.alexa2Devices,
apcups: this.config.apcupsDevices,
ble: this.config.bleDevices,
deconz: this.config.deconzDevices,
ecovacsdeebot: this.config.ecovacsdeebotDevices,
enocean: this.config.enoceanDevices,
esphome: this.config.esphomeDevices,
eusec: this.config.eusecDevices,
fhemTFAsensors: this.config.fhemTFAsensorsDevices,
fritzdect: this.config.fritzdectDevices,
fullybrowser: this.config.fullybrowserDevices,
fullybrowserV3: this.config.fullybrowserV3Devices,
fullyMQTT: this.config.fullyMQTTDevices,
ham: this.config.hamDevices,
harmony: this.config.harmonyDevices,
hmiP: this.config.hmiPDevices,
hmrpc: this.config.hmrpcDevices,
homeconnect: this.config.homeconnectDevices,
homekitController: this.config.homekitControllerDevices,
hs100: this.config.hs100Devices,
hue: this.config.hueDevices,
hueExt: this.config.hueExtDevices,
innogy: this.config.innogyDevices,
jeelink: this.config.jeelinkDevices,
loqedSmartLock: this.config.loqedSmartLockDevices,
lupusec: this.config.lupusecDevices,
maxcube: this.config.maxcubeDevices,
meross: this.config.merossDevices,
mihome: this.config.mihomeDevices,
mihomeGW: this.config.mihomeDevices,
mihomeVacuum: this.config.mihomeVacuumDevices,
mqttClientZigbee2Mqtt: this.config.mqttClientZigbee2MqttDevices,
mqttNuki: this.config.mqttNukiDevices,
musiccast: this.config.musiccastDevices,
netatmo: this.config.netatmoDevices,
nukiExt: this.config.nukiExtDevices,
nut: this.config.nutDevices,
ping: this.config.pingDevices,
proxmox: this.config.proxmoxDevices,
ring: this.config.ringDevices,
roomba: this.config.roombaDevices,
shelly: this.config.shellyDevices,
smartgarden: this.config.smartgardenDevices,
sonoff: this.config.sonoffDevices,
sonos: this.config.sonosDevices,
sureflap: this.config.sureflapDevices,
switchbotBle: this.config.switchbotBleDevices,
tado: this.config.tadoDevices,
tapo: this.config.tapoDevices,
tradfri: this.config.tradfriDevices,
tuya: this.config.tuyaDevices,
unifi: this.config.unifiDevices,
viessmann: this.config.viessmannDevices,
wifilight: this.config.wifilightDevices,
wled: this.config.wledDevices,
xsense: this.config.xsenseDevices,
yeelight: this.config.yeelightDevices,
zigbee: this.config.zigbeeDevices,
zigbee2MQTT: this.config.zigbee2mqttDevices,
zwave2: this.config.zwaveDevices,
};
this.configMaxMinutes = {
alexa2: this.config.alexa2MaxMinutes,
apcups: this.config.apcupsMaxMinutes,
ble: this.config.bleMaxMinutes,
deconz: this.config.deconzMaxMinutes,
ecovacsdeebot: this.config.ecovacsdeebotMaxMinutes,
enocean: this.config.enoceanMaxMinutes,
esphome: this.config.esphomeMaxMinutes,
eusec: this.config.eusecMaxMinutes,
fhemTFAsensors: this.config.fhemTFAsensorsMaxMinutes,
fritzdect: this.config.fritzdectMaxMinutes,
fullybrowser: this.config.fullybrowserMaxMinutes,
fullybrowserV3: this.config.fullybrowserV3MaxMinutes,
fullyMQTT: this.config.fullyMQTTMaxMinutes,
ham: this.config.hamMaxMinutes,
harmony: this.config.harmonyMaxMinutes,
hmiP: this.config.hmiPMaxMinutes,
hmrpc: this.config.hmrpcMaxMinutes,
homeconnect: this.config.homeconnectMaxMinutes,
homekitController: this.config.homekitControllerMaxMinutes,
hs100: this.config.hs100MaxMinutes,
hue: this.config.hueMaxMinutes,
hueExt: this.config.hueextMaxMinutes,
innogy: this.config.innogyMaxMinutes,
jeelink: this.config.jeelinkMaxMinutes,
loqedSmartLock: this.config.loqedSmartLockMaxMinutes,
lupusec: this.config.lupusecMaxMinutes,
maxcube: this.config.maxcubeMaxMinutes,
meross: this.config.merossMaxMinutes,
mihome: this.config.mihomeMaxMinutes,
mihomeGW: this.config.mihomeMaxMinutes,
mihomeVacuum: this.config.mihomeVacuumMaxMinutes,
mqttClientZigbee2Mqtt: this.config.mqttClientZigbee2MqttMaxMinutes,
mqttNuki: this.config.mqttNukiMaxMinutes,
musiccast: this.config.musiccastMaxMinutes,
netatmo: this.config.netatmoMaxMinutes,
nukiExt: this.config.nukiextendMaxMinutes,
nut: this.config.nutMaxMinutes,
ping: this.config.pingMaxMinutes,
proxmox: this.config.proxmoxMaxMinutes,
ring: this.config.ringMaxMinutes,
roomba: this.config.roombaMaxMinutes,
shelly: this.config.shellyMaxMinutes,
smartgarden: this.config.smartgardenMaxMinutes,
sonoff: this.config.sonoffMaxMinutes,
sonos: this.config.sonosMaxMinutes,
sureflap: this.config.sureflapMaxMinutes,
switchbotBle: this.config.switchbotMaxMinutes,
tado: this.config.tadoMaxMinutes,
tapo: this.config.tapoMaxMinutes,
tradfri: this.config.tradfriMaxMinutes,
tuya: this.config.tuyaMaxMinutes,
unifi: this.config.unifiMaxMinutes,
viessmann: this.config.viessmannMaxMinutes,
wifilight: this.config.wifilightMaxMinutes,
wled: this.config.wledMaxMinutes,
xsense: this.config.xsenseMaxMinutes,
yeelight: this.config.yeelightMaxMinutes,
zigbee: this.config.zigbeeMaxMinutes,
zigbee2MQTT: this.config.zigbee2mqttMaxMinutes,
zwave2: this.config.zwaveMaxMinutes,
};
try {
// create list with selected adapters for monitor devices
for (const [id] of Object.entries(arrApart)) {
if (this.configSetAdapter[id]) {
this.selAdapter.push(arrApart[id]);
this.adapterSelected.push(tools.capitalize(id));
}
}
// Check if an adapter to monitor devices is selected.
if (this.adapterSelected.length >= 1) {
// show list in debug log
this.log.debug(JSON.stringify(this.selAdapter));
this.log.info(`Number of selected adapters to monitor devices: ${this.adapterSelected.length}. Loading data from: ${this.adapterSelected.join(', ')} ...`);
} else {
this.log.info(`No adapters selected to monitor devices.`);
}
// create Blacklist
await crud.createBlacklist(this);
// create user defined list with time of error for instances
await crud.createTimeListInstances(this);
//create datapoints for each adapter if selected
for (const [id] of Object.entries(arrApart)) {
try {
if (!this.configCreateOwnFolder) {
await crud.deleteDPsForEachAdapter(this, id);
await crud.deleteHtmlListDatapoints(this, id);
} else {
if (this.configSetAdapter && this.configSetAdapter[id]) {
await crud.createDPsForEachAdapter(this, id);
// create HTML list datapoints
if (!this.configCreateHtmlList) {
await crud.deleteHtmlListDatapoints(this, id);
} else {
await crud.createHtmlListDatapoints(this, id);
}
this.log.debug(`Created datapoints for ${tools.capitalize(id)}`);
}
}
} catch (error) {
this.log.error(`[onReady - create and fill datapoints for each adapter] - ${error}`);
}
}
// create HTML list datapoints
if (!this.configCreateHtmlList) {
await crud.deleteHtmlListDatapoints(this);
await crud.deleteHtmlListDatapointsInstances(this);
} else {
await crud.createHtmlListDatapoints(this);
if (this.config.checkAdapterInstances) await crud.createHtmlListDatapointsInstances(this);
}
if (!this.config.checkAdapterInstances) await crud.deleteHtmlListDatapointsInstances(this);
// instances and adapters
if (this.configCreateInstanceList) {
// instances
await crud.createDPsForInstances(this);
await this.getAllInstanceData();
// adapter updates
await crud.createAdapterUpdateData(this, adapterUpdateListDP);
} else {
await crud.deleteDPsForInstances(this);
}
await this.main();
// update last contact data in interval
await this.refreshData();
// send overview for low battery devices
if (this.config.checkSendBatteryMsgDaily) await this.sendScheduleNotifications('lowBatteryDevices');
// send overview of offline devices
if (this.config.checkSendOfflineMsgDaily) await this.sendScheduleNotifications('offlineDevices');
// send overview of upgradeable devices
if (this.config.checkSendUpgradeMsgDaily) await this.sendScheduleNotifications('updateDevices');
// send overview of updatable adapters
if (this.config.checkSendAdapterUpdateMsgDaily) await this.sendScheduleNotifications('updateAdapter');
// send overview of deactivated instances
if (this.config.checkSendInstanceDeactivatedDaily) await this.sendScheduleNotifications('deactivatedInstance');
// send overview of instances with error
if (this.config.checkSendInstanceFailedDaily) await this.sendScheduleNotifications('errorInstance');
} catch (error) {
this.log.error(`[onReady] - ${error}`);
this.terminate ? this.terminate(15) : process.exit(15);
}
} // <-- onReady end
/**
* main function
*/
async main() {
this.log.debug(`Function started main`);
this.mainRunning = true;
// cancel run if no adapter is selected
if (this.adapterSelected.length === 0) return;
// fill counts and lists of all selected adapter
try {
for (let i = 0; i < this.selAdapter.length; i++) {
await crud.createData(this, i);
await crud.createLists(this);
}
await crud.writeDatapoints(this); // fill the datapoints
this.log.debug(`Created and filled data for all adapters`);
} catch (error) {
this.log.error(`[main - create data of all adapter] - ${error}`);
}
// fill datapoints for each adapter if selected
if (this.configCreateOwnFolder) {
try {
for (const [id] of Object.entries(arrApart)) {
if (this.configSetAdapter && this.configSetAdapter[id]) {
for (const deviceData of this.listAllDevicesRaw.values()) {
// list device only if selected adapter matched with device
if (!deviceData.adapterID.includes(id)) continue;
await crud.createLists(this, id);
}
await crud.writeDatapoints(this, id); // fill the datapoints
this.log.debug(`Created and filled data for ${tools.capitalize(id)}`);
}
}
} catch (error) {
this.log.error(`[main - create and fill datapoints for each adapter] - ${error}`);
}
}
this.mainRunning = false;
this.log.debug(`Function finished: ${this.main.name}`);
} //<--End of main function
// 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
*/
async onObjectChange(id, obj) {
if (obj) {
try {
// The object was changed
//this.log.debug(`object ${id} changed: ${JSON.stringify(obj)}`);
if (this.config.checkAdapterInstances && id.startsWith('system.adapter.')) {
//read new instance data and add it to the lists
await this.getInstanceData(id);
} else {
if (Array.from(this.listAllDevicesRaw.values()).some((obj) => obj.mainSelector === id)) {
if (!this.mainRunning) {
await this.main();
} else {
return;
}
} else {
return;
}
}
} catch (error) {
this.log.error(`Issue at object change: ${error}`);
}
} else {
try {
// The object was deleted
this.log.debug(`object ${id} deleted`);
// delete instance data in map
if (this.listInstanceRaw.has(id)) {
this.listInstanceRaw.delete(id);
}
// delete device data in map
if (this.listAllDevicesRaw.has(id)) {
this.listAllDevicesRaw.delete(id);
}
//unsubscribe of Objects and states
this.unsubscribeForeignObjects(id);
this.unsubscribeForeignStates(id);
} catch (error) {
this.log.error(`Issue at object deletion: ${error}`);
}
}
}
/**
* Is called if a subscribed state changes
* @param {string} id
* @param {ioBroker.State | null | undefined} state
*/
async onStateChange(id, state) {
if (state) {
// this.log.debug(`State changed: ${id} changed ${state.val}`);
try {
/*=============================================
= Instances / Adapter =
=============================================*/
if (this.config.checkAdapterInstances) {
// Adapter Update data
if (id.endsWith('updatesJson')) {
await this.renewAdapterUpdateData(id);
}
// Instanz data
if (Array.from(this.listInstanceRaw.values()).some((obj) => Object.values(obj).includes(id))) {
await this.renewInstanceData(id, state);
}
}
/*=============================================
= Devices =
=============================================*/
if (Array.from(this.listAllDevicesRaw.values()).some((obj) => Object.values(obj).includes(id))) {
await this.renewDeviceData(id, state);
}
} catch (error) {
this.log.error(`Issue at state change: ${error}`);
}
} else {
// The state was deleted
this.log.debug(`state ${id} deleted`);
}
}
/**
* @param {ioBroker.Message} obj
*/
onMessage(obj) {
const devices = [];
const instances = [];
const instancesTime = [];
let countDevices = 0;
let countInstances = 0;
switch (obj.command) {
case 'devicesList':
if (obj.message) {
try {
for (const deviceData of this.listAllDevicesRaw.values()) {
const label = `${deviceData.Adapter}: ${deviceData.Device}`;
const valueObjectDevices = {
deviceName: deviceData.Device,
adapter: deviceData.Adapter,
path: deviceData.Path,
};
devices[countDevices] = { label: label, value: JSON.stringify(valueObjectDevices) };
countDevices++;
}
const sortDevices = devices.slice(0);
sortDevices.sort(function (a, b) {
const x = a.label;
const y = b.label;
return x < y ? -1 : x > y ? 1 : 0;
});
this.sendTo(obj.from, obj.command, sortDevices, obj.callback);
} catch (error) {
this.log.error(`[onMessage - deviceList for blacklisttable] - ${error}`);
}
}
break;
case 'instancesList':
if (obj.message) {
try {
for (const [instance, instanceData] of this.listInstanceRaw) {
const label = `${instanceData.Adapter}: ${instance}`;
const valueObjectInstances = {
adapter: instanceData.Adapter,
instanceID: instance,
};
instances[countInstances] = { label: label, value: JSON.stringify(valueObjectInstances) };
countInstances++;
}
const sortInstances = instances.slice(0);
sortInstances.sort(function (a, b) {
const x = a.label;
const y = b.label;
return x < y ? -1 : x > y ? 1 : 0;
});
this.sendTo(obj.from, obj.command, sortInstances, obj.callback);
} catch (error) {
this.log.error(`[onMessage - instanceList] - ${error}`);
}
}
break;
case 'instancesListTime':
if (obj.message) {
try {
for (const [instance, instanceData] of this.listInstanceRaw) {
const label = `${instanceData.Adapter}: ${instance}`;
const valueObjectInstances = {
adapter: instanceData.Adapter,
instanceName: instance,
};
instancesTime[countInstances] = { label: label, value: JSON.stringify(valueObjectInstances) };
countInstances++;
}
const sortInstances = instancesTime.slice(0);
sortInstances.sort(function (a, b) {
const x = a.label;
const y = b.label;
return x < y ? -1 : x > y ? 1 : 0;
});
this.sendTo(obj.from, obj.command, sortInstances, obj.callback);
} catch (error) {
this.log.error(`[onMessage - instanceList] - ${error}`);
}
}
break;
}
}
/**
* refresh data with interval
* is neccessary to refresh lastContact data, especially of devices without state changes
*/
async refreshData() {
if (isUnloaded) return; // cancel run if unloaded was called.
const nextTimeout = this.config.updateinterval * 1000;
// devices data
await tools.checkLastContact(this);
await crud.createLists(this);
await crud.writeDatapoints(this);
// devices data in own adapter folder
if (this.configCreateOwnFolder) {
for (const [id] of Object.entries(arrApart)) {
if (this.configSetAdapter && this.configSetAdapter[id]) {
await crud.createLists(this, id);
await crud.writeDatapoints(this, id);
this.log.debug(`Created and filled data for ${tools.capitalize(id)}`);
}
}
}
// instance and adapter data
if (this.configCreateInstanceList) {
await this.createInstanceList();
await this.writeInstanceDPs();
}
// Clear existing timeout
if (this.refreshDataTimeout) {
this.clearTimeout(this.refreshDataTimeout);
this.refreshDataTimeout = null;
}
this.refreshDataTimeout = this.setTimeout(async () => {
this.log.debug('Updating Data');
await this.refreshData();
}, nextTimeout);
} // <-- refreshData end
/*=============================================
= functions to get data =
=============================================*/
/**
* @param {object} id - deviceID
* @param {object} i - each Device
*/
async getDeviceName(id, i) {
try {
//id = id.replace(/[\]\\[.*,;'"`<>\\\s?]/g, '-');
const currDeviceString = id.slice(0, id.lastIndexOf('.') + 1 - 1);
const shortCurrDeviceString = currDeviceString.slice(0, currDeviceString.lastIndexOf('.') + 1 - 1);
const shortshortCurrDeviceString = shortCurrDeviceString.slice(0, shortCurrDeviceString.lastIndexOf('.') + 1 - 1);
// Get device name
const deviceObject = await this.getForeignObjectAsync(currDeviceString);
const shortDeviceObject = await this.getForeignObjectAsync(shortCurrDeviceString);
const shortshortDeviceObject = await this.getForeignObjectAsync(shortshortCurrDeviceString);
let deviceName;
let folderName;
let deviceID;
switch (this.selAdapter[i].adapterID) {
case 'fullybrowser':
deviceName = (await tools.getInitValue(this,currDeviceString + this.selAdapter[i].id)) + ' ' + (await tools.getInitValue(this,currDeviceString + this.selAdapter[i].id2));
break;
// Get ID with short currDeviceString from objectjson
case 'hueExt':
case 'hmrpc':
case 'nukiExt':
case 'wled':
case 'mqttNuki':
case 'loqedSmartLock':
case 'viessmann':
case 'homekitController':
case 'ring':
if (shortDeviceObject && typeof shortDeviceObject === 'object' && shortDeviceObject.common) {
deviceName = shortDeviceObject.common.name;
}
break;
// Get ID with short short currDeviceString from objectjson (HMiP Devices)
case 'hmiP':
if (shortshortDeviceObject && typeof shortshortDeviceObject === 'object' && shortshortDeviceObject.common) {
deviceName = shortshortDeviceObject.common.name;
}
break;
// Get ID with short currDeviceString from datapoint
case 'mihomeVacuum':
case 'roomba':
folderName = shortCurrDeviceString.slice(shortCurrDeviceString.lastIndexOf('.') + 1);
deviceID = await tools.getInitValue(this,shortCurrDeviceString + this.selAdapter[i].id);
deviceName = `I${folderName} ${deviceID}`;
break;
//Get ID of foldername
case 'tado':
case 'wifilight':
case 'fullybrowserV3':
case 'sonoff':
deviceName = currDeviceString.slice(currDeviceString.lastIndexOf('.') + 1);
break;
// Format Device name
case 'sureflap':
if (deviceObject && typeof deviceObject === 'object' && deviceObject.common) {
deviceName = deviceObject.common.name
// @ts-ignore FIXME: fix syntax error
.replace(/'/g, '')
.replace(/\(\d+\)/g, '')
.trim()
.replace('Hub', 'Hub -')
.replace('Device', 'Device -');
}
break;
//Get ID of foldername
case 'yeelight':
deviceName = shortCurrDeviceString.slice(shortCurrDeviceString.lastIndexOf('.') + 1);
break;
// Get ID with main selektor from objectjson
default:
if (this.selAdapter[i].id !== 'none' || this.selAdapter[i].id !== undefined) deviceName = await tools.getInitValue(this,currDeviceString + this.selAdapter[i].id);
if (deviceName === null || deviceName === undefined) {
if (deviceObject && typeof deviceObject === 'object' && deviceObject.common) {
deviceName = deviceObject.common.name;
}
}
break;
}
return deviceName;
} catch (error) {
this.log.error(`[getDeviceName] - ${error}`);
}
}
/**
* calculate Signalstrength
* @param {object} deviceQualityState - State value
* @param {object} adapterID - adapter name
*/
async calculateSignalStrength(deviceQualityState, adapterID) {
let linkQuality;
let linkQualityRaw;
let mqttNukiValue;
if (deviceQualityState != null) {
const { val } = deviceQualityState;
if (typeof val === 'number') {
if (this.config.trueState) {
linkQuality = val;
} else {
switch (adapterID) {
case 'roomba':
case 'sonoff':
case 'smartgarden':
linkQuality = `${val}%`;
linkQualityRaw = val;
break;
case 'lupusec':
case 'fullybrowserV3':
linkQuality = val;
break;
default:
if (val <= -255) {
linkQuality = ' - ';
} else if (val < 0) {
linkQualityRaw = Math.min(Math.max(2 * (val + 100), 0), 100);
linkQuality = `${linkQualityRaw}%`;
} else if (val >= 0) {
linkQualityRaw = parseFloat(((100 / 255) * val).toFixed(0));
linkQuality = `${linkQualityRaw}%`;
}
break;
}
}
} else if (typeof val === 'string') {
switch (adapterID) {
case 'netatmo':
linkQuality = val;
break;
case 'nukiExt':
linkQuality = ' - ';
break;
case 'mqttNuki':
linkQuality = val;
mqttNukiValue = parseInt(linkQuality);
if (this.config.trueState) {
linkQuality = val;
} else if (mqttNukiValue < 0) {
linkQualityRaw = Math.min(Math.max(2 * (mqttNukiValue + 100), 0), 100);
linkQuality = `${linkQualityRaw}%`;
}
break;
}
}
} else {
linkQuality = ' - ';
}
return [linkQuality, linkQualityRaw];
}
/**
* get battery data
* @param {object} deviceBatteryState - State value
* @param {object} deviceLowBatState - State value
* @param {object} faultReportingState - State value
* @param {object} adapterID - adapter name
*/
async getBatteryData(deviceBatteryState, deviceLowBatState, faultReportingState, adapterID) {
let batteryHealth = '-';
let isBatteryDevice = false;
let batteryHealthRaw;
let batteryHealthUnitRaw;
switch (adapterID) {
case 'hmrpc':
if (deviceBatteryState === undefined) {
if (faultReportingState !== undefined && faultReportingState !== 6) {
batteryHealth = 'ok';
isBatteryDevice = true;
} else if (deviceLowBatState !== undefined && deviceLowBatState !== 1) {
batteryHealth = 'ok';
isBatteryDevice = true;
} else if (deviceLowBatState !== undefined) {
batteryHealth = 'low';
isBatteryDevice = true;
}
} else if (deviceBatteryState !== 0 && deviceBatteryState < 6) {
batteryHealth = `${deviceBatteryState}V`;
batteryHealthRaw = deviceBatteryState;
batteryHealthUnitRaw = 'V';
isBatteryDevice = true;
}
break;
case 'xsense':
if (deviceBatteryState === undefined) {
// do nothin brdge has no battery
isBatteryDevice = false;
} else if (deviceBatteryState >= 0) {
batteryHealthRaw = deviceBatteryState;
batteryHealthUnitRaw = '';
isBatteryDevice = true;
switch (batteryHealthRaw) {
case 1:
batteryHealth = 'low';
break;
case 2:
batteryHealth = 'medium';
break;
case 3:
batteryHealth = 'ok';
break;
default:
batteryHealth = 'error';
}
}
break;
default:
if (deviceBatteryState === undefined) {
if (deviceLowBatState !== undefined) {
if (deviceLowBatState !== true && deviceLowBatState !== 'NORMAL' && deviceLowBatState !== 1) {
batteryHealth = 'ok';
isBatteryDevice = true;
} else if (deviceLowBatState !== true) {
batteryHealth = 'low';
isBatteryDevice = true;
}
}
} else {
if (typeof deviceBatteryState === 'string') {
if (deviceBatteryState === 'high' || deviceBatteryState === 'medium')
{
batteryHealth = 'ok';
isBatteryDevice = true;
}
else if (deviceBatteryState === 'low') {
batteryHealth = 'low';
isBatteryDevice = true;
}
} else {
batteryHealth = `${deviceBatteryState}%`;
batteryHealthRaw = deviceBatteryState;
batteryHealthUnitRaw = '%';
isBatteryDevice = true;
}
}
break;
}
return [batteryHealth, isBatteryDevice, batteryHealthRaw, batteryHealthUnitRaw];
}
/**
* set low bat indicator
* @param {object} deviceBatteryState
* @param {object} deviceLowBatState
* @param {object} faultReportState
* @param {object} adapterID
*/
async setLowbatIndicator(deviceBatteryState, deviceLowBatState, faultReportState, adapterID) {
let lowBatIndicator = false;
if (deviceLowBatState !== undefined || faultReportState !== undefined) {
switch (adapterID) {
case 'hmrpc':
if (deviceLowBatState === 1 || deviceLowBatState === true || faultReportState === 6) {
lowBatIndicator = true;
}
break;
default:
if (typeof deviceLowBatState === 'number' && deviceLowBatState === 0) {
lowBatIndicator = true;
} else if (typeof deviceLowBatState === 'string' && deviceLowBatState !== 'NORMAL') {
lowBatIndicator = true;
} else if (typeof deviceLowBatState === 'boolean' && deviceLowBatState) {
lowBatIndicator = true;
}
}
} else if (typeof deviceBatteryState === 'number' && deviceBatteryState < this.config.minWarnBatterie) {
lowBatIndicator = true;
} else if (typeof deviceBatteryState === 'string' && deviceBatteryState === 'low') {
lowBatIndicator = true;
}
return lowBatIndicator;
}
/**
* get Last Contact
* @param {object} selector - Selector
*/
async getLastContact(selector) {
const lastContact = tools.getTimestamp(selector);
let lastContactString;
lastContactString = `${this.formatDate(new Date(selector), 'hh:mm')}`;
if (Math.round(lastContact) > 100) {
lastContactString = `${Math.round(lastContact / 60)} ${translations.hours[this.config.userSelectedLanguage]}`;
}
if (Math.round(lastContact / 60) > 48) {
lastContactString = `${Math.round(lastContact / 60 / 24)} ${translations.days[this.config.userSelectedLanguage]}`;
}
return lastContactString;
}
/**
* get online state and time
* @param {object} timeSelector - device Timeselector
* @param {string} adapterID - ID of Adapter
* @param {string} unreachDP - Datapoint of Unreach
* @param {object} linkQuality - Linkquality Value
* @param {object} deviceUnreachState - State of deviceUnreach datapoint
* @param {string} deviceStateSelectorHMRPC - Selector of device state (like .state)
* @param {string} rssiPeerSelectorHMRPC - HM RSSI Peer Datapoint
*/
async getOnlineState(timeSelector, adapterID, unreachDP, linkQuality, deviceUnreachState, deviceStateSelectorHMRPC, rssiPeerSelectorHMRPC) {
let lastContactString;
let deviceState = 'Online';
try {
const deviceTimeSelector = await this.getForeignStateAsync(timeSelector);
const deviceUnreachSelector = await this.getForeignStateAsync(unreachDP);
const deviceStateSelector = await this.getForeignStateAsync(deviceStateSelectorHMRPC); // for hmrpc devices
const rssiPeerSelector = await this.getForeignStateAsync(rssiPeerSelectorHMRPC);
const lastDeviceUnreachStateChange = deviceUnreachSelector != undefined ? tools.getTimestamp(deviceUnreachSelector.lc) : tools.getTimestamp(timeSelector.ts);
// ignore disabled device from zigbee2MQTT
if (adapterID === 'zigbee2MQTT') {
const is_device_disabled = await tools.isDisabledDevice(this, unreachDP.substring(0, unreachDP.lastIndexOf('.')));
if (is_device_disabled) {
return [null, 'disabled', '0%'];
}
}
// If there is no contact since user sets minutes add device in offline list
// calculate to days after 48 hours
switch (unreachDP) {
case 'none':
if (deviceTimeSelector) lastContactString = await this.getLastContact(deviceTimeSelector.ts);
break;
default:
//State changed
if (adapterID === 'hmrpc') {
if (linkQuality !== ' - ' && deviceTimeSelector) {
if (deviceUnreachState === 1) {
lastContactString = await this.getLastContact(deviceTimeSelector.lc);
} else {
lastContactString = await this.getLastContact(deviceTimeSelector.ts);
}
} else {
if (deviceStateSelector) {
// because old hm devices don't send rssi states
lastContactString = await this.getLastContact(deviceStateSelector.ts);
} else if (rssiPeerSelector) {
// because old hm sensors don't send rssi/state values
lastContactString = await this.getLastContact(rssiPeerSelector.ts);
}
}
} else {
if ((!deviceUnreachState || deviceUnreachState === 0) && deviceTimeSelector) {
lastContactString = await this.getLastContact(deviceTimeSelector.lc);
} else {
if (deviceTimeSelector) lastContactString = await this.getLastContact(deviceTimeSelector.ts);
}
break;
}
}
/*=============================================
= Set Online Status =
=============================================*/
let lastContact;
if (deviceTimeSelector) lastContact = tools.getTimestamp(deviceTimeSelector.ts);
if (this.configMaxMinutes !== undefined) {
switch (adapterID) {
case 'hmrpc':
if (this.configMaxMinutes[adapterID] <= 0) {
if (deviceUnreachState === 1) {
deviceState = 'Offline'; //set online state to offline
if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing
}
} else if (lastDeviceUnreachStateChange > this.configMaxMinutes[adapterID] && deviceUnreachState === 1) {
deviceState = 'Offline'; //set online state to offline
if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing
}
break;
case 'proxmox':
if (this.configMaxMinutes[adapterID] <= 0) {
if (deviceUnreachState !== 'running' && deviceUnreachState !== 'online') {
deviceState = 'Offline'; //set online state to offline
if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing
}
} else if (lastDeviceUnreachStateChange > this.configMaxMinutes[adapterID] && deviceUnreachState !== 'running' && deviceUnreachState !== 'online') {
deviceState = 'Offline'; //set online state to offline
if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing
}
break;
case 'hmiP':
case 'maxcube':
if (this.configMaxMinutes[adapterID] <= 0) {
if (deviceUnreachState) {
deviceState = 'Offline'; //set online state to offline
if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing
}
} else if (lastDeviceUnreachStateChange > this.configMaxMinutes[adapterID] && deviceUnreachState) {
deviceState = 'Offline'; //set online state to offline
if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing
}
break;
case 'apcups':
case 'hue':
case 'hueExt':
case 'ping':
case 'deconz':
case 'shelly':
case 'sonoff':
case 'tradfri':
case 'unifi':
case 'zigbee':
case 'zigbee2MQTT':
if (this.configMaxMinutes[adapterID] <= 0) {
if (!deviceUnreachState) {
deviceState = 'Offline'; //set online state to offline
if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing
}
} else if (!deviceUnreachState && lastDeviceUnreachStateChange > this.configMaxMinutes[adapterID]) {
deviceState = 'Offline'; //set online state to offline
if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing
}
break;
case 'mqttClientZigbee2Mqtt':
if (this.configMaxMinutes[adapterID] <= 0) {
if (deviceUnreachState !== 'online') {
deviceState = 'Offline'; //set online state to offline
if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing
}
} else if (deviceUnreachState !== 'online' && lastDeviceUnreachStateChange > this.configMaxMinutes[adapterID]) {
deviceState = 'Offline'; //set online state to offline
if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing
}
break;
case 'mihome':
if (deviceUnreachState !== undefined) {
if (this.configMaxMinutes[adapterID] <= 0) {
if (!deviceUnreachState) {
deviceState = 'Offline'; //set online state to offline
if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing
}
} else if (lastContact && lastContact > this.configMaxMinutes[adapterID]) {
deviceState = 'Offline'; //set online state to offline
if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing
}
} else {
if (this.config.mihomeMaxMinutes <= 0) {
if (this.configMaxMinutes[adapterID] <= 0) {
deviceState = 'Offline'; //set online state to offline
if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing
}
} else if (lastContact && lastContact > this.configMaxMinutes[adapterID]) {
deviceState = 'Offline'; //set online state to offline
if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing
}
}
break;
case 'smartgarden':
if (this.configMaxMinutes[adapterID] <= 0) {
if (deviceUnreachState === 'OFFLINE') {
deviceState = 'Offline'; //set online state to offline
if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing
}
} else if (deviceUnreachState === 'OFFLINE' && lastDeviceUnreachStateChange > this.configMaxMinutes[adapterID]) {
deviceState = 'Offline'; //set online state to offline
if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing
}
break;
default:
if (this.configMaxMinutes[adapterID] <= 0) {
if (!deviceUnreachState) {
deviceState = 'Offline'; //set online state to offline
if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing
}
} else if (lastContact && lastContact > this.configMaxMinutes[adapterID]) {
deviceState = 'Offline'; //set online state to offline
if (linkQuality !== ' - ') linkQuality = '0%'; // set linkQuality to nothing
}
break;
}
}
return [lastContactString, deviceState, linkQuality];
} catch (error) {
this.log.error(`[getLastContact] - ${error}`);
}
}
/**
* @param {any} adapterID
* @param {string | number | boolean | null} deviceUpdateSelector
*/
async checkDeviceUpdate(adapterID, deviceUpdateSelector) {
let isUpgradable;
switch (adapterID) {
case 'hmiP':
if (deviceUpdateSelector === 'UPDATE_AVAILABLE') {
isUpgradable = true;
} else {
isUpgradable = false;
}
break;
case 'ring':
if (deviceUpdateSelector !== 'Up to Date') {
isUpgradable = true;
} else {
isUpgradable = false;
}
break;
default:
if (deviceUpdateSelector !== null && typeof deviceUpdateSelector === 'boolean') {
if (deviceUpdateSelector) {
isUpgradable = true;
} else if (!deviceUpdateSelector) {
isUpgradable = false;
}
}
}
return isUpgradable;
}
/**
* fill the lists for user
* @param {object} device
*/
async theLists(device) {
// Raw List with all devices for user
if (device.Status !== 'disabled') {
this.listAllDevicesUserRaw.push({
Device: device.Device,
Adapter: device.Adapter,
Instance: device.instance,
'Instance connected': device.instancedeviceConnected,
isBatteryDevice: device.isBatteryDevice,
Battery: device.Battery,
BatteryRaw: device.BatteryRaw,
BatteryUnitRaw: device.BatteryUnitRaw,
isLowBat: device.LowBat,
'Signal strength': device.SignalStrength,
'Signal strength Raw': device.SignalStrengthRaw,
'Last contact': device.LastContact,
'Update Available': device.Upgradable,
Status: device.Status,
});
// List with all devices
this.listAllDevices.push({
[translations.Device[this.config.userSelectedLanguage]]: device.Device,
[translations.Adapter[this.config.userSelectedLanguage]]: device.Adapter,
[translations.Battery[this.config.userSelectedLanguage]]: device.Battery,
[translations.Signal_strength[this.config.userSelectedLanguage]]: device.SignalStrength,
[translations.Last_Contact[this.config.userSelectedLanguage]]: device.LastContact,
[translations.Status[this.config.userSelectedLanguage]]: device.Status,
});
// LinkQuality lists
if (device.SignalStrength != ' - ') {
this.linkQualityDevices.push({
[translations.Device[this.config.userSelectedLanguage]]: device.Device,
[translations.Adapter[this.config.userSelectedLanguage]]: device.Adapter,
[translations.Signal_strength[this.config.userSelectedLanguage]]: device.SignalStrength,
});
}
// Battery lists
if (device.isBatteryDevice) {
this.batteryPowered.push({
[translations.Device[this.config.userSelectedLanguage]]: device.Device,
[translations.Adapter[this.config.userSelectedLanguage]]: device.Adapter,
[translations.Battery[this.config.userSelectedLanguage]]: device.Battery,
[translations.Status[this.config.userSelectedLanguage]]: device.Status,
});
}
// Low Bat lists
if (device.LowBat && device.Status !== 'Offline') {
this.batteryLowPowered.push({
[translations.Device[this.config.userSelectedLanguage]]: device.Device,
[translations.Adapter[this.config.userSelectedLanguage]]: device.Adapter,
[translations.Battery[this.config.userSelectedLanguage]]: device.Battery,
});
}
// Offline List
if (device.Status === 'Offline') {
this.offlineDevices.push({
[translations.Device[this.config.userSelectedLanguage]]: device.Device,
[translations.Adapter[this.config.userSelectedLanguage]]: device.Adapter,
[translations.Last_Contact[this.config.userSelectedLanguage]]: device.LastContact,
});
}
// Device update List
if (device.Upgradable === true || device.Upgradable === 1) {
this.upgradableList.push({
[translations.Device[this.config.userSelectedLanguage]]: device.Device,
[translations.Adapter[this.config.userSelectedLanguage]]: device.Adapter,
});
}
}
}
/**
* @param {string | string[]} id
* @param {ioBroker.State} state
*/
async renewDeviceData(id, state) {
let batteryData;
let signalData;
let oldLowBatState;
let contactData;
let oldStatus;
let isLowBatValue;
const deviceID = id.slice(0, id.lastIndexOf('.') + 1 - 1);
const deviceData = this.listAllDevicesRaw.get(deviceID);
if (deviceData) {
// On statechange update available datapoint
switch (id) {
// device connection
case deviceData.instanceDeviceConnectionDP:
if (state.val !== deviceData.instancedeviceConnected) {
deviceData.instancedeviceConnected = state.val;
}
break;
// device updates
case deviceData.UpdateDP:
if (state.val !== deviceData.Upgradable) {
deviceData.Upgradable = await this.checkDeviceUpdate(deviceData.adapterID, state.val);
if (deviceData.Upgradable === true) {
if (this.config.checkSendDeviceUpgrade && !this.blacklistNotify.includes(deviceData.Path)) {
await this.sendStateNotifications('Devices', 'updateDevice', deviceID);
}
}
}
break;
// device signal
case deviceData.SignalStrengthDP:
signalData = await this.calculateSignalStrength(state, deviceData.adapterID);
deviceData.SignalStrength = signalData[0];
break;
// device battery
case deviceData.batteryDP:
if (deviceData.isBatteryDevice) {
oldLowBatState = deviceData.LowBat;
if (state.val === 0 && deviceData.BatteryRaw >= 5) return;
batteryData = await this.getBatteryData(state.val, oldLowBatState, deviceData.faultReport, deviceData.adapterID);
deviceData.Battery = batteryData[0];
deviceData.BatteryRaw = batteryData[2];
deviceData.BatteryUnitRaw = batteryData[3];
if (deviceData.LowBatDP !== 'none') {
isLowBatValue = await tools.getInitValue(this,deviceData.LowBatDP);
} else {
isLowBatValue = undefined;
}
deviceData.LowBat = await this.setLowbatIndicator(state.val, isLowBatValue, deviceData.faultReport, deviceData.adapterID);
if (deviceData.LowBat && oldLowBatState !== deviceData.LowBat) {
if (this.config.checkSendBatteryMsg && !this.blacklistNotify.includes(deviceData.Path)) {
await this.sendStateNotifications('Devices', 'lowBatDevice', deviceID);
}
}
}
break;
// device low bat
case deviceData.LowBatDP:
if (deviceData.isBatteryDevice) {
oldLowBatState = deviceData.LowBat;
batteryData = await this.getBatteryData(deviceData.BatteryRaw, state.val, deviceData.faultReport, deviceData.adapterID);
deviceData.Battery = batteryData[0];
deviceData.BatteryRaw = batteryData[2];
deviceData.BatteryUnitRaw = batteryData[3];
deviceData.LowBat = await this.setLowbatIndicator(deviceData.BatteryRaw, state.val, deviceData.faultReport, deviceData.adapterID);
if (deviceData.LowBat && oldLowBatState !== deviceData.LowBat) {
if (this.config.checkSendBatteryMsg && !this.blacklistNotify.includes(deviceData.Path)) {
await this.sendStateNotifications('Devices', 'lowBatDevice', deviceID);
}
}
}
break;
//device error / fault reports
case deviceData.faultReportDP:
if (deviceData.isBatteryDevice) {
oldLowBatState = deviceData.LowBat;
batteryData = await this.getBatteryData(deviceData.BatteryRaw, oldLowBatState, state.val, deviceData.adapterID);
deviceData.Battery = batteryData[0];
deviceData.BatteryRaw = batteryData[2];
deviceData.BatteryUnitRaw = batteryData[3];
deviceData.LowBat = await this.setLowbatIndicator(deviceData.BatteryRaw, undefined, state.val, deviceData.adapterID);
if (deviceData.LowBat && oldLowBatState !== deviceData.LowBat) {
if (this.config.checkSendBatteryMsg && !this.blacklistNotify.includes(deviceData.Path)) {
await this.sendStateNotifications('Devices', 'lowBatDevice', deviceID);
}
}
}
break;
// device unreach
case deviceData.UnreachDP:
if (deviceData.instancedeviceConnected !== undefined) {
if (deviceData.UnreachState !== state.val) {
oldStatus = deviceData.Status;
deviceData.UnreachState = state.val;
contactData = await this.getOnlineState(
deviceData.timeSelector,
deviceData.adapterID,
deviceData.UnreachDP,
deviceData.SignalStrength,
deviceData.UnreachState,
deviceData.deviceStateSelectorHMRPC,
deviceData.rssiPeerSelectorHMRPC,
);
if (contactData !== undefined && contactData !== null) {
deviceData.LastContact = contactData[0];
deviceData.Status = contactData[1];
deviceData.SignalStrength = contactData[2];
}
if (this.config.checkSendOfflineMsg && oldStatus !== deviceData.Status && !this.blacklistNotify.includes(deviceData.Path)) {
// check if the generally deviceData connected state is for a while true
if (await tools.getTimestampConnectionDP(this, deviceData.instanceDeviceConnectionDP, 50000)) {
await this.sendStateNotifications('Devices', 'onlineStateDevice', deviceID);
}
}
}
}
break;
}
}
}
/**
* get all Instances at start
*/
async getAllInstanceData() {
try {
const allInstances = `system.adapter.*`;
await this.getInstanceData(allInstances);
} catch (error) {
this.log.error(`[getInstance] - ${error}`);
}
}
/**
* get instance data
*@param {string} instanceObject
*/
async getInstanceData(instanceObject) {
try {
const instanceAliveDP = await this.getForeignStatesAsync(`${instanceObject}.alive`);
this.adapterUpdatesJsonRaw = await this.getAdapterUpdateData(adapterUpdateListDP);
for (const [id] of Object.entries(instanceAliveDP)) {
if (!(typeof id === 'string' && id.startsWith(`system.adapter.`))) continue;
// get instance name
const instanceID = await this.getInstanceName(id);
// get instance connected to host data
const instanceConnectedHostDP = `system.adapter.${instanceID}.connected`;
const instanceConnectedHostVal = await tools.getInitValue(this,instanceConnectedHostDP);
// get instance connected to device data
const instanceConnectedDeviceDP = `${instanceID}.info.connection`;
let instanceConnectedDeviceVal;
if (instanceConnectedDeviceDP !== undefined && typeof instanceConnectedDeviceDP === 'boolean') {
instanceConnectedDeviceVal = await tools.getInitValue(this,instanceConnectedDeviceDP);
} else {
instanceConnectedDeviceVal = 'N/A';
}
// get adapter version
const instanceObjectPath = `system.adapter.${instanceID}`;
let adapterName;
let adapterVersion;
let adapterAvailableUpdate = '';
let instanceMode;
let scheduleTime = 'N/A';
const instanceObjectData = await this.getForeignObjectAsync(instanceObjectPath);
if (instanceObjectData) {
// @ts-ignore
adapterName = tools.capitalize(instanceObjectData.common.name);
adapterVersion = instanceObjectData.common.version;
instanceMode = instanceObjectData.common.mode;
if (instanceMode === 'schedule') {
scheduleTime = instanceObjectData.common.schedule;
}
}
const updateEntry = this.adapterUpdatesJsonRaw.find(
entry => entry.adapter.toLowerCase() === adapterName.toLowerCase()
);
if (updateEntry) {
adapterAvailableUpdate = updateEntry.newVersion;
} else {
adapterAvailableUpdate = ' - ';
}
let isAlive;
let isHealthy;
let instanceStatus;
if (instanceMode === 'schedule') {
const instanceStatusRaw = await this.checkScheduleisHealty(instanceID, scheduleTime);
isAlive = instanceStatusRaw[0];
isHealthy