/* 
** Watchdog **
** Skript um Geräte zu Überwachen ob diese noch erreichbar sind und wie der aktuelle Batteriestand ist.
** Github Link: https://github.com/ciddi89/ioBroker_device_watchdog
** ioBroker Topiclink: https://forum.iobroker.net/topic/52108/zigbee-geräte-überwachen
** Thanks to JohannesA for the first work and great idea!
** Last change on 02.03.2022
*/
const watchDogVersion = '0.0.3';
//Hauptpfad wo die Datenpunkte gespeichert werden sollen. Kann bei Bedarf angepasst werden.
const basePath = "0_userdata.0.Datenpunkte.DeviceWatchdog.";
//Für Telegram Benachrichtigung
const sendTelegram = false;                 //Soll per Telegram eine Nachricht gesendet werden? true = Ja / false = Nein
const userTelegram = '';                    //leer lassen falls jeder User eine Nachricht bekommen soll.
//Für Pushover Benachrichtigung
const sendPushover      = true;             //Soll per Pushover eine Nachricht gesendet werden? true = Ja / false = Nein
const devicePushover    = 'All';
const titelPushover     = 'ioBroker Watchdog';
//Für Jarvis Notification
const sendJarvis    = false;                //Soll per Jarvis Notifications eine Nachricht gesendet werden? true = Ja / false = Nein
const titleJarvis   = 'WatchDog-Script'
//Soll eine Meldung erfolgen falls der Batteriestand der Geräte gering ist? Hinweis: Die Meldung kommt 3x in einer Woche als Auflistung!
const sendBatterieMsg = true;
//Soll bei Skript Neustart eine Meldung der Batteriestände gesendet werden?
const sendBatterieMsgAtStart = false;
//Ab wieviel % Restbatterie soll eine Meldung erfolgen?
const batteryWarningMin = 75;
//Soll eine Meldung erfolgen falls die Anzahl der "Offline-Geräte" im Vergleich zur letzten Prüfung höher ist?
const sendOfflineMsg = true;
//Welche Geräte sollen überwacht werden?
const watchZigbee       = true;     // Zigbee Adapter
const watchBle          = true;     // Ble Adapter z.B. MiFlora Sensoren
const watchMqttXiaomi   = false;    // MQTT Xiaomi Antenna
const trueLinkQuality   = false;    // Soll der Echt-Wert der Linkqualität (0-255 oder RSSI-Wert) verwendet werde? true = Ja / false = Nein
/*****************************************************************
**
** Ab hier bitte nichts mehr ändern! Außer man weiß was man tut!
**
******************************************************************/
//Pfad der einzelenden Datenpunkte
const stateDevicesCount             = basePath + "devices_count_all";
const stateDevicesLinkQuality       = basePath + "devices_link_quality_list";
const stateDevicesOfflineCount      = basePath + "devices_offline_count";
const stateDevicesOffline           = basePath + "devices_offline_list";
const stateDevicesWithBatteryCount  = basePath + "devices_battery_count";
const stateDevicesWithBattery       = basePath + "devices_battery_list";
const stateDevicesInfoList          = basePath + "devices_list_all";
const stateDevicesLastCheck         = basePath + "lastCheck";
const watchdogLog                   = basePath + "watchdogLog";
//Funktion zur Erstellung der Datenpunkte
async function doStates() {
   if (!(await existsStateAsync(stateDevicesCount))) await createStateAsync(stateDevicesCount, 0, { read: true, write: true, desc: "Anzahl Geräte gesamt", name: "Anzahl Geräte gesamt",type: 'number' });
   if (!(await existsStateAsync(stateDevicesLinkQuality))) await createStateAsync(stateDevicesLinkQuality, "", { read: true, write: true, desc: "Liste Geräte Signalstärke", name: "Liste Geräte Signalstärke", type: 'string' });
   if (!(await existsStateAsync(stateDevicesOfflineCount))) await createStateAsync(stateDevicesOfflineCount, 0, { read: true, write: true, desc: "Anzahl Geräte offline", name: "Anzahl Geräte offline",type: 'number' });
   if (!(await existsStateAsync(stateDevicesOffline))) await createStateAsync(stateDevicesOffline, "", { read: true, write: true, desc: "Liste Geräte offline", name: "Liste Geräte offline",type: 'string' });
   if (!(await existsStateAsync(stateDevicesWithBattery))) await createStateAsync(stateDevicesWithBattery, "", {read: true, write: true, desc: "Liste Geräte mit Batterie", name: "Liste Geräte mit Batterie", type: 'string'});
   if (!(await existsStateAsync(stateDevicesWithBatteryCount))) await createStateAsync(stateDevicesWithBatteryCount, 0, {read: true, write: true, desc: "Anzahl Geräte mit Batterie", name: "Anzahl Geräte mit Batterie", type: 'number'});
   if (!(await existsStateAsync(stateDevicesInfoList))) await createStateAsync(stateDevicesInfoList, "", {read: true, write: true, desc: "Liste aller Geräte", name: "Liste aller Geräte", type: 'string'}); 
   if (!(await existsStateAsync(stateDevicesLastCheck))) await createStateAsync(stateDevicesLastCheck, "", {read: true, write: true, desc: "Zeitpunkt letzter Überprüfung", name: "Zeitpunkt letzter Überprüfung", type: 'string'});
   if (!(await existsStateAsync(watchdogLog))) await createStateAsync(watchdogLog, "", {read: true, write: true, desc: "Log vom Device Watchdog", name: "Device Watchdog Log", type: 'string'});
}
//Die Mainfunction.
async function deviceWatchdog() {
   let maxMinutes              = 300; // "Gerät offline" - Wert in Minuten: Gilt erst, wenn Gerät länger als X Minuten keine Meldung gesendet hat 
   let arrOfflineDevices       = []; //JSON-Info alle offline-Geräte
   let arrLinkQualityDevices   = []; //JSON-Info alle offline-Geräte
   let arrBatteryPowered       = []; //JSON-Info alle batteriebetriebenen Geräte
   let arrListAllDevices       = []; //JSON-Info Gesamtliste mit Info je Gerät
   let currDeviceString;
   let currDeviceBatteryString;
   let currRoom;
   let deviceName;
   let linkQuality;
   let lastContact;
   let lastContactString;
   let offlineDevicesCount;
   let deviceCounter           = 0;
   let batteryPoweredCount     = 0;
   let batteryHealth;
   let adapterName;
   const myArrDev              = [];
   const myArrBlacklist        = [];
   if (watchZigbee) {
       myArrDev.push({"theSelektor":"zigbee.0.*.link_quality","theName":"common","linkQual":"zigbee","batt":"zigbee"})
   }
   if (watchBle) {
       myArrDev.push({"theSelektor":"ble.0.*.rssi","theName":"common","linkQual":"ble","batt":"none"})
   }
   if (watchMqttXiaomi) {
       myArrDev.push({"theSelektor":"mqtt.0.xiaomiantenna.*.status","theName":"Objectname2Level","linkQual":"none","batt":"none"})
       myArrDev.push({"theSelektor":"mqtt.0.xiaomiantenna.sensors.sensor.*_batt.state","theName":"Objectname1Level","linkQual":"none","batt":"dpvalue"})
   }
   for(let x=0; x<myArrDev.length;x++) {
       var device = $(myArrDev[x].theSelektor);
       device.each(function (id, i) {
   
           currDeviceString = id.slice(0, (id.lastIndexOf('.') + 1) - 1);
           adapterName = getObject(currDeviceString)._id[0].toUpperCase() + getObject(currDeviceString)._id.slice(1, (id.indexOf('.') + 1) - 1);
           //hier braucht man eine function, die den hostnamen findet:
           if (myArrDev[x].theName=="common")  deviceName=getObject(currDeviceString).common.name
           if (myArrDev[x].theName=="dp") {
                                       let ida=id.split('.');
                                       let mySelect=$(ida[0]+'.'+ida[1]+'.'+ida[2]+'.*');
                                       mySelect.each(function (ad, i) {
                                           if (ad.includes(myArrDev[x].thedpName)) deviceName=getState(ad).val
                                       });
                                       }
           currRoom = getObject(id, 'rooms').enumNames[0];
           if(typeof currRoom == 'object') currRoom = currRoom.de;
                                       
   
           // 1. Link-Qualität des Gerätes ermitteln
           //---------------------------------------
           linkQuality = "";
           if (trueLinkQuality) {
               linkQuality = getState(id).val;
           } 
           else {
               if (getState(id).val < 0) {
                   linkQuality = Math.min(Math.max(2 * (getState(id).val + 100), 0), 100) + "%"; // Linkqualität von RSSI in % umrechnen
               } else {
                   linkQuality = parseFloat((100/255 * getState(id).val).toFixed(0)) + "%"; // Linkqualität in % verwenden
               }
           };
           arrLinkQualityDevices.push({device: deviceName, adapter: adapterName, room: currRoom, link_quality: linkQuality})
   
           // 2. Wann bestand letzter Kontakt zum Gerät
           //------------------------      
           lastContact = Math.round((new Date() - new Date(getState(id).ts)) / 1000 / 60);
           // 2b. wenn seit X Minuten kein Kontakt mehr besteht, nimm Gerät in Liste auf
           //Rechne auf Tage um, wenn mehr als 48 Stunden seit letztem Kontakt vergangen sind
           lastContactString=Math.round(lastContact) + " Minuten";
           if (Math.round(lastContact) > 100) {
               lastContactString=Math.round(lastContact/60) + " Stunden";
           } 
           if (Math.round(lastContact/60) > 48) {
               lastContactString=Math.round(lastContact/60/24) + " Tagen";
           } 
           if (lastContact > maxMinutes) {
               arrOfflineDevices.push({device: deviceName, adapter: adapterName, room: currRoom, lastContact: lastContactString});
           }
   
           // 3. Batteriestatus abfragen
           currDeviceBatteryString = currDeviceString + ".battery";
           if (existsState(currDeviceBatteryString)) {
               batteryHealth = getState(currDeviceBatteryString).val + "%"; // Batteriestatus in %
               arrBatteryPowered.push({device: deviceName, adapter: adapterName, room: currRoom, battery: batteryHealth});
           } 
           else {
               batteryHealth = "-";
           }
       
           arrListAllDevices.push({device: deviceName, adapter: adapterName, room: currRoom, battery: batteryHealth, lastContact: lastContactString, link_quality: linkQuality});
   
       });
   
   
       // 1b. Zähle, wie viele Geräte existieren
       //---------------------------------------------       
       deviceCounter = arrLinkQualityDevices.length;
           //falls keine Geräte vorhanden sind, passe Datenpunkt-Inhalt der Geräte-Liste an
           if (deviceCounter == 0) { 
               arrLinkQualityDevices.push({device: "--keine--", room: "", link_quality: ""})
               arrListAllDevices.push({device: "--keine--", room: "", battery: "", lastContact: "", link_quality: ""});
           }
   
       // 2c. Wie viele Geräte sind offline?
       //------------------------   
       offlineDevicesCount = arrOfflineDevices.length;
           //falls keine Geräte vorhanden sind, passe Datenpunkt-Inhalt der Geräte-Liste an
           if (offlineDevicesCount == 0) { 
               arrOfflineDevices.push({device: "--keine--", room: "", lastContact: ""})
           }
   
       // 3c. Wie viele Geräte sind batteriebetrieben?
       //------------------------   
       batteryPoweredCount = arrBatteryPowered.length;
           //falls keine Geräte vorhanden sind, passe Datenpunkt-Inhalt der Geräte-Liste an
           if (batteryPoweredCount == 0) { 
               arrBatteryPowered.push({device: "--keine--", room: "", battery: ""})
           }
   
       // SETZE STATES
       await setStateAsync(stateDevicesCount, deviceCounter);
       await setStateAsync(stateDevicesLinkQuality, JSON.stringify(arrLinkQualityDevices));
       await setStateAsync(stateDevicesOfflineCount, offlineDevicesCount);
       await setStateAsync(stateDevicesOffline, JSON.stringify(arrOfflineDevices));
       await setStateAsync(stateDevicesWithBatteryCount, batteryPoweredCount);
       await setStateAsync(stateDevicesWithBattery, JSON.stringify(arrBatteryPowered));
       await setStateAsync(stateDevicesInfoList, JSON.stringify(arrListAllDevices));
       await setStateAsync(stateDevicesLastCheck, [formatDate(new Date(), "DD.MM.YYYY"),' - ',formatDate(new Date(), "hh:mm:ss")].join(''));
       // Sende Benachrichtigungen falls sich die Anzahl der "Offline-Geräte" im Vergleich zur letzten Prüfung erhöht hat.
       if (sendOfflineMsg) {
           let infotext = "";
           let offlineDevicesCountOld = getState(stateDevicesOfflineCount).val;
           if (offlineDevicesCount > offlineDevicesCountOld) {
               if (offlineDevicesCount == 1) {
                   infotext = "Folgendes Gerät ist seit einiger Zeit nicht erreichbar: \n";
               } else {
                   infotext = "Folgende " + offlineDevicesCount + " Geräte sind seit einiger Zeit nicht erreichbar: \n";
               }
               for (const id of arrOfflineDevices) {
                   infotext = infotext + "\n" + id["device"] + " " + id["room"] + " (" + id["lastContact"] + ")";
               };
               log(infotext);
               await setStateAsync(watchdogLog, infotext);
               if (sendJarvis) {
               await setStateAsync("jarvis.0.addNotification", '{"title":"'+ titleJarvis +' (' + formatDate(new Date(), "DD.MM.YYYY - hh:mm:ss") + ')","message":" ' + offlineDevicesCount + ' Geräte sind nicht erreichbar","display": "drawer"}');
               };
               if (sendPushover) {
                   await pushover("Watchdog Alarm: " + infotext)
               };
               if (sendTelegram) {
                   await telegram("Watchdog Alarm: " + infotext)
               }
           }
       }
}};
//Telegram function
async function telegram (msg) {
   sendTo('telegram', { 
       text: msg,
       user: userTelegram,
       parse_mode: 'HTML'
   })
};
//Pushover function
async function pushover (msg) {
   sendTo("pushover", {
       title: titelPushover,
       message: msg,
       device: devicePushover
   })
};
async function checkBatterie () {
   if (sendBatterieMsg) {
       let weakCount = 0;
       let batteryData = JSON.parse(getState(stateDevicesWithBattery).val);
       let infotext = "";
       for (const id of batteryData) {
           let batteryValue = id["battery"].replace("%", "");
           if (batteryValue < batteryWarningMin) {
               infotext = infotext + "\n" + id["device"] + " " + id["room"] + " (" + id["battery"] + ")";
               ++weakCount;
           }
       }
       if (weakCount > 0) {
           log("Batteriezustand: " + infotext);
           await setStateAsync(watchdogLog, infotext);
           if (sendJarvis) {
               await setStateAsync("jarvis.0.addNotification", '{"title":"'+ titleJarvis +' (' + formatDate(new Date(), "DD.MM.YYYY - hh:mm:ss") + ')","message":" ' + weakCount + ' Geräte mit schwacher Batterie","display": "drawer"}'); 
           };
           if (sendPushover) {
               await pushover("Batteriezustand: " + infotext)
           }
           if (sendTelegram) {
               await telegram("Batteriezustand: " + infotext)
           }
       } 
       else {
           await setStateAsync(watchdogLog, "Batterien der Geräte in Ordnung");
       }
   }
};
//Das Skript wird nach jeder vollen Stunde in der 6 minute ausgeführt
schedule("6 */1 * * *", async function () {
   log("Run Device-Watchdog");
   await doStates().then(deviceWatchdog);
});
//Das Skript wird einmal nach Skriptstart ausgeführt
setTimeout (async function () {
   log("Run Device-Watchdog");
   await doStates().then(deviceWatchdog);
   if (sendBatterieMsgAtStart) {
       await checkBatterie()
   }
}, 300);
//Script überprüft an vordefinierten Zeitpunkten den Batteriestand der Geräte und macht entsprechend Meldung, wenn der Batteriestatus unter x% fällt
// Hinweis: 
// Dies passiert 3x pro Woche
if (sendBatterieMsg) {
   schedule('{"time":{"exactTime":true,"start":"12:50"},"period":{"days":1,"dows":"[2,4,6]"}}', async function () {
       await checkBatterie();
   })
}