Weiter zum Inhalt
  • Home
  • Aktuell
  • Tags
  • 0 Ungelesen 0
  • Kategorien
  • Unreplied
  • Beliebt
  • GitHub
  • Docu
  • Hilfe
Skins
  • Hell
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dunkel
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Standard: (Kein Skin)
  • Kein Skin
Einklappen
ioBroker Logo

Community Forum

donate donate
  1. ioBroker Community Home
  2. Deutsch
  3. Skripten / Logik
  4. JavaScript
  5. [TypeSkript] Wetter.com Forecast/Vorhersage

NEWS

  • Der neue Monatsrückblick für Mai und Juni 2026 ist online!
    BluefoxB
    Bluefox
    8
    1
    1.1k

  • wichtiges UPDATE für controller 7.2.2 im stable
    HomoranH
    Homoran
    10
    1
    3.6k

  • Neues YouTube-Video: Visualisierung im Devices-Adapter
    BluefoxB
    Bluefox
    16
    1
    6.2k

[TypeSkript] Wetter.com Forecast/Vorhersage

Geplant Angeheftet Gesperrt Verschoben JavaScript
76 Beiträge 11 Kommentatoren 7.8k Aufrufe 17 Beobachtet
  • Älteste zuerst
  • Neuste zuerst
  • Meiste Stimmen
Antworten
  • In einem neuen Thema antworten
Anmelden zum Antworten
Dieses Thema wurde gelöscht. Nur Nutzer mit entsprechenden Rechten können es sehen.
  • Bl4ckst4rB Offline
    Bl4ckst4rB Offline
    Bl4ckst4r
    schrieb am zuletzt editiert von
    #47

    Moin zusammen,

    ich möchte mich nur kurz für das Skript bedanken. ☺️

    Bis zum Ende des Free Tarifs habe ich den AccuWeather-Adapter genutzt. Da ich Wetter.com aber "privat" sowieso schon verwende, passt das Skript natürlich umso besser.

    Ich steuere damit meinen Sonnenschutz und lasse die Rollos abhängig von der Anzahl der Sonnenstunden sowie der Tageshöchsttemperatur herunterfahren. Das umstellen war dank deines Skriptes quasi "Plug and Play".

    Daher ein großes Dankeschön fürs Teilen! Das hat mir auf jeden Fall die Arbeit erspart, selbst etwas Eigenes zu entwickeln und bietet der Community definitiv einen echten Mehrwert. :)

    Viele Grüße
    Bl4ckst4r

    1 Antwort Letzte Antwort
    1
    • Pedder007P Pedder007

      Hi, na' da habe ich mir jetzt einen geleistet .... natürlich ist das ein anderer Dienst!!!

      Keine Ahnung, aber das kommt davon, wenn man zu viel parallel macht... :-(

      Sorry für den Unsinn hier, ich gelobe Besserung :-)

      S Online
      S Online
      Schimi
      schrieb am zuletzt editiert von
      #48

      @Pedder007 wenn du jetzt noh sagst, dass das Skript weiter funktioniert... wäre ich erleichtert....

      weil ich nach sie vor nicht weiss woran es liegen könnte 🙈

      Pedder007P 1 Antwort Letzte Antwort
      0
      • S Schimi

        @Pedder007 wenn du jetzt noh sagst, dass das Skript weiter funktioniert... wäre ich erleichtert....

        weil ich nach sie vor nicht weiss woran es liegen könnte 🙈

        Pedder007P Offline
        Pedder007P Offline
        Pedder007
        schrieb am zuletzt editiert von Pedder007
        #49

        @Schimi ENTWARNUNG
        Das Script läuft einwandfrei und hat z. B. heute um kurz vor 04:00 und nochmal um kurz vor 16:00 aktualisiert.
        Das Problem saß vor der Konsole - hatte den falschen Key, also den vom anderen Anbieter, genutzt.
        Sorry war am WE unterwegs und hätte das evtl. oben klarer formulieren sollen.
        Danke Dir für Dein vorbildliches Care-Taking ;-)

        Pedder
        All @Proxmox/Trixie auf HP Elitedesk 800 G4; Zigbee: ZigStar (LAN), ~110Devices
        Unifi, Motioneye/3Reolinks, PiHole, Bosch CS7800i via BBQKees/EMS-ESP, Fronius/BYD 11kWp via Modbus
        Under construction: Smart-WoMo auf Raspi4

        1 Antwort Letzte Antwort
        1
        • S Online
          S Online
          Schimi
          schrieb am zuletzt editiert von
          #50

          Danke für die Rückmeldung :-)

          1 Antwort Letzte Antwort
          0
          • S Online
            S Online
            Schimi
            schrieb am zuletzt editiert von Schimi
            #51

            Habe mal denn Umgang mit dem Limit optimiert (und dabei etwas weiter gemacht), da ich gestern ins Limit gelaufen bin....

            die neue Version findet sich im ersten Post im "Spoiler"

            Falls sich Bugs eingeschlichen haben, solltet ihr vielleicht die letzte 1.9.2 Version erstmal irgendwo zwischenspeichern ;-)

            Der API-KEY und die anzahl der Tage werden nun auch in Datenpunkten eingegeben...


            Changelog: Wetter.com Forecast API (v1.9.2 ➔ v2.6.1)

            Dieser Bericht dokumentiert die Evolution des Skripts von einem einfachen API-Abruf hin zu einer performanten, typsicheren und ausfallsicheren ioBroker-Infrastruktur-Komponente.

            ⚙️ v2.6.x – Perfektionierung & Usability

            2.6.1 (Hotfix): * Fix: Konfigurations-Datenpunkte (api_key, forecast_days) werden nun mit write: true angelegt.

            Retroaktiver Fix: Ein extendObjectAsync entsperrt automatisch Bestandsdatenpunkte, die in Vorversionen als Read-Only angelegt wurden.

            2.6.0 (Ultra-Performance): * RAM-Cache (ensuredPaths): Eliminiert hunderte synchrone existsObject()-Aufrufe an die JS-Engine. I/O-Overhead beim Struktur-Check sinkt nach dem ersten Lauf auf nahezu null.

            Zero-Latency: Die künstliche Drosselung wcomWait() wurde vollständig entfernt, da das System durch Caching und Batching überlastungsfrei agiert.

            Memory-Management: Timeout-Clearing im HTTP-Request integriert, um Speicherlecks zu verhindern.

            Semantischer Fallback: wind_speed_max fällt auf wind.avg zurück, falls die API sporadisch den Maximalwert weglässt.

            🚀 v2.5.x – Batching & Deadlock-Schutz

            2.5.0:

            State-Write-Batching: Schreibvorgänge (setStateChangedAsync) eines Tages werden in einem internen Puffer gebündelt und per Promise.all simultan abgesetzt.

            Lock-Härtung: Der HTTP-Call ist nun in ein Promise.race mit einem 10-Sekunden-Timeout gekapselt. Verhindert unendliche Blockaden des isFetching-Locks bei fehlerhaften API-Servern.

            Type-Safety: Optionale Verkettung (data.hourly ?? []) schützt vor unvollständigen JSON-Antworten.

            API-Resilienz: Erweiterte isNaN-Prüfung in wcomExtractValue(), um korrupte Strings der API abzufangen.

            ✨ v2.3.x bis v2.4.x – Dynamisierung

            2.4.0 (Dynamische Tage): * FORECAST_DAYS aus dem Code in den Datenpunkt info.forecast_days ausgelagert.

            Auto-Trigger: Das Skript reagiert sofort auf Änderungen dieses Wertes, passt den Abruf an und löscht überschüssige Tagesordner (cleanupObsoleteDays) rekursiv.

            2.3.0 (API-Key Trigger): * Neuer on()-Trigger für info.api_key. Bei Eingabe eines neuen Keys wird sofort ein Test-Abruf (source: 'key_update') erzwungen, der den regulären Restart-Blocker überspringt.

            🛡️ v2.1.x bis v2.2.x – Systemhärtung & Controller-Entlastung

            2.2.0:

            Zeitzonen-Fix: new Date() statt toISOString() behebt falsche Tageswechsel-Berechnungen auf Systemen mit UTC-Offset.

            I/O-Reduktion: Einführung von wcomEnsureState() zur Minimierung von createStateAsync-Spam.

            2.1.0:

            Zero-Churn (Hourly): Das destruktive, rekursive Löschen (deleteObjectAsync) von Stundenwerten wurde durch sauberes Überschreiben abgelöst. Verhindert massive Object-Events und Controller-Spikes.

            Null-Safety: wcomExtractValue() gibt strikt 0 statt null zurück, was Typerrors in Number-States verhindert.

            Koordinaten-Fix: Längen- und Breitengrade von exakt 0 (Äquator/Nullmeridian) werden nicht mehr als false verworfen.

            Native Logs: Das Log-Level debug wird nun korrekt an die ioBroker-Engine durchgereicht.

            🔒 v2.0.x – Security & Architektur-Refactoring

            2.0.1: Standardisierung des Key-Speicherorts auf 0_userdata.0.wetter_com.info.api_key.

            2.0.0: * Security: API-Key aus dem Klartext-Quellcode entfernt.

            Semaphore (isFetching): Verhindert Race-Conditions und Doppel-Abrufe, wenn Timer und manuelle Trigger gleichzeitig feuern.

            Async-HTTP: httpGet wurde in ein asynchrones Promise gekapselt, um Netzwerkfehler im zentralen try/catch sicher fangen zu können.

            Daily-Reset: Fehleranfälliger Mitternachts-Cron für requests_today wurde durch datumsbasierte, asynchrone Prüfung vor jedem API-Call ersetzt.

            📉 v1.9.3 – Budget-Stabilität

            1.9.3: * Restart-Schutz: Skript-Neustarts lösen keinen API-Call mehr aus, wenn am selben Tag bereits Daten abgerufen wurden (schützt das 100-Calls/Monat Limit bei der Entwicklung).

            Erweiterter Economy-Mode: Unterscheidung der Trigger-Quellen (morning, afternoon, start).

            Notlauf-Modus: Fällt das Budget auf einen kritischen Wert, wird nur noch jeden zweiten Tag aktualisiert.

            Pedder007P 1 Antwort Letzte Antwort
            0
            • nik82N Offline
              nik82N Offline
              nik82
              Most Active
              schrieb am zuletzt editiert von nik82
              #52

              @schimi
              Vielen Dank, was hast du am Limit genau geändert?
              Bei mir läufts einwandfrei, aber gleich in dem Zuge noch eine Frage:
              Wann wird eigentlich immer aktualisiert? Wir haben jetzt 8:22 Uhr und ich habe immer noch den Vortag drin:

              image.png

              Oder ist das genau wie bei dir der Fall das ich jetzt das Limit erreicht habe:

              image.png


              EDIT: Hat sich erledigt dir Frage, bei mir ist auch das Monatslimit erreicht:

              7625b2d6-bedd-4898-845a-46787ff5bf69-image.png

              1 Antwort Letzte Antwort
              0
              • S Online
                S Online
                Schimi
                schrieb am zuletzt editiert von
                #53

                Das sieht nach dem Problem wie bei mir aus...

                Das ist das wichtigste aus dem Changelog:

                1.9.3: *

                • Restart-Schutz: Skript-Neustarts lösen keinen API-Call mehr aus, wenn am selben Tag bereits Daten abgerufen wurden (schützt das 100-Calls/Monat Limit bei der Entwicklung).
                • Erweiterter Economy-Mode: Unterscheidung der Trigger-Quellen (morning, afternoon, start).
                • Notlauf-Modus: Fällt das Budget auf einen kritischen Wert, wird nur noch jeden zweiten Tag aktualisiert.

                Wie gut es klappt sehen wir Ende März ;-)

                Dazu kommt, dass die API und die Tage im Datenpunkt angegeben werden und wenn man dieses ändert, das Skript sich sofort aktualisiert und mit den daten weiterarbeitet.

                1 Antwort Letzte Antwort
                1
                • nik82N Offline
                  nik82N Offline
                  nik82
                  Most Active
                  schrieb am zuletzt editiert von
                  #54

                  Habe gerade auf die neue Version umgestellt, jetzt warten wir mal bis März und schauen weiter.
                  Aber das Script ist auf jedenfall echt Tip Top!

                  1 Antwort Letzte Antwort
                  1
                  • S Schimi

                    Habe mal denn Umgang mit dem Limit optimiert (und dabei etwas weiter gemacht), da ich gestern ins Limit gelaufen bin....

                    die neue Version findet sich im ersten Post im "Spoiler"

                    Falls sich Bugs eingeschlichen haben, solltet ihr vielleicht die letzte 1.9.2 Version erstmal irgendwo zwischenspeichern ;-)

                    Der API-KEY und die anzahl der Tage werden nun auch in Datenpunkten eingegeben...


                    Changelog: Wetter.com Forecast API (v1.9.2 ➔ v2.6.1)

                    Dieser Bericht dokumentiert die Evolution des Skripts von einem einfachen API-Abruf hin zu einer performanten, typsicheren und ausfallsicheren ioBroker-Infrastruktur-Komponente.

                    ⚙️ v2.6.x – Perfektionierung & Usability

                    2.6.1 (Hotfix): * Fix: Konfigurations-Datenpunkte (api_key, forecast_days) werden nun mit write: true angelegt.

                    Retroaktiver Fix: Ein extendObjectAsync entsperrt automatisch Bestandsdatenpunkte, die in Vorversionen als Read-Only angelegt wurden.

                    2.6.0 (Ultra-Performance): * RAM-Cache (ensuredPaths): Eliminiert hunderte synchrone existsObject()-Aufrufe an die JS-Engine. I/O-Overhead beim Struktur-Check sinkt nach dem ersten Lauf auf nahezu null.

                    Zero-Latency: Die künstliche Drosselung wcomWait() wurde vollständig entfernt, da das System durch Caching und Batching überlastungsfrei agiert.

                    Memory-Management: Timeout-Clearing im HTTP-Request integriert, um Speicherlecks zu verhindern.

                    Semantischer Fallback: wind_speed_max fällt auf wind.avg zurück, falls die API sporadisch den Maximalwert weglässt.

                    🚀 v2.5.x – Batching & Deadlock-Schutz

                    2.5.0:

                    State-Write-Batching: Schreibvorgänge (setStateChangedAsync) eines Tages werden in einem internen Puffer gebündelt und per Promise.all simultan abgesetzt.

                    Lock-Härtung: Der HTTP-Call ist nun in ein Promise.race mit einem 10-Sekunden-Timeout gekapselt. Verhindert unendliche Blockaden des isFetching-Locks bei fehlerhaften API-Servern.

                    Type-Safety: Optionale Verkettung (data.hourly ?? []) schützt vor unvollständigen JSON-Antworten.

                    API-Resilienz: Erweiterte isNaN-Prüfung in wcomExtractValue(), um korrupte Strings der API abzufangen.

                    ✨ v2.3.x bis v2.4.x – Dynamisierung

                    2.4.0 (Dynamische Tage): * FORECAST_DAYS aus dem Code in den Datenpunkt info.forecast_days ausgelagert.

                    Auto-Trigger: Das Skript reagiert sofort auf Änderungen dieses Wertes, passt den Abruf an und löscht überschüssige Tagesordner (cleanupObsoleteDays) rekursiv.

                    2.3.0 (API-Key Trigger): * Neuer on()-Trigger für info.api_key. Bei Eingabe eines neuen Keys wird sofort ein Test-Abruf (source: 'key_update') erzwungen, der den regulären Restart-Blocker überspringt.

                    🛡️ v2.1.x bis v2.2.x – Systemhärtung & Controller-Entlastung

                    2.2.0:

                    Zeitzonen-Fix: new Date() statt toISOString() behebt falsche Tageswechsel-Berechnungen auf Systemen mit UTC-Offset.

                    I/O-Reduktion: Einführung von wcomEnsureState() zur Minimierung von createStateAsync-Spam.

                    2.1.0:

                    Zero-Churn (Hourly): Das destruktive, rekursive Löschen (deleteObjectAsync) von Stundenwerten wurde durch sauberes Überschreiben abgelöst. Verhindert massive Object-Events und Controller-Spikes.

                    Null-Safety: wcomExtractValue() gibt strikt 0 statt null zurück, was Typerrors in Number-States verhindert.

                    Koordinaten-Fix: Längen- und Breitengrade von exakt 0 (Äquator/Nullmeridian) werden nicht mehr als false verworfen.

                    Native Logs: Das Log-Level debug wird nun korrekt an die ioBroker-Engine durchgereicht.

                    🔒 v2.0.x – Security & Architektur-Refactoring

                    2.0.1: Standardisierung des Key-Speicherorts auf 0_userdata.0.wetter_com.info.api_key.

                    2.0.0: * Security: API-Key aus dem Klartext-Quellcode entfernt.

                    Semaphore (isFetching): Verhindert Race-Conditions und Doppel-Abrufe, wenn Timer und manuelle Trigger gleichzeitig feuern.

                    Async-HTTP: httpGet wurde in ein asynchrones Promise gekapselt, um Netzwerkfehler im zentralen try/catch sicher fangen zu können.

                    Daily-Reset: Fehleranfälliger Mitternachts-Cron für requests_today wurde durch datumsbasierte, asynchrone Prüfung vor jedem API-Call ersetzt.

                    📉 v1.9.3 – Budget-Stabilität

                    1.9.3: * Restart-Schutz: Skript-Neustarts lösen keinen API-Call mehr aus, wenn am selben Tag bereits Daten abgerufen wurden (schützt das 100-Calls/Monat Limit bei der Entwicklung).

                    Erweiterter Economy-Mode: Unterscheidung der Trigger-Quellen (morning, afternoon, start).

                    Notlauf-Modus: Fällt das Budget auf einen kritischen Wert, wird nur noch jeden zweiten Tag aktualisiert.

                    Pedder007P Offline
                    Pedder007P Offline
                    Pedder007
                    schrieb am zuletzt editiert von
                    #55

                    @Schimi Danke, läuft nun auch bei mir :-)

                    Pedder
                    All @Proxmox/Trixie auf HP Elitedesk 800 G4; Zigbee: ZigStar (LAN), ~110Devices
                    Unifi, Motioneye/3Reolinks, PiHole, Bosch CS7800i via BBQKees/EMS-ESP, Fronius/BYD 11kWp via Modbus
                    Under construction: Smart-WoMo auf Raspi4

                    1 Antwort Letzte Antwort
                    0
                    • nik82N Offline
                      nik82N Offline
                      nik82
                      Most Active
                      schrieb am zuletzt editiert von
                      #56

                      @schimi
                      Hi, hätte eine kurze Frage: Ich hab mit Iobroker wegen anderem Thema jetzt viel rumprobieren müssen und hab dazu immer wieder Iobroker neustarten müssen, deswegen hab ich leider jetzt schon einen "Monatslimit erreicht".
                      Gibts da eine Möglichkeit das zu unterbinden, auch wenn man mal mehr Neustarts macht? :-)
                      Danke im Voraus.

                      HomoranH 1 Antwort Letzte Antwort
                      0
                      • nik82N nik82

                        @schimi
                        Hi, hätte eine kurze Frage: Ich hab mit Iobroker wegen anderem Thema jetzt viel rumprobieren müssen und hab dazu immer wieder Iobroker neustarten müssen, deswegen hab ich leider jetzt schon einen "Monatslimit erreicht".
                        Gibts da eine Möglichkeit das zu unterbinden, auch wenn man mal mehr Neustarts macht? :-)
                        Danke im Voraus.

                        HomoranH Nicht stören
                        HomoranH Nicht stören
                        Homoran
                        schrieb am zuletzt editiert von
                        #57

                        @nik82 sagte in [TypeSkript] Wetter.com Forecast/Vorhersage:

                        Gibts da eine Möglichkeit das zu unterbinden, auch wenn man mal mehr Neustarts macht?

                        hast du vorher die Instanz komplett deaktiviert?

                        kein Support per PN! - Fragen im Forum stellen - Benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.
                        Das Forum freut sich über eine Spende. Benutzt dazu den Spendenbutton oben rechts. Danke!
                        der Installationsfixer: curl -fsL https://iobroker.net/fix.sh | bash -

                        1 Antwort Letzte Antwort
                        0
                        • nik82N Offline
                          nik82N Offline
                          nik82
                          Most Active
                          schrieb am zuletzt editiert von
                          #58

                          Nein da hab ich nicht dran gedacht ehrlich gesagt, aber die Scripts starten ja dann wieder wenn ich die Instanz starte oder?
                          Klar wenn ich mehrere Starts hintereinander mache dann macht das Sinn, aber ich habe mit meiner Testerei immer so dermaßen den iobroker zerschossen, dass ich einfach meine Proxmox VM jedes mal wieder hergestellt habe.
                          Ist auch nicht so wichtig, weil da wird ja nicht so oft so krass dran rumgefummelt, aber wenns im Script nur eine Kleinigkeit ist, dann könnte man das ja eventuell einbauen :-)

                          1 Antwort Letzte Antwort
                          0
                          • S Online
                            S Online
                            Schimi
                            schrieb am zuletzt editiert von
                            #59

                            Deswegen bin ich auch immer ins Limit... aber ausser das du das Skript deaktivierst und erst nach deinen experimenten wieder aktivierst, fällt mir nichts ein....

                            Falls du Skripte testest, könntest du eine zweite JavaSkript Instanz dafür nehmen, wenn diese dann neugestartet wird, bleiben die anderen Skripte unangetastet...

                            ansonsten bleibt nur einen weiteren API key zu beantragen...

                            1 Antwort Letzte Antwort
                            1
                            • nik82N Offline
                              nik82N Offline
                              nik82
                              Most Active
                              schrieb am zuletzt editiert von nik82
                              #60

                              Alles klar, passt, danke dir :-)
                              Ich glaube ich hab mir schonmal einen zweiten Key besorgt, aber der checkt wahrscheinlich die gleiche externe IP und sperrt dann auch wieder, kann das sein?

                              EDIT
                              Hab jetzt grad mal mit einer anderen Emailadresse ein neuen Api Key angefordert und dann eingetragen, log sagt dann aber wieder Monatslimit erreicht.
                              Hab jetzt Fritzbox (aber leider erst nach neuen Api Key Eintrag) gestartet, jetzt hab ich neue externe IP, aber immer noch Limit. Kann das sein?
                              Muss ich evtl. neue externe IP haben und dann erst den neuen Api Key eintragen?

                              1 Antwort Letzte Antwort
                              0
                              • S Online
                                S Online
                                Schimi
                                schrieb am zuletzt editiert von
                                #61

                                wenn du einen eneun API key einträgst, musst du die Datenpunkte auch auf 0 setzen, er fragt die Datenpunkte ab...

                                Die API haut nun (ich meine) einen 404 oder so raus und sagt nicht speziell dass, das Limit voll ist (deswegen zähle ich selber)

                                1 Antwort Letzte Antwort
                                1
                                • nik82N Offline
                                  nik82N Offline
                                  nik82
                                  Most Active
                                  schrieb am zuletzt editiert von
                                  #62

                                  @schimi
                                  So sorry, da hät ich natürlich selber drauf kommen können das ich den Datenpunkt auf 0 setze.
                                  Jetzt gehts wieder. Danke dir. :-)

                                  1 Antwort Letzte Antwort
                                  0
                                  • J Offline
                                    J Offline
                                    Jogis
                                    schrieb am zuletzt editiert von
                                    #63

                                    hallo, ich habe mich auch für den skript entschieden, bis 29.03 hat noch alles gut funktioniert und dann die Meldung "script.js.wetter_com: [Wetter.com] Monatslimit erreicht (100/100). Abruf gestoppt." seit dem funktioniert der Abruf vom Daten nicht. Was muss ich machen, das wieder Daten aktualisiert werden? Grüße

                                    1 Antwort Letzte Antwort
                                    0
                                    • S Online
                                      S Online
                                      Schimi
                                      schrieb am zuletzt editiert von Schimi
                                      #64

                                      @Jogis schaue mal ob du die neuste Version hast....

                                      Dann wäre es vielleicht mal nicht verkehrt, das Skript zu Stoppen > den "wetter_com" Ordner und userdata komplett löschen und das Skript neu starten damit es die Datenpunkte neu anlegt.

                                      Der API-Key wird nun im entsprechenden Datenpunkt gespeichert.....

                                      die aktuelle Version läuft bei mir nun schon "recht lange" fehlerfrei... diesen Monat habe ich auch den iobroker nicht so oft gestartet (dadurch war das 100 Limit kein Problem).
                                      Falls dir immernoch zuviele abfragen angezeigt werden... entweder bis zum 01.05 warten oder einen neuen API key beantragen...

                                      b91cb225-6089-465a-85f4-7ad043fbe411-image.jpeg

                                      1 Antwort Letzte Antwort
                                      0
                                      • J Offline
                                        J Offline
                                        Jogis
                                        schrieb am zuletzt editiert von
                                        #65

                                        Ich habe den Zähler "0_userdata.0.wetter_com.info.requests_month 100" auf 0 gesetzt jetzt kommen Daten wieder, benutze skript 2.6.1, iobroker läuft schon paar Monate und wird nicht neu gestartet. Frage nur warum im Monat April der Zähler nicht auf 0 gestellt war und die Daten nicht aktualisiert wurden? API key ist der gleiche.

                                        1 Antwort Letzte Antwort
                                        0
                                        • S Schimi

                                          [Skript] Wetter.com Forecast API v4.0 (Meteonomiqs) - Update v3.2.1

                                          Dieses performante TypeScript importiert Wetterdaten der neuen Wetter.com API (Meteonomiqs v4.0) ressourcenschonend in den ioBroker.

                                          🌟 Funktionen

                                          • Intelligente Prioritäts-Staffelung (Neu in v3.1.0): Standardmäßig erfolgen 3 Abrufe pro Tag (Nacht, Mittag, Abend). Wird das Monatsbudget gegen Monatsende knapp, greift ein Tier-System: Unwichtigere Abrufe (z.B. Mittag) fallen automatisch weg, um die Grundversorgung (Morgen/Abend) bis zum Monatsende durchgehend zu sichern.

                                          • Tages-Zusammenfassung (Summary): Max/Min Temperaturen, Regenrisiko & -menge, Windböen, Bewölkung und Luftfeuchtigkeit (bis 16 Tage).

                                          • Tagesabschnitte (Spaces): Detaillierte Daten für Vormittag, Nachmittag, Abend und Nacht.

                                          • Stündliche Vorhersage (Hourly): Präzise Stundenwerte für heute und morgen (48h).

                                          • Limit- & Cooldown-Schutz: Ein auf 5 Stunden reduzierter Cooldown verhindert, dass fehlerhafte Skript-Neustarts dein Limit aufbrauchen. Ein erweiterter Startup-Check prüft beim Systemstart direkt, ob deine geplante Update-Taktung überhaupt ins Monatslimit passt.

                                          • Ultra-Performance & Auto-Cleanup: Ein RAM-Cache minimiert die Schreiblast auf deinem System (Zero-Call Rerendering). Bei der Reduzierung der Vorhersage-Tage in den Einstellungen werden veraltete Datenpunkte sofort rekursiv gelöscht.

                                          🛠 Installation

                                          Erstelle im ioBroker ein neues Skript (z.B. im Ordner common).

                                          Wähle oben rechts als Typ zwingend TypeScript (NICHT JavaScript) aus.

                                          Kopiere den Code hinein, speichere und starte das Skript.

                                          ⚙️ Konfiguration

                                          1. Im Skript (CONFIG-Bereich):

                                          UPDATES: Beliebig anpassbare Liste für die Abrufzeiten (inkl. der Prioritäts-Staffelung).

                                          ENABLE_HOURLY / ENABLE_SPACES: Schaltet Detail-Ebenen ein/aus.

                                          LOCATION: Holt Koordinaten standardmäßig aus ioBroker-Settings, kann hier manuell überschrieben werden.

                                          2. Im ioBroker-Objektbaum (0_userdata.0.wetter.info):

                                          api_key: Hier zwingend deinen Meteonomiqs-Key eintragen!

                                          forecast_days: Anzahl der abzurufenden Tage (Standard: 7, max. 14).

                                          force_reset: Button für einen manuellen Reset des Monatszählers.

                                          📝 Hinweise zur VIS & Technik

                                          • Monitoring: Im Ordner info findest du Datenpunkte (Anfragen heute/Monat, letzter Sync), um deinen API-Verbrauch in der VIS zu überwachen.

                                          • Lokalisierung: Wochentage und Wetter-Texte (z.B. "Leicht bewölkt") passen sich dynamisch der ioBroker-Systemsprache an.

                                          • Icons: Zu jedem Wetterzustand wird direkt eine passende, von der API definierte Icon-URL als Datenpunkt ausgeliefert.

                                          • Free API-Key: Hier bei Meteonomiqs anfordern (Das 100 Calls/Monat Free-Tier reicht dank des neuen Tier-Systems völlig aus).

                                          letztes Update: 15.08.2026 - 12:50 Uhr

                                          /**
                                          * [Wetter.com / Meteonomiqs PWA v4.0 -> ioBroker]  (TrueScript / TypeScript)
                                          *
                                          * CHANGELOG:
                                          * - 3.2.1: 2026-08-06 - HOTFIX: 'current' wurde beim Neustart nicht angelegt
                                          *   BUGFIX: wcomUpdateCurrent() lief ausschliesslich am Ende von wcomProcessForecastData().
                                          *           Wurde der Start-Abruf uebersprungen (Cooldown, Budget, fehlender API-Key),
                                          *           entstand der Ordner nie - im Extremfall bis zum naechsten planmaessigen Abruf.
                                          *           Der Aufbau haengt jetzt nicht mehr am Abruf: 'current' wird beim Skriptstart
                                          *           immer erzeugt und aus den bereits vorhandenen Stunden-Datenpunkten gefuellt.
                                          *
                                          * - 3.2.0: 2026-08-06 - CURRENT-ORDNER (aktuelle Stunde)
                                          *   FEATURE: Neuer Zweig '<DP_PATH>.current' mit den Werten der laufenden Stunde -
                                          *            analog zu den Adaptern 'daswetter' und 'open-meteo-weather'.
                                          *            Wird STUENDLICH aktualisiert (Cron :01), nicht nur beim API-Abruf.
                                          *            Quelle sind die bereits vorhandenen Stunden-Datenpunkte, nicht der RAM-Cache:
                                          *            dadurch funktioniert 'current' auch nach einem Skript-Neustart sofort weiter
                                          *            und kostet keinen einzigen zusaetzlichen API-Call.
                                          *            Der Tageswechsel wird ueber 'date_iso' aufgeloest - zwischen 00:00 und dem
                                          *            01:10-Abruf liegt "heute" noch in day_1, danach in day_0.
                                          *   HINWEIS: 'current' ist die VORHERSAGE fuer die laufende Stunde, keine Messung.
                                          *            Echte Momentanwerte liefert /nowcast bzw. /stations - beides kostet je
                                          *            Abfrage einen API-Call und passt damit nicht in ein 100er-Kontingent.
                                          *
                                          * - 3.1.0: 2026-08-06 - DREI ABRUFE PRO TAG MIT PRIORITAETS-STAFFELUNG
                                          *   FEATURE: Abrufzeiten liegen jetzt als Liste in CONFIG.UPDATES - beliebig viele Eintraege.
                                          *            01:10 (Tagesplanung) | 11:40 (Korrektur Nachmittag) | 18:40 (Nacht + Folgetag)
                                          *   FEATURE: Jeder Abruf hat eine 'tier'-Stufe. Wird das Budget knapp, fallen die Abrufe in
                                          *            umgekehrter Wichtigkeit weg (3 -> 2 -> 1/Tag), statt dass das Skript gegen Ende
                                          *            des Monats komplett stehen bleibt.
                                          *   FEATURE: Retry verbraucht keine Notreserve mehr (CONFIG.RESERVE_CALLS).
                                          *   FEATURE: Startup-Check prueft jetzt auch, ob die Taktung ueberhaupt ins Monatsbudget passt.
                                          *   CHANGE:  Cooldown 8 h -> 5 h (kuerzester Abstand der neuen Zeiten betraegt 6,5 h).
                                          *
                                          * - 3.0.0: 2026-08-06 - API-KONFORMITAET, BUGFIXES & DATENERWEITERUNG
                                          *   BUGFIX (kritisch): Cooldown blockierte dauerhaft einen der beiden Abrufe.
                                          *                      12 h Cooldown bei 11 h Cron-Abstand => Nachmittags-Abruf lief NIE.
                                          *                      Neu: 8 h Cooldown + automatische Plausibilitaetspruefung beim Start.
                                          *   BUGFIX (kritisch): Icon wurde selbst gebaut (`d_<state>.svg`). Die API liefert das
                                          *                      korrekte Icon inkl. Nacht- (n_), Unwetter- (d_e_) und Wind-Variante
                                          *                      (d_w_) im Feld `weather.icon`. Wird jetzt uebernommen.
                                          *   BUGFIX: Monats-/Tageszaehler wurden nur per Cron zurueckgesetzt. Lief ioBroker um
                                          *           00:00 am 1. nicht, blieb der Zaehler stehen -> Skript pausierte den ganzen Monat.
                                          *           Neu: Perioden-Marker (info.requests_month_period / _today_date), Reset lazy.
                                          *   BUGFIX: API-Calls wurden nur bei HTTP 200 gezaehlt. 4xx/5xx verbrauchen aber Kontingent.
                                          *           Neu: jede beantwortete Anfrage wird gezaehlt.
                                          *   BUGFIX: `spaces[i]` wurde per Index auf `summary[i]` gemappt. Neu: Zuordnung ueber `date`.
                                          *   BUGFIX: Datums-Strings "YYYY-MM-DD" wurden als UTC-Mitternacht geparst -> in Zeitzonen
                                          *           westlich von UTC um einen Tag verschoben. Neu: lokale Interpretation.
                                          *   BUGFIX: Stunden-Zuordnung basierte auf der Server-Zeitzone. Neu: Zeitzone des Ortes
                                          *           aus `location.timezone` der API (Intl), Fallback auf Serverzeit.
                                          *   BUGFIX: Vergangene Stunden blieben mit veralteten Werten stehen. Neu: `valid`-Flag.
                                          *   FEATURE: Aenderung von `forecast_days` rendert aus dem RAM-Cache - ohne API-Call.
                                          *   FEATURE: Neue Datenpunkte aus der v4.0-Spec (alle aus DERSELBEN Antwort, 0 Extra-Calls):
                                          *            Wetterwarnungen, Astronomie (Sonne/Mond), Luftdruck/Taupunkt/Windchill, Schnee.
                                          *   FEATURE: JSON-Aggregate `forecast_json` / `hourly_json` fuer VIS/jarvis/Material-Widgets.
                                          *   FEATURE: Ein Retry bei Netzwerkfehler/5xx, damit ein Abruffenster nicht verloren geht.
                                          *   REFACTOR: Tabellengetriebene Feld-Definitionen statt duplizierter setState-Bloecke.
                                          *
                                          * KONTEXT:
                                          * - Endpoint: GET https://forecast.meteonomiqs.com/v4_0/forecast/{lat}/{lon}
                                          * - Auth:     Header `x-api-key`, Sprache ueber `Accept-Language`
                                          * - Standort: automatisch aus system.config (Breiten-/Laengengrad des ioBroker-Systems)
                                          *
                                          * ZIELE:
                                          * - Zero-Churn (setStateChanged), Zero-I/O-Overhead (RAM-Cache fuer Objektpfade),
                                          *   typsichere Auswertung, harte Budget-Einhaltung.
                                          */
                                          
                                          // =====================================================================================
                                          // KONFIGURATION
                                          // =====================================================================================
                                          const CONFIG = {
                                             // --- Datenpunkte fuer Benutzereingaben ---
                                             DP_API_KEY: '0_userdata.0.wetter.info.api_key',
                                             DP_FORECAST_DAYS: '0_userdata.0.wetter.info.forecast_days',
                                             DP_FORCE_RESET: '0_userdata.0.wetter.info.force_reset',
                                             DP_FORCE_UPDATE: '0_userdata.0.wetter.info.force_update',
                                          
                                             /**
                                              * --- Zeitsteuerung ---
                                              * Feste Zeiten, keine Randomisierung. Jeder Eintrag hat eine Prioritaetsstufe:
                                              *
                                              *   tier = "wie viele Abrufe pro Tag muss das Restbudget noch tragen,
                                              *           damit dieser Abruf laufen darf?"
                                              *
                                              * Wird das Budget knapp, faellt zuerst tier 3 weg, dann tier 2. tier 1 laeuft
                                              * bis zuletzt (notfalls nur noch jeden zweiten Tag). Dadurch bricht die
                                              * Versorgung nie komplett ab, sie wird nur gröber.
                                              *
                                              *   01:10  tier 1  Tagesplanung        - die vollstaendige Vorhersage fuer den Tag
                                              *   11:40  tier 2  Nachmittags-Korrektur - Gewitter, Boeen, Hitzespitze vor der kritischen Phase
                                              *   18:40  tier 3  Abend + Folgetag    - Nachtfrost, Sturm, Vorschau auf morgen
                                              */
                                             UPDATES: [
                                                 { cron: '10 1 * * *', id: 'nacht', tier: 1, name: 'Nacht (Tagesplanung)' },
                                                 { cron: '40 11 * * *', id: 'mittag', tier: 2, name: 'Mittag (Korrektur Nachmittag)' },
                                                 { cron: '40 18 * * *', id: 'abend', tier: 3, name: 'Abend (Nacht + Folgetag)' }
                                             ] as { cron: string; id: string; tier: number; name: string }[],
                                          
                                             // Cooldown MUSS kleiner sein als der kuerzeste Abstand zwischen zwei Abrufen.
                                             // 01:10 -> 11:40 = 10,5 h | 11:40 -> 18:40 = 7,0 h | 18:40 -> 01:10 = 6,5 h  => 5 h ist sicher.
                                             // Der Startup-Check rechnet das nach und meldet sich, falls es nicht passt.
                                             MIN_HOURS_BETWEEN_UPDATES: 5,
                                          
                                             // Notreserve: so viele Calls bleiben fuer manuelle Aktionen frei. Automatische
                                             // Wiederholungsversuche (Retry) greifen die Reserve nicht an.
                                             RESERVE_CALLS: 5,
                                          
                                             // --- API ---
                                             BASE_URL: 'https://forecast.meteonomiqs.com/v4_0',
                                             ICON_BASE_URL: 'https://cs3.wettercomassets.com/wcomv5/images/icons/weather',
                                             HTTP_TIMEOUT_MS: 20000,
                                             MAX_RETRIES: 1,             // zusaetzliche Versuche bei Netzwerkfehler / HTTP 5xx
                                             RETRY_DELAY_MS: 30000,
                                          
                                             // --- Budget ---
                                             MONTHLY_LIMIT: 100,
                                          
                                             // --- Objektbaum ---
                                             DP_PATH: '0_userdata.0.wetter',
                                             DEFAULT_LANGUAGE: 'de',
                                             MAX_FORECAST_DAYS: 14,      // API liefert max. 14 Tage
                                             CLEANUP_UP_TO_DAY: 25,      // Aufraeumgrenze fuer alte Strukturen
                                          
                                             // --- Umfang der Datenpunkte (alles aus einem einzigen API-Call) ---
                                             ENABLE_SPACES: true,        // Tagesabschnitte morning/afternoon/evening/night
                                             ENABLE_HOURLY: true,        // Stundenwerte
                                             HOURLY_DAYS: 2,             // fuer wie viele Tage (0 = aus, 2 = heute + morgen)
                                             ENABLE_CURRENT: true,       // Ordner 'current' mit den Werten der laufenden Stunde
                                             CURRENT_CRON: '1 * * * *',  // stuendlich um :01 aus den Stunden-Datenpunkten uebernehmen
                                             ENABLE_WARNINGS: true,      // highestWarning (Warngruppe, Schweregrad)
                                             ENABLE_ASTRO: true,         // Sonnenauf-/untergang, Daemmerung, Mond
                                             ENABLE_EXTENDED: true,      // Luftdruck, Taupunkt, Windchill
                                             ENABLE_SNOW: true,          // Schneefallgrenze, Frischschnee, Schneemenge
                                             ENABLE_JSON: true,          // forecast_json / hourly_json fuer VIS-Widgets
                                          
                                             LOG_LEVEL: 'info' as 'debug' | 'info' | 'warn' | 'error',
                                          
                                             LOCATION: {
                                                 LAT: '',
                                                 LON: '',
                                                 FORCE_MANUAL: false
                                             }
                                          };
                                          
                                          // =====================================================================================
                                          // TYPEN
                                          // =====================================================================================
                                          type FetchSource = 'scheduled' | 'start' | 'key_update' | 'days_update' | 'force_reset' | 'manual';
                                          type FieldGroup = 'core' | 'warn' | 'astro' | 'extended' | 'snow';
                                          
                                          interface Ctx {
                                             lang: string;
                                             tz: string;
                                          }
                                          
                                          interface FieldDef {
                                             id: string;
                                             name: string;
                                             type: iobJS.CommonType;
                                             role: string;
                                             unit?: string;
                                             digits?: number;
                                             group?: FieldGroup;
                                             get: (src: any, ctx: Ctx) => any;
                                          }
                                          
                                          interface SystemConfig {
                                             lat: string;
                                             lon: string;
                                             lang: string;
                                          }
                                          
                                          // =====================================================================================
                                          // GLOBALER ZUSTAND
                                          // =====================================================================================
                                          let isFetching = false;
                                          let lastPayload: any = null;          // RAM-Cache der letzten API-Antwort
                                          let lastPayloadTs = 0;
                                          let daysDebounce: any = null;
                                          let authFailed = false;               // Sperre nach HTTP 401/403, bis der Key geaendert wird
                                          const ensuredPaths = new Set<string>();
                                          
                                          // =====================================================================================
                                          // HILFSFUNKTIONEN
                                          // =====================================================================================
                                          
                                          /** Filtert Log-Ausgaben anhand des konfigurierten Log-Levels. */
                                          function wcomLog(msg: string, level: 'debug' | 'info' | 'warn' | 'error' = 'info'): void {
                                             const levels = { debug: 0, info: 1, warn: 2, error: 3 };
                                             if (levels[level] >= levels[CONFIG.LOG_LEVEL]) log(`[Wetter.com] ${msg}`, level);
                                          }
                                          
                                          function wcomPad(n: number | string): string { return String(n).padStart(2, '0'); }
                                          
                                          function wcomSleep(ms: number): Promise<void> {
                                             return new Promise(resolve => setTimeout(resolve, ms));
                                          }
                                          
                                          /** Rundet und faengt NaN/Infinity ab (verhindert Float-Rauschen und damit unnoetige State-Writes). */
                                          function wcomRound(v: number, digits: number = 1): number {
                                             if (typeof v !== 'number' || !isFinite(v)) return 0;
                                             const p = Math.pow(10, digits);
                                             return Math.round(v * p) / p;
                                          }
                                          
                                          /**
                                          * Extrahiert typsicher numerische Werte aus den verschachtelten API-Objekten
                                          * ({value}|{avg}|{sum}|{max}|{min}) und faengt korrupte Rueckgaben ab.
                                          */
                                          function wcomExtractValue(val: any): number {
                                             if (val === null || val === undefined) return 0;
                                             if (typeof val === 'number') return isFinite(val) ? val : 0;
                                             if (typeof val === 'boolean') return val ? 1 : 0;
                                             if (typeof val === 'object') {
                                                 for (const key of ['value', 'avg', 'sum', 'max', 'min']) {
                                                     if (val[key] !== undefined && val[key] !== null) return wcomExtractValue(val[key]);
                                                 }
                                                 return 0;
                                             }
                                             const parsed = parseFloat(String(val));
                                             if (isNaN(parsed)) {
                                                 if (String(val).trim() !== '') wcomLog(`Unerwarteter Nicht-Zahlenwert von API: "${val}"`, 'debug');
                                                 return 0;
                                             }
                                             return parsed;
                                          }
                                          
                                          /**
                                          * Parst API-Datumsangaben. "YYYY-MM-DD" wird bewusst LOKAL interpretiert -
                                          * `new Date('2026-08-06')` waere UTC-Mitternacht und in Zeitzonen westlich von
                                          * UTC damit der Vortag.
                                          */
                                          function wcomParseApiDate(input: string | Date): Date | null {
                                             if (!input) return null;
                                             if (input instanceof Date) return isNaN(input.getTime()) ? null : input;
                                             const s = String(input).trim();
                                             const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(s);
                                             const d = m ? new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])) : new Date(s);
                                             return isNaN(d.getTime()) ? null : d;
                                          }
                                          
                                          /** Formatiert nach DD.MM.YYYY. */
                                          function wcomFormatDate(input: string | Date): string {
                                             const d = wcomParseApiDate(input);
                                             if (!d) return '';
                                             return `${wcomPad(d.getDate())}.${wcomPad(d.getMonth() + 1)}.${d.getFullYear()}`;
                                          }
                                          
                                          /** Ausgeschriebener Wochentag in der Systemsprache. */
                                          function wcomGetDayName(dateStr: string, locale: string): string {
                                             const d = wcomParseApiDate(dateStr);
                                             if (!d) return '';
                                             try { return d.toLocaleDateString(locale, { weekday: 'long' }); }
                                             catch (e) { return d.toLocaleDateString('de', { weekday: 'long' }); }
                                          }
                                          
                                          /**
                                          * Rechnet einen UTC-Zeitstempel in die Ortszeit des Vorhersageortes um.
                                          * Nutzt die von der API gelieferte `location.timezone` - dadurch stimmen die
                                          * Stunden-Datenpunkte auch dann, wenn der ioBroker-Server in einer anderen
                                          * Zeitzone laeuft als der abgefragte Ort.
                                          */
                                          function wcomLocalParts(utcIso: string, tz: string): { date: string; hour: string } | null {
                                             const d = utcIso ? new Date(utcIso) : null;
                                             if (!d || isNaN(d.getTime())) return null;
                                             try {
                                                 const parts: any = {};
                                                 const fmt = new Intl.DateTimeFormat('en-GB', {
                                                     timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit',
                                                     hour: '2-digit', hourCycle: 'h23'
                                                 });
                                                 for (const p of fmt.formatToParts(d)) parts[p.type] = p.value;
                                                 const hour = parts.hour === '24' ? '00' : parts.hour;
                                                 return { date: `${parts.year}-${parts.month}-${parts.day}`, hour: hour };
                                             } catch (e) {
                                                 return {
                                                     date: `${d.getFullYear()}-${wcomPad(d.getMonth() + 1)}-${wcomPad(d.getDate())}`,
                                                     hour: wcomPad(d.getHours())
                                                 };
                                             }
                                          }
                                          
                                          /** Zieht "HH:MM" aus einem ISO-String mit Offset (z. B. 2026-08-06T06:17:49+02:00). */
                                          function wcomIsoTime(iso: any): string {
                                             const m = /T(\d{2}):(\d{2})/.exec(String(iso || ''));
                                             return m ? `${m[1]}:${m[2]}` : '';
                                          }
                                          
                                          /** Tageslaenge in Stunden aus Sonnenauf- und -untergang. */
                                          function wcomDayLength(sunrise: any, sunset: any): number {
                                             const a = new Date(String(sunrise || '')); const b = new Date(String(sunset || ''));
                                             if (isNaN(a.getTime()) || isNaN(b.getTime())) return 0;
                                             return wcomRound((b.getTime() - a.getTime()) / 3600000, 2);
                                          }
                                          
                                          /**
                                          * Liefert die vollstaendige Icon-URL. Bevorzugt das von der API gelieferte
                                          * Dateifeld - dieses beruecksichtigt Tag/Nacht (d_/n_), Unwetterwarnung (d_e_)
                                          * und Windwarnung (d_w_). Nur als Fallback wird aus dem State gebaut.
                                          */
                                          function wcomIconUrl(weather: any, isNight?: boolean): string {
                                             const raw = weather && typeof weather.icon === 'string' ? weather.icon.trim() : '';
                                             const file = raw !== '' ? raw : `${isNight ? 'n' : 'd'}_${weather && weather.state !== undefined && weather.state !== null ? weather.state : 999}.svg`;
                                             return `${CONFIG.ICON_BASE_URL}/${file}`;
                                          }
                                          
                                          /** Wolkenbedeckung in Prozent. Stuendlich liefert die API Oktas (`eights`). */
                                          function wcomCloudsPercent(clouds: any): number {
                                             if (clouds === null || clouds === undefined) return 0;
                                             if (typeof clouds === 'number') return clouds;
                                             if (typeof clouds.avg === 'number') return clouds.avg;
                                             if (typeof clouds.eights === 'number') return wcomRound((clouds.eights / 8) * 100, 1);
                                             if (typeof clouds.low === 'number' || typeof clouds.middle === 'number' || typeof clouds.high === 'number') {
                                                 return Math.max(Number(clouds.low) || 0, Number(clouds.middle) || 0, Number(clouds.high) || 0);
                                             }
                                             return wcomExtractValue(clouds);
                                          }
                                          
                                          const MOON_PHASE: Record<string, string[]> = {
                                             de: ['', 'Neumond', 'Zunehmender Sichelmond', 'Zunehmender Halbmond', 'Zunehmender Dreiviertelmond',
                                                 'Vollmond', 'Abnehmender Dreiviertelmond', 'Abnehmender Halbmond', 'Abnehmender Sichelmond'],
                                             en: ['', 'New Moon', 'Waxing Crescent', 'First Quarter', 'Waxing Gibbous',
                                                 'Full Moon', 'Waning Gibbous', 'Last Quarter', 'Waning Crescent']
                                          };
                                          const MOON_ZODIAC: Record<string, string[]> = {
                                             de: ['', 'Widder', 'Stier', 'Zwillinge', 'Krebs', 'Löwe', 'Jungfrau', 'Waage', 'Skorpion', 'Schütze', 'Steinbock', 'Wassermann', 'Fische'],
                                             en: ['', 'Aries', 'Taurus', 'Gemini', 'Cancer', 'Leo', 'Virgo', 'Libra', 'Scorpio', 'Sagittarius', 'Capricorn', 'Aquarius', 'Pisces']
                                          };
                                          
                                          function wcomLookup(table: Record<string, string[]>, lang: string, idx: any): string {
                                             const list = table[String(lang).slice(0, 2).toLowerCase()] || table.de;
                                             const i = Number(idx);
                                             return (Number.isInteger(i) && i > 0 && i < list.length) ? list[i] : '';
                                          }
                                          
                                          /** Prueft, ob eine Feldgruppe laut CONFIG angelegt werden soll. */
                                          function wcomGroupEnabled(group?: FieldGroup): boolean {
                                             switch (group) {
                                                 case 'warn': return CONFIG.ENABLE_WARNINGS;
                                                 case 'astro': return CONFIG.ENABLE_ASTRO;
                                                 case 'extended': return CONFIG.ENABLE_EXTENDED;
                                                 case 'snow': return CONFIG.ENABLE_SNOW;
                                                 default: return true;
                                             }
                                          }
                                          
                                          // =====================================================================================
                                          // FELD-DEFINITIONEN (tabellengetrieben statt duplizierter setState-Bloecke)
                                          // =====================================================================================
                                          
                                          const DAY_FIELDS: FieldDef[] = [
                                             { id: 'date', name: 'Datum', type: 'string', role: 'text', get: d => wcomFormatDate(d.date) },
                                             { id: 'date_iso', name: 'Datum (ISO)', type: 'string', role: 'date', get: d => d.date || '' },
                                             { id: 'day_name', name: 'Wochentag', type: 'string', role: 'text', get: (d, c) => wcomGetDayName(d.date, c.lang) },
                                             { id: 'temp_max', name: 'Max. Temperatur', type: 'number', role: 'value.temperature.max', unit: '°C', digits: 1, get: d => d.temperature?.max },
                                             { id: 'temp_min', name: 'Min. Temperatur', type: 'number', role: 'value.temperature.min', unit: '°C', digits: 1, get: d => d.temperature?.min },
                                             { id: 'temp_avg', name: 'Durchschnittstemperatur', type: 'number', role: 'value.temperature', unit: '°C', digits: 1, get: d => d.temperature?.avg },
                                             { id: 'weather_state', name: 'Wetter-Code', type: 'number', role: 'value', digits: 0, get: d => d.weather?.state },
                                             { id: 'weather_text', name: 'Wetter', type: 'string', role: 'weather.state', get: d => d.weather?.text },
                                             { id: 'weather_icon', name: 'Icon URL', type: 'string', role: 'weather.icon', get: d => wcomIconUrl(d.weather, false) },
                                             { id: 'prec_probability', name: 'Regenrisiko', type: 'number', role: 'value.precipitation.probability', unit: '%', digits: 0, get: d => d.prec?.probability },
                                             { id: 'prec_sum', name: 'Niederschlagsmenge', type: 'number', role: 'value.precipitation', unit: 'mm', digits: 2, get: d => d.prec?.sum },
                                             { id: 'prec_class', name: 'Niederschlagsklasse (0-3)', type: 'number', role: 'value', digits: 0, get: d => d.prec?.class },
                                             { id: 'rainy_hours', name: 'Regenstunden', type: 'number', role: 'value', unit: 'h', digits: 1, get: d => d.rainyHours },
                                             { id: 'sun_hours', name: 'Sonnenstunden', type: 'number', role: 'value.sun', unit: 'h', digits: 1, get: d => d.sunHours },
                                             { id: 'clouds', name: 'Bewölkung', type: 'number', role: 'value.clouds', unit: '%', digits: 1, get: d => wcomCloudsPercent(d.clouds) },
                                             { id: 'humidity', name: 'Relative Feuchte', type: 'number', role: 'value.humidity', unit: '%', digits: 0, get: d => d.relativeHumidity },
                                             { id: 'wind_speed', name: 'Windgeschwindigkeit (Ø)', type: 'number', role: 'value.speed.wind', unit: 'km/h', digits: 1, get: d => d.wind?.avg },
                                             { id: 'wind_speed_max', name: 'Max. Windgeschwindigkeit', type: 'number', role: 'value.speed.wind.max', unit: 'km/h', digits: 1, get: d => d.wind?.max ?? d.wind?.avg },
                                             { id: 'wind_gusts', name: 'Windböen', type: 'number', role: 'value.speed.wind.gust', unit: 'km/h', digits: 1, get: d => d.wind?.gusts },
                                             { id: 'wind_direction', name: 'Windrichtung', type: 'string', role: 'weather.direction', get: d => d.wind?.direction },
                                             { id: 'wind_direction_short', name: 'Windrichtung (kurz)', type: 'string', role: 'weather.direction', get: d => d.wind?.text },
                                             { id: 'wind_degree', name: 'Windrichtung (Grad)', type: 'number', role: 'value.direction.wind', unit: '°', digits: 0, get: d => d.wind?.degree },
                                          
                                             // --- Erweiterte meteorologische Werte ---
                                             { id: 'pressure', name: 'Luftdruck', type: 'number', role: 'value.pressure', unit: 'hPa', digits: 0, group: 'extended', get: d => d.pressure },
                                             { id: 'windchill_min', name: 'Gefühlt min.', type: 'number', role: 'value.temperature.min', unit: '°C', digits: 1, group: 'extended', get: d => d.windchill?.min },
                                             { id: 'windchill_max', name: 'Gefühlt max.', type: 'number', role: 'value.temperature.max', unit: '°C', digits: 1, group: 'extended', get: d => d.windchill?.max },
                                          
                                             // --- Schnee ---
                                             { id: 'snow_sum', name: 'Schnee (Wasseräquivalent)', type: 'number', role: 'value.precipitation', unit: 'mm', digits: 2, group: 'snow', get: d => d.prec?.snow },
                                             { id: 'fresh_snow_min', name: 'Neuschnee min.', type: 'number', role: 'value', unit: 'cm', digits: 1, group: 'snow', get: d => d.prec?.freshSnowRangeMin },
                                             { id: 'fresh_snow_max', name: 'Neuschnee max.', type: 'number', role: 'value', unit: 'cm', digits: 1, group: 'snow', get: d => d.prec?.freshSnowRangeMax },
                                             { id: 'snowline_min', name: 'Schneefallgrenze min.', type: 'number', role: 'value', unit: 'm', digits: 0, group: 'snow', get: d => d.snowLine?.min },
                                             { id: 'snowline_max', name: 'Schneefallgrenze max.', type: 'number', role: 'value', unit: 'm', digits: 0, group: 'snow', get: d => d.snowLine?.max },
                                          
                                             // --- Wetterwarnung ---
                                             { id: 'warn_active', name: 'Warnung aktiv', type: 'boolean', role: 'indicator.alarm', group: 'warn', get: d => !!d.highestWarning },
                                             { id: 'warn_group', name: 'Warnung Gruppe', type: 'string', role: 'text', group: 'warn', get: d => d.highestWarning?.group },
                                             { id: 'warn_text', name: 'Warnung Text', type: 'string', role: 'text', group: 'warn', get: d => d.highestWarning?.text },
                                             { id: 'warn_severity', name: 'Warnung Schweregrad', type: 'string', role: 'text', group: 'warn', get: d => d.highestWarning?.severity },
                                             { id: 'warn_severity_int', name: 'Warnung Schweregrad (0-4)', type: 'number', role: 'value.severity', digits: 0, group: 'warn', get: d => d.highestWarning?.severityInt }
                                          ];
                                          
                                          const ASTRO_FIELDS: FieldDef[] = [
                                             { id: 'sunrise', name: 'Sonnenaufgang', type: 'string', role: 'date.sunrise', get: a => wcomIsoTime(a.sunrise) },
                                             { id: 'sunset', name: 'Sonnenuntergang', type: 'string', role: 'date.sunset', get: a => wcomIsoTime(a.sunset) },
                                             { id: 'dawn', name: 'Morgendämmerung', type: 'string', role: 'text', get: a => wcomIsoTime(a.dawn) },
                                             { id: 'dusk', name: 'Abenddämmerung', type: 'string', role: 'text', get: a => wcomIsoTime(a.dusk) },
                                             { id: 'suntransit', name: 'Sonnenhöchststand', type: 'string', role: 'text', get: a => wcomIsoTime(a.suntransit) },
                                             { id: 'sunrise_iso', name: 'Sonnenaufgang (ISO)', type: 'string', role: 'date.sunrise', get: a => a.sunrise || '' },
                                             { id: 'sunset_iso', name: 'Sonnenuntergang (ISO)', type: 'string', role: 'date.sunset', get: a => a.sunset || '' },
                                             { id: 'day_length', name: 'Tageslänge', type: 'number', role: 'value', unit: 'h', digits: 2, get: a => wcomDayLength(a.sunrise, a.sunset) },
                                             { id: 'moonrise', name: 'Mondaufgang', type: 'string', role: 'text', get: a => wcomIsoTime(a.moonrise) },
                                             { id: 'moonset', name: 'Monduntergang', type: 'string', role: 'text', get: a => wcomIsoTime(a.moonset) },
                                             { id: 'moonphase', name: 'Mondphase (1-8)', type: 'number', role: 'value', digits: 0, get: a => a.moonphase },
                                             { id: 'moonphase_text', name: 'Mondphase', type: 'string', role: 'text', get: (a, c) => wcomLookup(MOON_PHASE, c.lang, a.moonphase) },
                                             { id: 'moonzodiac', name: 'Mond-Tierkreiszeichen (1-12)', type: 'number', role: 'value', digits: 0, get: a => a.moonzodiac },
                                             { id: 'moonzodiac_text', name: 'Mond-Tierkreiszeichen', type: 'string', role: 'text', get: (a, c) => wcomLookup(MOON_ZODIAC, c.lang, a.moonzodiac) }
                                          ];
                                          
                                          const SPACE_FIELDS: FieldDef[] = [
                                             { id: 'temp', name: 'Temperatur', type: 'number', role: 'value.temperature', unit: '°C', digits: 1, get: s => s.temperature?.avg ?? s.temperature },
                                             { id: 'temp_min', name: 'Temperatur min.', type: 'number', role: 'value.temperature.min', unit: '°C', digits: 1, get: s => s.temperature?.min },
                                             { id: 'temp_max', name: 'Temperatur max.', type: 'number', role: 'value.temperature.max', unit: '°C', digits: 1, get: s => s.temperature?.max },
                                             { id: 'weather_state', name: 'Wetter-Code', type: 'number', role: 'value', digits: 0, get: s => s.weather?.state },
                                             { id: 'text', name: 'Wetter', type: 'string', role: 'weather.state', get: s => s.weather?.text },
                                             { id: 'icon', name: 'Icon URL', type: 'string', role: 'weather.icon', get: s => wcomIconUrl(s.weather, !!s.isNight) },
                                             { id: 'is_night', name: 'Nacht', type: 'boolean', role: 'indicator', get: s => !!s.isNight },
                                             { id: 'prec_prob', name: 'Regenrisiko', type: 'number', role: 'value.precipitation.probability', unit: '%', digits: 0, get: s => s.prec?.probability },
                                             { id: 'prec_sum', name: 'Niederschlagsmenge', type: 'number', role: 'value.precipitation', unit: 'mm', digits: 2, get: s => s.prec?.sum },
                                             { id: 'wind_speed', name: 'Windgeschwindigkeit', type: 'number', role: 'value.speed.wind', unit: 'km/h', digits: 1, get: s => s.wind?.avg },
                                             { id: 'wind_gusts', name: 'Windböen', type: 'number', role: 'value.speed.wind.gust', unit: 'km/h', digits: 1, get: s => s.wind?.gusts },
                                             { id: 'wind_direction_short', name: 'Windrichtung (kurz)', type: 'string', role: 'weather.direction', get: s => s.wind?.text },
                                             { id: 'wind_degree', name: 'Windrichtung (Grad)', type: 'number', role: 'value.direction.wind', unit: '°', digits: 0, get: s => s.wind?.degree },
                                             { id: 'clouds', name: 'Bewölkung', type: 'number', role: 'value.clouds', unit: '%', digits: 1, get: s => wcomCloudsPercent(s.clouds) },
                                             { id: 'humidity', name: 'Relative Feuchte', type: 'number', role: 'value.humidity', unit: '%', digits: 0, get: s => s.relativeHumidity },
                                             { id: 'sun_hours', name: 'Sonnenstunden', type: 'number', role: 'value.sun', unit: 'h', digits: 1, get: s => s.sunHours },
                                          
                                             { id: 'pressure', name: 'Luftdruck', type: 'number', role: 'value.pressure', unit: 'hPa', digits: 0, group: 'extended', get: s => s.pressure },
                                             { id: 'windchill_min', name: 'Gefühlt min.', type: 'number', role: 'value.temperature.min', unit: '°C', digits: 1, group: 'extended', get: s => s.windchill?.min },
                                             { id: 'windchill_max', name: 'Gefühlt max.', type: 'number', role: 'value.temperature.max', unit: '°C', digits: 1, group: 'extended', get: s => s.windchill?.max },
                                          
                                             { id: 'snowline_min', name: 'Schneefallgrenze min.', type: 'number', role: 'value', unit: 'm', digits: 0, group: 'snow', get: s => s.snowLine?.min },
                                             { id: 'snowline_max', name: 'Schneefallgrenze max.', type: 'number', role: 'value', unit: 'm', digits: 0, group: 'snow', get: s => s.snowLine?.max },
                                             { id: 'fresh_snow_max', name: 'Neuschnee max.', type: 'number', role: 'value', unit: 'cm', digits: 1, group: 'snow', get: s => s.prec?.freshSnowRangeMax },
                                          
                                             { id: 'warn_active', name: 'Warnung aktiv', type: 'boolean', role: 'indicator.alarm', group: 'warn', get: s => !!s.highestWarning },
                                             { id: 'warn_text', name: 'Warnung Text', type: 'string', role: 'text', group: 'warn', get: s => s.highestWarning?.text },
                                             { id: 'warn_severity_int', name: 'Warnung Schweregrad (0-4)', type: 'number', role: 'value.severity', digits: 0, group: 'warn', get: s => s.highestWarning?.severityInt }
                                          ];
                                          
                                          const HOUR_FIELDS: FieldDef[] = [
                                             { id: 'time', name: 'Uhrzeit (Ortszeit)', type: 'string', role: 'text', get: h => h.__time },
                                             { id: 'from', name: 'Zeitstempel (UTC)', type: 'string', role: 'date', get: h => h.from || h.dateWithTimezone || '' },
                                             { id: 'valid', name: 'Wert aktuell', type: 'boolean', role: 'indicator', get: () => true },
                                             { id: 'temp', name: 'Temperatur', type: 'number', role: 'value.temperature', unit: '°C', digits: 1, get: h => h.temperature },
                                             { id: 'windchill', name: 'Gefühlt', type: 'number', role: 'value.temperature', unit: '°C', digits: 1, get: h => h.windchill },
                                             { id: 'weather_state', name: 'Wetter-Code', type: 'number', role: 'value', digits: 0, get: h => h.weather?.state },
                                             { id: 'weather_text', name: 'Wetter', type: 'string', role: 'weather.state', get: h => h.weather?.text },
                                             { id: 'weather_icon', name: 'Icon URL', type: 'string', role: 'weather.icon', get: h => wcomIconUrl(h.weather, !!h.isNight) },
                                             { id: 'is_night', name: 'Nacht', type: 'boolean', role: 'indicator', get: h => !!h.isNight },
                                             { id: 'prec_prob', name: 'Regenwahrscheinlichkeit', type: 'number', role: 'value.precipitation.probability', unit: '%', digits: 0, get: h => h.prec?.probability },
                                             { id: 'prec_sum', name: 'Niederschlagsmenge', type: 'number', role: 'value.precipitation', unit: 'mm', digits: 2, get: h => h.prec?.sum },
                                             { id: 'wind_speed', name: 'Windgeschwindigkeit', type: 'number', role: 'value.speed.wind', unit: 'km/h', digits: 1, get: h => h.wind?.avg },
                                             { id: 'wind_gusts', name: 'Windböen', type: 'number', role: 'value.speed.wind.gust', unit: 'km/h', digits: 1, get: h => h.wind?.gusts },
                                             // ACHTUNG: 'wind_dir' bleibt bewusst die Langform ("Westwind") wie in v2.7.0 -
                                             // bestehende VIS-Bindings brechen dadurch nicht. Die Kurzform kommt zusaetzlich dazu.
                                             { id: 'wind_dir', name: 'Windrichtung', type: 'string', role: 'weather.direction', get: h => h.wind?.direction },
                                             { id: 'wind_dir_short', name: 'Windrichtung (kurz)', type: 'string', role: 'weather.direction', get: h => h.wind?.text },
                                             { id: 'wind_degree', name: 'Windrichtung (Grad)', type: 'number', role: 'value.direction.wind', unit: '°', digits: 0, get: h => h.wind?.degree },
                                             { id: 'humidity', name: 'Relative Feuchte', type: 'number', role: 'value.humidity', unit: '%', digits: 0, get: h => h.relativeHumidity },
                                             { id: 'clouds', name: 'Bewölkung', type: 'number', role: 'value.clouds', unit: '%', digits: 1, get: h => wcomCloudsPercent(h.clouds) },
                                             { id: 'sun_hours', name: 'Sonnenstunden', type: 'number', role: 'value.sun', unit: 'h', digits: 2, get: h => h.sunHours },
                                          
                                             { id: 'pressure', name: 'Luftdruck', type: 'number', role: 'value.pressure', unit: 'hPa', digits: 0, group: 'extended', get: h => h.pressure },
                                             { id: 'dewpoint', name: 'Taupunkt', type: 'number', role: 'value.temperature.dewpoint', unit: '°C', digits: 1, group: 'extended', get: h => h.dewpoint },
                                          
                                             { id: 'fresh_snow', name: 'Neuschnee', type: 'number', role: 'value', unit: 'cm', digits: 1, group: 'snow', get: h => h.freshSnow },
                                             { id: 'snowline', name: 'Schneefallgrenze', type: 'number', role: 'value', unit: 'm', digits: 0, group: 'snow', get: h => h.prec?.snowLine },
                                          
                                             { id: 'warn_active', name: 'Warnung aktiv', type: 'boolean', role: 'indicator.alarm', group: 'warn', get: h => !!h.highestWarning },
                                             { id: 'warn_text', name: 'Warnung Text', type: 'string', role: 'text', group: 'warn', get: h => h.highestWarning?.text },
                                             { id: 'warn_severity_int', name: 'Warnung Schweregrad (0-4)', type: 'number', role: 'value.severity', digits: 0, group: 'warn', get: h => h.highestWarning?.severityInt }
                                          ];
                                          
                                          // =====================================================================================
                                          // OBJEKTBAUM
                                          // =====================================================================================
                                          
                                          /** Legt Ordner (device/channel) an. Nutzt den RAM-Cache, um I/O zu vermeiden. */
                                          async function wcomEnsureSubStructure(path: string, name: string, type: 'device' | 'channel' = 'channel'): Promise<void> {
                                             if (!path || ensuredPaths.has(path)) return;
                                             if (!existsObject(path)) {
                                                 await extendObjectAsync(path, { type: type, common: { name: name }, native: {} });
                                             }
                                             ensuredPaths.add(path);
                                          }
                                          
                                          /** Legt einen Datenpunkt an. Nutzt den RAM-Cache, um I/O zu vermeiden. */
                                          async function wcomEnsureState(path: string, init: any, type: iobJS.CommonType, name: string, role: string, unit?: string, writeable: boolean = false): Promise<void> {
                                             if (ensuredPaths.has(path)) return;
                                             if (!existsObject(path)) {
                                                 await createStateAsync(path, init, false, { name, type, role, unit: unit || '', read: true, write: writeable } as any);
                                             }
                                             ensuredPaths.add(path);
                                          }
                                          
                                          function wcomInitValue(type: iobJS.CommonType): any {
                                             return type === 'number' ? 0 : (type === 'boolean' ? false : '');
                                          }
                                          
                                          function wcomCoerce(raw: any, f: FieldDef): any {
                                             if (f.type === 'number') return wcomRound(wcomExtractValue(raw), f.digits === undefined ? 1 : f.digits);
                                             if (f.type === 'boolean') return !!raw;
                                             return raw === null || raw === undefined ? '' : String(raw);
                                          }
                                          
                                          /**
                                          * Legt alle Datenpunkte einer Feldtabelle an und puffert die Schreibvorgaenge.
                                          * Ein einziger Ort fuer Anlegen + Schreiben - dadurch koennen Definition und
                                          * Schreibvorgang nicht mehr auseinanderlaufen.
                                          */
                                          async function wcomRenderFields(basePath: string, labelPrefix: string, fields: FieldDef[], src: any, ctx: Ctx, buffer: Promise<any>[]): Promise<void> {
                                             const active = fields.filter(f => wcomGroupEnabled(f.group));
                                          
                                             await Promise.all(active.map(f =>
                                                 wcomEnsureState(`${basePath}.${f.id}`, wcomInitValue(f.type), f.type, `${labelPrefix}${f.name}`, f.role, f.unit)
                                             ));
                                          
                                             for (const f of active) {
                                                 let raw: any;
                                                 try { raw = f.get(src, ctx); }
                                                 catch (e) { raw = null; wcomLog(`Feld '${f.id}' konnte nicht gelesen werden: ${e}`, 'debug'); }
                                                 buffer.push(setStateChangedAsync(`${basePath}.${f.id}`, wcomCoerce(raw, f), true));
                                             }
                                          }
                                          
                                          // =====================================================================================
                                          // SYSTEM / STANDORT
                                          // =====================================================================================
                                          
                                          /** Liest Koordinaten und Sprache aus den ioBroker-Systemeinstellungen. */
                                          async function wcomGetSystemSettings(): Promise<SystemConfig | null> {
                                             if (CONFIG.LOCATION.FORCE_MANUAL) {
                                                 const lat = parseFloat(CONFIG.LOCATION.LAT);
                                                 const lon = parseFloat(CONFIG.LOCATION.LON);
                                                 if (isNaN(lat) || isNaN(lon)) {
                                                     wcomLog('FORCE_MANUAL aktiv, aber die manuellen Koordinaten sind ungültig.', 'error');
                                                     return null;
                                                 }
                                                 const sysLang = await wcomGetSystemLanguage();
                                                 return { lat: lat.toFixed(3), lon: lon.toFixed(3), lang: sysLang };
                                             }
                                          
                                             const obj: any = await getObjectAsync('system.config');
                                             const common = obj && obj.common ? obj.common : null;
                                             if (!common || common.latitude === undefined || common.latitude === null || common.longitude === undefined || common.longitude === null) {
                                                 wcomLog('Kein Standort in den ioBroker-Systemeinstellungen hinterlegt (Einstellungen -> System -> Standort).', 'error');
                                                 return null;
                                             }
                                          
                                             const lat = parseFloat(String(common.latitude));
                                             const lon = parseFloat(String(common.longitude));
                                             if (isNaN(lat) || isNaN(lon)) {
                                                 wcomLog('Standort in system.config ist nicht numerisch.', 'error');
                                                 return null;
                                             }
                                          
                                             return { lat: lat.toFixed(3), lon: lon.toFixed(3), lang: common.language || CONFIG.DEFAULT_LANGUAGE };
                                          }
                                          
                                          async function wcomGetSystemLanguage(): Promise<string> {
                                             try {
                                                 const obj: any = await getObjectAsync('system.config');
                                                 return (obj && obj.common && obj.common.language) || CONFIG.DEFAULT_LANGUAGE;
                                             } catch (e) { return CONFIG.DEFAULT_LANGUAGE; }
                                          }
                                          
                                          // =====================================================================================
                                          // HTTP
                                          // =====================================================================================
                                          
                                          /** HTTP GET mit hartem Timeout - verhindert einen Deadlock im isFetching-Lock. */
                                          async function wcomHttpGetAsync(url: string, options: any): Promise<any> {
                                             let timeoutId: any = null;
                                          
                                             const fetchPromise = new Promise((resolve, reject) => {
                                                 httpGet(url, options, (err: any, response: any) => {
                                                     if (err) reject(err); else resolve(response);
                                                 });
                                             });
                                          
                                             const timeoutPromise = new Promise((_, reject) => {
                                                 timeoutId = setTimeout(() => reject(new Error(`HTTP Timeout nach ${CONFIG.HTTP_TIMEOUT_MS / 1000} s`)), CONFIG.HTTP_TIMEOUT_MS + 2000);
                                             });
                                          
                                             try {
                                                 return await Promise.race([fetchPromise, timeoutPromise]);
                                             } finally {
                                                 if (timeoutId) clearTimeout(timeoutId);
                                             }
                                          }
                                          
                                          // =====================================================================================
                                          // BUDGET & ZAEHLER
                                          // =====================================================================================
                                          
                                          function wcomMonthKey(d: Date): string { return `${d.getFullYear()}-${wcomPad(d.getMonth() + 1)}`; }
                                          function wcomDayKey(d: Date): string { return `${d.getFullYear()}-${wcomPad(d.getMonth() + 1)}-${wcomPad(d.getDate())}`; }
                                          
                                          async function wcomGetNum(id: string, fallback = 0): Promise<number> {
                                             const s = await getStateAsync(id);
                                             return s && s.val !== null && s.val !== undefined ? Number(s.val) : fallback;
                                          }
                                          
                                          /**
                                          * Setzt Monats-/Tageszaehler anhand von Perioden-Markern zurueck.
                                          * Robuster als ein reiner Cron: laeuft ioBroker zum Umschaltzeitpunkt nicht,
                                          * wird beim naechsten Aufruf trotzdem korrekt zurueckgesetzt.
                                          */
                                          async function wcomRollPeriods(): Promise<void> {
                                             const now = new Date();
                                          
                                             const monthKey = wcomMonthKey(now);
                                             const storedMonth = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_month_period`);
                                             const storedMonthVal = storedMonth && storedMonth.val ? String(storedMonth.val) : '';
                                             if (storedMonthVal !== monthKey) {
                                                 if (storedMonthVal) wcomLog(`Monatswechsel erkannt (${storedMonthVal} -> ${monthKey}). Monatszähler zurückgesetzt.`, 'info');
                                                 await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month`, 0, true);
                                                 await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month_period`, monthKey, true);
                                             }
                                          
                                             const dayKey = wcomDayKey(now);
                                             const storedDay = await getStateAsync(`${CONFIG.DP_PATH}.info.requests_today_date`);
                                             const storedDayVal = storedDay && storedDay.val ? String(storedDay.val) : '';
                                             if (storedDayVal !== dayKey) {
                                                 await setStateAsync(`${CONFIG.DP_PATH}.info.requests_today`, 0, true);
                                                 await setStateAsync(`${CONFIG.DP_PATH}.info.requests_today_date`, dayKey, true);
                                             }
                                          }
                                          
                                          /** Hoechste konfigurierte Prioritaetsstufe = Abrufe pro Tag im Vollbetrieb. */
                                          function wcomMaxTier(): number {
                                             return CONFIG.UPDATES.reduce((m, u) => Math.max(m, u.tier), 1);
                                          }
                                          
                                          /**
                                          * Prueft Monatsbudget, Cooldown und die gestaffelte Sparlogik.
                                          *
                                          * @param source Ursprung des Abrufs.
                                          * @param tier   Prioritaetsstufe (nur bei automatischen Abrufen relevant).
                                          *               Ein Abruf der Stufe N laeuft nur, wenn das Restbudget noch
                                          *               N Abrufe pro Tag bis Monatsende traegt.
                                          * @param label  Klartextname fuer die Logausgabe.
                                          */
                                          async function wcomCheckBudget(source: FetchSource, tier: number = 0, label: string = ''): Promise<boolean> {
                                             await wcomRollPeriods();
                                          
                                             const currentUsage = await wcomGetNum(`${CONFIG.DP_PATH}.info.requests_month`, 0);
                                          
                                             if (currentUsage >= CONFIG.MONTHLY_LIMIT) {
                                                 wcomLog(`Monatslimit erreicht (${currentUsage}/${CONFIG.MONTHLY_LIMIT}). Pausiert bis zum 01. des Folgemonats.`, 'warn');
                                                 await setStateAsync(`${CONFIG.DP_PATH}.info.status`, 'Monatslimit erreicht', true);
                                                 return false;
                                             }
                                          
                                             const automatic = (source === 'scheduled' || source === 'start');
                                             if (!automatic) return true;  // manuelle Aktionen: nur das harte Limit gilt
                                          
                                             // Cooldown schuetzt vor Skript-Neustart-Schleifen.
                                             const lastTs = await wcomGetNum(`${CONFIG.DP_PATH}.info.last_sync_ts`, 0);
                                             if (lastTs > 0) {
                                                 const diffHours = (Date.now() - lastTs) / 3600000;
                                                 if (diffHours < CONFIG.MIN_HOURS_BETWEEN_UPDATES) {
                                                     wcomLog(`Abruf übersprungen (Cooldown). Letzter Abruf vor ${diffHours.toFixed(1)} h, Minimum ${CONFIG.MIN_HOURS_BETWEEN_UPDATES} h.`, 'debug');
                                                     return false;
                                                 }
                                             }
                                          
                                             // Ein Skriptstart wird wie der niedrigst priorisierte Abruf behandelt: er darf
                                             // nie die Reserve anknabbern, die die planmaessigen Abrufe am Leben haelt.
                                             const effTier = tier > 0 ? tier : wcomMaxTier();
                                          
                                             const now = new Date();
                                             const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
                                             const daysLeft = daysInMonth - now.getDate();      // volle Tage nach heute
                                             const needed = effTier * (daysLeft + 1);           // effTier Abrufe/Tag inkl. heute
                                          
                                             if (currentUsage + needed <= CONFIG.MONTHLY_LIMIT) return true;
                                          
                                             if (effTier > 1) {
                                                 wcomLog(`SPARMODUS: "${label || source}" übersprungen. Das Restbudget trägt keine ${effTier} Abrufe/Tag mehr bis Monatsende (Verbrauch ${currentUsage}/${CONFIG.MONTHLY_LIMIT}, ${daysLeft + 1} Tage offen).`, 'warn');
                                                 return false;
                                             }
                                          
                                             // Stufe 1 ist der letzte verbliebene Abruf: statt ganz auszusetzen nur noch
                                             // jeden zweiten Tag - lieber gröber als gar nicht.
                                             if (now.getDate() % 2 !== 0) {
                                                 wcomLog(`NOTLAUF: "${label || source}" übersprungen (Budget kritisch, Abruf nur noch an geraden Tagen). Verbrauch ${currentUsage}/${CONFIG.MONTHLY_LIMIT}.`, 'warn');
                                                 return false;
                                             }
                                          
                                             return true;
                                          }
                                          
                                          /**
                                          * Zaehlt eine beantwortete API-Anfrage gegen das Kontingent.
                                          * Bewusst auch bei Fehlerantworten (ausser 401/403) - diese verbrauchen beim
                                          * Anbieter ebenfalls Kontingent. In v2.7.0 wurde nur HTTP 200 gezaehlt, dadurch
                                          * konnte der lokale Zaehler dauerhaft vom echten Verbrauch abweichen.
                                          */
                                          async function wcomCountRequest(): Promise<void> {
                                             await setStateAsync(`${CONFIG.DP_PATH}.info.requests_today`, (await wcomGetNum(`${CONFIG.DP_PATH}.info.requests_today`)) + 1, true);
                                             const month = (await wcomGetNum(`${CONFIG.DP_PATH}.info.requests_month`)) + 1;
                                             await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month`, month, true);
                                             await setStateAsync(`${CONFIG.DP_PATH}.info.requests_left`, Math.max(0, CONFIG.MONTHLY_LIMIT - month), true);
                                          }
                                          
                                          /** Schreibt die Sync-Zeitstempel nach einem erfolgreichen Abruf. */
                                          async function wcomUpdateSyncInfo(): Promise<void> {
                                             const now = new Date();
                                             const ts = `${wcomPad(now.getDate())}.${wcomPad(now.getMonth() + 1)}.${now.getFullYear()} ${wcomPad(now.getHours())}:${wcomPad(now.getMinutes())}`;
                                             await setStateAsync(`${CONFIG.DP_PATH}.info.last_sync`, ts, true);
                                             await setStateAsync(`${CONFIG.DP_PATH}.info.last_sync_ts`, now.getTime(), true);
                                          }
                                          
                                          // =====================================================================================
                                          // INITIALISIERUNG DER INFO-DATENPUNKTE
                                          // =====================================================================================
                                          async function wcomEnsureBaseStructure(): Promise<void> {
                                             await wcomEnsureSubStructure(CONFIG.DP_PATH, 'Wetter.com Forecast', 'device');
                                             await wcomEnsureSubStructure(`${CONFIG.DP_PATH}.info`, 'Informationen');
                                          
                                             await Promise.all([
                                                 wcomEnsureState(`${CONFIG.DP_PATH}.info.last_sync`, '', 'string', 'Letztes Update', 'text'),
                                                 wcomEnsureState(`${CONFIG.DP_PATH}.info.last_sync_ts`, 0, 'number', 'Letztes Update (Timestamp)', 'value.time'),
                                                 wcomEnsureState(`${CONFIG.DP_PATH}.info.requests_month`, 0, 'number', 'Anfragen Monat', 'value'),
                                                 wcomEnsureState(`${CONFIG.DP_PATH}.info.requests_month_period`, '', 'string', 'Abrechnungsmonat', 'text'),
                                                 wcomEnsureState(`${CONFIG.DP_PATH}.info.requests_today`, 0, 'number', 'Anfragen heute', 'value'),
                                                 wcomEnsureState(`${CONFIG.DP_PATH}.info.requests_today_date`, '', 'string', 'Zähltag', 'text'),
                                                 wcomEnsureState(`${CONFIG.DP_PATH}.info.requests_left`, CONFIG.MONTHLY_LIMIT, 'number', 'Verbleibende Anfragen', 'value'),
                                                 wcomEnsureState(`${CONFIG.DP_PATH}.info.status`, '', 'string', 'Status', 'text'),
                                                 wcomEnsureState(`${CONFIG.DP_PATH}.info.last_error`, '', 'string', 'Letzter Fehler', 'text'),
                                                 wcomEnsureState(`${CONFIG.DP_PATH}.info.forecast_date`, '', 'string', 'Vorhersage erstellt (API)', 'date'),
                                                 wcomEnsureState(`${CONFIG.DP_PATH}.info.next_update`, '', 'string', 'Nächstes API-Update', 'date'),
                                                 wcomEnsureState(`${CONFIG.DP_PATH}.info.location`, '', 'string', 'Verwendeter Standort', 'text'),
                                                 wcomEnsureState(`${CONFIG.DP_PATH}.info.timezone`, '', 'string', 'Zeitzone des Standorts', 'text'),
                                                 wcomEnsureState(`${CONFIG.DP_PATH}.info.elevation`, 0, 'number', 'Höhe über NN', 'value', 'm'),
                                                 // Benutzereingaben
                                                 wcomEnsureState(CONFIG.DP_API_KEY, '', 'string', 'Wetter.com API Key', 'text', '', true),
                                                 wcomEnsureState(CONFIG.DP_FORECAST_DAYS, 7, 'number', 'Vorhersage Tage', 'value', '', true),
                                                 wcomEnsureState(CONFIG.DP_FORCE_RESET, false, 'boolean', 'Monatszähler zurücksetzen', 'button', '', true),
                                                 wcomEnsureState(CONFIG.DP_FORCE_UPDATE, false, 'boolean', 'Jetzt aktualisieren', 'button', '', true)
                                             ]);
                                          
                                             if (CONFIG.ENABLE_JSON) {
                                                 await Promise.all([
                                                     wcomEnsureState(`${CONFIG.DP_PATH}.forecast_json`, '[]', 'string', 'Vorhersage (JSON)', 'json'),
                                                     wcomEnsureState(`${CONFIG.DP_PATH}.hourly_json`, '[]', 'string', 'Stundenwerte (JSON)', 'json')
                                                 ]);
                                             }
                                          }
                                          
                                          // =====================================================================================
                                          // HAUPTLOGIK
                                          // =====================================================================================
                                          
                                          async function wcomFetchWeatherData(source: FetchSource = 'start', tier: number = 0, label: string = ''): Promise<void> {
                                             if (isFetching) {
                                                 wcomLog('Abruf läuft bereits (Lock aktiv). Abbruch.', 'debug');
                                                 return;
                                             }
                                             isFetching = true;
                                          
                                             try {
                                                 await wcomEnsureBaseStructure();
                                          
                                                 // Schreibrechte auf den Eingabe-Datenpunkten sicherstellen (Migration alter Installationen)
                                                 if (source === 'start') {
                                                     await extendObjectAsync(CONFIG.DP_API_KEY, { common: { write: true } });
                                                     await extendObjectAsync(CONFIG.DP_FORECAST_DAYS, { common: { write: true } });
                                                 }
                                          
                                                 const apiKeyState = await getStateAsync(CONFIG.DP_API_KEY);
                                                 const apiKey = apiKeyState && apiKeyState.val ? String(apiKeyState.val).trim() : '';
                                                 if (apiKey.length < 10) {
                                                     wcomLog(`Bitte einen gültigen API-Key in '${CONFIG.DP_API_KEY}' eintragen.`, 'error');
                                                     await setStateAsync(`${CONFIG.DP_PATH}.info.status`, 'Kein API-Key', true);
                                                     return;
                                                 }
                                          
                                                 if (authFailed && (source === 'start' || source === 'scheduled')) {
                                                     wcomLog('Abruf übersprungen: der API-Key wurde zuletzt abgelehnt. Bitte Key korrigieren.', 'warn');
                                                     return;
                                                 }
                                          
                                                 const forecastDays = await wcomGetForecastDays();
                                          
                                                 if (!(await wcomCheckBudget(source, tier, label))) return;
                                          
                                                 const settings = await wcomGetSystemSettings();
                                                 if (!settings) {
                                                     await setStateAsync(`${CONFIG.DP_PATH}.info.status`, 'Kein Standort', true);
                                                     return;
                                                 }
                                          
                                                 const url = `${CONFIG.BASE_URL}/forecast/${settings.lat}/${settings.lon}`;
                                                 const options = {
                                                     timeout: CONFIG.HTTP_TIMEOUT_MS,
                                                     headers: { 'x-api-key': apiKey, 'Accept-Language': settings.lang }
                                                 };
                                          
                                                 wcomLog(`Abruf gestartet (${label || source}) für ${settings.lat}/${settings.lon}, ${forecastDays} Tage.`, 'info');
                                          
                                                 let data: any = null;
                                          
                                                 // Wiederholungsversuche duerfen die Notreserve nicht aufbrauchen.
                                                 const mayRetry = async () => (await wcomGetNum(`${CONFIG.DP_PATH}.info.requests_left`, CONFIG.MONTHLY_LIMIT)) > CONFIG.RESERVE_CALLS;
                                          
                                                 for (let attempt = 0; attempt <= CONFIG.MAX_RETRIES; attempt++) {
                                                     let response: any = null;
                                                     let netError: any = null;
                                          
                                                     try { response = await wcomHttpGetAsync(url, options); }
                                                     catch (e) { netError = e; }
                                          
                                                     if (netError) {
                                                         const retry = attempt < CONFIG.MAX_RETRIES && await mayRetry();
                                                         wcomLog(`Netzwerkfehler: ${netError.message || netError}`, retry ? 'warn' : 'error');
                                                         await setStateAsync(`${CONFIG.DP_PATH}.info.last_error`, String(netError.message || netError), true);
                                                         if (retry) { await wcomSleep(CONFIG.RETRY_DELAY_MS); continue; }
                                                         await setStateAsync(`${CONFIG.DP_PATH}.info.status`, 'Netzwerkfehler', true);
                                                         return;
                                                     }
                                          
                                                     const code = response ? Number(response.statusCode) : 0;
                                          
                                                     // 401/403 werden vom Gateway vor der Verrechnung abgewiesen und daher nicht gezaehlt.
                                                     if (code !== 401 && code !== 403) await wcomCountRequest();
                                          
                                                     if (code === 429) {
                                                         wcomLog('API-Kontingent erschöpft (HTTP 429). Zähler wird auf das Limit gesetzt.', 'error');
                                                         await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month`, CONFIG.MONTHLY_LIMIT, true);
                                                         await setStateAsync(`${CONFIG.DP_PATH}.info.status`, 'HTTP 429 - Limit erschöpft', true);
                                                         return;
                                                     }
                                                     if (code === 401 || code === 403) {
                                                         // Sperre setzen, damit wiederholte Skript-Neustarts nicht dauerhaft
                                                         // gegen die API laufen. Wird beim Ändern des API-Keys aufgehoben.
                                                         authFailed = true;
                                                         wcomLog(`API-Key abgelehnt (HTTP ${code}). Automatische Abrufe pausiert, bis der Key geändert wird.`, 'error');
                                                         await setStateAsync(`${CONFIG.DP_PATH}.info.status`, `HTTP ${code} - Key ungültig`, true);
                                                         return;
                                                     }
                                                     if (code >= 500) {
                                                         const retry = attempt < CONFIG.MAX_RETRIES && await mayRetry();
                                                         wcomLog(`Serverfehler HTTP ${code}.`, retry ? 'warn' : 'error');
                                                         if (retry) { await wcomSleep(CONFIG.RETRY_DELAY_MS); continue; }
                                                         await setStateAsync(`${CONFIG.DP_PATH}.info.status`, `HTTP ${code}`, true);
                                                         return;
                                                     }
                                                     if (code !== 200) {
                                                         wcomLog(`Unerwarteter Statuscode HTTP ${code}.`, 'error');
                                                         await setStateAsync(`${CONFIG.DP_PATH}.info.status`, `HTTP ${code}`, true);
                                                         return;
                                                     }
                                          
                                                     try {
                                                         data = typeof response.data === 'string' ? JSON.parse(response.data) : response.data;
                                                     } catch (e) {
                                                         wcomLog('API-Antwort konnte nicht als JSON gelesen werden.', 'error');
                                                         await setStateAsync(`${CONFIG.DP_PATH}.info.status`, 'JSON-Parsefehler', true);
                                                         return;
                                                     }
                                                     break;
                                                 }
                                          
                                                 if (!data || !Array.isArray(data.summary) || data.summary.length === 0) {
                                                     wcomLog('API-Antwort enthält keine Vorhersagedaten.', 'error');
                                                     await setStateAsync(`${CONFIG.DP_PATH}.info.status`, 'Leere Antwort', true);
                                                     return;
                                                 }
                                          
                                                 lastPayload = data;
                                                 lastPayloadTs = Date.now();
                                                 authFailed = false;
                                          
                                                 await wcomUpdateSyncInfo();
                                                 await wcomProcessForecastData(data, settings.lang, forecastDays);
                                                 await setStateAsync(`${CONFIG.DP_PATH}.info.status`, 'OK', true);
                                                 await setStateAsync(`${CONFIG.DP_PATH}.info.last_error`, '', true);
                                          
                                             } catch (e: any) {
                                                 wcomLog(`Script-Fehler: ${e && e.message ? e.message : e}`, 'error');
                                                 try { await setStateAsync(`${CONFIG.DP_PATH}.info.last_error`, String(e && e.message ? e.message : e), true); } catch (ignore) { /* noop */ }
                                             } finally {
                                                 isFetching = false;
                                             }
                                          }
                                          
                                          async function wcomGetForecastDays(): Promise<number> {
                                             const raw = await wcomGetNum(CONFIG.DP_FORECAST_DAYS, 7);
                                             return Math.max(1, Math.min(Math.round(raw) || 7, CONFIG.MAX_FORECAST_DAYS));
                                          }
                                          
                                          // =====================================================================================
                                          // VERARBEITUNG
                                          // =====================================================================================
                                          
                                          async function wcomProcessForecastData(data: any, lang: string, forecastDays: number): Promise<void> {
                                             const tz = (data.location && data.location.timezone) ? String(data.location.timezone) : Intl.DateTimeFormat().resolvedOptions().timeZone;
                                             const ctx: Ctx = { lang: lang, tz: tz };
                                          
                                             await wcomEnsureSubStructure(CONFIG.DP_PATH, 'Wetter.com Forecast', 'device');
                                          
                                             // Metadaten aus der Antwort
                                             await Promise.all([
                                                 setStateChangedAsync(`${CONFIG.DP_PATH}.info.forecast_date`, String(data.forecastDate || ''), true),
                                                 setStateChangedAsync(`${CONFIG.DP_PATH}.info.next_update`, String(data.nextUpdate || ''), true),
                                                 setStateChangedAsync(`${CONFIG.DP_PATH}.info.timezone`, tz, true),
                                                 setStateChangedAsync(`${CONFIG.DP_PATH}.info.location`,
                                                     data.location && data.location.coordinates
                                                         ? `${data.location.coordinates.latitude}, ${data.location.coordinates.longitude}` : '', true),
                                                 setStateChangedAsync(`${CONFIG.DP_PATH}.info.elevation`,
                                                     wcomExtractValue(data.location && data.location.coordinates ? data.location.coordinates.elevation : 0), true)
                                             ]);
                                          
                                             const summary: any[] = Array.isArray(data.summary) ? data.summary : [];
                                             const maxDays = Math.min(summary.length, forecastDays);
                                          
                                             // Tagesabschnitte ueber das Datum zuordnen statt ueber den Array-Index.
                                             const spacesByDate = new Map<string, any>();
                                             if (Array.isArray(data.spaces)) {
                                                 for (const s of data.spaces) if (s && s.date) spacesByDate.set(String(s.date), s);
                                             }
                                          
                                             // Stundenwerte nach Ortsdatum gruppieren.
                                             const hourlyByDate = new Map<string, any[]>();
                                             if (Array.isArray(data.hourly)) {
                                                 for (const h of data.hourly) {
                                                     const parts = wcomLocalParts(h.from || h.dateWithTimezone || h.date, tz);
                                                     const key = parts ? parts.date : String(h.date || '');
                                                     if (!key) continue;
                                                     h.__hour = parts ? parts.hour : '00';
                                                     h.__time = `${h.__hour}:00`;
                                                     if (!hourlyByDate.has(key)) hourlyByDate.set(key, []);
                                                     hourlyByDate.get(key)!.push(h);
                                                 }
                                             }
                                          
                                             const jsonDays: any[] = [];
                                             const jsonHours: any[] = [];
                                             let totalWrites = 0;
                                          
                                             for (let i = 0; i < maxDays; i++) {
                                                 const day = summary[i];
                                                 if (!day) continue;
                                          
                                                 const dayKey = String(day.date || '');
                                                 const dayPath = `${CONFIG.DP_PATH}.day_${i}`;
                                                 const buffer: Promise<any>[] = [];
                                          
                                                 await wcomEnsureSubStructure(dayPath, `Tag ${i}`);
                                                 await wcomRenderFields(dayPath, `Tag ${i}: `, DAY_FIELDS, day, ctx, buffer);
                                          
                                                 // --- Astronomie ---
                                                 if (CONFIG.ENABLE_ASTRO && day.astronomy) {
                                                     const astroPath = `${dayPath}.astro`;
                                                     await wcomEnsureSubStructure(astroPath, 'Sonne & Mond');
                                                     await wcomRenderFields(astroPath, `Tag ${i}: `, ASTRO_FIELDS, day.astronomy, ctx, buffer);
                                                 }
                                          
                                                 // --- Tagesabschnitte ---
                                                 if (CONFIG.ENABLE_SPACES) {
                                                     const spaceDay = spacesByDate.get(dayKey);
                                                     if (spaceDay) {
                                                         const spacesPath = `${dayPath}.spaces`;
                                                         await wcomEnsureSubStructure(spacesPath, 'Tagesabschnitte');
                                                         const labels: Record<string, string> = { morning: 'Morgen', afternoon: 'Mittag', evening: 'Abend', night: 'Nacht' };
                                                         for (const seg of ['morning', 'afternoon', 'evening', 'night']) {
                                                             const sData = spaceDay[seg];
                                                             if (!sData) continue;
                                                             const sPath = `${spacesPath}.${seg}`;
                                                             await wcomEnsureSubStructure(sPath, labels[seg]);
                                                             await wcomRenderFields(sPath, `Tag ${i} ${labels[seg]}: `, SPACE_FIELDS, sData, ctx, buffer);
                                                         }
                                                     } else {
                                                         wcomLog(`Keine Tagesabschnitte für ${dayKey} in der API-Antwort.`, 'debug');
                                                     }
                                                 }
                                          
                                                 // --- Stundenwerte ---
                                                 if (CONFIG.ENABLE_HOURLY && i < CONFIG.HOURLY_DAYS) {
                                                     const hourlyPath = `${dayPath}.hourly`;
                                                     await wcomEnsureSubStructure(hourlyPath, 'Stündlich');
                                                     const hours = hourlyByDate.get(dayKey) || [];
                                                     const delivered = new Set<string>();
                                          
                                                     for (const h of hours) {
                                                         const hPath = `${hourlyPath}.${h.__hour}`;
                                                         delivered.add(h.__hour);
                                                         await wcomEnsureSubStructure(hPath, `${h.__hour}:00 Uhr`);
                                                         await wcomRenderFields(hPath, `Tag ${i} ${h.__hour}:00: `, HOUR_FIELDS, h, ctx, buffer);
                                          
                                                         if (CONFIG.ENABLE_JSON && i === 0) {
                                                             jsonHours.push({
                                                                 date: dayKey, time: h.__time, from: h.from || '',
                                                                 temp: wcomRound(wcomExtractValue(h.temperature), 1),
                                                                 icon: wcomIconUrl(h.weather, !!h.isNight),
                                                                 text: h.weather?.text || '',
                                                                 precProb: wcomRound(wcomExtractValue(h.prec?.probability), 0),
                                                                 precSum: wcomRound(wcomExtractValue(h.prec?.sum), 2),
                                                                 wind: wcomRound(wcomExtractValue(h.wind?.avg), 1),
                                                                 gusts: wcomRound(wcomExtractValue(h.wind?.gusts), 1),
                                                                 humidity: wcomRound(wcomExtractValue(h.relativeHumidity), 0)
                                                             });
                                                         }
                                                     }
                                          
                                                     // Bereits vergangene Stunden liefert die API nicht mehr mit:
                                                     // Werte stehen lassen, aber als nicht mehr aktuell markieren.
                                                     for (let hh = 0; hh < 24; hh++) {
                                                         const label = wcomPad(hh);
                                                         if (delivered.has(label)) continue;
                                                         const validPath = `${hourlyPath}.${label}.valid`;
                                                         if (existsObject(validPath)) buffer.push(setStateChangedAsync(validPath, false, true));
                                                     }
                                                 }
                                          
                                                 // --- JSON-Aggregat ---
                                                 if (CONFIG.ENABLE_JSON) {
                                                     jsonDays.push({
                                                         day: i,
                                                         date: wcomFormatDate(day.date),
                                                         dateIso: dayKey,
                                                         dayName: wcomGetDayName(day.date, lang),
                                                         tempMin: wcomRound(wcomExtractValue(day.temperature?.min), 1),
                                                         tempMax: wcomRound(wcomExtractValue(day.temperature?.max), 1),
                                                         text: day.weather?.text || '',
                                                         icon: wcomIconUrl(day.weather, false),
                                                         state: wcomRound(wcomExtractValue(day.weather?.state), 0),
                                                         precProb: wcomRound(wcomExtractValue(day.prec?.probability), 0),
                                                         precSum: wcomRound(wcomExtractValue(day.prec?.sum), 2),
                                                         wind: wcomRound(wcomExtractValue(day.wind?.avg), 1),
                                                         gusts: wcomRound(wcomExtractValue(day.wind?.gusts), 1),
                                                         windDir: day.wind?.text || '',
                                                         sunHours: wcomRound(wcomExtractValue(day.sunHours), 1),
                                                         clouds: wcomRound(wcomCloudsPercent(day.clouds), 0),
                                                         humidity: wcomRound(wcomExtractValue(day.relativeHumidity), 0),
                                                         pressure: wcomRound(wcomExtractValue(day.pressure), 0),
                                                         sunrise: wcomIsoTime(day.astronomy?.sunrise),
                                                         sunset: wcomIsoTime(day.astronomy?.sunset),
                                                         warnActive: !!day.highestWarning,
                                                         warnText: day.highestWarning?.text || '',
                                                         warnSeverity: wcomRound(wcomExtractValue(day.highestWarning?.severityInt), 0)
                                                     });
                                                 }
                                          
                                                 totalWrites += buffer.length;
                                                 await Promise.all(buffer);
                                             }
                                          
                                             if (CONFIG.ENABLE_JSON) {
                                                 await Promise.all([
                                                     setStateChangedAsync(`${CONFIG.DP_PATH}.forecast_json`, JSON.stringify(jsonDays), true),
                                                     setStateChangedAsync(`${CONFIG.DP_PATH}.hourly_json`, JSON.stringify(jsonHours), true)
                                                 ]);
                                             }
                                          
                                             await wcomCleanupObsoleteDays(maxDays);
                                             await wcomUpdateCurrent('nach Abruf');
                                             wcomLog(`Update abgeschlossen: ${maxDays} Tage, ${totalWrites} Werte verarbeitet.`, 'info');
                                          }
                                          
                                          // =====================================================================================
                                          // CURRENT - Werte der laufenden Stunde
                                          // =====================================================================================
                                          
                                          /** Zusatzfelder in 'current', die nicht aus den Stundendaten stammen. */
                                          const CURRENT_EXTRA: { id: string; name: string; type: iobJS.CommonType; role: string; unit?: string; from: string; astro?: boolean }[] = [
                                             { id: 'date', name: 'Datum', type: 'string', role: 'text', from: 'date' },
                                             { id: 'day_name', name: 'Wochentag', type: 'string', role: 'text', from: 'day_name' },
                                             { id: 'temp_min_today', name: 'Min. Temperatur heute', type: 'number', role: 'value.temperature.min', unit: '°C', from: 'temp_min' },
                                             { id: 'temp_max_today', name: 'Max. Temperatur heute', type: 'number', role: 'value.temperature.max', unit: '°C', from: 'temp_max' },
                                             { id: 'sunrise', name: 'Sonnenaufgang', type: 'string', role: 'date.sunrise', from: 'astro.sunrise', astro: true },
                                             { id: 'sunset', name: 'Sonnenuntergang', type: 'string', role: 'date.sunset', from: 'astro.sunset', astro: true }
                                          ];
                                          
                                          function wcomCurrentFields(): FieldDef[] {
                                             return HOUR_FIELDS.filter(f => wcomGroupEnabled(f.group));
                                          }
                                          
                                          async function wcomEnsureCurrentStates(): Promise<void> {
                                             const base = `${CONFIG.DP_PATH}.current`;
                                             // Wurzel mit absichern: 'current' kann vor dem ersten erfolgreichen Abruf angelegt werden.
                                             await wcomEnsureSubStructure(CONFIG.DP_PATH, 'Wetter.com Forecast', 'device');
                                             await wcomEnsureSubStructure(base, 'Aktuelle Stunde');
                                          
                                             await Promise.all([
                                                 ...wcomCurrentFields().map(f =>
                                                     wcomEnsureState(`${base}.${f.id}`, wcomInitValue(f.type), f.type, `Jetzt: ${f.name}`, f.role, f.unit)),
                                                 ...CURRENT_EXTRA.filter(e => !e.astro || CONFIG.ENABLE_ASTRO).map(e =>
                                                     wcomEnsureState(`${base}.${e.id}`, wcomInitValue(e.type), e.type, `Jetzt: ${e.name}`, e.role, e.unit)),
                                                 wcomEnsureState(`${base}.source`, '', 'string', 'Jetzt: Quell-Datenpunkt', 'text'),
                                                 wcomEnsureState(`${base}.updated`, '', 'string', 'Jetzt: zuletzt übernommen', 'text')
                                             ]);
                                          }
                                          
                                          /** Kopiert einen Datenpunktwert, sofern die Quelle existiert. */
                                          async function wcomCopyState(from: string, to: string, buffer: Promise<any>[]): Promise<void> {
                                             if (!existsObject(from)) return;
                                             const s = await getStateAsync(from);
                                             if (!s || s.val === null || s.val === undefined) return;
                                             buffer.push(setStateChangedAsync(to, s.val as any, true));
                                          }
                                          
                                          /**
                                          * Fuellt '<DP_PATH>.current' mit den Werten der laufenden Stunde.
                                          *
                                          * Quelle sind bewusst die bereits geschriebenen Stunden-Datenpunkte und nicht der
                                          * RAM-Cache: so liefert 'current' auch direkt nach einem Skript-Neustart wieder
                                          * Werte, ohne dass dafuer ein API-Call faellig wird.
                                          *
                                          * Die Tageszuordnung laeuft ueber 'date_iso' statt ueber einen festen Index -
                                          * zwischen 00:00 und dem 01:10-Abruf steht der heutige Tag noch in day_1.
                                          */
                                          async function wcomUpdateCurrent(reason: string = 'stündlich'): Promise<void> {
                                             if (!CONFIG.ENABLE_CURRENT) return;
                                          
                                             if (!CONFIG.ENABLE_HOURLY || CONFIG.HOURLY_DAYS < 1) {
                                                 wcomLog("ENABLE_CURRENT benötigt ENABLE_HOURLY mit HOURLY_DAYS >= 1. 'current' bleibt leer.", 'warn');
                                                 return;
                                             }
                                          
                                             try {
                                                 await wcomEnsureCurrentStates();
                                                 const base = `${CONFIG.DP_PATH}.current`;
                                          
                                                 const tzState = await getStateAsync(`${CONFIG.DP_PATH}.info.timezone`);
                                                 const tz = tzState && tzState.val ? String(tzState.val) : Intl.DateTimeFormat().resolvedOptions().timeZone;
                                                 const now = wcomLocalParts(new Date().toISOString(), tz);
                                                 if (!now) return;
                                          
                                                 // Tagesordner suchen, dessen Datum dem heutigen Ortsdatum entspricht
                                                 let dayIdx = -1;
                                                 for (let i = 0; i < CONFIG.HOURLY_DAYS; i++) {
                                                     const st = await getStateAsync(`${CONFIG.DP_PATH}.day_${i}.date_iso`);
                                                     if (st && String(st.val) === now.date) { dayIdx = i; break; }
                                                 }
                                          
                                                 const buffer: Promise<any>[] = [];
                                                 const srcHour = dayIdx >= 0 ? `${CONFIG.DP_PATH}.day_${dayIdx}.hourly.${now.hour}` : '';
                                          
                                                 if (!srcHour || !existsObject(`${srcHour}.time`)) {
                                                     buffer.push(setStateChangedAsync(`${base}.valid`, false, true));
                                                     await Promise.all(buffer);
                                                     wcomLog(`current: keine Stundendaten für ${now.date} ${now.hour}:00 vorhanden.`, 'debug');
                                                     return;
                                                 }
                                          
                                                 const srcDay = `${CONFIG.DP_PATH}.day_${dayIdx}`;
                                          
                                                 for (const f of wcomCurrentFields()) {
                                                     await wcomCopyState(`${srcHour}.${f.id}`, `${base}.${f.id}`, buffer);
                                                 }
                                                 for (const e of CURRENT_EXTRA) {
                                                     if (e.astro && !CONFIG.ENABLE_ASTRO) continue;
                                                     await wcomCopyState(`${srcDay}.${e.from}`, `${base}.${e.id}`, buffer);
                                                 }
                                          
                                                 const ts = new Date();
                                                 buffer.push(
                                                     setStateChangedAsync(`${base}.source`, srcHour, true),
                                                     setStateAsync(`${base}.updated`,
                                                         `${wcomPad(ts.getDate())}.${wcomPad(ts.getMonth() + 1)}.${ts.getFullYear()} ${wcomPad(ts.getHours())}:${wcomPad(ts.getMinutes())}`, true)
                                                 );
                                          
                                                 await Promise.all(buffer);
                                                 wcomLog(`current aktualisiert (${reason}): ${now.date} ${now.hour}:00 aus day_${dayIdx}.`, 'debug');
                                          
                                             } catch (e: any) {
                                                 wcomLog(`current konnte nicht aktualisiert werden: ${e && e.message ? e.message : e}`, 'error');
                                             }
                                          }
                                          
                                          /** Entfernt Tagesordner, die durch eine reduzierte Vorhersagedauer obsolet sind. */
                                          async function wcomCleanupObsoleteDays(activeDays: number): Promise<void> {
                                             for (let i = activeDays; i <= CONFIG.CLEANUP_UP_TO_DAY; i++) {
                                                 const path = `${CONFIG.DP_PATH}.day_${i}`;
                                                 if (existsObject(path)) {
                                                     await deleteObjectAsync(path, true);
                                                     // Cache-Eintraege des geloeschten Teilbaums verwerfen
                                                     for (const p of Array.from(ensuredPaths)) if (p === path || p.indexOf(`${path}.`) === 0) ensuredPaths.delete(p);
                                                     wcomLog(`Obsoleten Tagesordner entfernt: day_${i}`, 'debug');
                                                 }
                                             }
                                          }
                                          
                                          /** Rendert die zuletzt geholte Antwort neu - ohne API-Call. */
                                          async function wcomRerenderFromCache(reason: string): Promise<boolean> {
                                             if (!lastPayload) return false;
                                             if (isFetching) return false;
                                             isFetching = true;
                                             try {
                                                 const settings = await wcomGetSystemSettings();
                                                 const lang = settings ? settings.lang : CONFIG.DEFAULT_LANGUAGE;
                                                 const days = await wcomGetForecastDays();
                                                 wcomLog(`${reason}: Neuaufbau aus dem Cache (Stand ${new Date(lastPayloadTs).toLocaleString()}) - kein API-Call nötig.`, 'info');
                                                 await wcomProcessForecastData(lastPayload, lang, days);
                                                 return true;
                                             } catch (e: any) {
                                                 wcomLog(`Neuaufbau aus dem Cache fehlgeschlagen: ${e && e.message ? e.message : e}`, 'error');
                                                 return false;
                                             } finally {
                                                 isFetching = false;
                                             }
                                          }
                                          
                                          // =====================================================================================
                                          // SELBSTPRUEFUNG DER ZEITPLANUNG
                                          // =====================================================================================
                                          
                                          /** Liest "M H * * *" und liefert die Minuten seit Mitternacht, sonst null. */
                                          function wcomCronMinutes(cron: string): number | null {
                                             const p = String(cron).trim().split(/\s+/);
                                             if (p.length < 5) return null;
                                             const m = Number(p[0]); const h = Number(p[1]);
                                             if (!Number.isInteger(m) || !Number.isInteger(h)) return null;
                                             return h * 60 + m;
                                          }
                                          
                                          /**
                                          * Prueft beim Start zwei Dinge, die sich sonst nur als "es kommen keine Daten"
                                          * bemerkbar machen:
                                          *  1. Ist der Cooldown kleiner als der kuerzeste Abstand zwischen zwei Abrufen?
                                          *     (Genau das liess in v2.7.0 den Nachmittags-Abruf dauerhaft ausfallen.)
                                          *  2. Passt die gewaehlte Taktung ueberhaupt in das Monatsbudget?
                                          */
                                          function wcomValidateSchedule(): void {
                                             const hhmm = (min: number) => `${wcomPad(Math.floor(min / 60))}:${wcomPad(min % 60)}`;
                                             const times = CONFIG.UPDATES
                                                 .map(u => ({ min: wcomCronMinutes(u.cron), u }))
                                                 .filter(x => x.min !== null) as { min: number; u: typeof CONFIG.UPDATES[0] }[];
                                          
                                             if (times.length === 0) { wcomLog('Keine gültigen Abrufzeiten konfiguriert.', 'error'); return; }
                                          
                                             times.sort((x, y) => x.min - y.min);
                                             const plan = times.map(t => `${hhmm(t.min)} (${t.u.name}, Stufe ${t.u.tier})`).join(' | ');
                                          
                                             // Kleinster Abstand im 24-h-Kreis
                                             let minGap = 24 * 60;
                                             for (let i = 0; i < times.length; i++) {
                                                 const next = times[(i + 1) % times.length];
                                                 const raw = next.min - times[i].min;
                                                 minGap = Math.min(minGap, raw > 0 ? raw : raw + 1440);
                                             }
                                             const gapHours = times.length > 1 ? minGap / 60 : 24;
                                          
                                             wcomLog(`Zeitplan: ${plan}`, 'info');
                                          
                                             if (CONFIG.MIN_HOURS_BETWEEN_UPDATES >= gapHours) {
                                                 wcomLog(`KONFIGURATIONSFEHLER: Cooldown (${CONFIG.MIN_HOURS_BETWEEN_UPDATES} h) ist größer/gleich dem kürzesten Abstand der Abrufzeiten (${gapHours.toFixed(1)} h). Mindestens ein Abruf wird IMMER übersprungen. Bitte MIN_HOURS_BETWEEN_UPDATES verkleinern.`, 'error');
                                             } else {
                                                 wcomLog(`Kürzester Abstand ${gapHours.toFixed(1)} h, Cooldown ${CONFIG.MIN_HOURS_BETWEEN_UPDATES} h - passt.`, 'debug');
                                             }
                                          
                                             // Budget-Plausibilitaet fuer den laengsten Monat
                                             const perDay = wcomMaxTier();
                                             const worstCase = perDay * 31;
                                             if (worstCase > CONFIG.MONTHLY_LIMIT) {
                                                 wcomLog(`BUDGET-WARNUNG: ${perDay} Abrufe/Tag ergeben in einem 31-Tage-Monat ${worstCase} Calls, das Limit liegt bei ${CONFIG.MONTHLY_LIMIT}. Die Sparstaffelung greift dann gegen Monatsende automatisch.`, 'warn');
                                             } else {
                                                 const spare = CONFIG.MONTHLY_LIMIT - worstCase;
                                                 wcomLog(`Budget: ${perDay} Abrufe/Tag = max. ${worstCase} Calls/Monat von ${CONFIG.MONTHLY_LIMIT} (${spare} Reserve).`, 'info');
                                             }
                                          }
                                          
                                          // =====================================================================================
                                          // ZEITSTEUERUNG & TRIGGER
                                          // =====================================================================================
                                          
                                          for (const upd of CONFIG.UPDATES) {
                                             schedule(upd.cron, () => wcomFetchWeatherData('scheduled', upd.tier, upd.name));
                                          }
                                          
                                          // Perioden-Reset zusaetzlich taeglich anstossen, damit die Zaehler in der
                                          // Oberflaeche zeitnah umspringen. Der eigentliche Reset ist lazy abgesichert.
                                          schedule('1 0 * * *', () => wcomRollPeriods().catch(e => wcomLog(String(e), 'error')));
                                          
                                          // 'current' stuendlich nachziehen - reine Umschichtung vorhandener Datenpunkte, kein API-Call.
                                          if (CONFIG.ENABLE_CURRENT) {
                                             schedule(CONFIG.CURRENT_CRON, () => wcomUpdateCurrent('stündlich'));
                                          }
                                          
                                          on({ id: CONFIG.DP_API_KEY, change: 'ne' }, (obj) => {
                                             if (obj.state && typeof obj.state.val === 'string' && obj.state.val.trim().length >= 10) {
                                                 authFailed = false; // neue Chance nach einem zuvor abgelehnten Key
                                                 wcomLog('API-Key geändert. Starte Test-Abruf.', 'info');
                                                 wcomFetchWeatherData('key_update');
                                             }
                                          });
                                          
                                          on({ id: CONFIG.DP_FORECAST_DAYS, change: 'ne' }, (obj) => {
                                             if (!obj.state || obj.state.val === null) return;
                                             // Entprellen: schnelle Aenderungen (Slider in VIS) zu einem Vorgang zusammenfassen.
                                             if (daysDebounce) clearTimeout(daysDebounce);
                                             daysDebounce = setTimeout(async () => {
                                                 daysDebounce = null;
                                                 wcomLog(`Vorhersage-Tage geändert auf ${obj.state.val}.`, 'info');
                                                 const done = await wcomRerenderFromCache('Änderung der Vorhersagedauer');
                                                 if (!done) wcomFetchWeatherData('days_update');
                                             }, 3000);
                                          });
                                          
                                          on({ id: CONFIG.DP_FORCE_RESET, change: 'any', val: true }, async () => {
                                             wcomLog('Manueller Reset: Monatszähler wird auf 0 gesetzt.', 'warn');
                                             await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month`, 0, true);
                                             await setStateAsync(`${CONFIG.DP_PATH}.info.requests_month_period`, wcomMonthKey(new Date()), true);
                                             await setStateAsync(CONFIG.DP_FORCE_RESET, false, true);
                                          });
                                          
                                          on({ id: CONFIG.DP_FORCE_UPDATE, change: 'any', val: true }, async () => {
                                             await setStateAsync(CONFIG.DP_FORCE_UPDATE, false, true);
                                             authFailed = false;
                                             wcomLog('Manueller Abruf ausgelöst (umgeht den Cooldown, Monatslimit bleibt aktiv).', 'info');
                                             wcomFetchWeatherData('manual');
                                          });
                                          
                                          // --- Skriptstart ---
                                          (async () => {
                                             wcomValidateSchedule();
                                             await wcomFetchWeatherData('start');
                                          
                                             // 'current' MUSS unabhaengig vom Start-Abruf aufgebaut werden.
                                             // Sonst entsteht genau die Luecke aus v3.2.0: greift beim Neustart der Cooldown
                                             // (oder fehlt der API-Key), laeuft wcomProcessForecastData() nicht - und damit
                                             // wurde der Ordner nie angelegt. Die Werte stammen ohnehin aus den bereits
                                             // vorhandenen Stunden-Datenpunkten, ein Abruf ist dafuer nicht noetig.
                                             await wcomUpdateCurrent('Skriptstart');
                                          })();
                                          

                                          P Offline
                                          P Offline
                                          pk68
                                          schrieb am zuletzt editiert von
                                          #66

                                          Hi @Schimi , danke für das Skript.

                                          Ich glaube, es gibt einen kleinen Bug bei den stündlichen Daten. Die Werte für die ersten beiden Stunden des Tages werden falsch zugeordnet. Zum Debuggen habe ich das Skript angepasst und den Datenpunkt "from" mitschreiben lassen. Da ergibt sich folgendes Bild:

                                          0_userdata.0.wetter_com.day_0.hourly.23.from = 2026-04-25T21:00:00Z
                                          0_userdata.0.wetter_com.day_1.hourly.00.from = 2026-04-26T22:00:00Z
                                          0_userdata.0.wetter_com.day_1.hourly.01.from = 2026-04-26T23:00:00Z
                                          0_userdata.0.wetter_com.day_1.hourly.02.from = 2026-04-26T00:00:00Z
                                          

                                          Die TImestamps sind ja UTC, also 2h Versatz zu Deutschland. Die Uhrzeiten sind alle ok, aber bei 00:00 und 01:00 Uhr stimmt das Datum nicht. Da müsste als Tag der 25. drin stehen.

                                          S 1 Antwort Letzte Antwort
                                          0

                                          Hey! Du scheinst an dieser Unterhaltung interessiert zu sein, hast aber noch kein Konto.

                                          Hast du es satt, bei jedem Besuch durch die gleichen Beiträge zu scrollen? Wenn du dich für ein Konto anmeldest, kommst du immer genau dorthin zurück, wo du zuvor warst, und kannst dich über neue Antworten benachrichtigen lassen (entweder per E-Mail oder Push-Benachrichtigung). Du kannst auch Lesezeichen speichern und Beiträge positiv bewerten, um anderen Community-Mitgliedern deine Wertschätzung zu zeigen.

                                          Mit deinem Input könnte dieser Beitrag noch besser werden 💗

                                          Registrieren Anmelden
                                          Antworten
                                          • In einem neuen Thema antworten
                                          Anmelden zum Antworten
                                          • Älteste zuerst
                                          • Neuste zuerst
                                          • Meiste Stimmen


                                          Support us

                                          ioBroker
                                          Community Adapters
                                          Donate

                                          456

                                          Online

                                          33.0k

                                          Benutzer

                                          83.6k

                                          Themen

                                          1.3m

                                          Beiträge
                                          Community
                                          Impressum | Datenschutz-Bestimmungen | Nutzungsbedingungen | Einwilligungseinstellungen
                                          ioBroker Community 2014-2026
                                          logo
                                          • Anmelden

                                          • Du hast noch kein Konto? Registrieren

                                          • Anmelden oder registrieren, um zu suchen
                                          • Erster Beitrag
                                            Letzter Beitrag
                                          0
                                          • Home
                                          • Aktuell
                                          • Tags
                                          • Ungelesen 0
                                          • Kategorien
                                          • Unreplied
                                          • Beliebt
                                          • GitHub
                                          • Docu
                                          • Hilfe