// Zaehlscript fuer Fenster, Brunnen, Rolladen, Lichter (Dimmer und state) und Hue // Autor Looxer01 July 2020, optimiert und erweitert Aug 2024 // Version 4.6 23.09.2024 // korrektur initialer Durchlauf / Text nur bis zum ersten Punkt // algemeiner Pfad hinzugefuegt // Version 4.7 24.09.2024 // Reihenfolge CreateState&Initialisierung geaendert // Korrektur paths // Version 4.8 29.09. // weiterer check bei initialsierung eingebaut // Loeschen von Subcriptions korrigiert wenn Datenpunkte geloescht wruden // Umlaute entfernt //------------------------------------------------------------------------------------------------------------------------------------------- // Einstellungsbereich // Datenpunkte werden automatisch angelegt. Muss im Javascript Tree erfolgen, zwingend count, active, text und status muessen angelegt werden // Pfade fuer die Zaehlungsergebnisse // Achtung in allen 3 Tabellen muss Gross und Kleinschreibung beruecksichtigt werden. Wenn in paths z.B. "MOTION" definiert ist, dann muss so auch in selectors definiert werden // CommonPath kann auch auf userdata gelegt werden. Einfach den Vorschlag uebernehmen const commonPath = "javascript.0.Counts."; //const commonPath = "0_userdata.0.Counts."; const paths = { Leuchten: { count: commonPath+'Leuchten.anzahlLichter', active: commonPath+'Leuchten.anzahlLichterAn', text: commonPath+'Leuchten.textLichterAn', status: commonPath+'Leuchten.Statusliste' }, Fenster: { count: commonPath+'Fenster.anzahlFenster', active: commonPath+'Fenster.anzahlFensterauf', text: commonPath+'Fenster.textFensterauf', status: commonPath+'Fenster.Statusliste' }, Rolladen: { count: commonPath+'Rolladen.anzahlRolladen', active: commonPath+'Rolladen.anzahlRolladenauf', text: commonPath+'Rolladen.textRolladenauf', status: commonPath+'Rolladen.Statusliste' }, MOTION: { count: commonPath+'Motion.anzahlMotion', active: commonPath+'Motion.anzahlMotionAktiv', text: commonPath+'Motion.textMotionAktiv', status: commonPath+'Motion.Statusliste' }, Brunnen: { count: commonPath+'Brunnen.anzahlBrunnen', active: commonPath+'Brunnen.anzahlBrunnenAn', text: commonPath+'Brunnen.textBrunnenAn', status: commonPath+'Brunnen.Statusliste' } // Weitere Daenpunkte koennen hier hinzugefuegt werden / loeschen von Datenpunkten falls nicht benoetigt }; // Array fuer benutzerdefinierte Zusammenfassungen in die Zuvor definierten Datenpunkte, z.B. Dimmer Hue Lampen // Der name wird gemapped mit den Namen in der Tabelle paths - bedeutet die 3 Selektoren "Lampen,Dimmer,Hue" werden im Pfad unter "Leuchten" zusammengefasst. // Beispielsweise koennten auch Fenster und Rolladen zusammengefasst werden const groupings = { Leuchten: ['Lampen', 'Dimmer', 'Hue'], // "Lampen und Dimmer und Hue werden unter Leuchten zusammengefasst" // Weitere Zusammenfassungen koennen hier hinzugefuegt werden / falls keine Groupings benoetigt werden, dann ersetzen mit const groupings = [] }; // Selectoren sind eine Art Filter um die richtigen Datenpunkte zu finden, die dann subscribed werden // Functions in iobroker entsprechen Gewerke in Homematic // Achtung die Namen aus den Datenpunkten muessen exakt mit den hier verwendeten Namen uebereinstimmmen: also z.B. MOTION = MOTION // der update der Hue lampen dauert etwas laenger, da die updates ueber die Hue-Bridge relativ langsam sind const selectors = { Lampen: $('channel[state.id=*.STATE](functions=Licht)'), // allen Leuchten wurde das Gewerk "Licht zugeordnet aber nur fuer den relevanten Kanal in Homematic" Dimmer: $('channel[state.id=*.LEVEL](functions=Licht)'), Brunnen: $('channel[state.id=*.STATE](functions=Brunnen)'),// allen Pumpen wurde das Gewerk "Brunnen zugeordnet aber nur fuer den relevanten Kanal in Homematic" Hue: $('channel[state.id=hue.0.*.on](functions=Hue)'), // in der objektliste habe ich den channels (Hue-Leuchten) die Funktion "Hue" zugeordnet Rolladen: $('channel[state.id=*.LEVEL](functions=Rollade)'),// allen Rollladen wurde das Gewerk "Rollade" zugeordnet aber nur fuer den relevanten Kanal in Homematic" Fenster: $('channel[state.id=*.STATE](functions=Verschluss)'), MOTION: $('channel[state.id=*.MOTION](functions=Sicherheit)') // Weitere Selektoren koennen hier hinzugefuegt werden / loeschen von Selektoren falls nicht benoetigt } const debug = true; const listSelectors = true; // listet das Ergebnis der Selektoren beimn Start des Programmes- dient zum Fehler finden falls die Zaehlung nicht funktioniert const logObjects = ['MOTION']; // die hier angegebenen Objekte (aus den selectors) werden in einer excel-lesbaren Datei geloggt // Weitere Objekte koennen hinzugefuegt werden, z.B. const logObjects = ['MOTION','Fenster']; oder keine Objekte: const logObjects = [] // Ende Einstellungsbereich //---------------------------------------------------------------------------------------------------------------------------------------------- const LogPath = "/opt/iobroker/log/Zaehlscript.csv" // falls ein externes log in CSV Format geschrieben werden soll. (anhand Array logObjects) log ("Scriptversion ist 4.8") if (listSelectors) { listMembers(); }; // Falls erforderlich, die Selector-Ergebnisse auflisten checkConsistency(paths, selectors, groupings); // Konsistenzpruefung der Arrays let modus = "init"; // waehrend des Durchlaufs kann festgestellt werden, ob es sich um die Initialisierung handelt / kein log schreiben bei initialisierung CreateStates(() => { // Anlegen der States mit Callback, um abzuwarten bis die States zur Verfuegung stehen initializeCounts(); // Zaehlungen initialisieren modus = "subscription"; // Nach Abschluss aller Initialisierungen Modus auf 'subscription' setzen }); setupSubscriptions(); // Die Subscriptions fuer die Selectors werden eingerichtet //----------------------------------------------------------------------------------------- // Function setupSubscriptions - Einrichtung der Subscriptions //----------------------------------------------------------------------------------------- function setupSubscriptions() { Object.keys(selectors).forEach(type => { selectors[type].on(obj => { Count(obj.id, obj.state.val, type); }); }); } //----------------------------------------------------------------------------------------- // Function recreateSubscriptions - Loeschen der Subscriptions //----------------------------------------------------------------------------------------- function clearSubscriptions(idToRemove) { unsubscribe(idToRemove); log(`Subscription ${idToRemove} wurde geloescht da der Datenpunkt nicht mehr existiert`,'warn'); } //--------------------------------------------------------------------------------------- // Function Count - Zentrale Routine die bei aenderung eines Zustandes aufgerufen wird // geht durch alle Datenpunkte des Selectors und zaehlt - falls ein relevantes Geraet eingeschaltet ist //----------------------------------------------------------------------------------------- function Count(triggerId, newState, type) { let primaryType = getPrimaryTypeForType(type); let typesToProcess = groupings[primaryType] || [primaryType]; if (!paths[primaryType]) { log(`Kein Pfad fuer Typ ${primaryType} gefunden`, 'warn'); return; } let path = paths[primaryType]; let counterAll = 0; let counterActive = 0; const text = []; let jsonString = "["; // Helper function to process each ID const processId = (id, selectorType) => { if (CheckDataPoint(id) === false) { clearSubscriptions(id) return; // beendet die Verarbeitung fuer diese ID } const status = getState(id).val; const object = getObject(id); if (shouldCount(status, selectorType, id)) { counterActive++; text.push(createTextEntry(object.common.name)); jsonString += createJsonEntry(selectorType, object.common.name, status); if (modus != "init" && logObjects.includes(type)) { writelog(type + "," + id + "," + object.common.name + ";" + status); } } counterAll++; }; // Iteration ueber die zu verarbeitenden Typen typesToProcess.forEach(selectorType => { selectors[selectorType]?.each((id) => processId(id, selectorType)); }); jsonString = `${jsonString}]`.replace(",]", "]"); logResult(type, counterAll, counterActive, triggerId, newState); setStateValues(path, counterAll, counterActive, text, jsonString); } //----------------------------------------------------------------------------------------- // Function getPrimaryTypeForType - Zentrale Routine die bei aenderung eines Zustandes aufgerufen wird // geht durch alle Datenpunkte des Selectors und zaehlt - falls ein relevantes Geraet eingeschaltet ist //----------------------------------------------------------------------------------------- function getPrimaryTypeForType(type) { // Suche den primaryType in den Gruppierungen, in denen dieser Typ enthalten ist for (let primaryType in groupings) { if (groupings[primaryType].includes(type)) { return primaryType; } } if ( type ) { return type; // Wenn nicht in den Gruppierungen, als einzelner Typ behandeln } return null; } //----------------------------------------------------------------------------------------- // Function shouldCount -check relevanz ob hochgezaehlt werden soll //----------------------------------------------------------------------------------------- function shouldCount(status, type, id) { return status === true || parseFloat(status) > 0; } //----------------------------------------------------------------------------------------- // Function createJsonEntry - Routine erzeugt Json record //----------------------------------------------------------------------------------------- function createJsonEntry(type, Name, status) { let CleanName = Name.split('.')[0]; return `{"Type":"${type}","Ort":"${CleanName}","Status":"${status}"},`; } //----------------------------------------------------------------------------------------- // Function createTextEntry - Routine erzeugt Text record //----------------------------------------------------------------------------------------- function createTextEntry(Name) { let CleanName = Name.split('.')[0]; return CleanName; } //----------------------------------------------------------------------------------------- // Function logResult - log falls debug = true //----------------------------------------------------------------------------------------- function logResult(type, counterAll, counterActive, triggerId, newState) { let commonName = "kein trigger identifiziert"; if (modus === "init" ) { commonName = "Initialisierung"}; // initialisierung laeuft if (triggerId !== null) { if (CheckDataPoint(triggerId)) { commonName = getObject(triggerId).common.name; } else { commonName = "Datenpunkt existiert nicht"; } } if (debug) { log(`Anzahl ${type}: ${counterAll} | ${type} aktiv: ${counterActive} | Ausloeser: ${commonName} | Status: ${newState}`, "info"); } } //----------------------------------------------------------------------------------------- // Function setStateValues - speichern der Zaehlung in den relevanten Datepunkten //----------------------------------------------------------------------------------------- function setStateValues(path, counterAll, counterActive, text, jsonString) { setState(path.status, jsonString); setState(path.active, counterActive); setState(path.count, counterAll); setState(path.text, text.toString()); } //----------------------------------------------------------------------------------------- // Function CreateStates - Anlegen der States mit Verzoegerung bis angelegt //----------------------------------------------------------------------------------------- function CreateStates(callback) { let createCount = 0; let expectedCount = 0; Object.keys(paths).forEach(type => { const path = paths[type]; expectedCount += 4; // Zaehlt die erwarteten neuen Datenpunkte createState(path.count, 0, { read: true, write: true, type: 'number', name: `Anzahl ${type}`, desc: `Anzahl ${type}` }, () => { createCount++; checkIfAllCreated(); }); createState(path.active, 0, { read: true, write: true, type: 'number', name: `Anzahl ${type} aktiv`, desc: `Anzahl ${type} aktiv` }, () => {createCount++; checkIfAllCreated();}); createState(path.text, "", { read: true, write: true, type: 'string', name: `Text ${type}`, desc: `Text ${type}` }, () => { createCount++;checkIfAllCreated(); }); createState(path.status, "[]", { read: true, write: true, type: 'string', name: `Status ${type} JSON Liste`, desc: `Status ${type} als JSON` }, () => {createCount++; checkIfAllCreated();}); }); //----------------------------------------------------------------------------------------- // Function CreateStates - Anlegen der States mit Verzoegerung bis angelegt //----------------------------------------------------------------------------------------- function checkIfAllCreated() { if (createCount === expectedCount && typeof callback === 'function') { callback(); } } } //----------------------------------------------------------------------------------------- // Function CheckDataPoint - Check States ob angelegt /falls objekte geloescht wurden //----------------------------------------------------------------------------------------- function CheckDataPoint(id) { return existsState(id) } //----------------------------------------------------------------------------------------- // Function initializeCounts - Initialisierung der Zaehler beim Starten des Programmes. // ermittelt aktuelle Zaehlung //----------------------------------------------------------------------------------------- function initializeCounts() { // Erst alle Typen, die in den groupings definiert sind Object.keys(groupings).forEach(primaryType => { Count(null, null, primaryType); }); // Dann alle Typen, die nicht in den groupings enthalten sind Object.keys(selectors).forEach(type => { if (!Object.keys(groupings).some(primaryType => groupings[primaryType].includes(type))) { Count(null, null, type); } }); } //----------------------------------------------------------------------------------------- // Function listMembers - Funktion zum Auflisten der Mitglieder aller Selektoren - dient zur Fehlerfindung //----------------------------------------------------------------------------------------- function listMembers() { for (const [key, selector] of Object.entries(selectors)) { log(`Liste der ${key}-Geraete:`, 'info'); selector.each((id) => { const obj = getObject(id); const name = obj && obj.common && obj.common.name ? obj.common.name : 'Unbekannt'; log(`- ID: ${id} | Name: ${name}`, 'info'); }); log(`Ende der ${key}-Liste`, 'info'); log('', 'info'); // Leere Zeile zwischen den Typen } } //----------------------------------------------------------------------------------------- // Function checkConsistency - Funktion zur ueberpruefung der uebereinstimmung der arrays //----------------------------------------------------------------------------------------- function checkConsistency(paths, selectors, groupings) { // Extrahiere die Schluessel aus paths und selectors const pathKeys = Object.keys(paths); const selectorKeys = Object.keys(selectors); // Wenn groupings angegeben ist, erstellen Sie eine Sammlung aller Keys aus groupings const groupKeys = groupings ? Object.keys(groupings) : []; // Sammle alle Mitglieder von groupings in einem Set const groupMembers = new Set(); for (const key of groupKeys) { if (groupings[key]) { groupings[key].forEach(member => groupMembers.add(member)); } } // Pruefen, ob alle Keys in groupings auch in paths vorhanden sind const missingInPaths = groupKeys.filter(key => !pathKeys.includes(key)); // Pruefen, ob alle Mitglieder der groupings in selectors vorhanden sind const missingInSelectors = [...groupMembers].filter(member => !selectorKeys.includes(member)); // Ausgabe der Ergebnisse if (missingInPaths.length === 0 && missingInSelectors.length === 0 ) { if(debug) {console.log('Alle Namen stimmen ueberein.');}; } else { if (missingInPaths.length > 0) { console.warn(`Die folgenden Keys fehlen in paths, obwohl sie in groupings vorhanden sind: ${missingInPaths.join(', ')}`); } if (missingInSelectors.length > 0) { console.warn(`Die folgenden Mitglieder der groupings fehlen in selectors: ${missingInSelectors.join(', ')}`); } } } //----------------------------------------------------------------------------------------------------- // Funktion zur Erzeugung von fuehrenden Nullen fuer das Datum Format //----------------------------------------------------------------------------------------------------- function addZero(i) { return i < 10 ? "0" + i : i; } //----------------------------------------------------------------------------------------- // Function writelog - Logeintrag schreiben //----------------------------------------------------------------------------------------- function writelog(string) { const fs = require('fs'); // enable write for external log const now = new Date(); const [year, month, day, Thour, Tmin, Tsec] = [ now.getFullYear(), addZero(now.getMonth() + 1), addZero(now.getDate()), addZero(now.getHours()), addZero(now.getMinutes()), addZero(now.getSeconds()) ]; const logdate = `${day}.${month}.${year}`; const logtime = `${Thour}:${Tmin}:${Tsec}`; fs.readFile(LogPath, 'utf8', (err, data) => { const entry = `${logdate} ;${logtime} ;${string}\n`; if (!err) { fs.appendFileSync(LogPath, entry); } else { log("Zaehlscript-Logging: Routine writelog - Logfile nicht gefunden - wird angelegt", "info"); const headerLine = "Datum;Uhrzeit;Type;GeraeteID;Geraetebezeichnung;Status"; fs.appendFileSync(LogPath, `${headerLine}\n${entry}`); } }); }