Weiter zum Inhalt

JavaScript

2.5k Themen 49.9k Beiträge

Hilfe für Skripterstellung mit JavaScript

NEWS

  • [TypeSkript] Wetter.com Forecast/Vorhersage

    Gesperrt
    79
    4 Stimmen
    79 Beiträge
    9k Aufrufe
    S
    Der Adapter ist im LATEST-Repo, weiter geht es hier: https://forum.iobroker.net/topic/85314/test-adapter-meteonomiqs-v0.2.7
  • Zendure SolarFlow (MQTT) - Nulleinspeisung & EVCC-Optimizer

    75
    1 Stimmen
    75 Beiträge
    10k Aufrufe
    S
    Gut – lesiflo hat den „warum überhaupt extern regeln"-Teil schon abgedeckt, das lasse ich raus. Kürzere Fassung, die nur noch das ergänzt, was noch offen ist: Hallo TG, lesiflos Punkt kann ich unterschreiben – die PI-Regelung zahlt sich genau bei den pulsierenden Lasten aus. Ich ergänze nur die zwei Sachen, die noch offen sind. Zum Schwanken um die Null Ich habe hier mal einen Produktivtag mit 4010 Regelzyklen (2-s-Takt) ausgewertet. Eingeschwungen, über 2695 Zyklen: Netz-Mittelwert +11 W Median +5 W Streuung σ 77 W Und dazu das Eigenrauschen meines Shelly 3EM in Ruhe, gemessen als Sprung zwischen zwei aufeinanderfolgenden unterschiedlichen Messwerten: Median 28 W p90 76 W p99 136 W Die Streuung der Regelung entspricht damit exakt dem Rauschen des Messgeräts. Ein guter Teil dessen, was du als Zappeln siehst, ist gar nicht die Regelung, sondern die Messung – besser als der eigene Sensor kann kein Regler werden. Schau als erstes, wie ruhig dein Zähler in einer Phase ganz ohne Lastwechsel liegt. Das setzt die Untergrenze für alles Weitere. Dass das interne HEMS näher an der Null wirkt, hat auch einen banalen Grund: es sitzt auf dem Gerät und regelt seinen Wechselrichter direkt. Bei mir läuft der Weg Zähler → MQTT → ioBroker → Skript → MQTT → Zendure, das sind gemessen rund 2,7 s Totzeit. Die holt kein externer Regler auf. Zum KP = 1.0 Die Intuition trügt hier gleich doppelt. Ein reiner P-Regler erreicht die Null grundsätzlich nie. Bei Streckenverstärkung 1 bleibt im Beharrungszustand Netz = Störgröße / (1 + KP) stehen – mit KP = 1,0 also die halbe Störgröße. 1000 W Lastsprung heißt 500 W Dauerabweichung. Genau dafür ist der I-Anteil da. Bei mir macht der auch die Hauptarbeit: in 88 % der Zyklen ist er größer als der P-Anteil (Median I = 448 W gegen P = 40 W). Falls du mit „direkte Steuerung" eher meinst, den gemessenen Netzwert 1:1 auf die Batterieleistung aufzuschlagen – das ist mathematisch ein reiner Integrator mit Verstärkung 1 pro Zyklus. Der erreicht zwar die Null, korrigiert aber den vollen Fehler, bevor die Wirkung der vorherigen Korrektur am Zähler ankommt. Bei 2–3 s Totzeit legt er im nächsten Zyklus nochmal voll nach, schießt über, dreht um. Das ist genau das Wippen, das du loswerden willst. Mein KI von 0,08 bei 2 s Takt sind rund 16 % Fehlerkorrektur pro Zyklus über das Integral – bewusst langsam genug für die Totzeit. Weniger Stellhandlungen bekämst du damit übrigens auch nicht: deren Zahl bestimmt der Regeltakt, nicht die Verstärkung. Was bei mir wirklich geholfen hat Mein Deadband stand auf 15 W und lag damit unter dem Median-Rauschen von 28 W – der Regler ist also permanent dem Sensorrauschen hinterhergefahren, über die Hälfte aller Stellbefehle waren Bewegungen ≤ 25 W. Wenn du weniger Stellhandlungen willst, ist das der Hebel: Deadband ungefähr auf den Rauschpegel deines Zählers. Nur nicht zu groß, sonst bleibt eine Dauerabweichung in dieser Höhe stehen. Und das Wippen speziell um den Nulldurchgang ist meistens der AC-Moduswechsel (Input ↔ Output), der ist bei der 2400 AC nicht instantan. Ich habe dafür eine Hysterese (Wechsel erst ab 100 W Anforderung) plus 30 s Sperrzeit danach. An einem realen Tag hat das 56 von 86 angeforderten Wechseln unterdrückt – blieben 30 echte übrig. Ohne so etwas schaltet das Gerät bei jeder Wolke hin und her.
  • Zendure zenSDK Lokal API, SmartMode, SolarFlow AC 800 Pro 2

    558
    12 Stimmen
    558 Beiträge
    71k Aufrufe
    D
    @maxclaudi sagte: @Daniel-8 Zum Zeitstempel (Timestamp): Ja, genau. Ich meine den Datenpunkt timestamp im JSON-Objekt,das vom Gerät über die API geliefert wird. Oder den Datenpunkt timeUpdate das den timestamp in lesbarer Zeit darstellt. Wenn das Gerät komplett vom Internet isoliert ist, kann es die Uhrzeit nicht mehr synchronisieren. Die interne Uhr läuft dann starr auf der eigenen Hardware-Basis weiter und fängt mit der Zeit an, leicht nachzugehen (zu driften). Wenn das System trotz dieser Drift absolut flüssig läuft und Deine Limits in Millisekunden quittiert werden, läuft Deine Firmware-Version lokal stabil und Du kannst es genau so weiterlaufen lassen. Dann werde ich dies mal beobachten. Ob diese Zeit überhaupt mal abweicht von der systemzeit. Ich werde es einfach mal so laufen lassen ohne umschreiben von dns. Wenn das Gerät als inselanlage läuft hat es ja oft auch keinen Internetzugang. Zur Bereitstellung des Testgeräts: Ich habe im Nachgang die Korrespondenz noch einmal genauer geprüft und wurde auf eine entsprechende Vertraulichkeit (NDA / Confidentiality Notice) hingewiesen. Bitte habt daher Verständnis, dass ich mich zu den Hintergründen der Bereitstellung nicht weiter öffentlich äußern werde. Kein Problem. Ist auf jedenfall gut das du was zum testen bekommst und damit ja auch weitere Erkenntnisse sammeln kannst ohne an dein eigenes System gehen zu müssen. Es kann sich ja nur positiv auswirken was du schon alles aufgeklärt hast hier. Vielen Dank nochmals.
  • [Skript] JONR-Saugroboter lokal steuern (hier: T5 Pro Gen1)

    2
    0 Stimmen
    2 Beiträge
    53 Aufrufe
    H
    Hallo zusammen, seit dem Beitrag oben sind ein paar Tage vergangen, in denen der Sauger täglich gelaufen ist. Und dabei ist einiges aufgefallen, was ich damals schlicht nicht wissen konnte. Ein Fehler war richtig übel, zwei Sachen waren einfach nur ärgerlich — und am Ende bin ich an eine Quelle geraten, die mir alle Fehlertexte des Geräts frei Haus geliefert hat. Ganz unten hängt das überarbeitete Skript. Wer das alte im Einsatz hat: bitte tauschen, das hatte einen Fehler, der auf Dauer richtig weh tut. 1. Der Ärger: Die Raumreinigung hat mir die Karte zerlegt Das ist der Grund, warum ich diesen Beitrag überhaupt schreibe. Irgendwann fiel meiner Frau auf, dass in der App plötzlich mehrere Karten lagen. Und die Raumnamen stimmten nicht mehr. Ich hab die Karte neu eingeteilt, alles benannt — zwei Tage später dasselbe Bild. Ich hab dann erst überall gesucht, nur nicht bei mir selbst: an der App, am WLAN, an der Firmware. Der Übeltäter war mein eigener Startbefehl. Im ersten Skript startete ich mit 2.1 — dem allgemeinen „los geht's". Der Haken: dieser Befehl startet einen Lauf ganz ohne Bezug zur gespeicherten Karte. Der Sauger fängt an zu fahren und zeichnet dabei eine neue. Alle Raumnummern, die ich mühsam ermittelt hatte, waren danach wertlos. Richtig ist der Raumbefehl 17.1, auch wenn man „alles" will: action(17.1, [{ piid: 8, value: 1 }, // 1 = ganze Wohnung { piid: 34, value: "[]" }]) // Raumliste -- LEER, aber als Array! Der Knackpunkt ist das "[]". Ein leerer String "" reicht nicht, das Gerät antwortet mit einem Fehler. Genau derselbe Stolperstein wie bei den Räumen, wo "6" scheitert und erst "[6]" geht — ich hatte ihn nur nicht auf den Fall „ganze Wohnung" übertragen. Wenn ihr nur eine Sache aus diesem Beitrag mitnehmt, dann diese. 2. „Alexa, Staubsauger aus" — und nichts passiert Der zweite Fund ist keiner am Sauger, aber er dürfte einige treffen, die ioBroker mit Alexa verbinden. Bei uns kam es immer wieder vor, dass Alexa brav „OK" sagte und der Sauger einfach weiterfuhr. Kein Fehler, kein Logeintrag, nichts. Erst dachte ich an Funkprobleme. Es lag an Karteileichen in Alexas Gerätekonto. Vor dem JONR stand hier ein Roborock über den mihome-vacuum-Adapter. Der ist längst weg — aber Alexa hatte sich seine Geräte gemerkt. Elf Stück, darunter eines, das schlicht „Staubsauger" hieß. Und jetzt kommt's: Sage ich „Alexa, Staubsauger aus", nimmt Alexa den exakten Gerätenamen-Treffer und nicht meine Routine. Der Befehl geht an die ioBroker-Skill, die findet das Gerät nicht mehr — und Alexa quittiert trotzdem mit „OK". Das Fiese: In der Alexa-App sieht man diese Geräte nicht. Sie tauchen nur auf, wenn man im alexa2-Adapter unter Smart-Home-Devices nachschaut. Wer also mal einen Adapter mit Smart-Home-Geräten hatte und ihn wieder losgeworden ist, sollte da mal reingucken. ⚠️ Falle beim Aufräumen, ich bin zweimal reingefallen: Alexa schreibt die Gerätenamen intern um — Leerzeichen und Umlaute werden zu Unterstrichen. Vergleicht man stur auf Gleichheit, sehen aktive Geräte wie Leichen aus. Vor dem Löschen also normalisieren und gegenprüfen. 3. Wie ich sowas überhaupt gefunden habe Beide Sachen oben haben eins gemeinsam: Sie passieren, wenn man nicht hinschaut, und hinterher ist nichts mehr da. Das ioBroker-Log wirft nach sieben Tagen weg, und eine Zeitreihe hatte ich gar nicht. Es gab hier einen Vorfall — der Sauger hatte sich in einer Gardine verfangen und wurde befreit — den ich am nächsten Tag schlicht nicht mehr rekonstruieren konnte. Danach habe ich das Skript um eine Beobachtung erweitert, und die hat sich seitdem mehrfach bezahlt gemacht: Ein Ereignis-Journal. Eine Textdatei pro Monat, die jede Zustandsänderung mitschreibt. Nichts Kompliziertes, fs.appendFileSync reicht — das ist in der javascript-Sandbox erlaubt. So sieht das aus: 10:58:38 | BEFEHL | Start Raum Schlafzimmer (Raum-ID 4) [von: Alexa (iot)] 10:58:40 | AKTION | ausgefuehrt: Start Raum Schlafzimmer (Raum-ID 4) 11:00:22 | BEFEHL | Pause [von: Alexa (iot)] 11:00:26 | AUFTRAG | Raum pausiert (Code 9) 11:01:16 | BEFEHL | Fortsetzen [von: Alexa (iot)] 11:01:19 | AUFTRAG | Raum fortgesetzt (Code 10) Das Wichtigste daran ist die eckige Klammer: die Quelle. ioBroker liefert bei jeder State-Änderung mit, wer sie ausgelöst hat — Alexa, die Admin-Oberfläche, VIS oder das Skript selbst. Genau daran habe ich gesehen, dass beim „Staubsauger aus" gar kein Befehl ankam. Ohne diese Zeile hätte ich weiter beim Sauger gesucht. Was man dabei wissen sollte: Befehle aus der Hersteller-App und Tasten am Gerät tauchen hier nie auf. Die gehen über die Cloud direkt an den Sauger, an uns vorbei. Ihre Wirkung sieht man trotzdem — der Status ändert sich ja. Zum Abfragetakt: Ich frage alle 60 Sekunden, und wenn der Sauger fährt, alle 15. Das ist ein Kompromiss. Man kann den Sauger mit Anfragen nämlich durchaus überfahren — läuft eine noch, gehen die nächsten ins Leere. Und ein Nebeneffekt, über den ich gestolpert bin: Meldungen, die nur ein paar Sekunden anstehen, verpasst man damit. Wer die zuverlässig braucht, muss enger abfragen. Ich lebe damit. 4. Die Fundgrube: Die Fehlertexte stehen im Mi-Home-Plugin Und jetzt der Teil, der auch für Leute mit ganz anderen Xiaomi-Geräten interessant sein dürfte. Im ersten Skript standen die Fehlertabellen leer: const ERROR = { 0: 'kein Fehler' }; Der Grund: Die Gerätespezifikation sagt bei den Fehlernummern nur „ist eine Zahl zwischen 0 und 65535" — welche Zahl was bedeutet, steht nirgends. Bei anderen Werten wie dem Status steht das sehr wohl da, nur bei den Fehlern eben nicht. Ich hatte mich damit abgefunden, die Bedeutungen nach und nach selbst zu sammeln: bei jeder Störung nachschauen, was die App anzeigt, und eine Zeile nachtragen. Das war unnötig. Wenn man in der Mi-Home-App auf sein Gerät tippt, lädt sie dafür ein eigenes kleines Programm nach — für jedes Gerätemodell ein anderes. Das ist die Oberfläche mit der Karte, den Knöpfen, den Meldungen. Und dieses Programm muss die Fehlertexte kennen, sonst könnte es sie nicht anzeigen. Man kommt frei dran. Bei mir waren es 3,7 MB, und der Text stand da völlig unverschlüsselt drin — in elf Sprachen, Deutsch dabei. So kommt ihr an eure eigenen — Schritt für Schritt Schritt 1: Den Modellnamen herausfinden Jedes Xiaomi-Gerät hat neben seinem hübschen Namen („Jonr T5 Pro") noch einen technischen, der so aussieht: xtl.vacuum.2407a. Der ist der Schlüssel zu allem Weiteren. Am einfachsten kommt ihr da über den Token-Extractor ran — dasselbe Werkzeug, mit dem ihr euch im ersten Beitrag (Abschnitt 2) schon Token und Device-ID geholt habt. Der spuckt für jedes Gerät einen Block aus, und da steht er drin: Name: Jonr T5 Pro ID: 1234567890 Token: a1b2c3d4e5f6... Model: xtl.vacuum.2407a <-- den brauchen wir Wer das Werkzeug nicht extra laufen lassen will: Der Name steht auch im ioBroker-Log, wenn man den mihome-vacuum-Adapter einmal auf das Gerät ansetzt. Bei mir war das sogar der ursprüngliche Anlass für das ganze Projekt, da stand nämlich: Model xtl.vacuum.2407a not supported Also: einmal kurz den Adapter dranhängen, ins Log schauen, Modellnamen abschreiben, Adapter wieder weg. Schritt 2: Die Spec-Seite eures Geräts aufrufen Und hier wird's unintuitiv, deshalb im Detail. Achtung, falsche Fährte: Im ersten Beitrag hatte ich miot-spec.org als Quelle genannt. Wenn ihr die Seite aufruft, seht ihr eine weiße Seite mit dem Wort „worked" darauf. Das ist kein Fehler und ihr habt euch nicht vertippt — das ist eine reine Datenschnittstelle für Programme, da gibt es nichts anzuschauen. Die Seite für Menschen ist home.miot-spec.com. Die hat auch eine Suche — man muss sie nur richtig füttern. Nach „Jonr" allein oder nach „T5 Pro" zu suchen bringt nichts Brauchbares. Was funktioniert: Seite oben auf Englisch umstellen — und falls euer Browser von sich aus eine Google-Übersetzung drüberlegt, die wieder ausschalten, sonst sucht ihr im übersetzten Text. Dann nach vacuum jonr suchen, also Geräteart plus Hersteller. Damit tauchen die Modelle auf. Wer seinen Modellnamen aus Schritt 1 schon hat, ist über die Adresszeile allerdings schneller — einfach selbst zusammenbauen: https://home.miot-spec.com/spec/xtl.vacuum.2407a └──────── immer gleich ────────┘└─ euer Modellname ─┘ Also https://home.miot-spec.com/spec/ in die Adresszeile, den Modellnamen aus Schritt 1 dranhängen, Enter. Fertig. Für den P20 Pro wäre das .../spec/xtl.vacuum.xm2216, und so weiter. Auf der Seite, die dann kommt, steht alles, was euer Gerät kann — jede Eigenschaft und jeder Befehl mit seiner Nummer. Das ist übrigens auch die Seite, auf der man nachschlägt, wenn man das Skript an ein anderes Modell anpassen will. Schritt 3: Das Plugin herunterladen Ziemlich weit oben auf dieser Seite sitzt ein Link auf Chinesisch: 下载米家插件 Das heißt „Mi-Home-Plugin herunterladen" — und das ist genau das, was wir wollen. Bei mir kam eine ZIP-Datei mit einem ellenlangen Namen und 3,7 MB Inhalt heraus. Schritt 4: Auspacken und nachschauen ZIP auspacken Interessant ist darin die Datei android/main.bundle Das ist eine große Textdatei. Sucht darin nach title: — dort stehen die Fehler, jeweils als Nummer mit zwei Verweisen: einer auf die Meldung, einer auf den Lösungsvorschlag Die Texte selbst stehen weiter unten in derselben Datei, ordentlich nach Sprachen sortiert Das muss man aber nicht von Hand machen — ganz unten in diesem Beitrag steht ein kleines Python-Skript, das einem die Fummelei abnimmt. Aufruf: python extract-codes.py android/main.bundle Es sucht sich die deutschen Texte selbst heraus und spuckt am Ende fertige Tabellen aus, die man direkt ins ioBroker-Skript kopieren kann. Braucht nur Python, sonst nichts. Falls es bei eurem Gerät die falsche Sprache erwischt, zeigt --sprachen alle gefundenen mit einem Beispielsatz an, und mit --sprache 0 nehmt ihr dann eine andere (0 ist normalerweise Englisch). Ich hatte bei meinen ersten Versuchen prompt Koreanisch im Ergebnis stehen — deshalb die Option. 🙂 Warum ich vorher nichts gefunden hatte: Ich hatte nach kleinen Zahlen gesucht. Die Fehlercodes liegen im 4000er-Bereich. 5. Die Tabellen für den T5 Pro Falls jemand denselben oder einen verwandten Sauger hat. Bei einem anderen Modell bitte selbst ziehen, die Nummern können abweichen. Was am Gerät schiefgehen kann Code Meldung Was zu tun ist 4001 Staubbehälter nicht installiert. Öffnen Sie die Roboterabdeckung und setzen Sie den Staubbehälter ein. 4003 Walzenbürste verheddert. Nehmen Sie die Abdeckung der Walzenbürste ab, nehmen Sie die Walzenbürste heraus und entfernen Sie verhedderte Haare und andere Verunreinigungen. 4004 Seitenbürste verheddert. Nehmen Sie die Seitenbürste ab und entfernen Sie die verhedderten Haare und andere Verschmutzungen. 4005 Antriebsrad blockiert. Drehen Sie das Antriebsrad und befreien Sie es von verhedderten Haaren oder anderen Verunreinigungen. 4006 Abwurfsensor hat Staub angesammelt. Wischen Sie den Tropfensensor mit einem trockenen Tuch ab und stellen Sie den Roboter in eine sichere Position, um ihn neu zu starten. 4007 Lasersensor verstopft. Entfernen Sie die Gegenstände, die den Lasersensor behindern. 4008 Radar des Lasersensors blockiert. Entfernen Sie den Schmutz, der den rotierenden Radar des Lasersensors blockiert, und klopfen Sie vorsichtig auf die Sensorabdeckung. 4009 Antriebsrad rutscht durch. Wischen Sie das Antriebsrad ab und bringen Sie den Roboter in eine andere Position, um ihn neu zu starten. 4011 Abdeckung des Lasersensors klemmt. Bitte entfernen Sie Objekte, die die Abdeckung des Lasersensors blockieren, oder bringen Sie den Roboter in eine andere Position, um ihn neu zu starten. 4012 Roboter steckt fest. Bitte entfernen Sie die Hindernisse um den Roboter herum, oder begeben Sie sich in einen anderen Bereich, um neu zu starten. 4013 Der Kollisionssensor ist blockiert. Bitte klopfen Sie leicht auf die vordere Stoßstange des Roboters und vergewissern Sie sich, dass die Stoßstange zurückfedert. 4014 Der Roboter hängt. Starten Sie den Roboter neu, nachdem Sie ihn wieder auf den Boden gestellt haben. 4016 Roboter gekippt. Starten Sie den Roboter neu, nachdem Sie ihn auf eine ebene Fläche gestellt haben. 4017 Start in Sperrzone nicht möglich. Starten Sie den Roboter neu, nachdem Sie ihn aus dem Sperrgebiet entfernt haben, oder löschen Sie das aktuelle Sperrgebiet. 4018 Start auf Teppich nicht möglich. Starten Sie den Roboter neu, nachdem Sie ihn aus dem Teppichbereich entfernt haben, oder ändern Sie die aktuelle Teppichreinigungsstrategie. 4020 Der Wischmopphalter hat sich verheddert. Nehmen Sie den Wischmopphalter ab und entfernen Sie verhedderte Haare und andere Verunreinigungen. 4021 Wischmopphalter nicht installiert. Bringen Sie beide Mop-Pad-Halterungen richtig an. Was an der Station schiefgehen kann Code Meldung Was zu tun ist 4501 Der Staubbeutel ist nicht installiert. Setzen Sie den Staubbeutel richtig ein. 4502 Nicht genügend Wasser oder kein Schmutzwassertank installiert. Nehmen Sie den Frischwassertank heraus, füllen Sie eine ausreichende Menge Wasser ein, setzen Sie den Frischwassertank wieder ein und vergewissern Sie sich, dass er ordnungsgemäß installiert ist. 4503 Das Schmutzwasser ist voll oder ein Schmutzwassertank ist nicht installiert. Nehmen Sie den Schmutzwassertank heraus, leeren Sie das gesamte Schmutzwasser, setzen Sie den Schmutzwassertank wieder ein und vergewissern Sie sich, dass er ordnungsgemäß installiert ist. 4506 Der Reinigungsschlitz des Mopps ist voll oder nicht installiert. 1. Entfernen Sie den Filter aus dem Mopp-Reinigungsschlitz, reinigen Sie die angesammelten Haare und Flecken auf dem Filter und installieren Sie ihn anschließend wieder; 2. Überprüfen Sie, ob Objekte den Ansauganschluss des Reinigungsbereichs des Wischmopps blockieren; 3. Stellen Sie sicher, dass der Deckel des Abwassertanks fest verschlossen ist; 4. Überprüfen Sie, ob der Abwassertank ordnungsgemäß installiert ist. 4507 Der Reinigungsschlitz des Mopps ist voll oder nicht installiert. 1. Entnehmen Sie den Filter aus dem Mop-Reinigungsschlitz, reinigen Sie alle Haare und Flecken, die sich auf dem Filter angesammelt haben, und installieren Sie ihn wieder; 2. Überprüfen Sie, ob Objekte den Ansauganschluss des Reinigungsbereichs des Wischmopps blockieren; 3. Nehmen Sie den Schmutzwassertankfilter aus dem Modul für die Wasserversorgung und den Abfluss heraus, reinigen Sie ihn gründlich und setzen Sie ihn wieder ein; 4. Vergewissern Sie sich, dass die Abdeckung des automatischen Wasserzufuhr- und -ablassmoduls fest geschlossen ist. 4901 Wasser kann nicht automatisch nachgefüllt werden. 1. Prüfen Sie, ob der Wasserdruck im Haushalt normal ist; 2. Überprüfen Sie, ob die Wasserzuleitung richtig installiert ist. 4902 Sie können das Wasser nicht automatisch ablassen. 1. Prüfen Sie, ob das Abflussrohr richtig installiert ist; 2. Öffnen Sie die Abdeckung des Schmutzwassertanks, nehmen Sie den Filter heraus, reinigen Sie ihn und setzen Sie ihn wieder ein. 4903 Unzureichende Reinigungslösung. Bitte geben Sie eine ausreichende Menge Reinigungslösung in das Modul für die automatische Wasserzufuhr und den Abfluss. Und noch ein Fund nebenbei Direkt neben dem Fehlercode sitzt ein zweiter Wert, den ich im alten Skript als „zweiter Fehlerslot, keine Ahnung" mitgeschleppt hatte. Der sprang immer mal auf Zahlen um die 1000. In der Spec heißt er schlicht message-notification. Ich hatte den Namen einfach nicht gelesen. Da stehen die Meldungen drin, die die App anzeigt: Code Meldung 1001 Ladevorgang abgeschlossen, Wiederaufnahme der Reinigung ab dem Unterbrechungspunkt 1002 Niedriger Akkustand, zurück zum Laden 1003 Firmware-Upgrade fehlgeschlagen, bitte erneut versuchen 1004 Firmware-Upgrade erfolgreich, probieren Sie es aus! 1005 Neue Firmware-Version verfügbar, aktualisieren Sie jetzt! 1006 Batterie ist schwach, schaltet sich bald ab, bitte laden Sie rechtzeitig 1007 Hauptgerät wird bald abgeschaltet, bitte bringen Sie es rechtzeitig zur Station zurück 1008 Reinigung abgeschlossen, zurück in die Station 1009 Bitte reinigen Sie den Schmutzwassertank rechtzeitig, um Geruch zu vermeiden 1010 Geplante Reinigung gestartet 1011 Aufgabenkonflikt, geplante Reinigung kann nicht ausgeführt werden 1012 Die geplante Reinigungsaufgabe kann nicht ausgeführt werden, da sie sich im Nicht-Stören-Zeitraum befindet. 1013 Niedriger Batteriestand, die geplante Reinigung kann nicht ausgeführt werden 1014 Ausgewählter Bereich nicht gefunden, Reinigung für diese Zeit beendet 1015 Verloren, bitte bringen Sie den Roboter zurück in die Station Und die Wartungserinnerungen: Code Meldung 2001 Bitte tauschen Sie die Seitenbürste regelmäßig aus 2002 Bitte ersetzen Sie die Walzenbürste regelmäßig 2003 Bitte ersetzen Sie regelmäßig das Filterelement des Staubbehälters 2004 Bitte ersetzen Sie die Wischmopps regelmäßig 2005 Bitte ersetzen Sie den Staubbeutel regelmäßig 2006 Bitte reinigen Sie den Reinigungsschlitz des Mopps regelmäßig. 2007 Bitte reinigen Sie den Filter des Schmutzwassertanks regelmäßig. 2008 Bitte wischen Sie den Sensor der Haupteinheit regelmäßig ab. Für mich war das praktisch nützlich: Die 1009 stand bei mir nach jedem Wischlauf im Log, und ich hatte sie für irgendein Trocknungs-Flag gehalten. Ist sie nicht — der Schmutzwassertank will geleert werden. Die Lösungstexte schicke ich mir inzwischen mit ins Telegram. Eine Fehlernummer allein hilft niemandem, der vor dem Gerät steht und wissen will, was er anfassen soll. 6. Was aus „Was nicht geht" inzwischen geht Im ersten Beitrag stand in Abschnitt 10 eine kurze Liste. Zwischenstand: Kartenansicht — weiterhin nein. Daran hat sich nichts geändert, die Kartendaten liegen in der Cloud. Das Plugin bringt hier übrigens auch nichts: Es enthält zwar die Grafiken der Oberfläche, aber die Karte selbst holt es sich zur Laufzeit. Zonenreinigung per Koordinaten — immer noch nicht ausprobiert. Nacharbeit nach einem Abbruch erzwingen — die Aktionen dafür (17.40 Mopp waschen, 17.41 Mopp trocknen, 17.42 Staub absaugen) habe ich weiterhin nicht getestet. Aber ich weiß inzwischen besser, wann die Station von selbst nacharbeitet, und das nimmt dem Punkt viel von seiner Dringlichkeit: Nach einem normal beendeten Reinigungslauf fährt sie die volle Kette — Staub absaugen, Mopp waschen, Mopp trocknen. Nach einem abgebrochenen Lauf ging sie bei mir direkt ins Trocknen, ohne Absaugen und Waschen. Ich vermute, sie überspringt schlicht, was nichts bringt: Der Lauf war eine Minute lang und der Mopp frisch gewaschen. Sauber bewiesen ist das aber nicht — ich habe bei dem Test zwei Dinge auf einmal verändert (kurzer Lauf und Abbruch), und damit lässt sich nicht sagen, welches davon den Ausschlag gab. Beobachte ich weiter; das Journal schreibt es ja jetzt mit. Neu dazugekommen ist ein Punkt, der oben noch nicht stand: Pause und Fortsetzen. Ich hatte befürchtet, dass das Fortsetzen mit demselben 2.1 ebenfalls die Karte frisst. Tut es nicht — und der Grund ist ganz logisch: Bei einer Pause läuft der Auftrag ja noch, mitsamt Raumliste. 2.1 hängt sich daran an. Nur aus dem Leerlauf heraus, wo es nichts zum Anhängen gibt, fängt es von vorne an — und das ist der Kartenfresser aus Abschnitt 1. 7. Das überarbeitete Skript Steht komplett unten in diesem Beitrag. Gegenüber der ersten Fassung: Räume starten über 17.1 statt 2.1 — der Kartenfehler von oben Fehler-, Stations- und Meldungstexte gefüllt, mit Lösungshinweis im Telegram Ereignis-Journal mit Quelle je Befehl Aufträge werden sauber beendet, bevor ein neuer startet. Vorher verpuffte jeder zweite Raumbefehl kommentarlos — man sagt „Bad saugen", während er noch im Flur unterwegs war, Alexa sagt OK, und nichts passiert. Das lag daran, dass der alte Auftrag nie richtig beendet wurde. Die Raumliste des laufenden Auftrags wird mitgelesen. Daran sieht man, ob ein Auftrag seinen Raumbezug noch hat — nützlich, wenn man wissen will, ob der Sauger nach einer Störung noch weiß, was er eigentlich tun wollte. Vollständige Statusliste. In der ersten Fassung fehlten zehn Statuswerte, darunter ausgerechnet der für „legt gerade eine neue Karte an". Das Skript meldete beim Kartenunfall nur „unbekannt (14)" — hätte es den Text gekannt, wäre mir das Wochen früher aufgefallen. Wie gehabt: IP, Token und Device-ID oben im Skript eintragen, Raum-IDs eintragen, fertig. t5-saugroboter.js — das komplette Skript /* * ============================================================================ * JONR T5 Pro (xtl.vacuum.2407a) lokal steuern -- ioBroker javascript-Adapter * Fassung 2, September 2026 * ============================================================================ * Spricht den Sauger direkt per miIO/MIoT ueber UDP an. Keine Cloud, kein * zusaetzliches npm-Paket -- nur die Node-Bordmittel dgram und crypto. * * Getestet mit dem JONR T5 Pro. Andere xtl.*-Modelle (P20 Pro = xm2216, * X1 MAX = xt3411 u. a.) sprechen dasselbe Protokoll, koennen aber * abweichende siid/piid haben -- Spec pruefen, siehe Forumsbeitrag. * * Keine Gewaehr, keine Wartungszusage. Verbesserungen gerne. * * --- Vor dem Start ausfuellen ------------------------------------------------ * IP, TOKEN und DID unten eintragen, ROOMS mit den eigenen Raum-IDs fuellen. * * --- Was sich gegenueber der ersten Fassung geaendert hat --------------------- * * Raeume starten jetzt ueber 17.1 statt 2.1. Der generische Startbefehl legt * eine NEUE KARTE an -- damit waren nach jedem Alexa-Start alle Raum-IDs * hinueber. Die Raumliste muss ein Array-STRING sein: "[3]", auch leer "[]". * * Fehlertexte, Stationsfehler und Meldungen sind gefuellt -- samt dem Text, * was der Nutzer tun soll. Der geht bei Stoerungen mit ins Telegram. * * Ereignis-Journal: schreibt jede Zustandsaenderung in eine Monatsdatei, * mit der QUELLE jedes Befehls (Alexa / App / Oberflaeche / Skript). * * Ein Auftrag wird sauber beendet, bevor ein neuer startet (ensureIdle). * Vorher verpuffte jeder zweite Raumbefehl kommentarlos. * * Die Raumliste des laufenden Auftrags (17.34) wird mitgelesen -- daran * sieht man, ob ein Auftrag seinen Raumbezug noch hat. * * --- Fallstricke, alle in der Praxis getroffen ------------------------------- * 1. Action-IIDs und Property-IIDs sind GETRENNTE Nummernkreise! * 17.46 als Action = set-clean-mode, 17.46 als Property = Reinigungshistorie. * 2. Das Geraet liefert bei get_properties teils WENIGER Ergebnisse als * angefragt. Zuordnung immer ueber die mitgelieferten siid/piid, nie ueber * die Reihenfolge. Batchgroesse 5 hat sich bewaehrt. * 3. Grosse Antworten liefert das Geraet GAR NICHT aus -- es sendet dann kein * Paket statt eines Fehlers. Also keine dicken JSON-Properties pollen. * 4. NIE zwei miIO-Clients gleichzeitig auf dem Geraet. Laeuft parallel ein * zweites Skript, quittiert der Sauger Befehle mit code 0 und tut nichts. * Das sieht wie ein falscher Parameter aus, ist aber eine Kollision. * 5. Befehle nicht ohne Pause hintereinander senden -- der Sauger verschluckt * sonst den naechsten. 1,5 s dazwischen reichen. * 6. Stoppen heisst Auftrag BEENDEN (17.3), nicht nur anhalten (2.2). * Sonst bleibt der Auftrag pausiert und der Sauger faehrt spaeter von * selbst weiter. * 7. Fortsetzen NACH einer Pause ist mit 2.1 dagegen in Ordnung -- da haengt * ein Auftrag, an den es sich anhaengen kann. Nur aus dem Leerlauf heraus * ist 2.1 der Kartenfresser. * ============================================================================ */ 'use strict'; const dgram = require('dgram'); const crypto = require('crypto'); const fs = require('fs'); // ---------------------------------------------------------------- Konfiguration const IP = '192.168.x.y'; // IP des Saugers (feste Adresse im Router vergeben!) const TOKEN = Buffer.from('DEIN_32_STELLIGER_TOKEN', 'hex'); const DID = 'DEINE_DID'; // Device-ID, kommt aus demselben Tool wie der Token const PORT = 54321; const BASE = '0_userdata.0.Saugroboter.T5'; /* Ereignis-Journal. Bewusst NICHT im ioBroker-Log: das rotiert nach 7 Tagen * (maxDays: 7) und genau daran ist die Aufarbeitung des Vorfalls vom * 02.09.2026 gescheitert -- die Fehlercodes waren weg, bevor jemand danach * gesucht hat. Monatsdateien, keine Rotation, Klartext. */ const JOURNAL_DIR = '/opt/iobroker/log'; /* Instanz des history-Adapters (am 03.09.2026 nachinstalliert). Die States, * die unten mit historisch=true angelegt werden, bekommen ihn als custom * eingetragen -- damit haengt die Aufzeichnung am Skript und nicht an einer * von Hand geklickten Adapterkonfiguration, die keiner mehr nachvollzieht. */ const HISTORY = 'history.0'; const POLL_IDLE = 60000; // ms — Sauger ruht const POLL_ACTIVE = 15000; // ms — Sauger arbeitet /* Raum-IDs der Karte "Zu Hause" (Stand 2026-08-24). * ACHTUNG: Gelten nur für diese Karte. Nach Neukartieren, Teilen oder * Zusammenlegen von Räumen werden IDs neu vergeben -> Tabelle neu erarbeiten * (Kochrezept steht in der Memory). Die App zeigt jeweils ID minus 2 an. */ const ROOMS = { Badezimmer: { id: 3, alexa: 'T5 Badezimmer' }, Schlafzimmer: { id: 4, alexa: 'T5 Schlafzimmer' }, Arbeitszimmer: { id: 5, alexa: 'T5 Arbeitszimmer' }, Kinderzimmer: { id: 6, alexa: 'T5 Kinderzimmer' }, Abstellraum: { id: 7, alexa: 'T5 Abstellraum' }, Wohnzimmer: { id: 8, alexa: 'T5 Wohnzimmer' }, Kueche: { id: 9, alexa: 'T5 Küche' }, Flur: { id: 11, alexa: 'T5 Flur' }, }; // siid.aiid der Aktionen, die wir auslösen const ACT = { START_SWEEP: { siid: 2, aiid: 1 }, // parameterlos STOP: { siid: 2, aiid: 2 }, // haelt nur die Fahrt an STOP_CLEAN: { siid: 17, aiid: 3 }, // beendet den AUFTRAG -- siehe stopAndHome() PAUSE: { siid: 2, aiid: 7 }, CHARGE: { siid: 16, aiid: 1 }, // zurück zur Basis START_CLEAN: { siid: 17, aiid: 1 }, // in: piid 8 = Art, piid 34 = Räume SET_FAN: { siid: 17, aiid: 26 }, // in: piid 13 SET_WATER: { siid: 17, aiid: 27 }, // in: piid 12 SET_MODE: { siid: 17, aiid: 46 }, // in: piid 10 }; // Werte const CLEAN_FULL = 1, CLEAN_AREA = 3; // piid 8 const MODE_BOTH = 0, MODE_MOP = 2; // piid 10 const FAN_MAX = 3; // piid 13 const WATER_MID = 1; // piid 12 /* Statuscodes (Property 2.2) — VOLLSTÄNDIG laut MIoT-Spec. * Bis 30.08.2026 fehlten hier 3, 11, 14, 15, 17, 19, 20, 21, 22 und 23. Genau * die drei wichtigen davon zeigten sich am 30.08. beim Kartierungs-Vorfall: * der ioBroker meldete nur "unbekannt (14)", statt "legt neue Karte an". */ const STATUS = { 1:'Idle', 2:'Busy', 3:'Wartet', 4:'Wischen', 5:'Saugen+Wischen', 6:'Pausiert', 7:'Saugen', 8:'Fehler', 9:'Lädt', 10:'Fährt zur Basis', 11:'Laden unterbrochen', 12:'Fährt zum Waschen', 13:'Geladen', 14:'⚠️ Legt NEUE KARTE an', 15:'Update läuft', 16:'Schläft', 17:'⚠️ Sucht seine Position', 18:'Station arbeitet', 19:'⚠️ Kartierung pausiert', 20:'Laden abgebrochen', 21:'Waschen unterbrochen', 22:'Gerät wird verbunden', 23:'Fährt zum Absaugen', }; /* Auftrags-Status (Property 17.9, clean-type-status). Das ist die verlässliche * Anzeige, ob noch ein Auftrag offen ist — NICHT der grobe Status 2.2: * der zeigt "Lädt", während im Hintergrund ein Auftrag pausiert. */ const CLEAN_TYPE = { 0:'kein Auftrag', 1:'bereit', 2:'Komplettreinigung', 3:'Komplett pausiert', 4:'Komplett fortgesetzt', 5:'Schnellkartierung', 6:'Kartierung pausiert', 7:'Kartierung fortgesetzt', 8:'Raumreinigung', 9:'Raum pausiert', 10:'Raum fortgesetzt', 11:'Zonenreinigung', 12:'Zone pausiert', 13:'Zone fortgesetzt', 14:'Teppichreinigung', 15:'Teppich pausiert', 16:'Teppich fortgesetzt', 17:'Punktreinigung', 18:'Punkt pausiert', }; /* Nur bei diesen beiden Werten ist das Gerät wirklich frei für einen neuen * Auftrag. Alles andere lehnt es ab — das ist die Abfrage, die die App als * "Der Auftrag muss erst abgebrochen werden" anzeigt. */ const CT_FREE = [0, 1]; /* Auftrags-Status, bei denen wirklich gereinigt wird (nicht pausiert). * Daran haengt die Anzeige der Alexa-Schalter — siehe poll(). */ const CT_RUNNING = [2, 4, 5, 7, 8, 10, 11, 13, 14, 16, 17]; /* Fehlercodes (17.35 / 17.79), Loesungswege und Meldungen (17.36). * --------------------------------------------------------------------------- * HERKUNFT (04.09.2026): Aus dem Mi-Home-GERAETEPLUGIN des T5 gezogen, das * ueber home.miot-spec.com frei herunterladbar ist (Bundle * signed_10068_1027179_3_ANDROID). Die Texte stehen dort im React-Native- * Bundle als Tabellen [code, {title, solution}] bzw. [code, keyword], die * Klartexte in elf Sprachen -- Deutsch inbegriffen. * * Damit ist der Eintrag "die Tabellen muessen empirisch wachsen" ueberholt. * Die MIoT-Spec liefert tatsaechlich keine Wertelisten, aber die App muss die * Texte ja kennen -- und ihr Plugin ist frei herunterladbar. * * ⚠️ Beim Firmware-/Plugin-Update kann die Tabelle wachsen. Ein Code, der * hier fehlt, landet im Journal als UNBEKANNT -- dann neues Plugin ziehen. */ // Geraetefehler (17.35) const ERROR = { 0: 'kein Fehler', 4001: 'Staubbehälter nicht installiert.', 4003: 'Walzenbürste verheddert.', 4004: 'Seitenbürste verheddert.', 4005: 'Antriebsrad blockiert.', 4006: 'Abwurfsensor hat Staub angesammelt.', 4007: 'Lasersensor verstopft.', 4008: 'Radar des Lasersensors blockiert.', 4009: 'Antriebsrad rutscht durch.', 4011: 'Abdeckung des Lasersensors klemmt.', 4012: 'Roboter steckt fest.', 4013: 'Der Kollisionssensor ist blockiert.', 4014: 'Der Roboter hängt.', 4016: 'Roboter gekippt.', 4017: 'Start in Sperrzone nicht möglich.', 4018: 'Start auf Teppich nicht möglich.', 4020: 'Der Wischmopphalter hat sich verheddert.', 4021: 'Wischmopphalter nicht installiert.', }; // Stationsfehler (17.79) -- eigener Wertebereich ab 4500 const STATION_ERROR = { 0: 'kein Fehler', 4501: 'Der Staubbeutel ist nicht installiert.', 4502: 'Nicht genügend Wasser oder kein Schmutzwassertank installiert.', 4503: 'Das Schmutzwasser ist voll oder ein Schmutzwassertank ist nicht installiert.', 4506: 'Der Reinigungsschlitz des Mopps ist voll oder nicht installiert.', 4507: 'Der Reinigungsschlitz des Mopps ist voll oder nicht installiert.', 4901: 'Wasser kann nicht automatisch nachgefüllt werden.', 4902: 'Sie können das Wasser nicht automatisch ablassen.', 4903: 'Unzureichende Reinigungslösung.', }; /* Was der Nutzer tun soll. Geht bei Stoerungen mit ins Telegram -- eine * Fehlernummer allein hilft niemandem, der vor dem Geraet steht. */ const FIX = { 4001: 'Öffnen Sie die Roboterabdeckung und setzen Sie den Staubbehälter ein.', 4003: 'Nehmen Sie die Abdeckung der Walzenbürste ab, nehmen Sie die Walzenbürste heraus und entfernen Sie verhedderte Haare und andere Verunreinigungen.', 4004: 'Nehmen Sie die Seitenbürste ab und entfernen Sie die verhedderten Haare und andere Verschmutzungen.', 4005: 'Drehen Sie das Antriebsrad und befreien Sie es von verhedderten Haaren oder anderen Verunreinigungen.', 4006: 'Wischen Sie den Tropfensensor mit einem trockenen Tuch ab und stellen Sie den Roboter in eine sichere Position, um ihn neu zu starten.', 4007: 'Entfernen Sie die Gegenstände, die den Lasersensor behindern.', 4008: 'Entfernen Sie den Schmutz, der den rotierenden Radar des Lasersensors blockiert, und klopfen Sie vorsichtig auf die Sensorabdeckung.', 4009: 'Wischen Sie das Antriebsrad ab und bringen Sie den Roboter in eine andere Position, um ihn neu zu starten.', 4011: 'Bitte entfernen Sie Objekte, die die Abdeckung des Lasersensors blockieren, oder bringen Sie den Roboter in eine andere Position, um ihn neu zu starten.', 4012: 'Bitte entfernen Sie die Hindernisse um den Roboter herum, oder begeben Sie sich in einen anderen Bereich, um neu zu starten.', 4013: 'Bitte klopfen Sie leicht auf die vordere Stoßstange des Roboters und vergewissern Sie sich, dass die Stoßstange zurückfedert.', 4014: 'Starten Sie den Roboter neu, nachdem Sie ihn wieder auf den Boden gestellt haben.', 4016: 'Starten Sie den Roboter neu, nachdem Sie ihn auf eine ebene Fläche gestellt haben.', 4017: 'Starten Sie den Roboter neu, nachdem Sie ihn aus dem Sperrgebiet entfernt haben, oder löschen Sie das aktuelle Sperrgebiet.', 4018: 'Starten Sie den Roboter neu, nachdem Sie ihn aus dem Teppichbereich entfernt haben, oder ändern Sie die aktuelle Teppichreinigungsstrategie.', 4020: 'Nehmen Sie den Wischmopphalter ab und entfernen Sie verhedderte Haare und andere Verunreinigungen.', 4021: 'Bringen Sie beide Mop-Pad-Halterungen richtig an.', 4501: 'Setzen Sie den Staubbeutel richtig ein.', 4502: 'Nehmen Sie den Frischwassertank heraus, füllen Sie eine ausreichende Menge Wasser ein, setzen Sie den Frischwassertank wieder ein und vergewissern Sie sich, dass er ordnungsgemäß installiert ist.', 4503: 'Nehmen Sie den Schmutzwassertank heraus, leeren Sie das gesamte Schmutzwasser, setzen Sie den Schmutzwassertank wieder ein und vergewissern Sie sich, dass er ordnungsgemäß installiert ist.', 4506: '1. Entfernen Sie den Filter aus dem Mopp-Reinigungsschlitz, reinigen Sie die angesammelten Haare und Flecken auf dem Filter und installieren Sie ihn anschließend wieder; 2. Überprüfen Sie, ob Objekte den Ansauganschluss…', 4507: '1. Entnehmen Sie den Filter aus dem Mop-Reinigungsschlitz, reinigen Sie alle Haare und Flecken, die sich auf dem Filter angesammelt haben, und installieren Sie ihn wieder; 2. Überprüfen Sie, ob Objekte den Ansauganschlu…', 4901: '1. Prüfen Sie, ob der Wasserdruck im Haushalt normal ist; 2. Überprüfen Sie, ob die Wasserzuleitung richtig installiert ist.', 4902: '1. Prüfen Sie, ob das Abflussrohr richtig installiert ist; 2. Öffnen Sie die Abdeckung des Schmutzwassertanks, nehmen Sie den Filter heraus, reinigen Sie ihn und setzen Sie ihn wieder ein.', 4903: 'Bitte geben Sie eine ausreichende Menge Reinigungslösung in das Modul für die automatische Wasserzufuhr und den Abfluss.', }; /* Meldungen aus 17.36 (message-notification). KEINE Fehler: Der Kanal traegt * auch reine Betriebsmeldungen (1008 "Reinigung abgeschlossen") und * Wartungserinnerungen (2001-2008). Loest deshalb bewusst keinen Alarm aus. */ const MSG = { 1001: 'Ladevorgang abgeschlossen, Wiederaufnahme der Reinigung ab dem Unterbrechungspunkt', 1002: 'Niedriger Akkustand, zurück zum Laden', 1003: 'Firmware-Upgrade fehlgeschlagen, bitte erneut versuchen', 1004: 'Firmware-Upgrade erfolgreich, probieren Sie es aus!', 1005: 'Neue Firmware-Version verfügbar, aktualisieren Sie jetzt!', 1006: 'Batterie ist schwach, schaltet sich bald ab, bitte laden Sie rechtzeitig', 1007: 'Hauptgerät wird bald abgeschaltet, bitte bringen Sie es rechtzeitig zur Station zurück', 1008: 'Reinigung abgeschlossen, zurück in die Station', 1009: 'Bitte reinigen Sie den Schmutzwassertank rechtzeitig, um Geruch zu vermeiden', 1010: 'Geplante Reinigung gestartet', 1011: 'Aufgabenkonflikt, geplante Reinigung kann nicht ausgeführt werden', 1012: 'Die geplante Reinigungsaufgabe kann nicht ausgeführt werden, da sie sich im Nicht-Stören-Zeitraum befindet.', 1013: 'Niedriger Batteriestand, die geplante Reinigung kann nicht ausgeführt werden', 1014: 'Ausgewählter Bereich nicht gefunden, Reinigung für diese Zeit beendet', 1015: 'Verloren, bitte bringen Sie den Roboter zurück in die Station', 2001: 'Bitte tauschen Sie die Seitenbürste regelmäßig aus', 2002: 'Bitte ersetzen Sie die Walzenbürste regelmäßig', 2003: 'Bitte ersetzen Sie regelmäßig das Filterelement des Staubbehälters', 2004: 'Bitte ersetzen Sie die Wischmopps regelmäßig', 2005: 'Bitte ersetzen Sie den Staubbeutel regelmäßig', 2006: 'Bitte reinigen Sie den Reinigungsschlitz des Mopps regelmäßig.', 2007: 'Bitte reinigen Sie den Filter des Schmutzwassertanks regelmäßig.', 2008: 'Bitte wischen Sie den Sensor der Haupteinheit regelmäßig ab.', }; /* Die vier Bauteil-Zustaende 17.51 bis 17.54. Die Spec liefert zwar die * Wertelisten (Normal / Uninstalled / Unusable), aber KEINE Beschreibung, * welches Bauteil jeweils gemeint ist. Deshalb heissen sie hier neutral. * Klaeren laesst sich das durch Ausprobieren: Mopp abnehmen, Staubbox * herausziehen -- und im Journal nachsehen, welcher Buchstabe springt. */ const PARTS = { '17.51': 'Bauteil A', '17.52': 'Bauteil B', '17.53': 'Bauteil C', '17.54': 'Bauteil D' }; const PART_STATE = { 0: 'normal', 1: 'nicht eingesetzt', 2: 'unbrauchbar', 3: 'Zustand 3' }; /* Wer hat geschaltet? ioBroker liefert die Quelle bei jedem State-Change mit. * Das ist die einzige Stelle, an der wir Alexa von App und Oberflaeche * unterscheiden koennen -- Befehle aus der Hersteller-App gehen ueber die * Cloud direkt ans Geraet und tauchen hier NIE auf (siehe Kopfkommentar). */ const QUELLE = { 'system.adapter.iot.0': 'Alexa (iot)', 'system.adapter.javascript.0': 'Skript', 'system.adapter.admin.0': 'Admin-Oberflaeche', 'system.adapter.web.0': 'VIS/Web', 'system.adapter.alexa2.0': 'alexa2', }; const quelleText = f => QUELLE[f] || (f || 'unbekannt'); // Sauger arbeitet an einem Reinigungsauftrag -> Schalter "T5" steht auf true const RUNNING = [2, 4, 5, 7]; const PAUSED = [6]; /* Sauger ist in Bewegung -> schnelleres Polling. Weiter gefasst als RUNNING: * auch Heimfahrt, Kartierung und Positionssuche sollen zügig nachgeführt werden, * ohne die Alexa-Schalter anzufassen. * * ⚠️ Status 18 (Station arbeitet) gehoert BEWUSST NICHT hierher, obwohl er es * bis zum 30.08. tat. Die Stationsarbeit ist die mit Abstand laengste Phase * (Trocknung bis zu zwei Stunden), und dabei passiert minutenlang nichts. Alle * 15 s zu pollen bringt keine Erkenntnis, belegt aber staendig den Funk — und * genau dort ist am 30.08. ein Alexa-Raumbefehl verlorengegangen. */ const ACTIVE = [2, 3, 4, 5, 6, 7, 10, 12, 14, 17, 19, 23]; // ---------------------------------------------------------------- Journal /* Schreibt eine Klartextzeile in die Monatsdatei. Beantwortet die drei Fragen, * die das ioBroker-Log NICHT beantwortet: * BEFEHL -- wer hat geschaltet (Alexa, Oberflaeche, Skript)? * FEHLER -- welcher Code lag an, und wie lange? * STATUS -- was hat das Geraet daraus gemacht? * * Das Journal darf die Steuerung unter keinen Umstaenden aufhalten: schlaegt * das Schreiben fehl (Rechte, Platte voll), wird das geschluckt. Ein stummes * Journal ist aergerlich, ein blockierter Saugbefehl waere schlimmer. */ function journal(kat, text) { try { const d = new Date(); const p = n => String(n).padStart(2, '0'); const monat = d.getFullYear() + '-' + p(d.getMonth() + 1); const zeit = monat + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds()); fs.appendFileSync(JOURNAL_DIR + '/t5-journal-' + monat + '.log', zeit + ' | ' + kat.padEnd(7) + ' | ' + text + '\n'); } catch (e) { /* bewusst stumm -- siehe Kommentar oben */ } } /* Merker fuer die Aenderungserkennung. Ohne den stuende bei 60-Sekunden-Takt * jede Minute derselbe Status im Journal. */ const last = {}; /** true, wenn sich der Wert seit dem letzten Poll geaendert hat. */ function geaendert(k, v) { const neu = !(k in last) || last[k] !== v; last[k] = v; return neu; } /* Push nur bei echten Stoerungen, nicht bei jedem Statuswechsel -- sonst ist * der Kanal nach einem einzigen Reinigungslauf zugemuellt. Bewusst NICHT * gemeldet wird Status 17 ("sucht seine Position"): der tritt auch im * Normalbetrieb nach jedem Start auf. Er steht im Journal, das reicht. */ async function meldung(text) { try { await sendToAsync('telegram.0', 'send', { text: '🤖 T5: ' + text }); } catch (e) { log('Telegram nicht erreichbar: ' + e.message, 'warn'); } } // ---------------------------------------------------------------- miIO-Protokoll const key = crypto.createHash('md5').update(TOKEN).digest(); const iv = crypto.createHash('md5').update(Buffer.concat([key, TOKEN])).digest(); const encrypt = d => { const c = crypto.createCipheriv('aes-128-cbc', key, iv); return Buffer.concat([c.update(d, 'utf8'), c.final()]); }; const decrypt = b => { const d = crypto.createDecipheriv('aes-128-cbc', key, iv); return Buffer.concat([d.update(b), d.final()]).toString('utf8').replace(/\0+$/, ''); }; let sock = null; let devId = null, stamp = 0, stampAt = 0, msgId = 100; let pending = null; let stopping = false; function openSocket() { sock = dgram.createSocket('udp4'); sock.on('message', m => { if (pending) { const p = pending; pending = null; p(m); } }); sock.on('error', e => log('Socket-Fehler: ' + e.message, 'warn')); } function tx(buf, timeout) { return new Promise((res, rej) => { if (!sock || stopping) return rej(new Error('Socket zu')); const t = setTimeout(() => { pending = null; rej(new Error('timeout')); }, timeout); pending = m => { clearTimeout(t); res(m); }; sock.send(buf, 0, buf.length, PORT, IP, e => { if (e) { clearTimeout(t); pending = null; rej(e); } }); }); } async function retry(fn, n = 4) { let last; for (let i = 0; i < n && !stopping; i++) { try { return await fn(); } catch (e) { last = e; } } throw last || new Error('abgebrochen'); } function helloPacket() { const b = Buffer.alloc(32, 0xff); b.writeUInt16BE(0x2131, 0); b.writeUInt16BE(32, 2); return b; } /* Der Zeitstempel des Geräts muss mitgezählt werden. Nach ein paar Minuten * Funkstille lieber neu synchronisieren, sonst verwirft das Gerät die Pakete. */ async function handshake() { const r = await retry(() => tx(helloPacket(), 4000)); devId = Buffer.from(r.subarray(8, 12)); stamp = r.readUInt32BE(12); stampAt = Date.now(); } async function ensureHandshake() { if (!devId || Date.now() - stampAt > 120000) await handshake(); } function packet(obj) { const enc = encrypt(JSON.stringify(obj)); const h = Buffer.alloc(16); h.writeUInt16BE(0x2131, 0); h.writeUInt16BE(32 + enc.length, 2); h.writeUInt32BE(0, 4); devId.copy(h, 8); h.writeUInt32BE(stamp + Math.floor((Date.now() - stampAt) / 1000), 12); const sum = crypto.createHash('md5').update(Buffer.concat([h, TOKEN, enc])).digest(); return Buffer.concat([h, sum, enc]); } async function call(method, params, timeout = 8000) { await ensureHandshake(); const r = await retry(() => tx(packet({ id: msgId++, method, params }), timeout)); return JSON.parse(decrypt(r.subarray(32))); } /** Properties lesen. Rückgabe: { "siid.piid": value } — fehlende bleiben weg. */ async function getProps(list) { const out = {}; for (let i = 0; i < list.length; i += 5) { // Batchgröße 5 const batch = list.slice(i, i + 5); try { const r = await call('get_properties', batch.map(p => ({ did: DID, siid: p.siid, piid: p.piid }))); for (const v of (r.result || [])) { if (v.code === 0) out[v.siid + '.' + v.piid] = v.value; // s. Fallstrick 2 } } catch (e) { log('get_properties fehlgeschlagen: ' + e.message, 'warn'); } } return out; } /** Aktion auslösen. `params` sind die in-Werte in der Reihenfolge der Spec. */ async function action(a, params = []) { const r = await call('action', { did: DID, siid: a.siid, aiid: a.aiid, in: params }); const code = r && r.result ? r.result.code : (r && r.error ? r.error.code : -1); if (code !== 0) throw new Error('Action ' + a.siid + '.' + a.aiid + ' -> code ' + code); return r; } // ---------------------------------------------------------------- States anlegen /* createStateAsync statt setObjectNotExistsAsync: letzteres ist Adapter- * Entwickler-API und in der javascript-Sandbox nicht verfügbar. * forceCreation=false lässt vorhandene States unangetastet. */ async function def(id, common, initial, historisch) { const full = BASE + '.' + id; const c = Object.assign({ read: true, write: false, role: 'state' }, common); await createStateAsync(full, initial === undefined ? null : initial, false, c); /* Aufzeichnung im history-Adapter. Bewusst NUR fuer die States, aus denen * sich ein Vorfall rekonstruieren laesst -- nicht fuer lastUpdate (aendert * sich jede Minute) und nicht fuer die Modus-States. changesOnly, damit * Ruhezeiten nichts kosten. */ if (historisch) { try { await extendObjectAsync(full, { common: { custom: { [HISTORY]: { enabled: true, changesOnly: true, debounce: 0, retention: 31536000, // ein Jahr in Sekunden maxLength: 0, changesRelogInterval: 0, aliasId: '', } } } }); } catch (e) { log('History fuer ' + id + ' nicht aktivierbar: ' + e.message, 'warn'); } } /* createStateAsync laesst ein vorhandenes Objekt in Ruhe. Damit spaetere * Aenderungen am common (z. B. ein neuer smartName) auch bei bestehenden * States ankommen, wird es hier nachgezogen. */ try { await extendObjectAsync(full, { common: c }); } catch (e) { log('common von ' + id + ' nicht aktualisierbar: ' + e.message, 'warn'); } } async function createStates() { // --- Anzeige await def('info.status', { name: 'Status (Code)', type: 'number', role: 'value' }, undefined, true); await def('info.statusText', { name: 'Status', type: 'string', role: 'text' }, undefined, true); await def('info.battery', { name: 'Akku', type: 'number', role: 'value.battery', unit: '%' }, undefined, true); await def('info.error', { name: 'Fehlercode', type: 'number', role: 'value' }, undefined, true); await def('info.errorText', { name: 'Fehler', type: 'string', role: 'text' }, undefined, true); await def('info.cleanMode', { name: 'Reinigungsart', type: 'number', role: 'value' }); await def('info.fanMode', { name: 'Saugstufe', type: 'number', role: 'value' }); await def('info.waterMode', { name: 'Wassermenge', type: 'number', role: 'value' }); await def('info.cleanType', { name: 'Auftrags-Status (Code)', type: 'number', role: 'value' }, undefined, true); await def('info.cleanTypeText', { name: 'Auftrags-Status', type: 'string', role: 'text' }, undefined, true); await def('info.saveMap', { name: 'Karte speichern aktiv', type: 'boolean', role: 'indicator' }); await def('info.online', { name: 'erreichbar', type: 'boolean', role: 'indicator.reachable' }, undefined, true); await def('info.lastUpdate', { name: 'letzte Abfrage', type: 'string', role: 'text' }); /* Neu am 03.09.2026: Die Station meldet ihre Stoerungen in einem EIGENEN * Fehlercode (17.79, "station-error"). Der wurde bis dahin gar nicht * abgefragt -- ein blinder Fleck genau dort, wo der Sauger die meiste Zeit * verbringt (Waschen, Trocknen, Absaugen). */ await def('info.stationError', { name: 'Stationsfehler (Code)', type: 'number', role: 'value' }, undefined, true); await def('info.stationErrorText', { name: 'Stationsfehler', type: 'string', role: 'text' }, undefined, true); /* 17.49 liefert die Stationsphase als KLARTEXT-String ("ChargeAsleep", * "HotDry", ...). Das ist die einzige Property des Geräts, die von sich * aus lesbar ist statt kodiert — und sie sagt genau das, was der grobe * Status 18 ("Station arbeitet") verschweigt: WAS die Station tut. */ await def('info.stationPhase', { name: 'Stationsphase', type: 'string', role: 'text' }, undefined, true); /* 17.36 ist laut MIoT-Spec "Message Notification" (uint16, Read/Notify) — * NICHT der zweite Fehlerkanal, als der er hier anfangs mitgefuehrt wurde. * Am 04.09.2026 in der Spec nachgelesen; der Name stand die ganze Zeit da. * Er fuehrt Codes im 1000er-Bereich im ganz normalen Betrieb. * * Beobachtet am 04.09.2026: * 1009 waehrend HotDry, wenn ein Moppwasch-Zyklus vorausging (2x). * Bleibt ueber das Ende von HotDry hinaus stehen und faellt erst * beim naechsten Auftrag auf 0. * 1008 beim Auftragsende auf der Rueckfahrt — nur 15 Sekunden lang. * * Der Spec-Name bestaetigt, was die kurze Standzeit von 1008 nahelegte: ein * EREIGNIS-Kanal, kein Zustandsmerker. 17.36 ist damit genau das, was ueber * 17.63 push-code nicht zu holen war (dort liefert das Geraet keinen Wert) * — nur eben lesbar. Die 17.63-Sackgasse ist damit umgangen. * * Einschraenkung (04.09.): Die App zeigt zu 1008 nichts an. Der * Kanal traegt also auch Meldungen, die dem Nutzer nie gezeigt werden. * * Konsequenz fuers Journal: Bekannte Codes werden im Klartext ausgewiesen, * UNBEKANNTE weiterhin markiert — die Tabelle waechst empirisch, genau wie * bei den Fehlercodes. Alarm loest 17.36 bewusst nicht aus. * * ⚠️ Grenze: Bei 15 Sekunden Standzeit kann ein Poll den Wert verpassen. * Das Journal zeigt also nicht garantiert jeden Code. Schnelleres Polling * waere der Preis — nicht wert, solange die Bedeutung unklar ist. */ await def('info.error2', { name: 'Meldungscode 17.36 (message-notification)', type: 'number', role: 'value' }, undefined, true); /* 17.34 traegt die Raumliste des laufenden Auftrags (z. B. "[4]"). Bis zum * 04.09.2026 wurde sie nur GESCHRIEBEN, nie gelesen — dabei ist sie der * direkte Indikator dafuer, ob ein Auftrag seinen Raumbezug behaelt. * * Warum das zaehlt: 2.1 START_SWEEP ist kontextabhaengig (siehe Pause- * Handler). Aus einem lebenden Auftrag heraus setzt es sauber fort, aus dem * Leerlauf legt es eine NEUE KARTE an. Die Power-Taste am Geraet duerfte * dasselbe ausloesen. Offene Frage: Wenn der Sauger sich festfaehrt, man ihn * befreit und die Taste drueckt — lebt der Raumauftrag dann noch? * Mit dieser Aufzeichnung beantwortet der naechste ECHTE Vorfall das von * selbst, ohne dass man ihn dafuer absichtlich stecken lassen muesste. */ await def('info.roomList', { name: 'Raumliste des Auftrags (17.34)', type: 'string', role: 'text' }, undefined, true); /* Bauteil-Zustaende. Ob ein Mopp fehlt oder die Staubbox nicht sitzt, ist * eine der wahrscheinlichsten Ursachen fuer App-Meldungen -- und war * bisher unsichtbar. Welcher Buchstabe welches Teil ist, klaert das * Journal (siehe PARTS). */ for (const t of Object.values(PARTS)) { await def('info.teile.' + t.replace(/ /g, '_'), { name: t + ' (Zustand)', type: 'string', role: 'text' }, undefined, true); } // --- Alexa-Schalter. Echte Schalter (read+write), nicht Buttons: // dadurch wirkt auch "aus", und die Routine braucht kein Zuruecksetzen. await def('T5', { name: 'T5 (ganzes Haus, saugen und wischen)', type: 'boolean', role: 'switch', write: true, smartName: { de: 'T5', smartType: 'SWITCH' }, }, false, true); await def('T5_zum_Wischen', { name: 'T5 zum Wischen (nur wischen)', type: 'boolean', role: 'switch', write: true, smartName: { de: 'T5 zum Wischen', smartType: 'SWITCH' }, }, false, true); await def('T5_Pause', { name: 'T5 Pause', type: 'boolean', role: 'switch', write: true, smartName: { de: 'T5 Pause', smartType: 'SWITCH' }, }, false, true); // --- Räume. Bewusst OHNE smartName: erst Grundfunktion testen, dann // einzeln fuer Alexa freigeben (smartName im Objekt ergaenzen). for (const [name, r] of Object.entries(ROOMS)) { await def('raum.' + name, { name: name + ' saugen (Raum-ID ' + r.id + ')', type: 'boolean', role: 'switch', write: true, smartName: { de: r.alexa, smartType: 'SWITCH' }, }, false, true); } } // ---------------------------------------------------------------- Ablaufsteuerung let pollTimer = null; let busy = false; // verhindert überlappende Funkdialoge /* Kurze Pause zwischen Funkbefehlen. Ohne sie verschluckt das Geraet den * naechsten Aufruf -- vier miIO-Dialoge in Folge sind ihm zu viel. */ const pause = ms => new Promise(r => setTimeout(r, ms)); /* Vor JEDEM neuen Auftrag: das Gerät freiräumen. * --------------------------------------------------------------------------- * Das Gerät nimmt keinen zweiten Auftrag an, solange der erste läuft oder * pausiert — es antwortet dann zwar mit code 0, tut aber nichts. Im Alltag * heisst das: "T5 Bad" gesagt, Arbeitszimmer gemeint, sofort korrigiert — und * der zweite Befehl verpufft. Die App fragt an dieser Stelle "Der Auftrag muss * erst abgebrochen werden"; genau diesen Abbruch holen wir hier selbst nach. * * Geprüft wird 17.9 (clean-type-status), nicht der grobe Status 2.2 — der zeigt * "Lädt", während im Hintergrund noch ein Auftrag pausiert. * * Abgebrochen wird mit 17.3 stop-clean (NICHT 2.2 — das hält nur die Fahrt an * und lässt den Auftrag pausiert stehen, siehe stopAndHome()). * * Antwortet das Gerät gar nicht, wird NICHT blind abgebrochen: dann lieber den * Startbefehl versuchen, als einen laufenden Auftrag auf Verdacht killen. */ async function ensureIdle(maxTries = 3) { for (let i = 0; i < maxTries; i++) { const p = await getProps([{ siid: 17, piid: 9 }]); const ct = p['17.9']; if (ct === undefined) { // keine Antwort -> nicht raten log('T5: Auftrags-Status nicht lesbar, starte trotzdem', 'warn'); return false; } if (CT_FREE.includes(ct)) return true; // frei log('T5: "' + (CLEAN_TYPE[ct] || ct) + '" ist noch offen -> wird abgebrochen', 'info'); await action(ACT.STOP_CLEAN); await pause(2500); // dem Gerät Zeit zum Aufräumen } log('T5: Auftrag liess sich nicht abbrechen, starte trotzdem', 'warn'); return false; } /** Vor jedem Start: Saugstufe, Wassermenge und Reinigungsart setzen. */ async function applyDefaults(mode) { await action(ACT.SET_FAN, [{ piid: 13, value: FAN_MAX }]); await pause(1500); await action(ACT.SET_WATER, [{ piid: 12, value: WATER_MID }]); await pause(1500); await action(ACT.SET_MODE, [{ piid: 10, value: mode }]); await pause(1500); } /* Ganzes Haus ueber 17.1 start-clean mit piid 8 = FullClean und piid 34 = "[]". * --------------------------------------------------------------------------- * 🔴 HIER LAG DER KARTEN-FEHLER (gefunden 30.08.2026). * * Bis zum 30.08. lief das ueber die parameterlose Action 2.1 (Start Sweep) — * und die startet einen Lauf OHNE Kartenbezug: Das Geraet faengt an zu fahren * und zeichnet dabei eine NEUE Karte. Alle Raum-IDs werden damit hinfaellig. * Jeder Start ueber Alexa hat so die Karte zerlegt, waehrend ein Start aus der * App sauber lief. * * Der Grund fuer den Umweg war ein Trugschluss vom 24.08.: 17.1 wurde damals * mit einem LEEREN STRING an piid 34 versucht und mit code -9999 abgelehnt. * Daraus wurde geschlossen, 17.1 tauge nicht fuers ganze Haus. Tatsaechlich * will das Geraet an piid 34 immer einen ARRAY-STRING, auch wenn er leer ist: * "[]" statt "". Genau derselbe Fallstrick wie bei der Raumreinigung, wo "6" * scheiterte und erst "[6]" funktionierte. * * Am 30.08. live verifiziert: 17.1 mit "[]" -> code 0, faehrt drei Sekunden * spaeter los, Auftrag "Komplettreinigung", KEINE Neukartierung. * Genau das schickt auch die App. */ const ROOMS_ALL = '[]'; // leer = ganzes Haus, aber als Array-String! async function startWholeHome(mode) { await ensureIdle(); // erst freiräumen, dann starten await applyDefaults(mode); await action(ACT.START_CLEAN, [{ piid: 8, value: CLEAN_FULL }, { piid: 34, value: ROOMS_ALL }]); } /* Raumreinigung. piid 34 traegt die Raumliste als Array-String ("[6]") -- * exakt das Format, das sich das Geraet bei einem Zeitplan-Lauf selbst setzt * (am 24.08. in 17.34 abgelesen: "[7,6]"). * * BEWUSST OHNE applyDefaults: Der Raumbefehl scheiterte reproduzierbar, wenn * kurz davor die drei Setzbefehle liefen -- als Einzelaufruf funktioniert er. * Die Modi stehen ohnehin noch vom letzten Start. */ async function startRooms(ids) { await ensureIdle(); // erst freiräumen, dann starten await pause(1500); await action(ACT.START_CLEAN, [{ piid: 8, value: CLEAN_AREA }, { piid: 34, value: JSON.stringify(ids) }]); } /* Stoppen heisst: Auftrag BEENDEN, nicht nur anhalten. * * Action 2.1/2.2 haelt lediglich die Fahrt an -- der Auftrag bleibt danach im * Zustand "Area Pause" stehen (17.9 = 9) und 17.34 traegt weiter die Raumliste. * Folgen, alle am 24.08. beobachtet: * - die App verweigert einen Moduswechsel ("Auftrag erst abbrechen") * - der Sauger nimmt die Reinigung spaeter von selbst wieder auf, etwa nachdem * in der Station die Mopps gewaschen wurden * - es laeuft keine Nacharbeit, weil der Auftrag nie abgeschlossen wurde * * 17.3 stop-clean beendet ihn richtig: 17.9 geht auf Idle, 17.34 wird leer. */ async function stopAndHome() { await action(ACT.STOP_CLEAN); await pause(1500); await action(ACT.CHARGE); } // ---------------------------------------------------------------- Chronik /* Schreibt jede ZUSTANDSAENDERUNG des Geraets ins Journal. * --------------------------------------------------------------------------- * Das ist der Teil, der auch Befehle sichtbar macht, die gar nicht ueber den * ioBroker laufen: Tasten am Geraet und alles aus der Hersteller-App gehen * ueber die Cloud direkt an den Sauger, wir sehen sie nie als Befehl -- aber * ihre WIRKUNG steht hier. Genau das hat am 02.09.2026 gefehlt. * * Telegram gibt es nur fuer echte Stoerungen (siehe meldung()). */ async function chronik(p, st) { if (geaendert('status', st)) { journal('STATUS', (STATUS[st] || 'unbekannt') + ' (Code ' + st + ')'); if (st === 8) await meldung('Fehlerzustand — ' + (await textVon('errorText')) + fixText(letzterFehler)); } if (p['17.9'] !== undefined && geaendert('cleanType', p['17.9'])) { journal('AUFTRAG', (CLEAN_TYPE[p['17.9']] || 'unbekannt') + ' (Code ' + p['17.9'] + ')' + (raeumeText(p['17.34']) ? ' — Raeume: ' + raeumeText(p['17.34']) : '')); } /* Die Raumliste bekommt eine EIGENE Aenderungserkennung, nicht nur den * Anhang oben: Sie kann sich leeren, waehrend 17.9 unveraendert stehen * bleibt — und genau dieser Fall ist die offene Frage (Auftrag verliert * seinen Raumbezug, ohne dass der Auftragsstatus es verraet). */ if (p['17.34'] !== undefined && geaendert('roomList', String(p['17.34']))) { journal('AUFTRAG', 'Raumliste: ' + (raeumeText(p['17.34']) || 'LEER')); } if (p['17.35'] !== undefined && geaendert('error', p['17.35'])) { const ec = p['17.35']; const txt = ERROR[ec] !== undefined ? ERROR[ec] : 'UNBEKANNT'; letzterFehler = p['17.35']; journal('FEHLER', 'Geraet: ' + txt + ' (Code ' + ec + ')' + (ERROR[ec] === undefined ? ' <-- Bedeutung nachtragen!' : '')); if (ec !== 0) await meldung('Fehler ' + ec + ' (' + txt + ') — Status: ' + (STATUS[st] || st) + fixText(ec)); } if (p['17.79'] !== undefined && geaendert('stationError', p['17.79'])) { const ec = p['17.79']; const txt = STATION_ERROR[ec] !== undefined ? STATION_ERROR[ec] : 'UNBEKANNT'; journal('FEHLER', 'Station: ' + txt + ' (Code ' + ec + ')' + (STATION_ERROR[ec] === undefined ? ' <-- Bedeutung nachtragen!' : '')); if (ec !== 0) await meldung('Stationsfehler ' + ec + ' (' + txt + ')' + fixText(ec)); } if (p['17.49'] !== undefined && geaendert('stationPhase', p['17.49'])) { journal('STATION', String(p['17.49'])); } if (p['17.36'] !== undefined && geaendert('error2', p['17.36'])) { journal('CODE', 'Slot 17.36: ' + p['17.36'] + (p['17.36'] === 0 ? ' (aus)' : MSG[p['17.36']] ? ' — ' + MSG[p['17.36']] : ' <-- UNBEKANNT, Bedeutung nachtragen!')); } for (const [k, t] of Object.entries(PARTS)) { if (p[k] === undefined || !geaendert(k, p[k])) continue; journal('TEIL', t + ': ' + (PART_STATE[p[k]] !== undefined ? PART_STATE[p[k]] : 'unbekannt') + ' (Code ' + p[k] + ')'); } if (p['16.1'] !== undefined && geaendert('akku10', Math.floor(p['16.1'] / 10))) { journal('AKKU', p['16.1'] + ' %'); } } /** Kleiner Helfer fuer die Telegram-Texte. */ async function textVon(id) { try { const v = await getStateAsync(BASE + '.info.' + id); return v && v.val ? String(v.val) : '?'; } catch (e) { return '?'; } } // ---------------------------------------------------------------- Polling let pollNr = 0; // fuer den Langsamtakt der Bauteil-Abfrage async function poll() { if (busy || stopping) return; busy = true; try { /* Die Bauteil-Zustaende aendern sich fast nie -- sie jedes Mal * mitzufragen wuerde nur den Funk belegen (getProps macht daraus * einen weiteren Batch). Deshalb nur jeden zehnten Durchlauf. * Stand 04.09.: 12 Properties = 3 Batches; mit den vier Bauteilen * sind es 16 = 4 Batches. Der Aufschlag faellt also nur bei jedem * zehnten Poll an. */ const mitTeilen = (pollNr++ % 10) === 0; const liste = [ { siid: 2, piid: 2 }, // Status { siid: 16, piid: 1 }, // Akku { siid: 17, piid: 35 }, // Fehlercode { siid: 17, piid: 10 }, // clean-mode { siid: 17, piid: 13 }, // fan-mode { siid: 17, piid: 12 }, // water-mode { siid: 17, piid: 9 }, // clean-type-status: haengt ein Auftrag? { siid: 17, piid: 34 }, // Raumliste des Auftrags — lebt der Raumbezug? { siid: 17, piid: 39 }, // save-map: legt er Karten neu an? { siid: 17, piid: 79 }, // station-error: Stoerung der Basisstation { siid: 17, piid: 49 }, // Stationsphase im Klartext ("HotDry" u. a.) { siid: 17, piid: 36 }, // message-notification (Spec!), KEIN Fehlercode ]; if (mitTeilen) for (const k of Object.keys(PARTS)) { const [si, pi] = k.split('.').map(Number); liste.push({ siid: si, piid: pi }); } const p = await getProps(liste); const st = p['2.2']; if (st === undefined) { // gar nichts gekommen await setStateAsync(BASE + '.info.online', false, true); return; } await setStateAsync(BASE + '.info.online', true, true); await setStateAsync(BASE + '.info.status', st, true); await setStateAsync(BASE + '.info.statusText', STATUS[st] || ('unbekannt (' + st + ')'), true); if (p['16.1'] !== undefined) await setStateAsync(BASE + '.info.battery', p['16.1'], true); if (p['17.35'] !== undefined) { await setStateAsync(BASE + '.info.error', p['17.35'], true); await setStateAsync(BASE + '.info.errorText', ERROR[p['17.35']] !== undefined ? ERROR[p['17.35']] : ('UNBEKANNT (' + p['17.35'] + ')'), true); } if (p['17.79'] !== undefined) { await setStateAsync(BASE + '.info.stationError', p['17.79'], true); await setStateAsync(BASE + '.info.stationErrorText', STATION_ERROR[p['17.79']] !== undefined ? STATION_ERROR[p['17.79']] : ('UNBEKANNT (' + p['17.79'] + ')'), true); } if (p['17.49'] !== undefined) await setStateAsync(BASE + '.info.stationPhase', String(p['17.49']), true); if (p['17.36'] !== undefined) await setStateAsync(BASE + '.info.error2', p['17.36'], true); if (p['17.34'] !== undefined) await setStateAsync(BASE + '.info.roomList', String(p['17.34']), true); for (const [k, t] of Object.entries(PARTS)) { if (p[k] === undefined) continue; await setStateAsync(BASE + '.info.teile.' + t.replace(/ /g, '_'), PART_STATE[p[k]] !== undefined ? PART_STATE[p[k]] : ('unbekannt (' + p[k] + ')'), true); } if (p['17.10'] !== undefined) await setStateAsync(BASE + '.info.cleanMode', p['17.10'], true); if (p['17.13'] !== undefined) await setStateAsync(BASE + '.info.fanMode', p['17.13'], true); if (p['17.12'] !== undefined) await setStateAsync(BASE + '.info.waterMode', p['17.12'], true); /* Auftrags-Status sichtbar machen. Ohne den sieht man im ioBroker "Lädt", * während in Wahrheit noch ein Auftrag pausiert — genau das Rätsel vom * 24.08. (er fuhr später von selbst wieder los). */ if (p['17.9'] !== undefined) { await setStateAsync(BASE + '.info.cleanType', p['17.9'], true); await setStateAsync(BASE + '.info.cleanTypeText', CLEAN_TYPE[p['17.9']] || ('unbekannt (' + p['17.9'] + ')'), true); } /* Frühwarnung: Steht save-map auf false, legt das Gerät bei jedem Start * eine NEUE Karte an und alle Raum-IDs im Skript sind hinfällig. */ if (p['17.39'] !== undefined) { await setStateAsync(BASE + '.info.saveMap', !!p['17.39'], true); if (!p['17.39']) log('T5: ⚠️ "Karte speichern" ist AUS — Raum-IDs drohen ungültig zu werden!', 'warn'); } await setStateAsync(BASE + '.info.lastUpdate', formatDate(new Date(), 'TT.MM.JJJJ SS:mm:ss'), true); await chronik(p, st); /* Schalter nachfuehren: faehrt er von selbst in die Station oder wird er * in der App gestoppt, sollen die Alexa-Schalter das widerspiegeln — * sonst zeigt die App "an", obwohl er laengst laedt. */ /* Der Schalter haengt am AUFTRAG, nicht nur am groben Status. * Sonst springt er auf "aus", waehrend die Station vor dem Losfahren die * Mopps waescht (Status 18) — obwohl der Auftrag laengst angenommen ist. * In der Alexa-App sah das aus, als waere der Befehl verpufft. * Bewusst additiv: der Schalter geht dadurch nur zusaetzlich AN, * nie zusaetzlich aus. */ const auftragLaeuft = p['17.9'] !== undefined && CT_RUNNING.includes(p['17.9']); const running = RUNNING.includes(st) || auftragLaeuft; const paused = PAUSED.includes(st); const mopOnly = p['17.10'] === MODE_MOP; await syncSwitch('T5', running && !mopOnly); await syncSwitch('T5_zum_Wischen', running && mopOnly); await syncSwitch('T5_Pause', paused); if (!running) for (const n of Object.keys(ROOMS)) await syncSwitch('raum.' + n, false); /* Polling-Tempo an ACTIVE, nicht an RUNNING: auch Heimfahrt, Kartierung * und Positionssuche sollen zügig nachgeführt werden. */ reschedule(ACTIVE.includes(st) ? POLL_ACTIVE : POLL_IDLE); } catch (e) { log('Polling-Fehler: ' + e.message, 'warn'); await setStateAsync(BASE + '.info.online', false, true); } finally { busy = false; } } /** Schalter nur anfassen, wenn er wirklich falsch steht (ack=true, kein Trigger). */ /* Was der Nutzer tun soll, an die Telegram-Meldung angehaengt. Eine Nummer * allein hilft niemandem, der vor dem Geraet steht — der Text kommt aus dem * Mi-Home-Plugin und ist derselbe, den die App anzeigt. */ let letzterFehler = 0; function fixText(code) { return FIX[code] ? '\n\n\u2192 ' + FIX[code] : ''; } /* Raumliste lesbar machen: "[4]" -> "4 (Schlafzimmer)". Leere Liste -> ''. * Das Journal soll ohne Nachschlagen verstaendlich sein — die ID allein sagt * niemandem etwas, und die App zeigt ohnehin ID minus 2 an. */ function raeumeText(roh) { if (roh === undefined || roh === null) return ''; const t = String(roh).trim(); if (t === '' || t === '[]') return ''; let ids; try { ids = JSON.parse(t); } catch (e) { return t; } if (!Array.isArray(ids) || !ids.length) return ''; const namen = {}; for (const [n, r] of Object.entries(ROOMS)) namen[r.id] = n; return ids.map(i => i + (namen[i] ? ' (' + namen[i] + ')' : ' (unbekannt)')).join(', '); } async function syncSwitch(id, soll) { const cur = await getStateAsync(BASE + '.' + id); if (!cur || cur.val !== soll) await setStateAsync(BASE + '.' + id, soll, true); } function reschedule(ms) { if (pollTimer) clearTimeout(pollTimer); if (stopping) return; pollTimer = setTimeout(poll, ms); } // ---------------------------------------------------------------- Steuer-Handler /** Gemeinsame Hülle: Fehler abfangen, danach zeitnah nachpollen. */ async function handle(what, fn, quelle) { /* Wer den Befehl gegeben hat, ist die wichtigste Information ueberhaupt -- * und die einzige, die im ioBroker-Log bisher fehlte. Am 02.09.2026 blieb * genau deshalb ungeklaert, wo Sarahs "Alexa, Staubsauger aus" gelandet * ist: es gab schlicht keine Zeile dazu. */ journal('BEFEHL', what + ' [von: ' + quelleText(quelle) + ']'); /* Auf freien Funk WARTEN statt den Befehl wegzuwerfen. * ------------------------------------------------------------------------ * 🔴 Am 30.08.2026 im Alltag aufgefallen: "Alexa, sauge das Schlafzimmer" -> * Alexa quittiert mit OK, am Sauger passiert nichts. Im Log stand nur * "Funk gerade belegt, ignoriert". Der Befehl war zufaellig waehrend eines * Polling-Zyklus eingetroffen und wurde verworfen. * * Fuer den Nutzer ist das der schlimmste Fehlerfall: Alexa bestaetigt, das * Geraet tut nichts, und es gibt keine Rueckmeldung. Ein Polling-Zyklus * dauert nur wenige Sekunden -- also warten wir ihn ab. * * Der Timeout ist die Notbremse, falls ein Funkdialog haengt: dann lieber * eine klare Fehlermeldung im Log als ein Befehl, der Minuten spaeter * ueberraschend losfaehrt. */ const wartenBis = Date.now() + 30000; while (busy && !stopping) { if (Date.now() > wartenBis) { log('T5: ' + what + ' — Funk 30 s lang belegt, Befehl aufgegeben', 'error'); /* Der fuer den Nutzer schlimmste Fall: Alexa quittiert, nichts * passiert. Der gehoert nicht nur ins Journal, sondern gemeldet. */ journal('AKTION', 'AUFGEGEBEN — Funk 30 s belegt: ' + what); await meldung('Befehl verworfen (Funk 30 s belegt): ' + what); return; } await pause(500); } if (stopping) return; busy = true; try { await fn(); log('T5: ' + what); journal('AKTION', 'ausgefuehrt: ' + what); } catch (e) { /* Der Status hilft bei der Ursachensuche: das Geraet lehnt Befehle auch * ab, wenn es gerade in der Station arbeitet (Mopp waschen/trocknen). */ let st = '?'; try { const s = await getStateAsync(BASE + '.info.statusText'); if (s) st = s.val; } catch (_) {} log('T5: ' + what + ' FEHLGESCHLAGEN — ' + e.message + ' (Status war: ' + st + ')', 'error'); journal('AKTION', 'FEHLGESCHLAGEN: ' + what + ' — ' + e.message + ' (Status: ' + st + ')'); await meldung('Befehl fehlgeschlagen: ' + what + ' — ' + e.message); } finally { busy = false; reschedule(3000); // Status bald nachziehen } } on({ id: BASE + '.T5', change: 'any', ack: false }, async obj => { const q = obj.state.from; if (obj.state.val) await handle('Start ganzes Haus (saugen+wischen)', () => startWholeHome(MODE_BOTH), q); else await handle('Stop und zur Basis', () => stopAndHome(), q); }); on({ id: BASE + '.T5_zum_Wischen', change: 'any', ack: false }, async obj => { const q = obj.state.from; if (obj.state.val) await handle('Start nur wischen', () => startWholeHome(MODE_MOP), q); else await handle('Stop und zur Basis', () => stopAndHome(), q); }); /* ✅ GEPRUEFT (04.09.2026): "Fortsetzen" ueber 2.1 START_SWEEP ist unbedenklich. * * Der Verdacht vom 30.08. — 2.1 startet einen Lauf OHNE Kartenbezug, also * verwirft es vielleicht auch beim Fortsetzen die Karte — hat sich NICHT * bestaetigt. Testlauf 04.09.: Schlafzimmer per Alexa gestartet, per Alexa * pausiert (17.9 = 9), per Alexa fortgesetzt. Ergebnis: 17.9 sprang auf 10 * ("Raum fortgesetzt"), Status 14 ("legt neue Karte an") trat NICHT auf. * * 2.1 verhaelt sich kontextabhaengig: * - aus dem Leerlauf heraus: neuer Lauf ohne Kartenbezug <- 30.08.-Fehler * - aus pausiertem Auftrag (17.9 = 9): sauberes Fortsetzen <- hier * Den Unterschied macht der lebende Auftragskontext: 17.34 traegt in der Pause * weiter die Raumliste, daran haengt sich 2.1 an. * * 17.2 pause-continue-work wird damit NICHT gebraucht. Der Tausch waere ein * Blindflug (piid 55 hat keine Werteliste und ist nicht lesbar) ohne Gewinn. */ on({ id: BASE + '.T5_Pause', change: 'any', ack: false }, async obj => { const q = obj.state.from; if (obj.state.val) await handle('Pause', () => action(ACT.PAUSE), q); else await handle('Fortsetzen', () => action(ACT.START_SWEEP), q); }); for (const [name, r] of Object.entries(ROOMS)) { on({ id: BASE + '.raum.' + name, change: 'any', ack: false }, async obj => { const q = obj.state.from; if (obj.state.val) await handle('Start Raum ' + name + ' (Raum-ID ' + r.id + ')', () => startRooms([r.id]), q); else await handle('Stop und zur Basis', () => stopAndHome(), q); }); } // ---------------------------------------------------------------- Start / Ende onStop(() => { journal('SYSTEM', '--- Skript beendet ---'); stopping = true; if (pollTimer) clearTimeout(pollTimer); try { if (sock) sock.close(); } catch (e) { /* egal */ } }, 2000); (async () => { await createStates(); openSocket(); log('T5-Skript gestartet — Sauger ' + IP + ', ' + Object.keys(ROOMS).length + ' Räume bekannt'); journal('SYSTEM', '--- Skript gestartet (' + Object.keys(ROOMS).length + ' Raeume, Journal aktiv) ---'); await poll(); })(); 8. Das Hilfsskript: Codes aus dem Plugin ziehen Das hier ist das Werkzeug aus Abschnitt 4. Es liest die main.bundle aus dem Mi-Home-Plugin und spuckt fertige Tabellen aus — für euer Gerät, nicht für meins. python extract-codes.py android/main.bundle Es sucht sich die deutschen Texte selbst heraus. Erwischt es die falsche Sprache, zeigt --sprachen alle gefundenen mit einem Beispielsatz, und --sprache 0 nimmt dann eine andere. Braucht nur Python 3, sonst nichts. Am Ende stehen fertige const ERROR = { … }-Blöcke auf dem Bildschirm, die ihr direkt ins Skript oben kopieren könnt, plus eine codes.json mit allem drin. extract-codes.py #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Fehler- und Meldungstexte aus einem Mi-Home-Geraeteplugin ziehen ================================================================ Die MIoT-Spec sagt bei Fehlernummern nur "uint16" und verraet nicht, welche Zahl was bedeutet. Die Mi-Home-App weiss es aber -- sie laedt fuer jedes Geraetemodell ein eigenes Plugin nach, und da stehen die Texte im Klartext drin. In mehreren Sprachen, Deutsch meist dabei. WOHER BEKOMME ICH DAS PLUGIN? 1. Modellname besorgen (z. B. xtl.vacuum.2407a) -- steht im Xiaomi Cloud Tokens Extractor in der Zeile "Model:", oder im ioBroker-Log, wenn der mihome-vacuum-Adapter "Model xyz not supported" meldet. 2. https://home.miot-spec.com/spec/<modellname> aufrufen 3. Dort oben auf den Link "下载米家插件" klicken (= Mi-Home-Plugin laden) 4. Das ZIP auspacken -- gebraucht wird daraus android/main.bundle BENUTZUNG python extract-codes.py android/main.bundle python extract-codes.py android/main.bundle --sprachen (Auswahl anzeigen) python extract-codes.py android/main.bundle --sprache 0 (andere waehlen) Ausgabe: fertige JS-Tabellen zum Kopieren und eine codes.json daneben. Braucht nur Python 3, keine zusaetzlichen Pakete. Getestet mit dem JONR T5 Pro. Bei anderen Geraeten kann das Bundle anders aufgebaut sein -- wenn nichts gefunden wird, lohnt ein Blick hinein: gesucht werden die Muster [<zahl>, { title: ... }] und keywordNNN: "text". """ import io, re, sys, json, collections args = [a for a in sys.argv[1:] if not a.startswith('--')] if not args: sys.exit(__doc__) roh = io.open(args[0], 'rb').read().decode('utf-8', errors='replace') # --- Alle Textbausteine sammeln ------------------------------------------ # Jeder Text steht mehrfach in der Datei: einmal pro Sprache. Die Sprachen # kommen dabei immer in derselben Reihenfolge -- das ist der Trick unten. def entschluesseln(t): """Aus dem Bundle kommt der ROHTEXT eines JS-Strings -- Umbrueche stehen darin noch als die zwei Zeichen Backslash-n. json.loads macht daraus echte Umbrueche; ohne das landet spaeter wortwoertlich \\r\\n im Text.""" try: return json.loads('"' + t + '"') except ValueError: return t varianten = collections.defaultdict(list) for m in re.finditer(r'(keyword\d+):\s*"((?:[^"\\]|\\.)*)"', roh): if not m.group(2).startswith('K.'): # blosse Verweise ueberspringen varianten[m.group(1)].append(entschluesseln(m.group(2))) if not varianten: sys.exit('Keine Textbausteine gefunden -- ist das wirklich die main.bundle?') # --- Wie viele Sprachen gibt es? ----------------------------------------- # Die meisten Bausteine sind vollstaendig uebersetzt. Deren Anzahl ist die # Sprachanzahl; unvollstaendige lassen wir fuer die Auswahl aussen vor. anzahl = collections.Counter(len(v) for v in varianten.values()).most_common(1)[0][0] voll = {k: v for k, v in varianten.items() if len(v) == anzahl} # --- Welcher Index ist Deutsch? ------------------------------------------ # Erkennung ueber typische Woerter und Umlaute. Weil die Sprachreihenfolge # ueberall gleich ist, reicht EIN Index fuer alle Bausteine. MARKER = re.compile( r'\b(Bitte|Sie|Ihre|nicht|und|oder|der|die|das|den|dem|ist|sind|wird|werden' r'|installiert|reinigen|entfernen|Roboter|Station|Fehler|voll|leer|Wasser)\b' r'|[äöüßÄÖÜ]') def sprachwert(i): t = [v[i] for v in voll.values()] return sum(1 for x in t if MARKER.search(x)) / len(t) werte = [sprachwert(i) for i in range(anzahl)] probe = lambda i: next(iter(voll.values()))[i] if '--sprachen' in sys.argv: print('Gefundene Sprachen (%d), mit Beispieltext:\n' % anzahl) for i in range(anzahl): marke = ' <- als Deutsch erkannt' if werte[i] == max(werte) else '' print(' %-2d %s%s' % (i, probe(i)[:66], marke)) print('\nMit --sprache <nummer> gezielt eine davon waehlen.') sys.exit(0) if '--sprache' in sys.argv: index = int(sys.argv[sys.argv.index('--sprache') + 1]) else: index = werte.index(max(werte)) print('Sprache %d automatisch als Deutsch erkannt.' % index) print('Probe: %s' % probe(index)[:66]) print('Sieht falsch aus? --sprachen zeigt alle, --sprache <n> waehlt.\n') # Unvollstaendig uebersetzte Bausteine: den letzten vorhandenen nehmen, # lieber ein Text in der falschen Sprache als eine Luecke. TEXT = {k: (v[index] if len(v) > index else v[-1]) for k, v in varianten.items()} # --- Die eigentlichen Tabellen ------------------------------------------- # Fehler: [4012, { title: ...keywordX, solution: ...keywordY }] fehler = {} for code, kt, ks in re.findall( r'\[(\d{1,5}),\s*\{\s*title:\s*[^,]*?\.(keyword\d+),' r'\s*solution:\s*[^}]*?\.(keyword\d+)\s*\}\]', roh): c = int(code) if c not in fehler and kt in TEXT: fehler[c] = (TEXT[kt], TEXT.get(ks, '')) # Meldungen: [1008, ...keywordX] meldungen = {} for code, kw in re.findall(r'\[(\d{1,5}),\s*[^\]]*?\.(keyword\d+)\]', roh): c = int(code) if c not in meldungen and c not in fehler and kw in TEXT: meldungen[c] = TEXT[kw] if not fehler and not meldungen: sys.exit('Keine Codetabellen gefunden. Bei diesem Geraet ist das Bundle\n' 'offenbar anders aufgebaut -- bitte von Hand hineinschauen.') def sauber(t): t = re.sub(r'\s+', ' ', t).strip() # Beim Code 4902 steht im Original "2. 2." -- Tippfehler des Herstellers. return re.sub(r'\b(\d+)\.\s+\1\.', r'\1.', t) # --- Ausgabe -------------------------------------------------------------- json.dump({'fehler': {str(k): {'titel': sauber(v[0]), 'loesung': sauber(v[1])} for k, v in sorted(fehler.items())}, 'meldungen': {str(k): sauber(v) for k, v in sorted(meldungen.items())}}, io.open('codes.json', 'w', encoding='utf-8'), ensure_ascii=False, indent=2) print('%d Fehler, %d Meldungen -> codes.json\n' % (len(fehler), len(meldungen))) print('Zum Kopieren ins ioBroker-Skript:\n') def js_block(name, eintraege, kurz=90): print('const %s = {' % name) for k, v in eintraege: v = sauber(v).replace('\\', '\\\\').replace("'", "\\'") if len(v) > kurz: v = v[:kurz - 1] + '…' print(" %d: '%s'," % (k, v)) print('};\n') js_block('ERROR', [(k, v[0]) for k, v in sorted(fehler.items()) if k < 4500]) js_block('STATION_ERROR', [(k, v[0]) for k, v in sorted(fehler.items()) if k >= 4500]) js_block('MSG', sorted(meldungen.items()), kurz=120) js_block('FIX', [(k, v[1]) for k, v in sorted(fehler.items()) if v[1]], kurz=220) Viele Grüße
  • JSON.parse erzeugt kein gültiges Array

    4
    0 Stimmen
    4 Beiträge
    78 Aufrufe
    L
    Viele Dank für die schnelle Antwort. Damit werde ich weiter kommen. 🙋‍♂️
  • Nach Update auf js-controller 7.2.2 keine Skripte mehr da

    17
    0 Stimmen
    17 Beiträge
    251 Aufrufe
    P
    @wendy2702 sagte: @pingo Probier mal im Terminal iob upload all Und eventuell auch mal Browser Cache leeren Danke. Das war die Lösung! Die Skripte sind wieder da. Yippie
  • openligadb css Aufrischung

    2
    1
    0 Stimmen
    2 Beiträge
    58 Aufrufe
    OliverIOO
    @matze55 da man es designmäßig es nie allen recht machen kann, mache ich meine widget meist mit einem gewissen grunddesign. darüber hinaus definiere ich css klassen über die dann jeder da noch was dazu definieren kann. diese css klassen sind hier bspw am gameday beschrieben. https://github.com/oweitman/ioBroker.openligadb#games-of-gameday-v2 wenn da noch was fehlt, dann gerne hier oder im eigentlichen support thread kommentieren. ich mache die dann gerne design neutral dazu, so das ihr euer gewünschtes design hinbekommt wenn man sich mit css klassen nicht so gut auskennt, empfehle ich den html code eines widgets mit den webdeveloper tools zu kopieren. da sind die vorgegebenen css regeln mit enthalten. diese der ki zu ngeben und sie zu bitten, entweder einfach diese hübsch zu machen oder genauere regeln der ki vorzugeben. evtl noch den tip den html code nicht zu verändern, sondern nur css regeln auszugeben die evtl vorhandene css regeln überschreiben. ich mache mal ein einfaches schnelles beispiel: hier mein prompt mit dem aktuellen html kannst du mir für den folgenden html code css regeln definieren, so das er etwas ansprechender aussieht <div class="vis-widget vis-tpl-openligadb-OLiDB GameDay v2" style=" overflow: visible; width: 327px; height: 355px; left: 21px; top: 112px; font-size: x-small; " id="w00030" > <style> #w00030 .oldb-icon { max-height: 25px; max-width: 25px; } #w00030 .oldb-tdicon { width: 25px; } #w00030 .oldb-tt { width: 100%; } #w00030 .oldb-tt td { white-space: nowrap; } #w00030 .oldb-wrap { white-space: normal !important; } #w00030 .oldb-goal { white-space: nowrap; display: inline-block; } #w00030 .oldb-full { width: 50%; } #w00030 .oldb-center { text-align: center; } #w00030 .oldb-right { text-align: right; } #w00030 .oldb-left { text-align: left; } #w00030 .oldb-datetime { font-weight: bold; } </style> <table class="oldb-tt"> <tbody> <tr> <td class="oldb-right oldb-datetime" colspan="3">08/28</td> <td class="" /> <td class="oldb-left oldb-datetime" colspan="3">08:30 PM</td> </tr> <tr> <td class="oldb-right oldb-full">FC Bayern München</td> <td class="oldb-center oldb-tdicon"> <img class="oldb-icon" src="https://upload.wikimedia.org/wikipedia/commons/1/1f/Logo_FC_Bayern_M%C3%BCnchen_%282002%E2%80%932017%29.svg" /> </td> <td class="oldb-center">5</td> <td class="oldb-center">:</td> <td class="oldb-center">1</td> <td class="oldb-center oldb-tdicon"> <img class="oldb-center oldb-icon" src="https://i.imgur.com/v0tkpNx.png" /> </td> <td class="oldb-left oldb-full">VfB Stuttgart</td> </tr> <tr> <td class="oldb-right oldb-datetime" colspan="3">08/29</td> <td class="" /> <td class="oldb-left oldb-datetime" colspan="3">03:30 PM</td> </tr> <tr> <td class="oldb-right oldb-full">RB Leipzig</td> <td class="oldb-center oldb-tdicon"> <img class="oldb-icon" src="https://i.imgur.com/Rpwsjz1.png" /> </td> <td class="oldb-center">3</td> <td class="oldb-center">:</td> <td class="oldb-center">0</td> <td class="oldb-center oldb-tdicon"> <img class="oldb-center oldb-icon" src="https://i.imgur.com/KSIk0Eu.png" /> </td> <td class="oldb-left oldb-full">Borussia Mönchengladbach</td> </tr> <tr> <td class="oldb-right oldb-full">1. FSV Mainz 05</td> <td class="oldb-center oldb-tdicon"> <img class="oldb-icon" src="https://upload.wikimedia.org/wikipedia/commons/9/9e/Logo_Mainz_05.svg" /> </td> <td class="oldb-center">0</td> <td class="oldb-center">:</td> <td class="oldb-center">0</td> <td class="oldb-center oldb-tdicon"> <img class="oldb-center oldb-icon" src="https://upload.wikimedia.org/wikipedia/commons/e/e3/SC_Paderborn_07_Logo.svg" /> </td> <td class="oldb-left oldb-full">SC Paderborn 07</td> </tr> <tr> <td class="oldb-right oldb-full">1. FC Union Berlin</td> <td class="oldb-center oldb-tdicon"> <img class="oldb-icon" src="https://assets.dfb.de/uploads/000/018/232/small_union-Berlin.jpg" /> </td> <td class="oldb-center">3</td> <td class="oldb-center">:</td> <td class="oldb-center">3</td> <td class="oldb-center oldb-tdicon"> <img class="oldb-center oldb-icon" src="https://i.imgur.com/X8NFkOb.png" /> </td> <td class="oldb-left oldb-full">Eintracht Frankfurt</td> </tr> <tr> <td class="oldb-right oldb-full">1. FC Köln</td> <td class="oldb-center oldb-tdicon"> <img class="oldb-icon" src="https://upload.wikimedia.org/wikipedia/commons/0/01/1._FC_Koeln_Logo_2014%E2%80%93.svg" /> </td> <td class="oldb-center">3</td> <td class="oldb-center">:</td> <td class="oldb-center">2</td> <td class="oldb-center oldb-tdicon"> <img class="oldb-center oldb-icon" src="https://i.imgur.com/gF0PfEl.png" /> </td> <td class="oldb-left oldb-full">TSG Hoffenheim</td> </tr> <tr> <td class="oldb-right oldb-full">SV 07 Elversberg</td> <td class="oldb-center oldb-tdicon"> <img class="oldb-icon" src="https://upload.wikimedia.org/wikipedia/commons/thumb/3/35/SV_Elversberg_Logo.svg/500px-SV_Elversberg_Logo.svg.png" /> </td> <td class="oldb-center">3</td> <td class="oldb-center">:</td> <td class="oldb-center">2</td> <td class="oldb-center oldb-tdicon"> <img class="oldb-center oldb-icon" src="https://www.bundesliga-reisefuehrer.de/sites/default/files/B04_Standard_Logo_RGB.png" /> </td> <td class="oldb-left oldb-full">Bayer 04 Leverkusen</td> </tr> <tr> <td class="oldb-right oldb-datetime" colspan="3">08/29</td> <td class="" /> <td class="oldb-left oldb-datetime" colspan="3">06:30 PM</td> </tr> <tr> <td class="oldb-right oldb-full">Borussia Dortmund</td> <td class="oldb-center oldb-tdicon"> <img class="oldb-icon" src="https://upload.wikimedia.org/wikipedia/commons/thumb/6/67/Borussia_Dortmund_logo.svg/960px-Borussia_Dortmund_logo.svg.png" /> </td> <td class="oldb-center">2</td> <td class="oldb-center">:</td> <td class="oldb-center">0</td> <td class="oldb-center oldb-tdicon"> <img class="oldb-center oldb-icon" src="https://upload.wikimedia.org/wikipedia/commons/f/f7/Hamburger_SV_logo.svg" /> </td> <td class="oldb-left oldb-full">Hamburger SV</td> </tr> <tr class="todaygameheader"> <td class="oldb-right oldb-datetime" colspan="3">08/30</td> <td class="" /> <td class="oldb-left oldb-datetime" colspan="3">03:30 PM</td> </tr> <tr class="todaygame"> <td class="oldb-right oldb-full">SC Freiburg</td> <td class="oldb-center oldb-tdicon"> <img class="oldb-icon" src="https://i.imgur.com/r3mvi0h.png" /> </td> <td class="oldb-center">-</td> <td class="oldb-center">:</td> <td class="oldb-center">-</td> <td class="oldb-center oldb-tdicon"> <img class="oldb-center oldb-icon" src="https://upload.wikimedia.org/wikipedia/commons/b/be/SV-Werder-Bremen-Logo.svg" /> </td> <td class="oldb-left oldb-full">SV Werder Bremen</td> </tr> <tr class="todaygameheader"> <td class="oldb-right oldb-datetime" colspan="3">08/30</td> <td class="" /> <td class="oldb-left oldb-datetime" colspan="3">05:30 PM</td> </tr> <tr class="todaygame"> <td class="oldb-right oldb-full">FC Augsburg</td> <td class="oldb-center oldb-tdicon"> <img class="oldb-icon" src="https://i.imgur.com/sdE62e2.png" /> </td> <td class="oldb-center">-</td> <td class="oldb-center">:</td> <td class="oldb-center">-</td> <td class="oldb-center oldb-tdicon"> <img class="oldb-center oldb-icon" src="https://upload.wikimedia.org/wikipedia/commons/9/97/FC_Schalke_04_Logo.png" /> </td> <td class="oldb-left oldb-full">FC Schalke 04</td> </tr> </tbody> </table> </div> hier die antwort Ja. Ich würde das Widget etwas moderner machen: dunkler Card-Hintergrund, klarere Spielzeilen, größere Logos und eine deutliche Hervorhebung der heutigen Spiele. Du kannst deine bisherigen CSS-Regeln im <style>-Block durch diese ersetzen: #w00030 { font-family: Arial, sans-serif; font-size: 12px !important; color: #f5f5f5; background: #181b20; border: 1px solid rgba(255,255,255,0.08); border-radius: 12px; padding: 10px; box-sizing: border-box; box-shadow: 0 6px 18px rgba(0,0,0,0.3); } /* Tabelle */ #w00030 .oldb-tt { width: 100%; border-collapse: separate; border-spacing: 0 4px; } /* Grundformatierung der Zellen */ #w00030 .oldb-tt td { white-space: nowrap; padding: 5px 3px; vertical-align: middle; } /* Vereinslogos */ #w00030 .oldb-icon { width: 28px; height: 28px; max-width: 28px; max-height: 28px; object-fit: contain; display: block; margin: 0 auto; } /* Spalte mit Vereinslogo */ #w00030 .oldb-tdicon { width: 32px; min-width: 32px; } /* Teamnamen */ #w00030 .oldb-full { width: 50%; font-weight: 500; } /* Datum / Uhrzeit */ #w00030 .oldb-datetime { font-size: 10px; font-weight: 600; color: #9ca3af; padding-top: 9px !important; padding-bottom: 2px !important; } /* Ergebnis */ #w00030 .oldb-tt tr:not(.todaygameheader) td:nth-child(3), #w00030 .oldb-tt tr:not(.todaygameheader) td:nth-child(4), #w00030 .oldb-tt tr:not(.todaygameheader) td:nth-child(5) { font-weight: 700; font-size: 14px; color: #ffffff; } /* heutige Spiel-Zeit */ #w00030 .todaygameheader td { color: #55d68b; font-weight: 700; } /* heutiges Spiel */ #w00030 .todaygame { background: rgba(55, 180, 110, 0.12); } /* erste und letzte Zelle des heutigen Spiels abrunden */ #w00030 .todaygame td:first-child { border-radius: 8px 0 0 8px; } #w00030 .todaygame td:last-child { border-radius: 0 8px 8px 0; } /* kleine Hover-Animation */ #w00030 .oldb-tt tr:not(.todaygameheader):hover { background: rgba(255,255,255,0.06); } /* Ausrichtung */ #w00030 .oldb-center { text-align: center; } #w00030 .oldb-right { text-align: right; } #w00030 .oldb-left { text-align: left; } #w00030 .oldb-wrap { white-space: normal !important; } #w00030 .oldb-goal { white-space: nowrap; display: inline-block; } Damit bekommt das Widget ungefähr einen modernen Dark-Mode-/Sports-App-Look. Besonders die heutigen Partien werden leicht grün hinterlegt, während bereits gespielte Begegnungen dezenter bleiben. Ein Punkt ist bei deinem aktuellen HTML allerdings ungünstig: Datum/Uhrzeit und Spiel sind jeweils zwei getrennte <tr>-Zeilen. Dadurch kann man mit reinem CSS nicht ganz so einfach jede Begegnung als perfekte einzelne „Karte“ gestalten. Mit dem vorhandenen Markup kann man aber schon ziemlich viel herausholen. Wenn du es noch etwas hochwertiger möchtest, würde ich zusätzlich die Teamnamen bei dem nur 327 px breiten Widget kleiner bzw. mit Ellipsis behandeln: #w00030 .oldb-full { max-width: 100px; overflow: hidden; text-overflow: ellipsis; } Dann wird beispielsweise ein sehr langer Vereinsname nicht das komplette Layout auseinanderziehen. Ich kann dir auch eine zweite Variante im Bundesliga-/DAZN-Stil mit schwarzen Spielkarten, großen Ergebnissen und farbigem „HEUTE“-Badge bauen. Ergebnis sieht dann so aus, nachdem ich das css einfach in den reiter kopiert habe [image: 2e10fc89-0fc0-42cb-8a57-22aa7aec0460-image.jpeg] da gibt es sicherlich noch verbesserungspotential auch habe ich vergessen der ki zu sagen, das man das html nicht anpassen kann, sondern nur die vorhandenen css regeln überschreiben kann oder das das html dynamisch erzeugt wird und die childs nicht immer das gleiche anzeigen. oder das er eine eigene css klasse definieren soll, die man dann in der widget konfiguration unter General/CSS Class eintragen kann, so zielt es aktuell nur auf die widgetid oder auch habe ich keine detaillierten Design Anweisungen gegeben. noch eine Anmerkung: ich habe die Oberfläche hier auf englisch eingestellt, daher die englische notation von datum und zeit.
  • Musikstatus für Alexa und oder Sonos

    javascript multimedia
    1
    1
    0 Stimmen
    1 Beiträge
    57 Aufrufe
    Niemand hat geantwortet
  • Fotos von ftp laden und in vis anzeigen.

    2
    0 Stimmen
    2 Beiträge
    92 Aufrufe
    OliverIOO
    @stenmic damit der iobroker die dateien kennt muss man sie dort registrieren. es kommt darauf an über welchen weg du das machen möchtest einmal gibt es die shell befehle https://forum.iobroker.net/topic/85025/lokale-webseite-auf-dem-iobroker-wohin-kopieren/4?_=1787249161950 oder du kannst es selbst über den admin tab dateien/files machen oder per skript über den javascript adapter https://forum.iobroker.net/topic/25541/gelöst-wie-writefile-in-js-script-nutzen?_=1787249430099
  • Laden EBikeAkku via Shelly SmartPlug M Gen3 Steckdose

    11
    1
    0 Stimmen
    11 Beiträge
    389 Aufrufe
    G
    @dodi666 Danke, ich sehe jetzt, das geht mit Shelly auch. Etwas komplizierter als bei Meross, aber vermutlich ausreichend. Auf die Idee mit dem Standby-Killer bin ich vorher nicht gekommen. Und die Shelly habe ich mir auch nur gekauft, weil auf "Olafs Pedal&Service"-Seite dieses auf den Schalter ladbare Programm angepriesen wurde. Ich habs probiert, fand aber, wenn dann gehört das Script in den IOBroker, nicht auf die Steckdose. Ich nutze mein Programm aber sowieso etwas anders. Ich fahre wöchentlich eine größere Tour und habe zwei Akkus. Die lade ich, wenn ich zurückkomme nur mäßig voll (70%) um dann schnell nochmal aufzufüllen wenns ungeplant losgeht. Das Laden von 30% aus würde mir zu lange dauern. Von daher ist mein Programm nicht völlig vergeblich!
  • E-Herd pulsen über Hausverbracuh erkennen.

    8
    0 Stimmen
    8 Beiträge
    286 Aufrufe
    MartinPM
    Die Übergangsphasen, wo die PV nur noch einen Bruchteil der Nennleistung liefert sind das Problem. Oder recht kleine Anlagen, wo die Nennleistung kaum größer ist, als die Anschlussleistung des Herdes...
  • Icecast2 server ton abgriff

    1
    0 Stimmen
    1 Beiträge
    68 Aufrufe
    Niemand hat geantwortet
  • Tankerkönig - Extended Skript

    javascript monitoring
    61
    4
    4 Stimmen
    61 Beiträge
    7k Aufrufe
    NashraN
    @Ro75 sagte: Ich sehe Änderungen in den Trends. Seit den gesetzlichen Änderungen fallen diese Trends nun anders aus. Was du machen kannst ist, den Datenpunkt reset_all_trends mal mit true setzen. Ro75. Hat leider nichts gebracht. Daraufhin mal alle DP gelöscht und Script neu gestartet..... Jetzt wird es angezeigt
  • Skript Luftdaten

    15
    10
    4 Stimmen
    15 Beiträge
    2k Aufrufe
    Ro75R
    Es kommt der Fehler 500 - vorrübergehend nicht zu erreichen. Das war vor längerer Zeit schonmal über eeiiinnnige Tage. Mehr kann ich dazu nicht sagen, da ich dieses Script abgelöst habe gegen mein Wetterscript was mit der OWM API arbeitet. Nur bin ich im Moment nicht bereit das ganze System hier zu veröffentlichen, da es 6 Adapter restlos ersetzt und 6500 Zeilen Code beinhaltet. Da ist mir der Support zu gewaltig, auch wenn die Konfig nur 5 Werte sind. Ro75.
  • Steuerung Laden Bosch EBikeAkku via MEROSS Steckdose

    Verschoben
    2
    0 Stimmen
    2 Beiträge
    163 Aufrufe
    G
    Der Adapter in IOBroker für diese Steckdose macht nur alle halbe Stunde einen Update auf den Status. Deshalb habe ich jetzt ein Script für Shelly SmartPlug M Gen3 geschrieben.
  • Wie lege ich den Datenpunkt an?

    24
    0 Stimmen
    24 Beiträge
    1k Aufrufe
    KusselinK
    @Ro75 sagte: ja. Allerdings wirst du auf Dauer keinen Spaß haben, denn da wird ja jeden Tag mindestens ein weiterer Datenpunkt angelegt. Wenn du daraus eine Historie ableiten willst, völlig ungeeignet - wurde aber schon weiter oben angesprochen. Ro75. Hi Ro75, ich habe es jetzt so gelöst: // ═══════════════════════════════════════════════════════════════ // Einspeisung – Tageswert (wird täglich überschrieben) // ═══════════════════════════════════════════════════════════════ // ── Quell-Datenpunkte ────────────────────────────────────────── const DP_HEUTE_KWH = 'javascript.0.Einspeisung.heute_kWh'; const DP_HEUTE_EURO = 'javascript.0.Einspeisung.heute_euro'; // ── Ziel-Datenpunkte (fest, werden täglich überschrieben) ────── const DP_TAG_KWH = '0_userdata.0.Einspeisung.letzter_tag_kWh'; const DP_TAG_EURO = '0_userdata.0.Einspeisung.letzter_tag_euro'; const DP_TAG_DATUM = '0_userdata.0.Einspeisung.letzter_tag_datum'; // ── Jahressummen ─────────────────────────────────────────────── const DP_JAHR_KWH = '0_userdata.0.Einspeisung.jahr_kWh'; const DP_JAHR_EURO = '0_userdata.0.Einspeisung.jahr_euro'; // ── Archiv-Präfix ────────────────────────────────────────────── const DP_ARCHIV = '0_userdata.0.Einspeisung.Archiv.'; // ── Schutz vor Doppeladdierung ───────────────────────────────── const DP_LETZTER_RUN = '0_userdata.0.Einspeisung.letzter_run_datum'; // ═══════════════════════════════════════════════════════════════ // Datenpunkte anlegen // ═══════════════════════════════════════════════════════════════ createState(DP_TAG_KWH, 0, { name: 'Einspeisung letzter Tag', unit: 'kWh', type: 'number', role: 'value.energy', read: true, write: true }); createState(DP_TAG_EURO, 0, { name: 'Einspeisevergütung letzter Tag', unit: '€', type: 'number', role: 'value.money', read: true, write: true }); createState(DP_TAG_DATUM, '', { name: 'Datum letzter gespeicherter Tag', type: 'string', role: 'text', read: true, write: true }); createState(DP_JAHR_KWH, 0, { name: 'Einspeisung Jahr gesamt', unit: 'kWh', type: 'number', role: 'value.energy', read: true, write: true }); createState(DP_JAHR_EURO, 0, { name: 'Einspeisevergütung Jahr gesamt', unit: '€', type: 'number', role: 'value.money', read: true, write: true }); createState(DP_LETZTER_RUN, '', { name: 'Letztes Ausführungsdatum', type: 'string', role: 'text', read: true, write: true }); // ═══════════════════════════════════════════════════════════════ // Hauptfunktion // ═══════════════════════════════════════════════════════════════ function tageswertSpeichern() { const d = new Date(); const datum = `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; // ── Schutz vor Doppeladdierung ───────────────────────────── const letzterRun = getState(DP_LETZTER_RUN).val; if (letzterRun === datum) { log(`⚠️ Tageswert für ${datum} wurde bereits gespeichert – Abbruch.`, 'warn'); return; } // ── Werte lesen ──────────────────────────────────────────── const tagesKwh = parseFloat(getState(DP_HEUTE_KWH).val) || 0; const tagesEuro = parseFloat(getState(DP_HEUTE_EURO).val) || 0; // ── Fehlerbehandlung: ungültige Werte ────────────────────── if (tagesKwh < 0 || tagesEuro < 0) { log(`❌ Ungültige Werte: ${tagesKwh} kWh / ${tagesEuro} € – Abbruch.`, 'error'); return; } if (tagesKwh === 0 && tagesEuro === 0) { log(`⚠️ Beide Werte sind 0 – kein Einspeisungstag oder Sensor-Fehler.`, 'warn'); } // ── Letzten Run-Datum speichern ──────────────────────────── setState(DP_LETZTER_RUN, datum, true); // ── Tagesdatenpunkte überschreiben ───────────────────────── setState(DP_TAG_KWH, tagesKwh, true); setState(DP_TAG_EURO, tagesEuro, true); setState(DP_TAG_DATUM, datum, true); // ── Jahressummen aufaddieren ─────────────────────────────── const neueKwh = Math.round(((parseFloat(getState(DP_JAHR_KWH).val) || 0) + tagesKwh) * 100) / 100; const neueEuro = Math.round(((parseFloat(getState(DP_JAHR_EURO).val) || 0) + tagesEuro) * 100) / 100; setState(DP_JAHR_KWH, neueKwh, true); setState(DP_JAHR_EURO, neueEuro, true); log(`✅ ${datum}: ${tagesKwh} kWh / ${tagesEuro} € gespeichert`, 'info'); log(`📊 Jahressumme: ${neueKwh} kWh / ${neueEuro} €`, 'info'); // ── Silvester: Jahresabschluss ins Archiv schreiben ──────── if (d.getMonth() === 11 && d.getDate() === 31) { jahresabschlussArchivieren(d.getFullYear(), neueKwh, neueEuro); } } // ═══════════════════════════════════════════════════════════════ // Jahresabschluss archivieren // ═══════════════════════════════════════════════════════════════ function jahresabschlussArchivieren(jahr, gesamtKwh, gesamtEuro) { const dpArchivKwh = DP_ARCHIV + jahr + '_kWh'; const dpArchivEuro = DP_ARCHIV + jahr + '_euro'; createState(dpArchivKwh, gesamtKwh, { name: 'Einspeisung Jahr ' + jahr, unit: 'kWh', type: 'number', role: 'value.energy', read: true, write: false }, () => { setState(dpArchivKwh, gesamtKwh, true); }); createState(dpArchivEuro, gesamtEuro, { name: 'Einspeisevergütung Jahr ' + jahr, unit: '€', type: 'number', role: 'value.money', read: true, write: false }, () => { setState(dpArchivEuro, gesamtEuro, true); }); log(`🗄️ Jahresabschluss ${jahr} archiviert: ${gesamtKwh} kWh / ${gesamtEuro} €`, 'info'); // ── Jahressummen zurücksetzen für neues Jahr ─────────────── setTimeout(() => { setState(DP_JAHR_KWH, 0, true); setState(DP_JAHR_EURO, 0, true); log('🔄 Jahressummen für neues Jahr zurückgesetzt.', 'info'); }, 3000); } // ═══════════════════════════════════════════════════════════════ // Täglich um 23:55 Uhr ausführen // ═══════════════════════════════════════════════════════════════ schedule('55 23 * * *', () => { tageswertSpeichern(); }); log('📋 Script "Einspeisung Jahressumme" gestartet – läuft täglich um 23:55 Uhr.', 'info');
  • Javascript Adapter: kein Logging unter dem Script mehr

    21
    0 Stimmen
    21 Beiträge
    448 Aufrufe
    fuzzy1955F
    @ofri2607 sagte: auch nicht? Ja danke, jetzt läuft es. Ich hatte den Logstatus auf "warn" statt "log" gestellt.
  • ioBroker Prozess- & Gesundheitsmonitor + Grafana + HTML

    javascript monitoring
    93
    5
    5 Stimmen
    93 Beiträge
    8k Aufrufe
    R
    ok danke denn ch bekam immer schon bei 40 % Ram Kritisch
  • Fenster offen Script

    15
    0 Stimmen
    15 Beiträge
    2k Aufrufe
    H
    Danke ! Genauso funktioniert es. Da wäre ich jetzt nicht drauf gekommen. Danke auch für die Schleife, habe 18 Fenster in der Überwachung und so reicht ein Blick aufs Tablet am Eingang um zu sehen, ob noch irgendwas offen ist.
  • Zeitsteuerung von Pittini

    1
    0 Stimmen
    1 Beiträge
    89 Aufrufe
    Niemand hat geantwortet

256

Online

33.1k

Benutzende

83.7k

Themen

1.3m

Beiträge