Anwendungsbeispiel JsonMod #1: METAR

Begonnen von herrmannj, 22 März 2020, 20:23:22

Vorheriges Thema - Nächstes Thema

herrmannj

Ab heute ist 98_JsonMod.pm verfügber. Das Modul lädt und verarbeitet JSON files zu Readings.
https://forum.fhem.de/index.php/topic,109398.0.html

Um die Verwendung zu beschreiben und nachvollziehbar zu gestalten liefere ich Anwendungsbeispiele, hier die Verarbeitung von METAR. Für ganz Eilige: ein kompletter list befindet sich am Ende der Seite.

METAR sind Wetterinformationen von Flugplätzen. Obwohl primär für Piloten gedacht sind die durchaus für alle interessant da METAR Informationen sehr akkurat sind und einen hohen Detailgrad enthalten, zum Beispiel in Bezug auf die Bewölkung.

Erster Schritt ist die Auswahl einer geeigneten Quelle. Ich habe mich für www.checkwx.com entschieden. Der Anbieter erfordert die kostenlose Registrierung zum Erhalt eines API Key. Dies ist der erste Schritt. Die Dokumentation der API findet sich hier: https://apidocs.checkwx.com/#metarmetar_fields

Der Anbieter stellt zwei Endpoints zur Verfügung. Entweder der reine METAR oder der dekodierte und aufbereitete METAR.

Im ersten Teil dieses Beispieles hole ich nur den METAR.

In FHEM wird dazu ein DEVICE angelegt. (EDDH ist ICAO Kürzel des Hamburger Flughafen, Euren nächsten Flugplatz findet ihr via google)
define MetarHH JsonMod https://api.checkwx.com/metar/EDDH

METAR werden klassischerweise "10 vor halb" und "10 vor voll" aktualisiert. Wir holen die Daten 5 Minuten später, also 25 und 55min jede Stunden. Der Cron lautet dazu 25,55 * * * *:
attr MetarHH interval 25,55 * * * *

Den API Key möchte der Anbieter als Teil eines HTTP HEADERS erhalten. Im ersten Schritt wird der KEY dem Modul übergeben ("ABCDEFG" durch den echten Api-key ersetzen):
set  MetarHH secret KEY ABCDEFG
Dann wird der HTTP HEADER erstellt. Das Modul wird [KEY] durch den gespeicherten Key ersetzen. Dieser ist jedoch weder im List noch in der fhem.cfg sichtbar und so gegen unbeabsichtigtes leaken geschützt.
attr MetarHH httpHeader X-API-Key: [KEY]

Im letzten Schritt muss das Modul ,,wissen" wie aus dem JSON file das gewünschte Reading entstehen soll. Die Doku https://apidocs.checkwx.com/#metarmetar_fields der API listet den Aufbau des JSON:
{
  "results": 1,
  "data": [
    "KRDU 082351Z 21007KT 10SM BKN250 13/M03 A3053 RMK AO2 SLP339 T01281033 10172 20128 53001"
  ]
}

Der rohe METAR steckt als erstes und einziges Element in einer Liste ('data'). Im Attribut 'readingList' werden die Regeln der Umwandlung festgelegt. (Achtung beim Semikolon bei der Eingabe via cmdline)
attr MetarHH readingList single(jsonPath('$.data[0]'), 'METAR', 'N/A');

'single(..);' ist die Anweisung ein einzelnes Reading zu erzeugen.
Argumente:
'jsonPath('$.data[0]')' dies ist die JSONPath Anweisung, beschreibt also wo im JSON die gesuchte Information liegt. '$' ist die Wurzel, '.data' die Bezeichnung der Liste (im JSON an eckigen Klammern zu erkennen) und 0 (eckige Klammern) das erste Element dieser Liste.
'METAR' ist der Name des Readings das erzeugt werden soll.
'N/A' (optional) ist ein default Wert. Sollte der Wert '$.data[0]' nicht in dem JSON gefunden werden, dann wird der default für das reading verwendet.

Zum Abschluss soll noch das jetzt erzeugte reading 'METAR' im 'state' verwendet werden:
attr MetarHH stateFormat METAR
(Optional können noch events-on... gesetzt werden was bei einem METAR keinen Sinn macht)

list MetarHH:

Internals:
   API_LAST_RES 1584887100.71965
   CFGFN     
   DEF        https://api.checkwx.com/metar/EDDH
   FUUID      5e777210-f33f-a6e8-4732-742c93c3f03aefac
   NAME       MetarHH
   NEXT       2020-03-22 15:55:00
   NR         54
   NTFY_ORDER 50-MetarHH
   SECRETS    KEY
   SOURCE     https://api.checkwx.com/metar/EDDH (200)
   STATE      EDDH 221350Z 10014KT CAVOK 07/M05 Q1036 TEMPO 10015G25KT
   TYPE       JsonMod
   CONFIG:
     IN_REQUEST 0
     SOURCE     https://api.checkwx.com/metar/EDDH
     SECRET:
   READINGS:
     2020-03-22 15:25:00   METAR           EDDH 221350Z 10014KT CAVOK 07/M05 Q1036 TEMPO 10015G25KT
Attributes:
   httpHeader X-API-Key: [KEY]
   interval   25,55 * * * *
   readingList single(jsonPath('$.data[0]'), 'METAR', 'N/A');
   stateFormat METAR


Im zweiten Teil verwende ich den anderen API Endpoint um die Informationen detaillierter darzustellen:
Aufbauend auf dem ersten Beispiel wird zuerst der API Endpoint angepasst (siehe https://apidocs.checkwx.com/#metarmetar_fields). Interval, Key, Header können bleiben wie sind.
defmod MetarHH JsonMod https://api.checkwx.com/metar/EDDH/decoded

Durch die Anpassung des Endpoints wird ein komplett anderes JSON geliefert. Aus der Dokumentation der API:

{
  "results": 1,
  "data": [
    {
      "wind": {
        "degrees": 210,
        "speed_kts": 7,
        "speed_mph": 8,
        "speed_mps": 4
      },
      "temperature": {
        "celsius": 13,
        "fahrenheit": 55
      },
      "dewpoint": {
        "celsius": -3,
        "fahrenheit": 27
      },
      "humidity": {
        "percent": 33
      },
      "barometer": {
        "hg": 30.53,
        "hpa": 1034,
        "kpa": 103.38,
        "mb": 1033.82
      },
      "visibility": {
        "miles": "10",
        "miles_float": 10,
        "meters": "16,100",
        "meters_float": 16100
      },
      "ceiling": {
        "code": "BKN",
        "text": "Broken",
        "feet_agl": 25000,
        "meters_agl": 7620
      },
      "elevation": {
        "feet": 426.51,
        "meters": 130
      },
      "location": {
        "coordinates": [
          -78.787472,
          35.877639
        ],
        "type": "Point"
      },
      "icao": "KRDU",
      "observed": "2020-03-08T23:51:00.000Z",
      "raw_text": "KRDU 082351Z 21007KT 10SM BKN250 13/M03 A3053 RMK AO2 SLP339 T01281033 10172 20128 53001",
      "station": {
        "name": "Raleigh-Durham International"
      },
      "clouds": [
        {
          "code": "BKN",
          "text": "Broken",
          "base_feet_agl": 25000,
          "base_meters_agl": 7620
        }
      ],
      "flight_category": "VFR",
      "conditions": []
    }
  ]
}

Das bereits erstellte reading 'METAR' zeigt daher fast unmittelbar etwas in der Art von 'HASH(0x5606bf11bdc8)'. FHEM sagt so dass der JSONPath Ausdruck jetzt ein JSON Object ergibt.

Der JSONPath Ausdruck unter readingList der neu zum METAR führt wird daher angepasst:
single(jsonPath('$.data[0].raw_text'), 'METAR', 'N/A');
Analog dazu können jetzt zusätzliche readings für weitere Werte erstellt werden:
single(jsonPathf('$.data[0].temperature.celsius', '%s °C'), 'temperature', 'N/A');
single(jsonPathf('$.data[0].humidity.percent', '%s %%'), 'humidity', 'N/A');
single(jsonPathf('$.data[0].dewpoint.celsius', '%s °C'), 'dewpoint', 'N/A');
single(jsonPathf('$.data[0].barometer.mb', '%s mb'), 'pressure', 'N/A');

Anstelle der jsonPath() Anweisung verwende ich hier jsonPathf(). Der jsonPathf() bekommt die gleichen Parameter wie jsonPath() und zusätzlich eine Format Anweisung. Dieser Parameter entspricht den Konventionen von printf und wird an dieser Stelle verwendet um die Einheiten (°C, %, mb) auszugeben. Achtung Sonderfall '%': muss im Format gedoppelt werden '%%' - das ist eine normale 'printf' Konvention.

Das JSON enthält weitere Werte die nach dem gleichen Schema ausgewählt und angezeigt werden können. Das Device lässt sich also sehr flexibel ausgestalten.

Für den Bereich ,,Bewölkung" ergibt sich eine Herausforderung. METAR liefern typischerweise keine oder 1 bis 3 Wolkenschichten. Im JSON sowie der API Beschreibung ist sichtbar, dass diese in einer Liste unterhalb der Data Liste geliefert werden.

Abhängig von der tatsächlich vorhandenen Bewölkung wird daher eine Liste variabler Länge geliefert (1..3 Elemente).
Eine Lösungsmöglichkeit ist die Verwendung von single() und der Angabe eines Default Wertes wie ,N/A'. Das könnte so aussehen:
single(jsonPathf('$.data[0].clouds[0].text'), 'Cloudlayer0 ', 'N/A');
single(jsonPathf('$.data[0].clouds[1].text'), 'Cloudlayer1 ', 'N/A');
single(jsonPathf('$.data[0].clouds[2].text'), 'Cloudlayer2 ', 'N/A');

Das ist einfach und je nach Situation auch die beste Lösung.

Die Alternative liegt in der Verwendung von multi() statt single(). Im Unterschied zu single() (1:1 Beziehung zum Reading) erstellt multi() eine variable Anzahl von Readings (0..n). Die erste Voraussetzung ist daher dass der JSONPath Ausdruck zu einer Liste auflöst. Gleichzeitig kann der Name des zu erzeugenden Readings nicht mehr statisch vorgegeben werden sondern muss erzeugt werden:
multi(jsonPath('$.data[0].clouds[*]'), concat('cloud_', index()), property('text'));
jsonPath('$.data[0].clouds[*]') löst zur ,,Liste der Clouds" auf. Diese Liste enthält JSON Objekte (könnte aber auch Listen oder Werte enthalten).

concat() verkettet Ausdrücke und wird hier verwendet um aus dem String ,cloud_' und dem index(), also der Position in der Liste, den readings Namen zu erzeugen.
Der dritte Ausdruck innerhalb multi() erzeugt den Inhalt des Readings. Jeder Eintrag in Clouds ist, siehe API DOKU, ein JSON Object. Json Objekte lassen sich als Gruppe von Key, Value Paaren beschreiben. Ein Key kann als property über den Namen angesprochen werden. Der verwendete Ausdruck property() macht genau das und property('text') füllt die erzeugten readings mit dem Text ('scattered', 'broken' etc. Das entspricht dem Bedeckungsgrad) des jeweiligen Wolkenlayers. Ein zweiter, optionaler Parameter von property() ist wieder ein Dafault der verwendet wird, wenn die gesuchte Eigenschaft nicht vorhanden ist. 

Nun möchte ich in einem 'Wolkenreading' nicht nur den Typ sondern auch die Höhe der Wolken dargestellt haben.  Laut API Doku ist 'base_meters_agl' eine weitere Eigenschaft und gibt die Höhe der Wolkenuntergrenze an.

Durch die Verwendung von conact() und property() können die Werte in einem Reading kombiniert werden.
multi(jsonPath('$.data[0].clouds[*]'), concat('cloud_', index()), concat(propertyf('base_meters_agl', '', '%s m/AGL, '), property('text')));
In concat() wird dabei an der Stelle des ersten property() die propertyf() Funktion verwendet. Diese bietet, wie oben, einen Format Parameter an der dritten Stelle.

Bei einem METAR gibt es die Besonderheit das möglicherweise gar keine Wolken vorhanden sind. Cloud[0] liefert in diesem Fall "Clear skies" oder den Text ,,CAVOK" (pilot speech für ,,keine Wolken") und gleichzeitig fehlt dann die Eigenschaft 'base_meters_agl'. Keine Wolken, keine Untergrenze. Durch den Default '' ist der Inhalt des Readings dann nur "Clear skies", ansonsten Untergrenze + ' m/AGL, ' + Text.

Hier das list des fertigen Device das sich einfach nachabuen lässt und zuverlässige die aktuelle Wettersituation liefert:

Internals:
   API_LAST_RES 1584902654.06215
   CFGFN     
   DEF        https://api.checkwx.com/metar/EDDH/decoded
   FUUID      5e777210-f33f-a6e8-4732-742c93c3f03aefac
   NAME       MetarHH
   NEXT       2020-03-22 19:55:00
   NR         54
   NTFY_ORDER 50-MetarHH
   SECRETS    KEY
   SOURCE     https://api.checkwx.com/metar/EDDH/decoded (200)
   STATE      EDDH 221750Z 09009KT 070V130 CAVOK 05/M08 Q1037 NOSIG
   TYPE       JsonMod
   CONFIG:
     IN_REQUEST 0
     SOURCE     https://api.checkwx.com/metar/EDDH/decoded
     SECRET:
   READINGS:
     2020-03-22 19:44:14   METAR           EDDH 221750Z 09009KT 070V130 CAVOK 05/M08 Q1037 NOSIG
     2020-03-22 19:44:14   cloud_0         Clear skies
     2020-03-22 19:44:14   dewpoint        -8 °C
     2020-03-22 19:44:14   humidity        39 %
     2020-03-22 19:44:14   pressure        1036.92 mb
     2020-03-22 19:44:14   temperature     5 °C
Attributes:
   httpHeader X-API-Key: [KEY]
   interval   25,55 * * * *
   readingList single(jsonPath('$.data[0].raw_text'), 'METAR', 'N/A');
single(jsonPathf('$.data[0].temperature.celsius', '%s °C'), 'temperature', 'N/A');
single(jsonPathf('$.data[0].humidity.percent', '%s %'), 'humidity', 'N/A');
single(jsonPathf('$.data[0].dewpoint.celsius', '%s °C'), 'dewpoint', 'N/A');
single(jsonPathf('$.data[0].barometer.mb', '%s mb'), 'pressure', 'N/A');
multi(jsonPath('$.data[0].clouds[*]'), concat('cloud_', index()), concat(propertyf('base_meters_agl', '', '%s m/AGL, '), property('text')));
   stateFormat METAR



rudolfkoenig

Sehr nett!
Kriegen wir das auch als attrTemplate? :)

herrmannj

solange ich nicht die templates schreiben muss, gern :)

Beta-User

Zitat von: herrmannj am 23 März 2020, 00:24:22
solange ich nicht die templates schreiben muss, gern :)
Falls das indirekt eine Frage gewesen sein sollte: Starthilfe würde ich geben. Das gilt umso mehr, falls sich jemand weiteres bereit erklären sollte, den 1. Maintainer dazu zu machen!
(Das war eine ausdrückliche Bitte, also falls grade jemand mitliest und den Gedanken hat, dass aktuell die richtige Zeit wäre, sich da einzudenken: feel free!!!
Ergänzend: Das hier sieht mir deutlich weniger komplex aus wie mqtt2 (jedenfalls in Teilen) oder HTTPMOD (das hat sehr viele Optionen, die ich auch nicht alle überblicke; auch hier wäre ich sehr glücklich, wenn sich da jemand angesprochen fühlen würde!) Es sei betont: templates zu erstellen oder zu verwalten ist nicht sehr schwer, und man kann meistens nicht direkt was kaputt machen! Im schlimmsten Fall funktioniert es eben nicht und der User muß nacharbeiten.)

So oder so wäre es aber hilfreich, wenn du das Modul "attrTemplate-fähig" machen würdest, sind wohl nur ein paar Zeilen Code, siehe z.B. den SetExtensions betreffenden Teil hier in https://svn.fhem.de/trac/changeset/17769/.
Server: HP-elitedesk@Debian 12, aktuelles FHEM@ConfigDB | CUL_HM (VCCU) | MQTT2: ZigBee2mqtt, MiLight@ESP-GW, BT@OpenMQTTGw | ZWave | SIGNALduino | MapleCUN | RHASSPY
svn: u.a Weekday-&RandomTimer, Twilight,  div. attrTemplate-files, MySensors

SebastianStorb

Vielen Dank für das schöne Tool, das ich sehr gut gebrauchen kann!
Mir ist eine Sache aufgefallen: Das Tool holt sich immer den vorherigen METAR und nicht den aktuellen ab:
Beispiel für EDDL (Düsseldorf International)
in FHEM:
METAR
EDDL 230420Z 06003KT CAVOK 06/03 Q1021 NOSIG
2020-04-23 06:55:00


Auf der Internetseite bei https://www.checkwx.com
Current
29 mins ago
EDDL 230450Z 07006KT CAVOK 04/02 Q1021 NOSIG

Previous
1 hour ago
EDDL 230420Z 06003KT CAVOK 06/03 Q1021 NOSIG


Es wird zwar der richtige Zeitcode angezeigt (hier 06:55:00), es handelt sich aber um die ATIS des Flughafens von 230420Z, d.h. vom 23. des Monats (also 23.04.2020) um 04.20 Uhr (UTC).

Lässt sich das noch anpassen, so dass der aktuellste METAR angezeigt wird?

herrmannj

Moin

JsonMod stellt dass dar was in dem JSON drin ist. Wenn da was falsch ist liegt es entweder am json, oder man zeigt mit der readinglist die falschen Elemente. Das müsstest du prüfen. Sollte checkwx veraltete Daten liefern dann schau mal ob man jemand anderes nehmen kann. Gibt ja einige.

Vg
Jörg

hixhupf

Ich habe folgendes Problem: ich lese ein JSON aus und hole mir den Füllstand meiner Zisterne:

{"Sensor":"Zisterne","IP":"192.168.18.90","Fuellstand":"1","Abstand":"277"}

Dazu habe ich dann folgende ReadingList geschrieben:

single(jsonPath('$.Fuellstand'),'Fuellstand','0');

Das funktioniert auch soweit, aber ab und zu meldet mein Sensor einen ungültigen Wert (der Füllstand ist dann über 100). Hat dazu jemand eine Idee? Wie verhindere ich, dass Werte über 100 verworfen werden? Vielleicht mit einem DOIF, das nur gültige Werte in einen Dummy schreiben?

Danke für eine Idee,
Sascha

yersinia

#7
Zitat von: hixhupf am 28 Mai 2020, 22:17:57single(jsonPath('$.Fuellstand'),'Fuellstand','0');

Das funktioniert auch soweit, aber ab und zu meldet mein Sensor einen ungültigen Wert (der Füllstand ist dann über 100). Hat dazu jemand eine Idee? Wie verhindere ich, dass Werte über 100 verworfen werden? Vielleicht mit einem DOIF, das nur gültige Werte in einen Dummy schreiben?
1. falscher Thread, das ist hier thematisch OT
2. falscher Thread, du hättest besser einen neuen aufgemacht
3. Wieso werden Werte über 100 verworfen? Wenn ich das richtig verstehe, dann gibt der Sensor Werte wie 123 zurück und das JSON sieht dann wie aus? Wenn die Software, die das JSON generiert, Werte über 100 verwirft, solltest du dort ansetzen. JsonMod liest nur die Quelldaten aus.
4. Alternativ: versuchs' mit einem Userreading - das setzt Werte über 100 auf 100:
urFuellstand { my $v = ReadingsNum($name,"Fuellstand",0);
if($v > 100) {
$v = 100;
}
return $v;
}
viele Grüße, yersinia
----
FHEM 6.4 (SVN) on RPi 4B with RasPi OS Bookworm (perl 5.36.0) | FTUI
nanoCUL->2x868(1x ser2net)@tsculfw, 1x433@Sduino | MQTT2 | Tasmota | ESPEasy
VCCU->14xSEC-SCo, 7xCC-RT-DN, 5xLC-Bl1PBU-FM, 3xTC-IT-WM-W-EU, 1xPB-2-WM55, 1xLC-Sw1PBU-FM, 1xES-PMSw1-Pl

hixhupf

Guten Morgen,

na ich hab gedacht weil es hier um das Json-Modul geht pack ich das hierhin. Ok, das nächste mal woanders.

Der Sensor liefert - warum auch immer - ab und zu Werte über 100:
{"Sensor":"Zisterne","IP":"192.168.18.90","Fuellstand":"156","Abstand":"0"}


Diese sind unsinnig, deshalb will ich sie verwerfen und gar nicht speichern. Ich probiere es dann mal mit deinem Vorschlag zum Userreading, danke. Allerdings bau ich es dann umgekehrt, sprich: setze das Reading nur, wenn der Wert <=100 ist. Dann sollte es passen.

Danke,
Sascha

dadoc

Hallo zusammen,
beim Versuch, Wetter(-vorhersage-)daten aus dem staatlichen spanischen aemet.es-API zu bekommen, stehe ich vor einer kniffeligen Aufgabe.
Der Ablauf ist dort nämlich so:
Man macht einen Aufruf unter Angabe des Ortscodes und mit dem (zuvor beantragten) API-Key, und man bekommt dieses Response:


{
  "descripcion" : "exito",
  "estado" : 200,
  "datos" : "https://opendata.aemet.es/opendata/sh/043b076e",
  "metadatos" : "https://opendata.aemet.es/opendata/sh/93a7c63d"
}


Mit der URL von "datos" kann man dann auch entsprechend Werte mit JsonMod ziehen - allerdings ist die URL nur fünf Minuten gültig (steht auch so in der Doku der API).
Ich bin kein Programmierer und daher völlig überfordert, ob es hier einen wie auch immer gearteten Workaround geben könnte.
Könnte es?
Vielen Dank & viele Grüße
Martin
Standort 1: FS20 mit CUL und FHEM auf Raspi. HM-Komponenten (Heizung, Rollladen, Schalter). HM IP über Raspimatic (testweise)
Standort 2: Homematic (Wired) über CCU2 und PocketHome HD
3 x Raspi3 mit piCorePlayer/Kodi für Multiroom Audio (+ Tablets/iPeng/iPods

DetlefR

So etwas Ähnliches habe ich schon mal gemacht.

Richte ein JsonMod ein define mywetter JsonMod https://opendata.aemet.es/opendata/sh/[key]
Dann extrahiere den "key" aus der ersten Abfrage und setze in in "mywetter" ein.set mywetter secret key 043b076e
Falls die Daten nicht gleich aktualisiert werden noch einmalset mywetter reread

Viel Spaß beim probieren.

dadoc

Danke! Ich hatte mir vorhin so etwas Ähnliches überlegt. JsonMod mit API-Key und Ortscode etc anlegen, um periodisch die data URL zu extrahieren. Diese in einen Dummy schreiben, und mit diesem ein zweites JsonMod anlegen, das dann periodisch die eigentlichen Wetterdaten holt. Nur war mir nicht klar, wie ich dem JsonMod die URL aus dem Dummy übergebe, ohne es per at oder doif periodisch neu zu definieren - oder darf man das, ohne von Profis ausgeschimpft zu werden?
Standort 1: FS20 mit CUL und FHEM auf Raspi. HM-Komponenten (Heizung, Rollladen, Schalter). HM IP über Raspimatic (testweise)
Standort 2: Homematic (Wired) über CCU2 und PocketHome HD
3 x Raspi3 mit piCorePlayer/Kodi für Multiroom Audio (+ Tablets/iPeng/iPods

DetlefR

#12
Zitatoder darf man das, ohne von Profis ausgeschimpft zu werden?
Sieht doch keiner.Oder?  ;)


Ich habe lange nach einer Möglichkeit gesucht die URL zu editieren. Habe aber nichts gefunden. Bis auf das.