NEWS

[gelöst] Meteohub Daten, XML parsen, JSON durchsuchen

  • Starter

    Vielleicht hat jemand schon mal ein ähnliches Problem gehabt oder kann mir allgemein einen Tipp zum durchsuchen von Objekten geben.

    Meine Problemstellung ist, dass ich Daten von einem Meteohub Server auswerten möchte. Meteohub ist eine ziemich mächtige Linux Software die Daten von Wetterstationen zum Beispiel von Oregon, Davis, Lacross oder anderen abholt und mit History aufbereitet. Natürlich kann man das auch direkt mit ioBroker angehen. Aber darum geht es hier nicht. Der Meteohub server hier funktioniert und bleibt für den Moment. Aber natürlich möchte ich diese Daten im ioBroker haben. Dazu dient Meteohub die Daten als XML an, das ein bisschen ein spezielles Format hat.

    hier ein stark verkürzter Auszug zum Verständnis

    <meteohub>
      <config>
        <language>de</language>
        <temp_sensor unit="c" print="°C">th0</temp_sensor>
        <hum_sensor unit="rel" print="%">th0</hum_sensor>
      </config>
      <data timeframe="actual">
        <item sensor="th0" cat="temp" unit="c">7.3</item>
        <item sensor="th0" cat="hum" unit="rel">20</item>
        <item sensor="th1" cat="temp" unit="c">13.6</item>
        <item sensor="th1" cat="hum" unit="rel">65</item>
     </data>
      <data timeframe="alltime">
        <item sensor="th0" cat="temp" unit="c">7.3</item>
        <item sensor="th0" cat="hum" unit="rel">20</item>
        <item sensor="th1" cat="temp" unit="c">13.6</item>
        <item sensor="th1" cat="hum" unit="rel">65</item>
      </data>
    </meteohub>
    

    <item> ist eine wahllose Aneinanderreihung von Datenpunkten, deren Reihenfolge sich auch ändert sobald mal ein Sensor ausfällt oder neu angelernt wird.
    Bisher hab ich auf einem Server ein php Skript liegen, welches die Daten abholt (curl) per SimpleXMLElement() zur Verfügung stellt und dann suche ich mir die Daten mit XPATH zusammen. Ein Beispiel

    $meteohub->data[0]->xpath('item[@sensor="th2"][@cat="temp"][@unit="c"]');
    

    Das PHP Skript soll nun natürlich weg und ich möchte alles im ioBroker per Javascript machen. Jetzt gibt es ein npm Modul xpath. Mit dem war ich bisher aber noch nicht erfolgreich. Falls das schon mal jemand benutzt hat bin ich für jeden Tipp dankbar.

  • Starter

    Nun zu meinen ersten Erfolgen in Javascript. Die Daten holen ist kein Problem:

    var mh_URL = 'all-sensors-xml.txt';
    const request = require('request'); 
    request(mh_URL, function (error, response, body) {
    if (error) 
      log("Fehler beim Herunterladen Meteohub XML: " + error, 'error');
    else {
      parseXML(body)
    };
    });
    Das funktioniert problemlos.
  • Starter

    Um die Daten nun zu zerlegen habe ich zwei Methoden die eingeschränkt funktionieren.

    XML2JS

    var xml2js = require('xml2js');
    parser = new xml2js.Parser();
    parser.parseString(xmldata, function(err,obj) {
      setState("meteohub.th0.temp.val", parseFloat(obj.meteohub.data[0].item[71]._));
    }
    

    oder XML2JSON

    var parser = require('xml2json');
    var mh_json = parser.toJson(xmldata);  // Convert XML to JSON
    var mh_data = JSON.parse(mh_json);     // Parse JSON into JS object
    setState("meteohub.th0.temp.val",mh_data.meteohub.data[0].item[50].$t);
    
    

    Beide Varianten funktionieren. Jetzt ist es aber durchaus unschön, über hart kodierte Indizes zuzugreifen, da diese sich auch immer wieder ändern. Gibt es eine schöne Möglichkeit die Daten zu durchsuchen oder zu adressieren? Klar kann ich mir selbst eine Funktion schreiben die mit vielen Schleifen den gesammten Objektbaum durchläuft und sich die Infos zusammen sammelt. Aber da gibt es doch bestimmt etwas schönes in der Art der indexOf() Funktion oder ähnlichem?

  • Starter

    Als letzte Info noch ein Beispiel wie die konvertierte JSON Struktur nach XML2JSON aussieht (auch stark verkürzt). Vielleicht kann ja auch jemand der mit obigen Modulen keinerlei Erfahrung hat mir trotzdem behilflich sein mit dem Durchsuchen der Javascript Objekte.

        "meteohub": {
            "config": {
                "language": "de",
                "temp_sensor": {
                    "unit": "c",
                    "print": "°C",
                    "$t": "th0"
                },
                "hum_sensor": {
                    "unit": "rel",
                    "print": "%",
                    "$t": "th0"
                }
             },
            "data": [
                {
                    "timeframe": "actual",
                    "item": [
                        {
                            "sensor": "th0",
                            "cat": "temp",
                            "unit": "c",
                            "$t": "7.3"
                        },
                        {
                            "sensor": "th0",
                            "cat": "hum",
                            "unit": "rel",
                            "$t": "20"
                        }
    	            ]
    	        },
                {
                    "timeframe": "alltime",
                    "item": [
                        {
                            "sensor": "th0",
                            "cat": "temp",
                            "unit": "c",
                            "$t": "7.3"
                        },
                        {
                            "sensor": "th0",
                            "cat": "hum",
                            "unit": "rel",
                            "$t": "20"
                        }
                     ]
                 }
             ]
         }
    }
    
    ich bräuchte eine Funktion, die mir zum Beispiel den Wert für timeframe=="actual" und sensor=="th0" mit cat=="temp" und unit=="c" zurück gibt.

  • wberger
    Was hältst Du davon, aus den Werten von "timeframe" und "cat" die ID des Datenpunktes zu bilden ? Beispiel: "meteohub.actual.temp".

    let data = obj.meteohub.data;
    for(let i = 0; i < data.length; i++) {
       let folder = 'meteohub.' + data[i].timeframe + '.';
       let item = data[i].item;
       for(let j = 0; j < item.length; j++) {
          let id = folder + item[j].cat;
          if(item[j].cat == 'temp' || item[j].cat == 'hum') setState(id, parseFloat(item[j].$t), true);
       }
    }
    
  • Starter

    paul53 Danke das war der Gedankenanstoß, den ich gebraucht hatte. Zwar keine fertige Funktion, aber ein schöner Ansatz für die Indizierung der Daten.
    Ich hab das mal noch auf die Schnelle ausprobiert. Ich muss noch etwas mehr filtern, da sonst mehrere Datenpunkte sich überschreiben (was du natürlich bei dem gekürzten file nicht sehen konntest). Außerdem baue ich den Baum etwas anders auf, da ein Sensor zum Beispiel "th0" immer Temperatur, Feuchte und manchmal auch Luftdruck hat. Danach möchte ich gruppieren. Aber das Prinzip bleibt das Selbe. Hier mal der quick'n'dirty adaptierte Code, der funktioniert und dann auch das Ergebniss entsprechend Screenshot liefert. Top. Ich mach das morgen mal noch etwas schön und berichte weiter.

    let data = mh_data.meteohub.data;  
    for(let i = 0; i < data.length; i++) {
      let folder = 'meteohub.' + data[i].timeframe + '.';
      let item = data[i].item;
      for(let j = 0; j < item.length; j++) {
         let id = folder + item[j].sensor + '.' + item[j].cat;
    
         if( (item[j].cat == 'temp') && (item[j].unit == 'c')) {
            createState(id,parseFloat(item[j].$t),state_temp);
         }
         if( (item[j].cat == 'hum') && (item[j].unit == 'rel')) {
            createState(id,parseFloat(item[j].$t));            
         }
      }
    }
    

    mh.jpg

  • Starter

    Der ursprüngliche Post ist schon ein paar Tage her und die Lösung steht auch schon hier. Zwischenzeitlich habe ich das Skript fertiggestellt und etwas aufgehübscht. Außerdem habe ich nochmals beide Varianten XML2JS und XML2JSON verglichen. Falls jemand das selbe Problem hat findet ihr hier den fertigen Code.

    Zuerst per XML2JS Modul.

    /* Meteohub
    Skript holt Daten aus Meteohub XML Datei, bereitet sie auf und stellt einige Datenpunkte zur Verfügung
    erstellt: 2020 Wolfgang Berger
    V 0.1 Initiale Version. Hartcodierte Zuordnung. Wird nicht funktionieren sobald der Windmesser online ist.
    V 0.2 jetzt Sensoren in Liste
    V 0.3 Kosmetische Überarbeitung
    */
    
    // Settings   ################################################################
    var mh_URL = 'http://localhost/all-sensors-xml.txt';
    
    // Includes    ################################################################
    const request = require('request');    
    var xml2js = require('xml2js');
    const util = require('util');
    
    parser = new xml2js.Parser();
    
    // Type definitions  ################################################################
    var state_temp = {      type: 'number',  unit: '°C',  read: true,  write: false,role: 'value.temperature' };
    var state_json = {                                     read: true,  write: false,role: 'mh.json' };    
    var state_humidity = {  type: 'number',  unit: '%rH',  read: true,  write: false,role: 'value.humidity' };                                  
    var state_speed = {     type: 'number',  unit: 'km/h',  read: true,  write: false,role: 'value.speed' };                                  
    var state_direction = { type: 'number',  unit: '°',  read: true,  write: false,role: 'value.direction' };                                  
    var state_pressure  = { type: 'number',  unit: 'mbar',  read: true,  write: false,role: 'value.pressure' };     
    var state_rainrate  = { type: 'number',  unit: 'mm/h',  read: true,  write: false,role: 'value.precipitation.hour' };         
    var state_rain  = {     type: 'number',  unit: 'mm',  read: true,  write: false,role: 'value.precipitation.today' };             
    
    // Now build State Tree ################################################################
    
    // Meteohub main branch for states
    
    createState('meteohubjs.json', { name: 'Meteohub',  desc: 'Meteohub Daten Juergen', type:'object' });
    // all the functions ################################################################
    
    function parseXML(xmldata,do_init) {
      parser.parseString(xmldata, function(err,obj){ 
        let data = obj.meteohub.data;        // only use the data section of the object. We ignore the config section for now.
        
        // Loop through data sections. each data[i] contains data for a given timeframe like actual, like 1h, 1day, etc.
        for(let i = 0; i < data.length; i++) {
          let folder = 'meteohubjs.' + data[i].$.timeframe + '.';       // timeframe can be "actual", "alltime" and much more. Each datasection only contains one timeframe value
          let item = data[i].item;
          //log(util.inspect(data[i].$.timeframe));
    
          // Now we have selected one timeframe and loop through all items
          // instead of building a state tree with hundreds of values, we pick only the ones that are interesting.
          for(let j = 0; j < item.length; j++) {
            let id = folder + item[j].$.sensor + '.' + item[j].$.cat;
    
            // Temperature Value in degrees Celsius
            if( (item[j].$.cat == 'temp') && (item[j].$.unit == 'c')) {
                if (do_init==true) {    createState(id,parseFloat(item[j]._),state_temp); }
                  else {    setState(id,parseFloat(item[j]._)); }
            }
    
            // Humidity as relative humidity
            if( (item[j].$.cat == 'hum') && (item[j].$.unit == 'rel')) {
                if (do_init==true) {   createState(id,parseFloat(item[j]._),state_humidity); }
                  else  {   setState(id,parseFloat(item[j]._)); }          
            }
    
            // Air pressure refered to sealevel
            if( (item[j].$.cat == 'sealevel') && (item[j].$.unit == 'hpa')) {   
                if (do_init==true) {   createState(id,parseFloat(item[j]._),state_pressure); }
                  else  {   setState(id,parseFloat(item[j]._)); }          
            }        
    
            // rain values in mm
            if( (item[j].$.sensor == 'rain0') && (item[j].$.unit == 'mm')) {
                if (do_init==true) { createState(id,parseFloat(item[j]._),state_rain);  }
                  else { setState(id,parseFloat(item[j]._));  }
            }
    
            // for windsensor currently we tend to catch nearly all, because the whitelist could be long. For example windspeed is interesting in different units.
            // Therefore instead of whitelisting the values we do the oposit and use a blacklist for the ones we don't like.
            if( (item[j].$.sensor == 'wind0') ) {
                if ((item[j].$.unit == 'time')) {
                    let dat = item[j]._;
                    let d = new Date(dat.substring(0,4) + '-' + dat.substring(4,6) + '-' + dat.substring(6,8) + 'T' + dat.substring(8,10) +':' + dat.substring(10,12) + ':'+ dat.substring(12,14) );
                    if (do_init==true) {    createState(id+'.'+item[j].$.unit,  d);    }
                      else {    setState(id+'.'+item[j].$.unit,  d);    }
                }
                else {
                    // first catch values in degrees celsius. Because there we can set the unit. Leave the others without unit.
                    if (item[j].$.unit == 'c') {
                      if (do_init==true) { createState(id+'.'+item[j].$.unit,  parseFloat(item[j]._),state_temp); }                           
                        else  { setState(id+'.'+item[j].$.unit,  parseFloat(item[j]._)); }               
                    }
                    else {
                        // Blacklisting for values we don't like
                        if (!(item[j].$.unit == 'f' || item[j].$.unit == 'en' || item[j].$.unit == 'nl' || item[j].$.unit == 'mph')) {
                        if (do_init==true) { createState(id+'.'+item[j].$.unit,  parseFloat(item[j]._)); }                           
                            else  { setState(id+'.'+item[j].$.unit,  parseFloat(item[j]._)); }               
                        }
                    }
                }
            }        
    
    
          }
    
    
    
        }
      
       //log('end build tree');
    
      });
    }  // end parseXML()
     
    
    function getXML(do_init) {
        // loads full meteohub xml file from the mh_URL and calls the parse function.
        // on very first execution do_init is true and all states are created. Each successive call only sets the state values.
        log('meteohub query data from '+mh_URL); // Debug output
        request(mh_URL, function (error, response, body) {
            if (error) log("Fehler beim Herunterladen Meteohub XML: " + error, 'error');
            else {
                //var start = new Date();
                parseXML(body,do_init);
                //var time = new Date() - start;            
                //log('Meteohub XML2JS Durchlaufzeit: '+time); 
            };
        });
    }
    
    // On first script execution, create the states
     let do_init=true;
     getXML(do_init);
    
    // Regular Update
    schedule('*/1 * * * *', function () {
        let do_init=false;
        getXML(do_init);  // regelmäßiger Update
    }); 
    

    Und nun noch der selbe Spass Alternativ per XML2JSON

    /* Meteohub
    Skript holt Daten aus Meteohub XML Datei, bereitet sie auf und stellt einige Datenpunkte zur Verfügung
    erstellt: 2020 Wolfgang Berger
    
    V 0.1 Initiale Version. Hartcodierte Zuordnung. Wird nicht funktionieren sobald der Windmesser online ist.
    V 0.2 jetzt Sensoren in Liste
    V 0.3 kosmetische Aufbereitung. Altlasten entfernt.
    */
    
    // Settings   ################################################################
    const mh_URL = 'http://localhost/all-sensors-xml.txt';
    
    
    // End of settings ###########################################################
    
    
    // Includes    ################################################################
    const request = require('request');    
    var parser = require('xml2json');
    const util = require('util');      // for debugging. Use log(util.inspect(obj));
    
    // Type definitions  ################################################################
    var state_temp = {      type: 'number',  unit: '°C',  read: true,  write: false,role: 'value.temperature' };
    var state_json = {                                     read: true,  write: false,role: 'mh.json' };    
    var state_humidity = {  type: 'number',  unit: '%rH',  read: true,  write: false,role: 'value.humidity' };                                  
    var state_speed = {     type: 'number',  unit: 'km/h',  read: true,  write: false,role: 'value.speed' };                                  
    var state_direction = { type: 'number',  unit: '°',  read: true,  write: false,role: 'value.direction' };                                  
    var state_pressure  = { type: 'number',  unit: 'mbar',  read: true,  write: false,role: 'value.pressure' };     
    var state_rainrate  = { type: 'number',  unit: 'mm/h',  read: true,  write: false,role: 'value.precipitation.hour' };         
    var state_rain  = {     type: 'number',  unit: 'mm',  read: true,  write: false,role: 'value.precipitation.today' };             
    
    // Now build State Tree ################################################################
    
    function parseXML(xmldata,do_init) {
       var mh_json = parser.toJson(xmldata);    // convert XML to JSON
       var mh_data = JSON.parse(mh_json);       // parse JSON into JS object
      
       let data = mh_data.meteohub.data;        // only use the data section of the object. We ignore the config section for now.
    
       // Loop through data sections. each data[i] contains data for a given timeframe like actual, like 1h, 1day, etc.
       for(let i = 0; i < data.length; i++) {
         let folder = 'meteohub.' + data[i].timeframe + '.';       // timeframe is can be "actual", "alltime" and much more. Each datasection only contains one timeframe value
         let item = data[i].item;
         //   log(util.inspect(data[i]));
    
         // Now we have selected one timeframe and loop through all items
         // instead of building a state tree with hundreds of values, we pick only the ones that are interesting.
         for(let j = 0; j < item.length; j++) {
            let id = folder + item[j].sensor + '.' + item[j].cat;
    
            // Temperature Value in degrees Celsius
            if( (item[j].cat == 'temp') && (item[j].unit == 'c')) {
                if (do_init==true) {    createState(id,parseFloat(item[j].$t),state_temp); }
                  else {    setState(id,parseFloat(item[j].$t)); }
            }
    
            // Humidity as relative humidity
            if( (item[j].cat == 'hum') && (item[j].unit == 'rel')) {
                if (do_init==true) {   createState(id,parseFloat(item[j].$t),state_humidity); }
                  else  {   setState(id,parseFloat(item[j].$t)); }          
            }
    
            // Air pressure refered to sealevel
            if( (item[j].cat == 'sealevel') && (item[j].unit == 'hpa')) {   
                if (do_init==true) {   createState(id,parseFloat(item[j].$t),state_pressure); }
                  else  {   setState(id,parseFloat(item[j].$t)); }          
            }        
    
            // rain values in mm
            if( (item[j].sensor == 'rain0') && (item[j].unit == 'mm')) {
                if (do_init==true) { createState(id,parseFloat(item[j].$t),state_rain);  }
                  else { setState(id,parseFloat(item[j].$t));  }
            }
    
            // for windsensor currently we tend to catch nearly all, because the whitelist could be long. For example windspeed is interesting in different units.
            // Therefore instead of whitelisting the values we do the oposit and use a blacklist for the ones we don't like.
            if( (item[j].sensor == 'wind0') ) {
                if ((item[j].unit == 'time')) {
                    let dat = item[j].$t;
                    let d = new Date(dat.substring(0,4) + '-' + dat.substring(4,6) + '-' + dat.substring(6,8) + 'T' + dat.substring(8,10) +':' + dat.substring(10,12) + ':'+ dat.substring(12,14) );
                    if (do_init==true) {    createState(id+'.'+item[j].unit,  d);    }
                      else {    setState(id+'.'+item[j].unit,  d);    }
                }
                else {
                    // first catch values in degrees celsius. Because there we can set the unit. Leave the others without unit.
                    if (item[j].unit == 'c') {
                      if (do_init==true) { createState(id+'.'+item[j].unit,  parseFloat(item[j].$t),state_temp); }                           
                        else  { setState(id+'.'+item[j].unit,  parseFloat(item[j].$t)); }               
                    }
                    else {
                        // Blacklisting for values we don't like
                        if (!(item[j].unit == 'f' || item[j].unit == 'en' || item[j].unit == 'nl' || item[j].unit == 'mph')) {
                        if (do_init==true) { createState(id+'.'+item[j].unit,  parseFloat(item[j].$t)); }                           
                            else  { setState(id+'.'+item[j].unit,  parseFloat(item[j].$t)); }               
                        }
                    }
                }
            }
    
         }
        }
    
    }  // end parseXML()
     
    function getXML(do_init) {
        // loads full meteohub xml file from the mh_URL and calls the parse function.
        // on very first execution do_init is true and all states are created. Each successive call only sets the state values.
        log('meteohub query data from '+mh_URL); // Debug output
        request(mh_URL, function (error, response, body) {
            if (error) log("Fehler beim Herunterladen Meteohub XML: " + error, 'error');
            else {
                var start = new Date();            
                parseXML(body,do_init);
                var time = new Date() - start;            
                log('Meteohub XML2JSON Durchlaufzeit: '+time);             
            };
        });
    }
    
    // On first script execution, create the states
     let do_init=true;
     getXML(do_init);
    
    // Regular Update
    schedule('*/1 * * * *', function () {
        let do_init=false;
        getXML(do_init);  // regelmäßiger Update
    });
     
     
    
  • Starter

    Die Notation mit XML2JSON ist hier etwas schöner, da sich die Objekte nicht ganz so oft hinter einem zusätzlichen Dollarzeichen verstecken.
    Aber den Aussschlag für die Nutzung dieser Version gab dann die Performance. Ich habe die Ausführungszeit im Skript mitgestoppt (Nur Parsen der Daten und setzen der States, kein Abrufen der XML Datei).

    XML2JS: 346ms
    XML2JSON: 146ms
    Beides auf einem Raspberry Pi 4. Ich persönlich hab mich damit für XML2JSON entschieden.

Suggested Topics

  • 15
  • 6
  • 4
  • 8
  • 11
  • 2
  • 9
  • 5

1.5k
Online

34.7k
Users

40.8k
Topics

559.1k
Posts