Test Adapter Material Design Widgets v0.2.x
-
@Oli kannst du dein Log Vis zippen?
die DMS Datei bekomme ich auf dem mac nicht auf. Habe zwar den Unarchiver drauf, der das eigentlich lt. Beschreibung kann, es endet aber in einem Fehler.
-
@nousefor82 sagte in Test Adapter Material Design Widgets v0.2.x:
Servus,
schon gefunden, wie es mit css funktioniert?
neee....bin ein CSS DAU
-
wie hast du das mit dem zeilenumbruch gemacht, bei mir klappt das nicht?
-
@Oli würdest du das abgewandelte Script noch hinzufügen? Finde die auswahl oben links genial und der Rest rechts ist auch ganz cool gemacht. Danke
-
-
wenn du mir sagst, wie ich das anstelle gerne
-
du musst einfach nur die Spaltenbreite definieren, ist neu in der Version 0.2.7
-
@Oli sagte in Test Adapter Material Design Widgets v0.2.x:
hier sind meine Einstellungen
Nimm mal bei der gemeinsamen für Datensatz[1] den Wert raus, also leer lassen.
-
@Scrounger sagte in Test Adapter Material Design Widgets v0.2.x:
Nimm mal bei der gemeinsamen für Datensatz[1] den Wert raus, also leer lassen.
Diesen DP habe ich bis jetzt noch nicht verstanden.
-
ist der gleiche Effekt, Achse bleibt verschwunden.
-
@Oli sagte in Test Adapter Material Design Widgets v0.2.x:
ist der gleiche Effekt, Achse bleibt verschwunden.
Geht bei mir, sende dir mal mein Widget. Datensätze musst du an deine anpassen.
[{"tpl":"tplVis-materialdesign-Chart-Line-History","data":{"g_fixed":false,"g_visibility":false,"g_css_font_text":false,"g_css_background":false,"g_css_shadow_padding":false,"g_css_border":true,"g_gestures":false,"g_signals":false,"g_last_change":false,"visibility-cond":"==","visibility-val":1,"visibility-groups-action":"hide","dataCount":"2","aggregate":"minmax","timeIntervalToShow":"1 hour","refreshMethod":"realtime","refreshTimeInterval":"10 minutes","pointStyle":"circle","showLegend":"true","legendPosition":"top","legendPointStyle":"true","showTooltip":"true","tooltipTimeFormats":"{\"millisecond\":\"lll:ss\",\"second\":\"lll:ss\",\"minute\":\"lll\",\"hour\":\"lll\",\"day\":\"lll\",\"week\":\"lll\",\"month\":\"lll\",\"quarter\":\"lll\",\"year\":\"lll\"}","tooltipShowColorBox":"true","lineSpanGaps0":"true","lineSpanGaps1":"true","xAxisPosition":"bottom","xAxisTimeFormats":"{\"millisecond\":\"H:mm:ss.SSS\",\"second\":\"H:mm:ss\",\"minute\":\"H:mm\",\"hour\":\"H\",\"day\":\"MMM\",\"week\":\"ll\",\"month\":\"MMM YYYY\",\"quarter\":\"[Q]Q - YYYY\",\"year\":\"YYYY\"}","xAxisValueDistanceToAxis":"12","xAxisShowAxis":"true","xAxisShowAxisLabels":"true","xAxisShowGridLines":"true","xAxisShowTicks":"true","yAxisValueDistanceToAxis":"14","showYAxis0":"true","yAxisPosition0":"left","yAxisShowGridLines0":"true","yAxisShowTicks0":"true","yAxisShowAxisBorder0":"true","showYAxis1":"true","yAxisPosition1":"right","yAxisShowGridLines1":"true","yAxisShowTicks1":"true","yAxisShowAxisBorder1":"true","signals-cond-0":"==","signals-val-0":true,"signals-icon-0":"/vis/signals/lowbattery.png","signals-icon-size-0":0,"signals-blink-0":false,"signals-horz-0":0,"signals-vert-0":0,"signals-hide-edit-0":false,"signals-cond-1":"==","signals-val-1":true,"signals-icon-1":"/vis/signals/lowbattery.png","signals-icon-size-1":0,"signals-blink-1":false,"signals-horz-1":0,"signals-vert-1":0,"signals-hide-edit-1":false,"signals-cond-2":"==","signals-val-2":true,"signals-icon-2":"/vis/signals/lowbattery.png","signals-icon-size-2":0,"signals-blink-2":false,"signals-horz-2":0,"signals-vert-2":0,"signals-hide-edit-2":false,"lc-type":"last-change","lc-is-interval":true,"lc-is-moment":false,"lc-format":"","lc-position-vert":"top","lc-position-horz":"right","lc-offset-vert":0,"lc-offset-horz":0,"lc-font-size":"12px","lc-font-family":"","lc-font-style":"","lc-bkg-color":"","lc-color":"","lc-border-width":"0","lc-border-style":"","lc-border-color":"","lc-border-radius":10,"lc-zindex":0,"historyAdapterInstance":"history.0","oid0":"daswetter.0.NextHours.Location_1.Day_1.current.temp_value","maxDataPoints":"10","time_interval_oid":"javascript.0.Eigene_Datenpunkte.timeinterval","colorScheme":"","lineThikness0":"1","lineTension0":"0.2","useFillColor0":true,"legendText0":"Garten","axisValueAppendText":"","xAxisValueFontSize":"12","yAxisTitleFontSize":"5","yAxisValueFontSize":"10","yAxisMinValue0":"-20","yAxisStep0":"","yAxisMaxValue0":"40","commonYAxis0":"","pointSize":"2","pointSizeHover":"5","legendBoxWidth":"5","tooltipValueMaxDecimals":"","tooltipBodyAppend":" °C","oid1":"hm-rpc.1.OEQ0670648.1.TEMPERATURE","lineThikness1":"1","lineTension1":"0.2","useFillColor1":true,"yAxisMinValue1":"-20","yAxisMaxValue1":"40","yAxisStep1":"","commonYAxis1":"","legendText1":"Bad","dataColor0":"#45fe25","dataColor1":"#3c3ffb","lineSpanGaps2":"true","showYAxis2":"true","yAxisPosition2":"left","yAxisShowGridLines2":"true","yAxisShowTicks2":"true","yAxisShowAxisBorder2":"true","oid2":"hue.0.Hue_temperature_sensor_1.temperature","lineThikness2":"1","useFillColor2":true,"lineTension2":"0.2","dataColor2":"#ce0303","legendText2":"Küche","yAxisMinValue2":"-20","yAxisMaxValue2":"40","yAxisStep2":"","xAxisMaxLabel":"20","xAxisValueLabelColor":"#ffffff","xAxisValueFontFamily":"RobotoCondensed-Light","yAxisValueFontFamily":"RobotoCondensed-Light","yAxisTitleColor":"#dedede","legendFontFamily":"RobotoCondensed-Light","legendFontColor":"#ffffff","legendFontSize":"15","yAxisValueLabelColor":"#bab5b5","yAxisMaxLabel0":"10","yAxisValueAppendText0":" °C","yAxisMaxLabel1":"10","yAxisValueAppendText1":" °C","yAxisMaxLabel2":"10","yAxisValueAppendText2":" °C"},"style":{"left":"16px","top":"57px","width":"1115px","height":"300px","z-index":"5","border-width":"1px","border-style":"solid","border-radius":"5px","border-color":"#fb0ee5"},"widgetSet":"materialdesign"}]
-
@Oli sagte in Test Adapter Material Design Widgets v0.2.x:
wenn du mir sagst, wie ich das anstelle gerne
@Oli
kannst du die Seite nicht einfach als .txt Datei hochladen -
geht bei dir auch nicht, pack mal alle Achsen auf eine Seite, dann erscheint beim 2 Eintrag auch keine Achse mehr
-
neuer Versuch
Log-Vis.txt -
@sigi234 sagte in Test Adapter Material Design Widgets v0.2.x:
@Scrounger sagte in Test Adapter Material Design Widgets v0.2.x:
Nimm mal bei der gemeinsamen für Datensatz[1] den Wert raus, also leer lassen.
Diesen DP habe ich bis jetzt noch nicht verstanden.
Ok versuche es Euch zu erklären.
Wir haben ein Diagramm mit zwei Datensätzen, Datensatz[0] & Datensatz[1].
Standardmäßig werden für beide Datensätze die y-Achsen angezeigt. Möchtet ihr aber das Datensatz[1] die y-Achse von Datensatz[0] mit benutzt, muss in 'gemeinsame y-Achse für Datensatz[1]' der Wert 0 geschrieben werden. Ist genauso wie beim flot adapter.@Oli
Wenn du in oberen Beispiel jetzt Datensatz[0] über die legende ausblendest, dann verschwindet natürlich auch die gemeinsame y-Achse. Wenn richtig verstehe ist das genau das verhalten bei dir.Damit ich besser nachvollziehen kann was ihr genau meint, wäre es besser ihr macht z.B. mit dem Tool ScreenToGif aufnahmen von den wigdets während der Bedienung. Dann sieht man genau was passiert.
-
hier das abgewandelte Script
/******************************************************************************* * --------------------------- * Log Script für ioBroker zum Aufbereiten des Logs für Visualisierungen (vis), oder um * auf Log-Ereignisse zu reagieren. * --------------------------- * * Das Script nimmt jeden neuen Logeintrag des ioBrokers und wendet entsprechend gesetzte * Filter an, um den Eintrag dann in den entsprechenden Datenpunkten dieses Scripts abzulegen. * Es stehen auch JSON-Datenpunkte zur Verfügung, mit diesen kann im vis eine * Tabelle ausgegeben werden (z.B. über das Widget 'basic - Table')- * * Aktuelle Version: https://github.com/Mic-M/iobroker.logfile-script * Support: https://forum.iobroker.net/topic/13971/vorlage-log-datei-aufbereiten-f%C3%BCr-vis-javascript * --------------------------- * Change Log: * 2.0.2 Mic + Changed certain functions to async to get rid of setTimout() and for the sake of better error handling. * + startTailingProcess(): ensure the tailing starts if the file is present (wait to be created) * 2.0.1a Mic Removed constant MERGE_LOGLINES_ACTIVE * 2.0.0a Mic Major improvements and fixes: * + Change from instant state update to schedule (STATE_UPDATE_SCHEDULE). The instant update, so once * new log entries coming in, caused several issues (setting and getting state values (getState() and * setState()) within <1ms simply does not work. * - Fix issue with merging log lines * + Moved global option MERGE_LOGLINES_ACTIVE to LOG_FILTER, for allowing turning on/off for each filter id. * + Several other code improvements * Note: For upgrading from previous version: replace script entirely, re-enter all your options, * and delete all existing states prior to first activation of this script. * ---------------------------------------------------------------------------------------------------- * 1.5.1 Mic - Set option MERGE_LOGLINES_ACTIVE to 'false' as default, as users reported issues. See * https://forum.iobroker.net/post/288772 . Also option MERGE_LOGLINES_ACTIVE being marked as "experimental" * in the comments. Requires further investigation. * 1.5 Mic - Fix issue with option MERGE_LOGLINES_ACTIVE * 1.4 Mic + New option MERGE_LOGLINES_TXT for an individual (e.g. localized) string other than 'entries'. - Fix JSON span class closing * 1.3 Mic + New option MERGE_LOGLINES_ACTIVE: Merge Loglines with same log message to only one line and adds leading * '[123 entries]' to log message. * 1.2 Mic - Fixed issue #6 (Button javascript.0.Log-Script.logXxxx.clearJSON not working reliably) * 1.1 Mic + 1. 1.0x script seems to work reliable per user feedback and my own test, so pushing into 1.1 stable. * + New state '.logMostRecent': provides just the most recent log entry to work with "on / * subscribe" on this state and trigger actions accordingly. * 1.02 alpha Mic - fix restarting at 0:00 (note: restarting is needed due to log file name change) * 1.01 alpha Mic - fix: creating new file system log file only if not yet existing * 1.00 alpha Mic + Entirely recoded to implement node-tail (https://github.com/lucagrulla/node-tail). * ---------------------------------------------------------------------------------------------------- * 0.8.1 Mic - Fix: L_SORT_ORDER_DESC was not defined (renamed constant name was not changed in config) * 0.8 Mic - Fix: Script caused a "file not found" error if executed right at or shortly after midnight. * 0.7 Mic - Fix: States "...clearDateTime" will not get an initial date value on first script start, * also fix for "on({id: ". * 0.6 Mic + Put 0.5.1 BETA into stable * + New option L_APPLY_CSS. If true, it will add <span class='log-info'>xxx</span> * to each log string. 'log-info' for level info, 'log-error' for error, etc. * This makes it easy to format a JSON table with CSS. * 0.5.1 BETA Mic + New States "Clear JSON log ..." and "Clear JSON log - Date/Time ...". * When the button "Clear JSON log" is pushed, the current date/time * will be set into the date/time state. Once refreshed * (per schedule in the script, e.g. after 2 minutes), the JSON * will be cleaned and display just newer logs. * Use Case: In vis, you can now add a button "clear log" or * "Mark as read". If you hit the button, the log will be * cleared and just new log items will be displayed. * *** THIS IS STILL BEING TESTED *** therefore a beta release... * 0.5 Mic + New parameter 'clean' to remove certain strings * from the log line. * + New parameter 'columns' for JSON output to specify which columns * to be shown, and in which order. * + New state "JSONcount" to have the number of log lines in state * - Fixed a few issues * 0.4 Mic - Bug fix: improved validation of log line consistency * 0.3 Mic + Added filtering and blacklist * - Several fixes * 0.2 Mic - Bug fix: corrected wrong function name * 0.1 Mic * Initial release *******************************************************************************/ /******************************************************************************* * WICHTIG - INSTALLATION ******************************************************************************/ /** * -------------------------------------------------------------------------- * Dieses Script benötigt node-tail (https://github.com/lucagrulla/node-tail). * -------------------------------------------------------------------------- * Option 1: Hinzufügen im JavaScript-Adapter: * 1. Im ioBroker links auf "Instanzen" klicken, dort den JS-Adapter wählen, etwa javascript.0 * 2. Unter "Zusätzliche NPM-Module" einfach "tail" (ohne Anführungszeichen) eingeben * 3. Speichern * * Option 2: Installation in der Konsole: * Wer das nicht im JS-Adapter hinzufügen möchte, kann auch so vorgehen: * 1. cd /opt/iobroker/node_modules/iobroker.js-controller/ * 2. npm install tail */ /******************************************************************************* * Konfiguration: Pfade ******************************************************************************/ // Pfad, unter dem die States (Datenpunkte) in den Objekten angelegt werden. // Kann man so bestehen lassen. const LOG_STATE_PATH = 'javascript.'+ instance + '.' + 'Log-Script'; // Pfad zum Log-Verzeichnis auf dem Server. // Standard-Pfad unter Linux: '/opt/iobroker/log/'. Wenn das bei dir auch so ist, dann einfach belassen. const LOG_FS_PATH = '/opt/iobroker/log/'; /******************************************************************************* * Konfiguration: Alle Logeinträge - Global ******************************************************************************/ // Zahl: Maximale Anzahl der letzten Logeinträge in den Datenpunkten. Alle älteren werden entfernt. // Bitte nicht allzu viele behalten, denn das kostet Performance. const LOG_NO_OF_ENTRIES = 100; // Sortierung der Logeinträge: true für descending (absteigend, also neuester oben), oder false für ascending (aufsteigend, also ältester oben) // Empfohlen ist true, damit neueste Einträge immer oben stehen. const L_SORT_ORDER_DESC = true; /** * Schwarze Liste (Black list) * Falls einer dieser Satzteile/Begriffe in einer Logzeile enthalten ist, dann wird der Log-Eintrag * komplett ignoriert, egal was weiter unten eingestellt wird. * Dies dient dazu, um penetrante Logeinträge gar nicht erst zu berücksichtigen. * Bitte beachten: * 1. Mindestens 3 Zeichen erforderlich, sonst wird es nicht berücksichtigt (würde auch wenig Sinn ergeben). * 2. Bestehende Datenpunkt-Inhalte dieses Scripts bei Anpassung dieser Option werden nicht nachträglich neu gefiltert, * sondern nur alle neu hinzugefügten Log-Einträge ab Speichern des Scripts werden berücksichtigt. */ const BLACKLIST_GLOBAL = [ '<==Disconnect system.user.admin from ::ffff:', // web.0 Adapter 'system.adapter.ical.0 terminated with code 0 (OK)', 'bring.0 Cannot get translations: RequestError', ' reconnected. Old secret ', // Sonoff 'Popup-News readed...', // info.0 Adapter '[warn] Projects disabled : set editorTheme.projects.enabled=true to enable', // see https://forum.iobroker.net/topic/12260/ 'snmp.0', '', ]; /** * Zusatz-Einstellung für Option "merge" unter "Konfiguration: Datenpunkte und Filter": * In MERGE_LOGLINES_TXT kann hier ein anderes Wort eingetragen werden, z.B. 'entries' oder 'Zeilen', damit [123 entries] * oder [123 Zeilen] vorangestellt wird anstatt [123 Einträge]. * HINWEIS: Falls MERGE_LOGLINES_TXT geändert wird: bitte alle Datenpunkte des Scripts löschen und dann Script neu starten. */ const MERGE_LOGLINES_TXT = 'Einträge'; /******************************************************************************* * Konfiguration: Datenpunkte und Filter ****************************************************************************** * Dies ist das Herzstück dieses Scripts: hier werden die Datenpunkte konfiguriert, die erstellt werden sollen. * Hierbei kannst du entsprechend Filter setzen, also Wörter/Begriffe, die in Logeinträgen enthalten sein * müssen, damit sie in den jeweiligen Datenpunkten aufgenommen werden. * -------------------------------------------------------------------------------------------------------------------------- * id: Ein Begriff ohne Leerzeichen, z.B. "error", "sonoff", homematic, etc. Die ID wird dann Teil der * Datenpunkte, z.B. "javascript.0.Log-Script.logHomematic.log" mit automatisch vorangestelltem "log". * -------------------------------------------------------------------------------------------------------------------------- * filter_all: ALLE Begriffe müssen in der Logzeile enthalten sein. Ist einer der Begriffe nicht enthalten, dann wird der * komplette Logeintrag auch nicht übernommen. Leeres Array [] eingeben, falls hier filtern nicht gewünscht. * -------------------------------------------------------------------------------------------------------------------------- * filter_any: Mindestens einer der gelisteten Begriffe muss enthalten sein. Leeres Array [] eingeben, falls hier filtern * nicht gewünscht. * -------------------------------------------------------------------------------------------------------------------------- * blacklist: Schwarze Liste: Wenn einer dieser Begriffe im Logeintrag enthalten ist, so wird der komplette Logeintrag * nicht übernommen, egal was vorher in filter_all oder filter_any definiert ist. * Mindestens 3 Zeichen erforderlich, sonst wird es nicht berücksichtigt. * HINWEIS: BLACKLIST_GLOBAL wird vorher schon angewendet, hier kannst du einfach nur noch eine individuelle * Blackliste pro id definieren. * -------------------------------------------------------------------------------------------------------------------------- * clean: Der Log-Eintrag wird um diese Zeichenfolgen bereinigt, d.h. diese werden entfernt, aber die restliche Zeile * bleibt bestehen. Z.B. um unerwünschte Zeichenfolgen zu entfernen oder Log-Ausgaben zu kürzen. * -------------------------------------------------------------------------------------------------------------------------- * columns: Nur für JSON (für vis). * Folgende Spalten gibt es: 'date','level','source','msg'. Hier können einzelne Spalten entfernt oder die * Reihenfolge verändert werden. Bitte keine anderen Spalten eintragen, sondern nur 'date','level','source','msg'. * -------------------------------------------------------------------------------------------------------------------------- * merge: Log-Einträge mit gleichem Text zusammenfassen. Beispiel: * ----------------------------------------------------------------------------------- * 2019-08-17 20:00:00.335 - info: javascript.0 script.js.Wetter: Wetterdaten abrufen. * 2019-08-17 20:15:00.335 - info: javascript.0 script.js.Wetter: Wetterdaten abrufen. * 2019-08-17 20:30:00.335 - info: javascript.0 script.js.Wetter: Wetterdaten abrufen. * ----------------------------------------------------------------------------------- * Daraus wird dann nur noch eine Logzeile mit letztem Datum/Uhrzeit und hinzufügen von "[3 Einträge]": * ----------------------------------------------------------------------------------- * 2019-08-17 20:30:00.335 - info: javascript.0 [3 Einträge] script.js.Wetter: Wetterdaten abrufen. * ----------------------------------------------------------------------------------- * * Zum aktivieren: true eintragen, zum deaktivieren: false eintragen. * -------------------------------------------------------------------------------------------------------------------------- * WEITERER HINWEIS: * Bestehende Datenpunkt-Inhalte dieses Scripts bei Anpassung dieser Option werden nicht nachträglich neu * gefiltert, sondern nur alle neu hinzugefügten Log-Einträge ab Speichern des Scripts werden berücksichtigt. * -------------------------------------------------------------------------------------------------------------------------- */ const LOG_FILTER = [ // Beispiel für individuellen Eintrag. Hier wird euer Hubschrauber-Landeplatz überwacht :-) Wir wollen nur Einträge // vom Adapter 'hubschr.0'. Dabei sollen entweder Wetterwarnungen, Alarme, oder UFOs gemeldet werden. Alles unter // Windstärke "5 Bft" interessiert uns dabei nicht, daher haben wir '0 Bft' bis '4 Bft' auf die Blackliste gesetzt. // Außerdem entfernen wir von der Log-Zeile die Zeichenfolgen '****', '!!!!' und 'ufo gesichtet', der Rest bleibt // aber bestehen. Zudem haben wir unter columns die Spaltenreihenfolge geändert. 'level' herausgenommen, und Quelle // ganz vorne. /* { id: 'hubschrauberlandeplatz', filter_all: ['hubschr.0'], filter_any: ['wetterwarnung', 'alarm', 'ufo'], blacklist: ['0 Bft', '1 Bft', '2 Bft', '3 Bft', '4 Bft'], clean: ['****', '!!!!', 'ufo gesichtet'], columns: ['source','date','msg'], merge: true, }, */ /* { id: 'all', // Beispiel "all": hier kommen alle Logeinträge rein, keine Filterung filter_all: ['', ''], // wird ignoriert, wenn leer filter_any: ['', ''], // wird ignoriert, wenn leer blacklist: ['', ''], // wird ignoriert, wenn leer clean: ['', '', ''], // wird ignoriert, wenn leer columns: ['date','level','source','msg'], // Spaltenreihenfolge für JSON (Tabelle in vis) merge: true, }, */ { id: 'info', filter_all: [' - info: '], // nur Logeinträge mit Level 'info' filter_any: ['', ''], blacklist: ['', ''], clean: ['', '', ''], columns: ['date','level','source','msg'], merge: true, }, { id: 'error', filter_all: [' - error: ', ''], // nur Logeinträge mit Level 'error' filter_any: [''], blacklist: ['', '', ''], clean: ['', '', ''], columns: ['date','level','source','msg'], merge: true, }, { id: 'warn', filter_all: [' - warn: ', ''], // nur Logeinträge mit Level 'warn' filter_any: [''], blacklist: ['', '', ''], clean: ['', '', ''], columns: ['date','level','source','msg'], merge: true, }, /* { // Beispiel, um einen bestimmten Adapter zu überwachen. // Hier werden alle Fehler und Warnungen des Homematic-Adapters hm-rpc.0 gelistet. id: 'homematic', filter_all: ['hm-rpc.0', ''], // hm-rpc.0 muss enthalten sein. filter_any: [' - error: ', ' - warn: '], // entweder error oder warn blacklist: ['', '', ''], clean: ['', '', ''], columns: ['date','level','source','msg'], merge: true, }, */ ]; /******************************************************************************* * Konfiguration: JSON-Log (für Ausgabe z.B. im vis) ******************************************************************************/ // Datumsformat für JSON Log. Z.B. volles z.B. Datum mit 'yyyy-mm-dd HH:MM:SS' oder nur Uhrzeit mit "HH:MM:SS". Die Platzhalter yyyy, mm, dd usw. // werden jeweils ersetzt. yyyy = Jahr, mm = Monat, dd = Tag, HH = Stunde, MM = Minute, SS = Sekunde. Auf Groß- und Kleinschreibung achten! // Die Verbinder (-, :, Leerzeichen, etc.) können im Prinzip frei gewählt werden. // Beispiele: 'HH:MM:SS' für 19:37:25, 'HH:MM' für 19:37, 'dd.mm. HH:MM' für '25.07. 19:37' const JSON_DATE_FORMAT = 'dd.mm. HH:MM'; // Max. Anzahl Zeichen der Log-Meldung im JSON Log. const JSON_LEN = 100; // Zahl: Maximale Anzahl der letzten Logeinträge in den Datenpunkten. Alle älteren werden entfernt. // Speziell für das JSON-Log zur Visualisierung, hier brauchen wir ggf. weniger als für L_NO_OF_ENTRIES gesamt. const JSON_NO_ENTRIES = 60; // Füge CSS-Klasse hinzu je nach Log-Level (error, warn, info, etc.), um Tabellen-Text zu formatieren. // Beispiel für Info: ersetzt "xxx" durch "<span class='log-info'>xxx</span>"" // Analog für error: log-error, warn: log-warn, etc. // Beim Widget "basic - Table" im vis können im Reiter "CSS" z.B. folgende Zeilen hinzugefügt werden, // um Warnungen in oranger und Fehler in roter Farbe anzuzeigen. // .log-warn { color: orange; } // .log-error { color: red; } const JSON_APPLY_CSS = true; // JSON_APPLY_CSS wird nur für die Spalte "level" (also error, info) angewendet, aber nicht für die // restlichen Spalten wie Datum, Log-Eintrag, etc. // Falls alle Zeilen formatiert werden sollen: auf false setzen. const JSON_APPLY_CSS_LIMITED_TO_LEVEL = true; /******************************************************************************* * Konfiguration: Wie oft Datenpunkte aktualisieren? ******************************************************************************/ // Neu reinkommende Logeinträge werden erst mal gesammelt (in Variable G_NewLogLinesArrayToProcess). Diese werden dann // regelmäßig in den Datenpunkten geschrieben. Sinnvoll ist hier nicht kürzer als 2-3 Sekunden, und nicht länger als // ein paar Minuten. Zu kurzes Intervall: Script kommt nicht mehr nach. Zu lange: falls viele Logeinträge reinkommen, // kann sich vieles "aufstauen" zur Abarbeitung. Benutze den "Cron"-Button oben rechts für komfortable Einstellung. const STATE_UPDATE_SCHEDULE = '*/20 * * * * *'; // alle 20 Sekunden /******************************************************************************* * Konfiguration: Konsolen-Ausgaben ******************************************************************************/ // Auf true setzen, wenn zur Fehlersuche einige Meldungen ausgegeben werden sollen. // Ansonsten bitte auf false stellen. const LOG_DEBUG = false; // Auf true setzen, wenn ein paar Infos dieses Scripts im Log ausgegeben werden dürfen, bei false bleiben die Infos komplett weg. const LOG_INFO = true; /******************************************************************************* * Experten-Konfiguration ******************************************************************************/ // Leer lassen! Nur setzen, falls ein eigener Filename für das Logfile verwendet wird für Debug. const DEBUG_CUSTOM_FILENAME = ''; // Regex für die Aufteilung des Logs in 1-Datum/Zeit, 3-Level, 5-Quelle und 7-Logtext. // Ggf. anzupassen bei anderem Datumsformat im Log. Wir erwarten ein Format // wie z.B.: '2018-07-22 12:45:02.769 - info: javascript.0 Stop script script.js.ScriptAbc' // Da als String, wurden alle Backslashes "\" mit einem zweiten Backslash escaped. const LOG_PATT = '([0-9_.\\-:\\s]*)(\\s+\\- )(silly|debug|info|warn|error|)(: )([a-z0-9.\\-]*)(\\s)(.*)'; // Debug: Ignore. Wenn dieses String in der Logzeile enthalten ist, dann ignorieren wir es. // Dient dazu, dass wir während des Scripts ins Log schreiben können, ohne dass das dieses Script berücksichtigt. const DEBUG_IGNORE_STR = '[LOGSCRIPT_IGNORE]'; // Muss ein individuelles String sein. Sonst gibt es ggf. eine Endlos-Schleife. // Debug: Prüfen, ob jede Logzeile erfasst wird, in dem wir diese direkt danach noch mal ins Log schreiben. // Bitte nur auf Anweisung vom Entwickler einschalten. Sonst wird jeder Logeintrag noch einmal wiederholt, // mit führendem DEBUG_EXTENDED_STR am Anfang und max. DEBUG_EXTENDED_NO_OF_CHARS an Anzahl Zeichen. const DEBUG_EXTENDED = false; const DEBUG_EXTENDED_STR = '[LOGSCRIPT_DEBUG_EXTENDED]'; // Muss ein individuelles String sein. Sonst gibt es ggf. eine Endlos-Schleife. const DEBUG_EXTENDED_NO_OF_CHARS = 120; /************************************************************************************************************************* * Ab hier nichts mehr ändern / Stop editing here! *************************************************************************************************************************/ /************************************************************************************************************************* * Global variables and constants *************************************************************************************************************************/ // Merge loglines: define pattern (and escape the merge text) // We added an additional backslash '\' to each backslash as these need to be escaped. const MERGE_REGEX_PATT = '^\[(\\d+)\\s' + escapeRegExp(MERGE_LOGLINES_TXT) + '\]\\s(.*)'; // This script requires tail. https://github.com/lucagrulla/node-tail let G_Tail = require('tail').Tail; // Please ignore the red wavy underline. The JavaScript editor does not recognize if node-tail is installed let G_tailOptions= {separator: /[\r]{0,1}\n/, fromBeginning: false} let G_tail; // being set later // Schedule for every midnight. So not set at this point. let G_Schedule_Midnight; // being set later // Schedule for logfile update let G_Schedule_StateUpdate; // being set later // We add here all the new log lines to be processed regularly (per STATE_UPDATE_SCHEDULE); let G_NewLogLinesArrayToProcess = []; /************************************************************************************************************************* * init - This is executed on every script (re)start. *************************************************************************************************************************/ // We do some timing here with setTimeout() to avoid warnings like if states not yet exist, etc. init(); function init() { // Create our states, if not yet existing. createLogStates(); // States should have been created, so continue setTimeout(function(){ // Subscribe on changes: Pressed button "clearJSON" subscribeClearJson(); // Start main function. main(); // Schedule writing changes into states clearSchedule(G_Schedule_StateUpdate); G_Schedule_StateUpdate = schedule(STATE_UPDATE_SCHEDULE, processNewLogsPerSchedule); // Every midnight at 0:00, we have a new log file. So, we schedule accordingly. clearSchedule(G_Schedule_Midnight); G_Schedule_Midnight = schedule('0 0 * * *', main); }, 2000); } /************************************************************************************************************************* * Main Function. Will be restarted every midnight by G_Schedule_Midnight. *************************************************************************************************************************/ async function main() { // First, we end the tailing. await endTailingProcess(); // now wait for endTailingProcess()... // Next, we start the new tailing process. let startTailingResult = await startTailingProcess(); // now wait for startTailingProcess()... if (startTailingResult) { monitorLogChanges(); } else { log('monitorLogChanges not executed as starting new Tailing Process was not successful', 'error'); } } function monitorLogChanges() { if (LOG_INFO) log('Start monitoring of the ioBroker log...') G_tail.on('line', function(newLogEntry) { // Check if we have DEBUG_IGNORE_STR in the new log line if(! newLogEntry.includes(DEBUG_IGNORE_STR)) { if (newLogEntry.length > 45) { // a log line with less than 45 chars is not a valid log line. // Cleanse and apply blacklist newLogEntry = cleanseLogLine(newLogEntry); // Push result into logArrayFinal G_NewLogLinesArrayToProcess.push(newLogEntry); // some debugging if (LOG_DEBUG) log (DEBUG_IGNORE_STR + '==============================================================='); if (LOG_DEBUG) log (DEBUG_IGNORE_STR + 'New Log Entry, Len (' + newLogEntry.length + '), content: [' + newLogEntry + ']'); // This is for debugging purposes, and it will log every new log entry once again. See DEBUG_EXTENDED option above. if (DEBUG_EXTENDED) { if (! newLogEntry.includes(DEBUG_EXTENDED_STR)) { // makes sure no endless loop here. log(DEBUG_EXTENDED_STR + newLogEntry.substring(0, DEBUG_EXTENDED_NO_OF_CHARS)); } } } } }); G_tail.on('error', function(error) { // Error Handling log('Tail error', error); if (error.includes('ENOENT: no such file or directory')) { // It looks like the log file was deleted. So we restart process. // Will also create a new log file if not existing. restartTailingProcess(); log('Tail process re-started due to file/directory not found error. It will create a new log file if it has been deleted.', 'warn') } else { log('Tailing process ended by the log script due to this error.', 'warn'); endTailingProcess(); } }); } /** * Called per schedule STATE_UPDATE_SCHEDULE. * It processes G_NewLogLinesArrayToProcess */ function processNewLogsPerSchedule() { if (! isLikeEmpty (G_NewLogLinesArrayToProcess) ) { // We use array spreads '...' to copy array. If not, array is changed by reference and not value. // That means, if we change the target array, it will also change the source array. // See https://stackoverflow.com/questions/7486085/copy-array-by-value let logArrayToProcess = [...G_NewLogLinesArrayToProcess]; G_NewLogLinesArrayToProcess.length = 0; // emptying array. https://stackoverflow.com/questions/4804235/difference-between-array-length-0-and-array /** * Apply the filters as set in LOG_FILTER and split up log levels into elements of an array * logArrayToProcessFiltered will look as follows: * logArrayToProcessFiltered = [ * ['info':'15.08.2019 09:27:55.476 info adapt.0 some log', 'error':''], * ['info':'15.08.2019 09:33:58.522 info adapt.0 some more log', 'error':''], * ['info':'', 'error':'15.08.2019 09:37:55.807 error adapt.0 some error log'] * ] */ let logArrayToProcessFiltered = []; for (let lpEntry of logArrayToProcess) { let logEntryFilteredArray = applyFilter(lpEntry); logArrayToProcessFiltered.push(logEntryFilteredArray); } // Further process and finally set states with our results. processLogArrayAndSetStates(logArrayToProcessFiltered); } } /************************************************************************************************************************* * Filtering *************************************************************************************************************************/ /** * This function applies the filters as set in LOG_FILTER. * Also, it splits up the log levels into elements of an array we return by this function. * @param {string} strLogEntry * @return {array} split up log levels as elements within this array, like: ['info':'logtext', 'error':'logtext'] etc. */ function applyFilter(strLogEntry) { // We add one element per each filter to the Array ('all', 'error', etc.) let logArrayProcessed = []; for (let j = 0; j < LOG_FILTER.length; j++) { logArrayProcessed[LOG_FILTER[j].id] = ''; } // We apply regex here. This will also eliminate all log lines without proper info // like date/time, log level, and entry. let arrSplitLogLine = logLineSplit(strLogEntry); if (arrSplitLogLine !== false) { if (isLikeEmpty(LOG_FILTER) === false) { // Now let's iterate over the filter array elements // We check if both the "all" and "any" filters apply. If yes, - and blacklist false - we add the log line. for (let k = 0; k < LOG_FILTER.length; k++) { if ( (strMatchesTerms(strLogEntry, LOG_FILTER[k].filter_all, 'every') === true) && (strMatchesTerms(strLogEntry, LOG_FILTER[k].filter_any, 'some') === true) && (strMatchesTerms(strLogEntry, LOG_FILTER[k].blacklist, 'blacklist') === false) ) { logArrayProcessed[LOG_FILTER[k].id] = logArrayProcessed[LOG_FILTER[k].id] + strLogEntry + "\n"; } // Now we remove terms if desired if (isLikeEmpty(LOG_FILTER[k].clean) === false) { for (let lpTerm of LOG_FILTER[k].clean) { if (lpTerm !== '') { logArrayProcessed[LOG_FILTER[k].id] = logArrayProcessed[LOG_FILTER[k].id].replace(lpTerm, ''); } } } } } } return logArrayProcessed; } /************************************************************************************************************************* * Further processing *************************************************************************************************************************/ /** * Further processes the log array and set states accordingly. * * @param arrayLogInput The Array of the log input. * Array is like: * [ * ['info':'15.08.2019 09:27:55.476 info adapt.0 some log', 'error':''], * ['info':'15.08.2019 09:33:58.522 info adapt.0 some more log', 'error':''], * ['info':'', 'error':'15.08.2019 09:37:55.807 error adapt.0 some error log'], * ] * Array is like: ['info':'logtext', 'error':'logtext'] etc. **/ function processLogArrayAndSetStates(arrayLogInput) { /***************** * [1] Build array from LOG_FILTER. Looks like: arrayFilterIds = ['info', 'error', 'warn']. * Also, build result array to keep our results. Lools like resultArr = [info: '', error: '', warn: ''] *****************/ let arrayFilterIds = []; let resultArr = []; for (let i = 0; i < LOG_FILTER.length; i++) { arrayFilterIds.push(LOG_FILTER[i].id); // each LOG_FILTER id into array resultArr[LOG_FILTER[i].id] = ''; } /***************** * [2] Process element by element, so ['info':'log test', 'error':'log test'] of given array. * We fill the result array accordingly. *****************/ for (let lpElement of arrayLogInput) { // Loop thru our new array arrayFilterIds and fill result array for (let k = 0; k < arrayFilterIds.length; k++) { // some variables let lpFilterId = arrayFilterIds[k]; // Filter ID from LOG_FILTER, like 'error', 'info', 'custom', etc. let lpNewLogLine = lpElement[lpFilterId]; // Current log line of provided array element of 'error', 'info', 'custom' etc. if (isLikeEmpty(lpNewLogLine)) { // No log content for the given filter id. if (LOG_DEBUG) log (DEBUG_IGNORE_STR + 'Filter [' + lpFilterId + ']: No match.'); } else { if (LOG_DEBUG) log (DEBUG_IGNORE_STR + 'Filter [' + lpFilterId + ']: Match! New Log Line length: (' + lpNewLogLine.length + ')'); // Append new log line to result array if (isLikeEmpty(resultArr[lpFilterId])) { resultArr[lpFilterId] = lpNewLogLine; } else { resultArr[lpFilterId] = lpNewLogLine + resultArr[lpFilterId]; // "\n" not needed, always added above } } } } /***************** * [3] We merge with the current state. *****************/ for (let k = 0; k < arrayFilterIds.length; k++) { let lpFilterId = arrayFilterIds[k]; // Filter ID from LOG_FILTER, like 'error', 'info', 'custom', etc. let lpStatePath1stPart = LOG_STATE_PATH + '.log' + cleanseStatePath(lpFilterId); // Get Path to state let lpNewFinalLog = resultArr[lpFilterId]; if (! isLikeEmpty(lpNewFinalLog) ) { // Get state value let strCurrentStateLog = getState(lpStatePath1stPart + '.log').val; // Get state contents of loop item // Add state log lines to our final log if (! isLikeEmpty(strCurrentStateLog)) { lpNewFinalLog = lpNewFinalLog + strCurrentStateLog; // "\n" not needed, always added above } // Convert to array for easier handling let lpNewFinalLogArray = lpNewFinalLog.split(/\r?\n/); // Remove duplicates lpNewFinalLogArray = arrayRemoveDublicates(lpNewFinalLogArray); // Remove empty values lpNewFinalLogArray = cleanArray(lpNewFinalLogArray); // Sort array descending lpNewFinalLogArray = sortLogArrayByDate(lpNewFinalLogArray, 'desc'); // Merge Loglines if multiple values and add leading '[123 entries]' to log message let doMerge = logFilterGetValueByKey(lpFilterId, 'merge'); if (doMerge || doMerge === 'true') { // also check for string 'true' in case user used string lpNewFinalLogArray = mergeLogLines(lpNewFinalLogArray); } // We need a separate array for JSON let lpNewFinalLogArrayJSON = lpNewFinalLogArray; // Let's remove elements if time of when button '.clearJSON' was pressed is greater than log date. lpNewFinalLogArrayJSON = clearJsonByDate(lpNewFinalLogArrayJSON, lpStatePath1stPart + '.clearJSON'); // Just keep the first x elements of the array lpNewFinalLogArray = lpNewFinalLogArray.slice(0, LOG_NO_OF_ENTRIES); lpNewFinalLogArrayJSON = lpNewFinalLogArrayJSON.slice(0, JSON_NO_ENTRIES); // Get just the most recent log entry into string let lpMostRecent = lpNewFinalLogArray[0]; // Sort ascending if desired if (!L_SORT_ORDER_DESC) { lpNewFinalLogArray = lpNewFinalLogArray.reverse(); lpNewFinalLogArrayJSON = lpNewFinalLogArrayJSON.reverse(); } // ** Finally set the states /////////////////////////////// // -1- Full Log, String, separated by "\n" /////////////////////////////// let strResult = lpNewFinalLogArray.join("\n"); if (LOG_DEBUG) log (DEBUG_IGNORE_STR + 'New length to be set into state: (' + strResult.length + '), state: [' + lpStatePath1stPart + '.log' + ']'); setState(lpStatePath1stPart + '.log', strResult); /////////////////////////////// // -2- JSON, with elements date and msg /////////////////////////////// // Let's put together the JSON let jsonArr = []; for (let j = 0; j < lpNewFinalLogArrayJSON.length; j++) { // Get 4 elements in array: datetime, level, source, message let arrSplitLogLine = logLineSplit(lpNewFinalLogArrayJSON[j]); if (arrSplitLogLine !== false) { let strLogMsg = arrSplitLogLine.message; // Reduce the length for each log message per configuration strLogMsg = strLogMsg.substr(0, JSON_LEN); // ++++++ // Build the final Array // ++++++ // We need this section to generate the JSON with the columns (which ones, and order) as specified in LOG_FILTER let objectJSONentry = {}; // object (https://stackoverflow.com/a/13488998) if (isLikeEmpty(LOG_FILTER[k].columns)) log('Columns not specified in LOG_FILTER', 'warn'); // Prepare CSS let strCSS1, strCSS2; let strCSS1_level, strCSS2_level; if (JSON_APPLY_CSS) { strCSS1 = "<span class='log-" + arrSplitLogLine.level + "'>"; strCSS2 = '</span>'; strCSS1_level = strCSS1; strCSS2_level = strCSS2; if (JSON_APPLY_CSS_LIMITED_TO_LEVEL) { strCSS1 = ''; strCSS2 = ''; } } for (let lpCol of LOG_FILTER[k].columns) { switch (lpCol) { case 'date' : objectJSONentry.date = strCSS1 + formatLogDateStr(arrSplitLogLine.datetime, JSON_DATE_FORMAT) + strCSS2; break; case 'level' : objectJSONentry.level = strCSS1_level + arrSplitLogLine.level + strCSS2_level; break; case 'source' : objectJSONentry.source = strCSS1 + arrSplitLogLine.source + strCSS2; break; case 'msg' : objectJSONentry.msg = strCSS1 + strLogMsg + strCSS2; break; default: //nothing; } } // Ok, so now we have the JSON entry. jsonArr.push(objectJSONentry); } } if (! isLikeEmpty(lpNewFinalLogArrayJSON)) { setState(lpStatePath1stPart + '.logJSON', JSON.stringify(jsonArr)); setState(lpStatePath1stPart + '.logJSONcount', lpNewFinalLogArrayJSON.length); } var datum = new Date(); var tag = datum.getDate(); var monat = (datum.getMonth()+ 1); var jahr = datum.getFullYear(); var stunde = datum.getHours(); var minute = datum.getMinutes(); var sekunde = datum.getSeconds(); if (tag < 10) tag = '0' + tag; if (monat < 10) monat= '0' + monat; if (stunde< 10) stunde= '0' + stunde; if (minute< 10) minute= '0' + minute; if (sekunde< 10) sekunde= '0' + sekunde; var ausführungsdatum = tag + "." + monat+"." + jahr; var ausführungszeit = stunde +":" + minute +":" + sekunde; setState('javascript.0.Log-Script.Stand', ausführungsdatum + ' - ' + ausführungszeit + ' Uhr'); } } } /************************************************************************************************************************* * Tailing functions *************************************************************************************************************************/ /** * Start new tailing process. */ async function startTailingProcess() { let tailStarted = false; // This // Path to iobroker log file let strFsFullPath = getCurrentFullFsLogPath(); // Create a new log file. It will created if it is not yet existing. // This will avoid an error if right after midnight the log file is not yet there const fs = require('fs'); if (fs.existsSync(strFsFullPath)) { // File is existing startTail(); } else { // File is not existing, so we create it. if (LOG_DEBUG) log (DEBUG_IGNORE_STR + 'Log file is not existing, so we need to create a blank file.'); fs.writeFile(strFsFullPath, '', function(err) { if (err) { log('The log file [' + strFsFullPath + '] could not be created.', 'error'); return log('fs.writeFile Error: ' + err, 'error'); } else { startTail(); } }); } function startTail() { // Now start new tailing instance if(LOG_INFO) log('Start new Tail process. File path to current log: [' + strFsFullPath + ']'); G_tail = new G_Tail(strFsFullPath, G_tailOptions); tailStarted = true; } return tailStarted; // We return true/false since this is an async function. } /************************ * Restart Tail. ************************/ function restartTailingProcess() { // End tailing endTailingProcess(); // Start new TAIL process, as we have a new log file every 0:00. startTailingProcess(); } /** * End the tailing gracefully. * Exit process: see here: https://stackoverflow.com/questions/5266152/how-to-exit-in-node-js/37592669#37592669 */ async function endTailingProcess() { // Properly set the exit code while letting the process exit gracefully. if ( typeof G_tail !== 'undefined' && G_tail ) { G_tail.unwatch(); // just in case. G_tail.exitCode = 1; if(LOG_DEBUG) log('Properly end the existing Tail process.'); } else { if(LOG_DEBUG) log('Tail process was not active, so nothing to stop.'); } return; // async return } /** * This will allow to set Json log to zero if button is pressed. */ function subscribeClearJson() { // Set current date to state if button is pressed let logSubscribe = ''; for (let i = 0; i < LOG_FILTER.length; i++) { let lpFilterId = cleanseStatePath(LOG_FILTER[i].id); let lpStateFirstPart = LOG_STATE_PATH + '.log' + lpFilterId; logSubscribe += ( (logSubscribe === '') ? '' : ', ') + lpFilterId; on({id: lpStateFirstPart + '.clearJSON', change: 'any', val: true}, function(obj) { let stateBtnPth = obj.id // e.g. [javascript.0.Log-Script.logInfo.clearJSON] let firstPart = stateBtnPth.substring(0, stateBtnPth.length-10); // get first part of obj.id, like "javascript.0.Log-Script.logInfo" let filterID = firstPart.slice(firstPart.lastIndexOf('.') + 1); // gets the filter id, like "logInfo" if (LOG_DEBUG) log(DEBUG_IGNORE_STR + 'Clear JSON states for [' + filterID + '].'); //______Forked by oliver: Show a notice in vis-Table, that log was deleted __________________________________________________ // We clear the according JSON states var datum = new Date(); var tag = datum.getDate(); var monat = (datum.getMonth()+ 1); var jahr = datum.getFullYear(); var stunde = datum.getHours(); var minute = datum.getMinutes(); var sekunde = datum.getSeconds(); if (tag < 10) tag = '0' + tag; if (monat < 10) monat= '0' + monat; if (stunde< 10) stunde= '0' + stunde; if (minute< 10) minute= '0' + minute; if (sekunde< 10) sekunde= '0' + sekunde; var loeschdatum = tag + "." + monat+"." + jahr; var loeschzeit = stunde +":" + minute +":" + sekunde; setState(firstPart + '.ClearDate', loeschdatum + ' - ' + loeschzeit + ' Uhr'); setState(firstPart + '.LetzterEintrag', ""); //____________________________________________________________________________________________________________ setState(firstPart + '.logJSON', '[]'); setState(firstPart + '.logJSONcount', 0); }); } if (LOG_DEBUG) log('Subscribing to Clear JSON Buttons: ' + logSubscribe) } /************************************************************************************************************************* * Script specific supporting functions *************************************************************************************************************************/ /** * Reformats a log date string accordingly * @param {string} strDate The date to convert * @param {string} format e.g. 'yyyy-mm-dd HH:MM:SS'. * */ function formatLogDateStr(strDate, format) { let strResult = format; strResult = strResult.replace('yyyy', strDate.substr(0,4)); strResult = strResult.replace('mm', strDate.substr(5,2)); strResult = strResult.replace('dd', strDate.substr(8,2)); strResult = strResult.replace('HH', strDate.substr(11,2)); strResult = strResult.replace('MM', strDate.substr(14,2)); strResult = strResult.replace('SS', strDate.substr(17,2)); return strResult; } /** * Cleanse the log line * @param {string} logLine The log line to be cleansed. * @return {string} The cleaned log line */ function cleanseLogLine(logLine) { let logLineResult = logLine.replace(/\u001b[.*?m/g, ''); // Remove color escapes - https://stackoverflow.com/questions/25245716/remove-all-ansi-colors-styles-from-strings if (logLineResult.substr(0,9) === 'undefined') logLineResult = logLineResult.substr(9,99999); // sometimes, a log line starts with the term "undefined", so we remove it. logLineResult = logLineResult.replace(/\s\s+/g, ' '); // Remove white space, tab stops, new line if(strMatchesTerms(logLineResult, BLACKLIST_GLOBAL, 'blacklist')) logLineResult = ''; // Check against global blacklist return logLineResult; } /** * Sorts the log array by date. We expect the first 23 chars of each element being a date in string format. * @param {array} inputArray Array to process * @param {string} order 'asc' or 'desc' for ascending or descending order */ function sortLogArrayByDate(inputArray, order) { var result = inputArray.sort(function(a,b){ // Turn your strings into dates, and then subtract them // to get a value that is either negative, positive, or zero. a = new Date(a.substr(0,23)); b = new Date(b.substr(0,23)); if (order === 'asc') { return a - b; } else { return b - a; } }); return result; } /** * Splits a given log entry into an array with 4 elements. * @param {string} inputValue Log line like '2018-07-22 11:47:53.019 - info: javascript.0 script.js ...' * @return {object} Array with 4 elements: * 0. datetime (e.g. 2018-07-22 11:47:53.019), * 1. level (e.g. info) * 2. source (e.g. javascript.0) * 3. message (e.g. script.js....) * Returns FALSE if no match or input value not valid */ function logLineSplit(inputValue) { // Get RegEx ready let mRegEx = new RegExp(LOG_PATT, 'g'); // Split let returnObj = {} let m; do { m = mRegEx.exec(inputValue); if (m) { returnObj.datetime = m[1]; returnObj.spaceAt2 = m[2]; returnObj.level = m[3]; returnObj.spaceAt4 = m[4]; returnObj.source = m[5]; returnObj.spaceAt6 = m[6]; returnObj.message = m[7]; } } while (m); // Now we check if we have valid entries we want if ((returnObj.datetime === undefined) || (returnObj.level === undefined) || (returnObj.source === undefined) || (returnObj.message === undefined) ) { return false; // no valid hits } // We can return the array now, since it meets all requirements return returnObj; } /** * Merges date/time, level, source and message to a logline * @param {array} inputValue Array with 4 elements: date/time, level, source, message * @return {string} Merged log line as string. Empty string '', if input value not valid. */ function logLineMerge(inputValue) { if (inputValue.length === 4) { let mergedLine = inputValue[0] + ' - ' + inputValue[1] + ': ' + inputValue[2] + ' ' + inputValue[3]; return mergedLine; } else { // We expect a size of 4, so go out return ''; } } /** * Merge Loglines if multiple values and add leading '[123 entries]' to log message * @param {array} logArray array of log entries * @return {array} the new merged log array */ function mergeLogLines(logArray) { // We use array spreads '...' to copy array. If not, array is changed by reference and not value. // That means, if we change the target array, it will also change the source array. // See https://stackoverflow.com/questions/7486085/copy-array-by-value let arrCopy = [...logArray]; let arrNew = []; for (let i = 0; i < arrCopy.length; i++) { if (! isLikeEmpty(arrCopy[i])) { let lpEntry = arrCopy[i]; let lineWithoutDate = lpEntry.substring(23); let lpLineSplit = logLineSplit(lpEntry); // Get multiple values let lpMulti = arrayGetElements(arrCopy, removeLeading123entries(lpLineSplit.message), false); let result = lpEntry; let lineCounter = 0; if (lpMulti.length > 1) { // Treffer - die aktuelle Zeile zählt ja auch mit. lineCounter = lpMulti.length; let hitLeadingNumber = -1; for (let hitLine of lpMulti) { let hitLineSplit = logLineSplit(hitLine); // Check if hit contains '[123 entries]'. If yes, get the number out of it into lineCounter. // If not, we just count with 1. hitLeadingNumber = checkForMultiEntry(hitLineSplit.message); if (hitLeadingNumber > 1) { lineCounter = hitLeadingNumber + lpMulti.length - 1; } } } else { lineCounter = 1; } if (lineCounter > 1) { // remove from array by filling empty value arrCopy = arrayReplaceElementsByValue(arrCopy, removeLeading123entries(lpLineSplit.message), '', false); // new result result = logLineMerge([lpLineSplit.datetime, lpLineSplit.level, lpLineSplit.source, '[' + lineCounter + ' ' + MERGE_LOGLINES_TXT + '] ' + removeLeading123entries(lpLineSplit.message)]); } arrNew.push(result); } } return arrNew; /** * @param {string} strInput A log message with potential leading '[123 entries]' * @return {string} string without leading '[123 entries]', if it is there */ function removeLeading123entries(strInput) { let mRegEx = new RegExp(MERGE_REGEX_PATT); let matches = mRegEx.exec(strInput); if (matches === null) { return strInput; } else { return matches[2]; } } /** * @param {string} strInput A log message checking for leading '[123 entries]' * @return {number} returns the number 123 from '[123 entries]' if any match, or -1 if not found */ function checkForMultiEntry(strInput) { // Get RegEx ready let mRegEx = new RegExp(MERGE_REGEX_PATT); let matches = mRegEx.exec(strInput); if (matches === null) { return -1; } else { return parseInt(matches[1]); } } } /************* * Get the file system path and filename of the current log file. * * ioBroker creates a log file every midnight at 0:00 under '/opt/iobroker/log/' * Syntax of the log file is: iobroker.YYYY-MM-DD.log * This function returns the full path to the log file, considering the current date/time when this function is called. * @return {string} Path and file name to log file. */ function getCurrentFullFsLogPath() { let strLogPathFinal = LOG_FS_PATH; if (strLogPathFinal.slice(-1) !== '/') strLogPathFinal = strLogPathFinal + '/'; let strFullLogPath = strLogPathFinal + DEBUG_CUSTOM_FILENAME; if (DEBUG_CUSTOM_FILENAME === '') strFullLogPath = strLogPathFinal + 'iobroker.' + getCurrentISODate() + '.log'; return strFullLogPath; } /** * Clear array: if stateForTimeStamp is greater or equal than log date, we remove the entire log entry * @param {array} inputArray Array of log entries * @param {string} stateForTimeStamp state of which we need the time stamp * @return {array} cleaned log */ function clearJsonByDate(inputArray, stateForTimeStamp) { let dtState = new Date(getState(stateForTimeStamp).ts); if (LOG_DEBUG) log (DEBUG_IGNORE_STR + 'Time of last change of state [' + stateForTimeStamp + ']: ' + dtState); let newArray = []; for (let lpLog of inputArray) { let dtLog = new Date(lpLog.substr(0,23)); if (dtLog.getTime() >= dtState.getTime()) { newArray.push(lpLog); } } return newArray; } /** * Create all States we need at this time. */ function createLogStates() { let logCleanIDs = ''; let statesArray = []; if (! isLikeEmpty(LOG_FILTER)) { for(let i = 0; i < LOG_FILTER.length; i++) { if (LOG_FILTER[i].id !== '') { let lpIDClean = cleanseStatePath(LOG_FILTER[i].id); logCleanIDs += ((logCleanIDs === '') ? '' : '; ') + lpIDClean; statesArray.push({ id:'log' + lpIDClean + '.log', name:'Filtered Log - ' + lpIDClean, type:"string", role: "state", def: ""}); statesArray.push({ id:'log' + lpIDClean + '.logJSON', name:'Filtered Log - ' + lpIDClean + ' - JSON', type:"string", role: "state", def: ""}); statesArray.push({ id:'log' + lpIDClean + '.logJSONcount', name:'Filtered Log - Count of JSON ' + lpIDClean, role: "state", type:"number", def: 0}); statesArray.push({ id:'log' + lpIDClean + '.clearJSON', name:'Clear JSON log ' + lpIDClean, role: "button", type:"boolean", def: false}); statesArray.push({ id:'log' + lpIDClean + '.ClearDate', name:'Löschdatum - ' + lpIDClean, type:"string", role: "state", def: ""}); statesArray.push({ id:'log' + lpIDClean + '.LetzterEintrag', name:'Letzter Eintrag - ' + lpIDClean, type:"string", role: "state", def: ""}); /** * Backward compatibility & cleanup: removing states not needed */ // State .logMostRecent removed with script version 2.0a onwards as it does not make sense any longer due to scheduled update let lpRetiredState = LOG_STATE_PATH + '.log' + lpIDClean + '.logMostRecent'; if (isState(lpRetiredState, true)) { deleteState(lpRetiredState); if (LOG_INFO) log('Remove retired state: ' + lpRetiredState); } // State .clearJSONtime removed with script version 1.2 onwards as we use now time stamp of button '.clearJSON'. lpRetiredState = LOG_STATE_PATH + '.log' + lpIDClean + '.clearJSONtime'; if (isState(lpRetiredState, true)) { deleteState(lpRetiredState); if (LOG_INFO) log('Remove retired state: ' + lpRetiredState); } } } if (LOG_DEBUG) log('createLogStates(): Clean IDs: ' + logCleanIDs); } for (let s=0; s < statesArray.length; s++) { createState(LOG_STATE_PATH + '.' + statesArray[s].id, { 'name': statesArray[s].name, 'desc': statesArray[s].name, 'type': statesArray[s].type, 'read': true, 'write': true, 'role': statesArray[s].role, 'def': statesArray[s].def, }); } createState('javascript.0.Log-Script.Stand', '', { name: 'Letzte Ausführung', desc: '', type: 'string', role: '',read: true, write: true }); createState('javascript.0.Log-Script.Loganzeige', '', { name: 'Loganzeige', desc: '', type: 'string', role: '',read: true, write: true }); } /** * LOG_FILTER: Get value by key. So if we provide 'error' as id, then we get the content of any other element, like of 'blacklist'. * @param {string} id the id, like 'error', 'warn', etc. * @param element the element of which we need the value, e.g. 'blacklist', 'merge', etc. * Returns the element's value, or number -1 of nothing found. */ function logFilterGetValueByKey(id, element) { // We need to get all ids of LOG_FILTER into array for (let i = 0; i < LOG_FILTER.length; i++) { if ( LOG_FILTER[i].id === id ) { if (LOG_FILTER[i][element] === undefined) { return -1; } else { return LOG_FILTER[i][element]; } } } return -1; } /************************************************************************************************************************* * onStop - Being executed once this ioBroker Script stops. *************************************************************************************************************************/ // This is to end the Tale. Not sure, if we indeed need it, but just in case... onStop(function myScriptStop () { endTailingProcess(); clearSchedule(G_Schedule_Midnight); clearSchedule(G_Schedule_StateUpdate); if (LOG_INFO) log('Stop LogScript gracefully.'); }, 0); /************************************************************************************************************************* * General supporting functions *************************************************************************************************************************/ /** * Remove Duplicates from Array * Source - https://stackoverflow.com/questions/23237704/nodejs-how-to-remove-duplicates-from-array * @param {array} inputArray Array to process * @return {array} Array without duplicates. */ function arrayRemoveDublicates(inputArray) { let uniqueArray; uniqueArray = inputArray.filter(function(elem, pos) { return inputArray.indexOf(elem) == pos; }); return uniqueArray; } /** * Clean Array: Removes all falsy values: undefined, null, 0, false, NaN and "" (empty string) * Source: https://stackoverflow.com/questions/281264/remove-empty-elements-from-an-array-in-javascript * @param {array} inputArray Array to process * @return {array} Cleaned array */ function cleanArray(inputArray) { var newArray = []; for (let i = 0; i < inputArray.length; i++) { if (inputArray[i]) { newArray.push(inputArray[i]); } } return newArray; } /** * Checks if Array or String is not undefined, null or empty. * 08-Sep-2019: added check for [ and ] to also catch arrays with empty strings. * @param inputVar - Input Array or String, Number, etc. * @return true if it is undefined/null/empty, false if it contains value(s) * Array or String containing just whitespaces or >'< or >"< or >[< or >]< is considered empty */ function isLikeEmpty(inputVar) { if (typeof inputVar !== 'undefined' && inputVar !== null) { let strTemp = JSON.stringify(inputVar); strTemp = strTemp.replace(/\s+/g, ''); // remove all whitespaces strTemp = strTemp.replace(/\"+/g, ""); // remove all >"< strTemp = strTemp.replace(/\'+/g, ""); // remove all >'< strTemp = strTemp.replace(/[+/g, ""); // remove all >[< strTemp = strTemp.replace(/]+/g, ""); // remove all >]< if (strTemp !== '') { return false; } else { return true; } } else { return true; } } /** * Returns the current date in ISO format "YYYY-MM-DD". * @return {string} Date in ISO format */ function getCurrentISODate() { let currDate = new Date(); return currDate.getFullYear() + '-' + zeroPad((currDate.getMonth() + 1), 2) + '-' + zeroPad(currDate.getDate(), 2); } /** * Fügt Vornullen zu einer Zahl hinzu, macht also z.B. aus 7 eine "007". * zeroPad(5, 4); // wird "0005" * zeroPad('5', 6); // wird "000005" * zeroPad(1234, 2); // wird "1234" :) * @param {string|number} num Zahl, die Vornull(en) bekommen soll * @param {number} places Anzahl Stellen. * @return {string} Zahl mit Vornullen wie gewünscht. */ function zeroPad(num, places) { let zero = places - num.toString().length + 1; return Array(+(zero > 0 && zero)).join("0") + num; } /** * Will just keep lower case letters, numbers, '-' and '_' and removes the rest * Also, capitalize first Letter. */ function cleanseStatePath(stringInput) { let strProcess = stringInput; strProcess = strProcess.replace(/([^a-z0-9_\-]+)/gi, ''); strProcess = strProcess.toLowerCase(); strProcess = strProcess.charAt(0).toUpperCase() + strProcess.slice(1); return strProcess; } /** * Checks if the string provided contains either every or some terms. * Source: https://stackoverflow.com/questions/36283767/javascript-select-the-string-if-it-matches-multiple-words-in-array * @param {string} strInput - The string on which we run this search * @param {array} arrayTerms - The terms we are searching, e.g. ["hue", "error", "raspberry"] * @param {string} type - 'every': all terms must match to be true, * 'some': at least one term (or more) must match * 'blacklist': different here: function will always * return FALSE, but if one of the arrayTerms contains * minimum 3 chars and is found in provided string, * we return TRUE (= blacklisted item found). * @return {boolean} true, if it contains ALL words, false if not all words (or none) * Also, will return true if arrayTerms is not array or an empty array */ function strMatchesTerms(strInput, arrayTerms, type) { if(type === 'blacklist') { if (Array.isArray(arrayTerms)) { let arrayTermsNew = []; for (let lpTerm of arrayTerms) { if (lpTerm.length >= 3) { arrayTermsNew.push(lpTerm); } } if(isLikeEmpty(arrayTermsNew) === false) { let bResultBL = arrayTermsNew.some(function(word) { return strInput.indexOf(word) > -1; }); return bResultBL; } else { return false; // return false if no items to be blacklisted } } else { return false; // we return false if the arrayTerms given is not an array. Want to make sure if we really should blacklist... } } else { if (Array.isArray(arrayTerms)) { if(type === 'every') { let bResultEvery = arrayTerms.every(function(word) { return strInput.indexOf(word) > -1; }); return bResultEvery; } else if(type === 'some') { let bResultSome = arrayTerms.some(function(word) { return strInput.indexOf(word) > -1; }); return bResultSome; } } else { return true; // we return true if the arrayTerms given is not an array } } } /** * Checks if a a given state or part of state is existing. * This is a workaround, as getObject() or getState() throw warnings in the log. * Set strict to true if the state shall match exactly. If it is false, it will add a wildcard * to the end. * See: https://forum.iobroker.net/topic/11354/ * @param {string} strStatePath Input string of state, like 'javas-cript.0.switches.Osram.Bedroom' * @param {boolean} [strict=true] Optional: Default is true. If true, it will work strict, if false, it will add a wildcard * to the end of the string * @return {boolean} true if state exists, false if not */ function isState(strStatePath, strict) { if(strict === undefined) strict = true; let mSelector; if (strict) { mSelector = $('state[id=' + strStatePath + '$]'); } else { mSelector = $('state[id=' + strStatePath + ']'); } if (mSelector.length > 0) { return true; } else { return false; } } /** * Removing Array element(s) by input value. * @param {array} arr the input array * @param {string} valRemove the value to be removed * @param {boolean} [exact=true] OPTIONAL: default is true. if true, it must fully match. if false, it matches also if valRemove is part of element string * @return {array} the array without the element(s) */ function arrayRemoveElementsByValue(arr, valRemove, exact) { for ( let i = 0; i < arr.length; i++){ if (exact) { if ( arr[i] === valRemove) { arr.splice(i, 1); i--; // required, see https://love2dev.com/blog/javascript-remove-from-array/ } } else { if (arr[i].indexOf(valRemove) != -1) { arr.splice(i, 1); i--; // see above } } } return arr; } /** * Escapes a string for use in RegEx as (part of) pattern * Source: https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex * @param {string} inputStr The input string to be escaped * @return {string} The escaped string */ function escapeRegExp(inputStr) { return inputStr.replace(/[.*+?^${}()|[]\]/g, '\\$&'); // $& means the whole matched string } /** * Get all elements of an array if found * @param {array} arr the input array * @param {string} valFind the value to find * @param {boolean} [exact=true] OPTIONAL: default is true. if true, it must fully match. if false, it matches also if valRemove is part of element string * @return {array} an array with all hits or empty array if no hits. */ function arrayGetElements(arr, valFind, exact) { let resultArr = []; for ( let i = 0; i < arr.length; i++){ if (exact) { if ( arr[i] === valFind) { resultArr.push(arr[i]); } } else { if (arr[i].indexOf(valFind) != -1) { resultArr.push(arr[i]); } } } return resultArr; } /** * Replace Array element(s) by input value. * @param {array} arr the input array * @param {string} valReplace the value to search for * @param {string} newValue the new value * @param {boolean} [exact=true] OPTIONAL: default is true. if true, it must fully match. if false, it matches also if valRemove is part of element string * @return {array} the array with replaced the element(s) */ function arrayReplaceElementsByValue(arr, valReplace, newValue, exact) { for ( let i = 0; i < arr.length; i++){ if (exact) { if ( arr[i] === valReplace) { arr[i] = newValue; } } else { if (arr[i].indexOf(valReplace) != -1) { arr[i] = newValue; } } } return arr; }
und hier mein zusätzliches Script
on({id:"javascript.0.Log-Script.Stand", change: 'any'}, function (obj) { var text = getState("javascript.0.Log-Script.logInfo.log").val; if (text.length > 0) { var date_array = text.split('.'); var date = date_array[0]; date_array = date.split(' '); date = date_array[0]; var zeit = date_array[1]; date_array = date.split('-'); var jahr = date_array[0]; var monat = date_array[1]; var tag = date_array[2]; date = tag + "." + monat + "." + jahr + " - " + zeit + " Uhr" } else { date = "" } setStateDelayed('javascript.0.Log-Script.logInfo.LetzterEintrag', date, 800); text = getState("javascript.0.Log-Script.logWarn.log").val; if (text.length > 0) { var date_array = text.split('.'); var date = date_array[0]; date_array = date.split(' '); date = date_array[0]; var zeit = date_array[1]; date_array = date.split('-'); var jahr = date_array[0]; var monat = date_array[1]; var tag = date_array[2]; date = tag + "." + monat + "." + jahr + " - " + zeit + " Uhr" } else { date = "" } setStateDelayed('javascript.0.Log-Script.logWarn.LetzterEintrag', date, 800); text = getState("javascript.0.Log-Script.logError.log").val; if (text.length > 0) { var date_array = text.split('.'); var date = date_array[0]; date_array = date.split(' '); date = date_array[0]; var zeit = date_array[1]; date_array = date.split('-'); var jahr = date_array[0]; var monat = date_array[1]; var tag = date_array[2]; date = tag + "." + monat + "." + jahr + " - " + zeit + " Uhr" } else { date = "" } setStateDelayed('javascript.0.Log-Script.logError.LetzterEintrag', date, 800); });
-
mein erster Versuch mit dem Programm
-
funktioniert nicht. zeig mal deine einstellungen bitte. egal was ich bei spaltenbreite eingebe, es macht keinen zeilenumbruch
-
hast du mein Wiget importiert? Da müssten doch alle Einstellungen schon drin sein
-
jo ich war zu dumm richtig zu klicken. jetzt gehts. danke dir