Wetterstation

Damit alle Messwerte einer Wetterstation mit openHAB angezeigt werden können, müssen diese irgendwie maschinell lesbar sein. Die Geräte, die man im Handel kaufen kann, haben aber keine Schnittstelle, die sowas ermöglicht. Auch funktionieren die allermeisten Systeme mit einer Batterie, die man periodisch auswechseln muss. Daher meine Fühler auf dem Dach montiert werden sollen, kommt diese Lösung nicht in Frage.

Als unsere PVA montiert wurde, habe ich gleich eine Ethernet-Kabel von Dach zum Keller eingezogen. Dieses ist mit dem POE-Switch kombiniert auch eine Stromquelle für Geräte. Daher habe ich mir nun eine Wetterstation gebaut, welche via REST-API die Daten im Heimnetz zur Verfügung stellt und mit POE gespiesen wird. Als Basis dient ein Raspberry Pi Zero mit POE-Hat. Daran habe ich die nötigen Sensoren und Bauteile verbunden.

Zuerst mal alles auf einem Breadboard entworfen und getestet:

Alle Sensoren sind verbunden, somit kann der Code entwickelt werden.

Es sieht komplizierter aus als es ist. Im Prinzip ist es ganz einfach:

Die Messdaten kommen als JSON in der Antwort auf eine HTTP GET Anfrage. Diese kann ich ganz einfach im openHAB pollen, anzeigen und loggen.

// http://pizero/values

{
  "updated": "2023-11-07, 14:25:08",
  "air": {
    "temperature": 11.04,
    "humidity": 63.79,
    "pressure": 951.82
  },
  "wind": {
    "speed": 0.9,
    "direction": 225
  },
  "rain": {
    "actual": 43,
    "hour": 0.0,
    "day": 5.03,
    "week": 11.73
  }
}

Install packages

sudo apt update
sudo apt upgrade

sudo apt install python3-dev
sudo apt install python3-pip

sudo pip3 install jedi
sudo pip3 install Flask 
sudo pip3 install adafruit-circuitpython-ahtx0
sudo pip3 install adafruit-circuitpython-bmp280

sudo raspi-config
#interface options - activate IC2
#interface - spi - enable
#reboot raspberry
shutdown -r now


# test run
sudo python -m flask --app serv.py run  --host=0.0.0.0 --port=80

# install as service
sudo cp rest-api@pi.service /etc/systemd/system/
sudo systemctl --system daemon-reload
sudo systemctl enable rest-api@pi
sudo systemctl start rest-api@pi

rest-api@pi.service

[Unit]
Description=Rest API Service for Raspberry Pi
After=network.target

[Service]
Type=simple
User=root
ExecStart=/usr/bin/python -m flask --app /home/username/serv.py run  --host=0.0.0.0 --port=80

[Install]
WantedBy=multi-user.target

The python code serv.py

from flask import Flask
import adafruit_ahtx0, adafruit_bmp280
import json, math, time, board, pickle
import RPi.GPIO as GPIO
from gpiozero import MCP3008, Button
from datetime import datetime
from collections import deque

GPIO.setwarnings(False) # Ignore warning for now
GPIO.setmode(GPIO.BCM)

file_path = "bucket_tips"

######################## air

i2c = board.I2C()
sensor_aht = adafruit_ahtx0.AHTx0(i2c)
sensor_bmp = adafruit_bmp280.Adafruit_BMP280_I2C(i2c)

def getAir():
    return {
        "temperature": round(sensor_aht.temperature, 2),
        "humidity": round(sensor_aht.relative_humidity, 2),
        "pressure": round(sensor_bmp.pressure, 2)
    }

######################## wind

GPIO.setup(12, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) #wind speed
half_turns = deque([])
read_span_sec = 10
anenometer_radius_meters = 0.09
anemometer_factor = 1.18
half_turns_multiplier = math.pi * anenometer_radius_meters * anemometer_factor

def spin(channel):
    global half_turns
    now = datetime.timestamp(datetime.now())
    half_turns.append(now)
    #print(f"anenometer turned half way {now}")
    #avoid the list filling too much
    start_read_span = now-read_span_sec
    if len(half_turns) > 0 and half_turns[0] < start_read_span:
        half_turns.popleft()

GPIO.add_event_detect(12, GPIO.RISING, callback=spin) 

def getWindspeed():
    global half_turns
    start_read_span = datetime.timestamp(datetime.now()) - read_span_sec
    while len(half_turns) > 0 and half_turns[0] < start_read_span:
        half_turns.popleft()
    #print(f"after removing {half_turns}")
    #print(f"turns in the past {read_span_sec} sec {len(half_turns)/2}")
    windspeed_meter_per_sec = ( half_turns_multiplier * len(half_turns) ) / read_span_sec
    return windspeed_meter_per_sec

adc = MCP3008(channel=0)
wind_direction_codes = {12:0, 41:22.5, 36:45, 84:67.5, 82:90, 87:112.5, 68:135, 77:157.5, 54:180, 60:202.5, 22:225, 25:247.5, 3:270, 10:292.5, 6:315, 17:337.5}

def getWinddirection():
    wind_direction_measurement = int(adc.value * 100)
    #print(f"wind_direction_measurement {wind_direction_measurement}")
    for wind_direction_code in wind_direction_codes:
        if wind_direction_measurement-1 <= wind_direction_code and wind_direction_code <= wind_direction_measurement+1:
            return wind_direction_codes[wind_direction_code]

def getWind():
    return {
        "speed": round(getWindspeed(), 2),
        "direction": getWinddirection()
    }

######################## rain


bucket_tips = deque([])
bucket_volume_mm = 0.2794
one_hour_sec = 60 * 60
one_day_sec = one_hour_sec * 24
one_week_sec = one_day_sec * 7

def bucket_tipped(channel):
    now_sec = datetime.timestamp(datetime.now())
    bucket_tips.append(now_sec)
    #print(f"added bucket tip {now_sec}")
    
    one_week_ago = now_sec - one_week_sec
    while len(bucket_tips) > 0 and bucket_tips[0] < one_week_ago:
        bucket_tips.popleft()
    #print(f"removed bucket tips older than a week. ")

    with open(file_path, 'wb') as file:
        pickle.dump(bucket_tips, file)
    #print(f"saved bucket tips to file: {file_path}")


rain_actual = MCP3008(channel=1)
rain_amount = Button(23)
rain_amount.when_pressed = bucket_tipped

def getRain():
    now_sec = datetime.timestamp(datetime.now())
    one_hour_ago = now_sec - one_hour_sec
    one_day_ago = now_sec - one_day_sec
    one_week_ago = now_sec - one_week_sec
    one_hour_tips = 0
    one_day_tips = 0
    one_week_tips = 0
    for bucket_tip in bucket_tips:
        if bucket_tip > one_hour_ago:
            one_hour_tips += 1
        if bucket_tip > one_day_ago:
            one_day_tips += 1
        if bucket_tip > one_week_ago:
            one_week_tips += 1
    #print(f"tips in the past hour: {one_hour_tips}, day: {one_day_tips}, week: {one_week_tips}")

    rain = {
        "actual": round((1-rain_actual.value)*100),
        "hour": round(one_hour_tips * bucket_volume_mm, 2),
        "day": round(one_day_tips * bucket_volume_mm, 2),
        "week": round(one_week_tips * bucket_volume_mm, 2),
    }

    return rain

######################## init stuff

def initialize():
    global bucket_tips
    try:
        with open(file_path, 'rb') as file:
            bucket_tips = pickle.load(file)
    except:
        print(f"could not read file {file_path}")
    print("initialize finished")
    
initialize()    
app = Flask(__name__)

######################## REST controller

@app.get("/values")
def values():
    air = getAir()
    rain = getRain()
    wind = getWind()
    updated = datetime.now().strftime("%Y-%m-%d, %H:%M:%S")
    print(f"values - updated: {updated}, air: {air}, rain: {rain}, wind: {wind}, ")

    x = {
      "updated": updated,
      "air": air,
      "wind": wind,
      "rain": rain
    }
    return json.dumps(x)

Die Elektronik habe ich auf ein Perma-Proto Board gelötet, welches auf den 40-Pin Header vom Raspberry PI Zero gesteckt werden kann. Die Pins kann man dann auf einfache Weise mit anderen Komponenten verbinden.

So habe ich alle Sensoren mit dem Board verbunden und für die Anschlüsse ausserhalb sind zwei RJ11 Buchsen montiert. Diese sind so platziert, dass kein Wasser eindringen kann.