Nachbars Wetterstationen

Wetter Apps sind immer bisschen Hit&Miss. Wie warm ist es wirklich draußen? Eigene Wetterstation ist teuer und etwas Overkill. Aber die Nachbarn haben doch eine. Warum nicht die anzapfen? Warum nicht gleich alle in der Umgebung anzapfen und den Durchschnitt daraus berechnen?

Die meisten Wetterstationen (zumindest die mit Wireless Außen-Gerät und Empfänger am Küchentisch) senden auf dem 433MHz Band. Das ist offen und jeder darf es benutzen. Also schnell mal ne Antenne draußen befestigt und schauen was so daher kommt. Die Daten ein wenig aufbereiten und in meine Datenbank schreiben, damit ich dann den Durchschnitt und Historische werte anzeigen kann.

Hardware

Viel braucht es dafür nicht. Ich verwende dazu das selbe Gerät wie für den Raspberry Pi Timelapser. Basis davon ist ein Raspberry Pi 4. Die Antenne und der Empfänger wird einfach an einen der USB Ports angeschlossen.

Raspberry Pi 4 B

https://www.raspberrypi.com/products/raspberry-pi-4-model-b

Der Klassiker. Die Leistung ist mehr als ausreichend für mehrere kleine Projekte wie dieses.

  • Quad Core CPU
  • 4GB Ram
  • 128GB MicroSD
  • 2x USB3.0 Ports

Nooelec NESDR Smart v5

https://www.nooelec.com/store/sdr/sdr-receivers/smart/nesdr-smart-sdr.html

Kleines aber feines Software-Defined Radio (SDR). Das ist ein Funkgerät, bei dem Modulation und Demodulation durch Software realisiert werden. Anders als bei alten Radios, wo man ein Knopferl drehen muss, macht es das SDR per Software.

Außerdem ist es kompatibel mit RTL-SDR und den meisten Libraries da draußen.

  • 100kHz-1.75GHz
  • RTL2832U & R820T2
  • Kleiner Footprint, blockt keine anderen USB Geräte.

Software

Basis den ganzen ist das Programm rtl_433. Die Werte als json ausgeben, mit Python zerstückeln und in die Datenbank und über MQTT verschicken.

rtl_433

https://github.com/merbanan/rtl_433

Installation recht easy, weil über viele Package Manager vorhanden. Am Pi zb:

apt-get install rtl-433

Ausführen dann easy mit

rtl_sdr

Ist zwar hübsch zum anschauen, aber schwierig zum automatisch auslesen. Lieber die Optionen für JSON output aktivieren.

rtl_433 -F json

Viel besser. Mit diesem Output kann man was anfangen

Python script

Ich möchte rtl_433 in einem Subprocess aufrufen und den Output dann analysieren. Jede Zeile in ihre Einzelteile zerlegen und die Wichtigen Infos weiter verarbeiten. In meinem Fall heißt das über MQTT weiter schicken.

import json
import os
import subprocess
from datetime import datetime
import time
import paho.mqtt.client as mqtt

MQTT_SERVER = "bigblackbox.local"
MQTT_TOPIC = "433_scraper/"
MQTT_PORT = 1883

print("Hello from Raspberry!")

command = ["rtl_433", "-C", "si", "-F", "json"]

# Open the subprocess and capture its output
sensors = []

mqttc = mqtt.Client("weatherpi")
mqttc.connect(MQTT_SERVER, MQTT_PORT)
print(f"MQTT connected to {MQTT_SERVER}:{MQTT_PORT}")

def on_publish(client, userdata, message):
    pass
    # print(f"Published: {message}")

mqttc.on_publish = on_publish

process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)

def send_over_mqtt(message):
    global mqttc
    print(message)

    model = message.get('model')
    id = message.get('id')
    subtopic = f"{model}/{id}"

    payload = str(message)

    mqttc.publish(MQTT_TOPIC + subtopic + "/json", payload)

    for key, value in message.items():
        mqttc.publish(MQTT_TOPIC + subtopic + "/" + key, value)

def update_sensors(new_message):
    """
    TODO: mitschreiben in text file
    :param new_message:
    :return:
    """
    print(f"{new_message}")
    global sensors
    # Check if a message with the same model and id exists in the list
    for i, message in enumerate(sensors):
        if message.get('model') == new_message.get('model') and message.get('id') == new_message.get('id') and message.get('channel') == new_message.get('channel'):

            date_format = '%Y-%m-%d %H:%M:%S'
            this_datetime = datetime.strptime(new_message.get("time"), date_format)
            last_datetime = datetime.strptime(message['time'], date_format)
            last_interval_s = (this_datetime - last_datetime).total_seconds()
            print(f"Seen {last_interval_s} seconds ago")
            if last_interval_s < 2:
                print("Ignoring message, too soon")
                break

            new_message['count'] = message['count'] + 1
            new_message['suspect_interval'] = last_interval_s
            # Update the existing message with the new values
            sensors[i] = new_message


            break
    else:
        print("New sensor found!")
        # If no existing message found, add the new message to the list
        new_message['count'] = 1
        sensors.append(new_message)

    dump_all_sensors(clear=True)

def dump_all_sensors(clear=False):
    print(f"- SENSORS [{len(sensors)}] ------------------------------------------------------------------")
    if clear:
        os.system('clear')
    for item in sensors:
        time = item.get('time').ljust(20)
        model = item.get('model').ljust(25)
        id = str(item.get('id')).rjust(8)
        count = str(item.get('count')).rjust(4)
        channel = str(item.get('channel'))
        interval = (str(item.get('suspect_interval'))+" s").rjust(8)
        ignore = ["time", "model", "id", "battery_ok", "mic", "suspect_interval", "count", "channel"]
        copied_dict = item.copy()
        for item in ignore:
            copied_dict.pop(item, None)

        print(f"{time} [{interval}, {count}] | {id} {model} CH{channel} | {copied_dict}")
    print(80 * "-")

mqttc.loop_start()

while True:
    output = process.stdout.readline()  # Read a line from the output
    if output == '' and process.poll() is not None:  # If no more output and the process has terminated
        break
    if output:
        jstring = json.loads(output.strip())
        # update_sensors(jstring)
        send_over_mqtt(jstring)

mqttc.loop_stop()

return_code = process.wait()
print("Process finished with return code:", return_code)

  • MQTT verbinden
  • MQTT starten
  • rtl_433 starten, stdout und stderr in Pipe umleiten
  • Endlosschleife.
    • Pipe stdout einlesen (was 433_rtl ausgibt)
    • Alle JSON fields was da drin stehen über MQTT weiter schicken

Github Repo für das Script hier:

https://github.com/thazaubara/weatherstation-scraper/blob/main/ws-scraper.py

Im MQTT Explorer schaut das dann so aus:

Alles unter dem Topic 433_scraper sind Hersteller von Wetterstationen. Die Subtopics sind IDs von einzelnen Wetterstationen. Es werden also einige empfangen. Allein unter Nexus-TH gibt es 4 verschiedene Wetterstationen.

Das Script sollte im Hintergrund ausgeführt werden um nicht den stdout zuzuspammen.

sudo python3 ws-scraper.py | logger &

Kann man auch als Cronjob @boot registrieren.

Die Logs gibts dann hier

tail -n 100 /var/log/messages

Node Red

In meinem Fall werden die Daten von Node Red auf meinem Server weiterverarbeitet.

Die Wichtigsten Topics (Wetterstationen) werden manuell ausgesucht und in den Graphen eingepflegt.

Nicht alle Wetterdaten davon sind brauchbar, weil manche messen auch die Innentemperatur von Wohnungen. Für meinen Zweck eher irrelevant.

Nicht alle Wetterstationen senden im gleichen Intervall. Daher sammle ich mit dem Node MULTI TIMED AVERAGE alle eingehenden Nachrichten pro Topic. Alle 5 Minuten wird dann aus allen Nachrichten einer Wetterstation der Durchschnitt gebildet. Somit habe ich am Ende von dem Node ein synchronisiertes Signal, das Daten von allen Wetterstationen enthält.

Javascript Code hier. Wird getriggert ON MESSAGE.

// Initialize context variables to store sums and counts for each topic
if (context.get('topicStats') === undefined) {
    context.set('topicStats', {});
}

// Function to calculate average for a specific topic
function calculateAverage(topic, message) {
    // Initialize sum and count for the topic if it doesn't exist
    if (!context.get('topicStats')[topic]) {
        context.get('topicStats')[topic] = { sum: 0, count: 0 };
    }

    // Add incoming value to sum and increment count for the topic
    context.get('topicStats')[topic].sum += message;
    context.get('topicStats')[topic].count++;

    // Check if count exceeds the maximum allowed
    const maxCount = 1000; // Maximum allowed count
    if (context.get('topicStats')[topic].count > maxCount) {
        node.error("Too many messages for '" + topic + "' (" + maxCount + "+). Resetting buffer.");
        // Reset sums and counts for the topic
        context.get('topicStats')[topic].sum = 0;
        context.get('topicStats')[topic].count = 0;
    }

    // Calculate average
    const average = context.get('topicStats')[topic].sum / context.get('topicStats')[topic].count;

    // Round the average to two decimal places
    const roundedAverage = parseFloat(average.toFixed(2));

    return roundedAverage;
}

function returnAverages() {
    const averages = {};
    // Iterate over topics and calculate averages
    for (const topic in context.get('topicStats')) {
        if (context.get('topicStats').hasOwnProperty(topic)) {
            const topic_average = context.get('topicStats')[topic].sum / context.get('topicStats')[topic].count;
            averages[topic] = parseFloat(topic_average.toFixed(2));
        }
    }
    // Reset sums and counts for all topics
    context.set('topicStats', {});
    return averages;
}

// Main function to handle incoming messages and trigger signal
if (msg.hasOwnProperty('reset') && msg.reset === true) {
    msg.payload = returnAverages();
    return msg;
} else {
    // If it's a regular message, calculate and store the average based on the topic
    const topic = msg.topic || 'default'; // Use 'default' topic if topic is not specified
    const average = calculateAverage(topic, msg.payload);

    // No output message for regular messages
    return null;
}

Die Daten gehen dann in den AVERAGE OBJCET Node, in dem der Durchschnitt über alle Wetterstationen gebildet wird. Hier kommt ein Wert heraus, der mir anzeigt wie warm es draußen wirklich ist. Gemessen von 8 Wetterstationen (zum Zeitpunkt des Screenshots).

Das ganze auch noch mit der Luftfeuchtigkeit, weil Schema F.

Javascript Code hier. Wird getriggert von ON MESSAGE, also alle 5 Minuten.

var sum = 0;
var count = 0;

for (var val of Object.values(msg.payload)) {
    sum += val;
    count++;
}

var average = sum/count;
msg.payload = parseFloat(average.toFixed(2));

return msg;

Und weil MQTT super ist, geht der Durchschnittswert auch hier wieder raus.

Außerdem werden alle Messages von den Wetterstationen (in ihrem eigenen Intervall) an die InfluxDB auf meinem Server geschickt. Der berechnete Durchschnitt auch. Intervall davon sind die 5 Minuten, bedingt durch den Trigger der die Nachrichten zusammenfasst.

Grafana

Alles das in der Datenbank ist, kann ganz einfach mit Grafana angezeigt werden. Läuft auch auf meinem Server.

Wie man gut erkennen kann in dem Details Fenster, sind aktuell 7 Wetterstationen aktiv und liefern Daten. Angezeigt werden sie als gestrichelte Linien im Graphen. Der Durchschnitt, der Wert der mich wirklich interessiert, ist in fett. Links in dem Panel wird der Letzte Wert vom Durchschnitt angezeigt, quasi mein gescraptes Thermometer. Funktioniert top!

Warum das Ganze?

Die Daten von einem Temperatursensor sind nicht wirklich aussagekräfitg, je nachdem wo er aufgestellt wird. Der sollte nämlich im Schatten, mit ausrechend Platz rundherum und ungefähr 1,50m vom Boden entfernt stehen. Wie viele Leute montieren Ihr Außengerät nicht nach diesen Standards?

Viele davon stehen auf einem Fensterbrett, wo es meistens direkter Sonneneinstrahlung ausgesetzt ist, was die Temperatur nach oben treibt. Außerdem speichern die Mauern auch Hitze, wodurch es sogar am Abend eine zu hohe Temperatur anzeigt.

Andere Sensoren stehen vielleicht neben einem Fenster wo es vom Keller kühl hinaus bläst. Oder Neben der Klimaanlage. Oder neben einer kalten Hauswand.

Ich hatte selbst das Problem, dass mein MQTT Temperatursensor in der Sonne montiert war. so kann es schnell mal passieren, dass der Sensor im Sommer 40°C anzeigt. Was natürlich nicht der Realität entspricht.

Die Grafik erklärt gut was ich versuche zu erklären.

Die Sensoren der Nachbarn sind komplett im gesamten Innenhof verstreut, wenn nicht sogar in anderen Bauten. Somit bekomme ich von allen Situationen Daten und kann davon ausgehen, In dem Fall fand ich es sinnvoll den Durchschnitt aller Sensoren zu nehmen um mir eine möglichst genaue Repräsentation der Realität zu liefern.