{"id":1269,"date":"2023-11-07T15:07:58","date_gmt":"2023-11-07T13:07:58","guid":{"rendered":"https:\/\/www.gressly.ch\/?page_id=1269"},"modified":"2024-02-03T11:38:57","modified_gmt":"2024-02-03T09:38:57","slug":"wetterstation","status":"publish","type":"page","link":"https:\/\/www.gressly.ch\/index.php\/elektronik\/wetterstation\/","title":{"rendered":"Wetterstation"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Damit alle Messwerte einer Wetterstation mit openHAB angezeigt werden k\u00f6nnen, m\u00fcssen diese irgendwie maschinell lesbar sein. Die Ger\u00e4te, die man im Handel kaufen kann, haben aber keine Schnittstelle, die sowas erm\u00f6glicht. Auch funktionieren die allermeisten Systeme mit einer Batterie, die man periodisch auswechseln muss. Daher meine F\u00fchler auf dem Dach montiert werden sollen, kommt diese L\u00f6sung nicht in Frage. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">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\u00fcr Ger\u00e4te. Daher habe ich mir nun eine Wetterstation gebaut, welche via REST-API die Daten im Heimnetz zur Verf\u00fcgung stellt und mit POE gespiesen wird. Als Basis dient ein Raspberry Pi Zero mit POE-Hat. Daran habe ich die n\u00f6tigen Sensoren und Bauteile verbunden.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Zuerst mal alles auf einem Breadboard entworfen und getestet:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"768\" src=\"https:\/\/www.gressly.ch\/wp-content\/uploads\/2023\/11\/P_20231028_150055-1024x768.jpg\" alt=\"\" class=\"wp-image-1271\" srcset=\"https:\/\/www.gressly.ch\/wp-content\/uploads\/2023\/11\/P_20231028_150055-1024x768.jpg 1024w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2023\/11\/P_20231028_150055-300x225.jpg 300w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2023\/11\/P_20231028_150055-768x576.jpg 768w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2023\/11\/P_20231028_150055-1536x1152.jpg 1536w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2023\/11\/P_20231028_150055-2048x1536.jpg 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Alle Sensoren sind verbunden, somit kann der Code entwickelt werden.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"768\" height=\"1024\" src=\"https:\/\/www.gressly.ch\/wp-content\/uploads\/2023\/11\/P_20231028_150106-768x1024.jpg\" alt=\"\" class=\"wp-image-1272\" srcset=\"https:\/\/www.gressly.ch\/wp-content\/uploads\/2023\/11\/P_20231028_150106-768x1024.jpg 768w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2023\/11\/P_20231028_150106-225x300.jpg 225w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2023\/11\/P_20231028_150106-1152x1536.jpg 1152w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2023\/11\/P_20231028_150106-1536x2048.jpg 1536w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2023\/11\/P_20231028_150106-scaled.jpg 1920w\" sizes=\"auto, (max-width: 768px) 100vw, 768px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Es sieht komplizierter aus als es ist. Im Prinzip ist es ganz einfach:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"1013\" src=\"https:\/\/www.gressly.ch\/wp-content\/uploads\/2023\/11\/weatherstation_wiring_Steckplatine-1024x1013.png\" alt=\"\" class=\"wp-image-1288\" srcset=\"https:\/\/www.gressly.ch\/wp-content\/uploads\/2023\/11\/weatherstation_wiring_Steckplatine-1024x1013.png 1024w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2023\/11\/weatherstation_wiring_Steckplatine-300x297.png 300w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2023\/11\/weatherstation_wiring_Steckplatine-768x760.png 768w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2023\/11\/weatherstation_wiring_Steckplatine-1536x1520.png 1536w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2023\/11\/weatherstation_wiring_Steckplatine-80x80.png 80w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2023\/11\/weatherstation_wiring_Steckplatine.png 1755w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Die Messdaten kommen als JSON in der Antwort auf eine HTTP GET Anfrage. Diese kann ich ganz einfach im openHAB pollen, anzeigen und loggen.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\/\/ http:\/\/pizero\/values\n\n{\n  \"updated\": \"2023-11-07, 14:25:08\",\n  \"air\": {\n    \"temperature\": 11.04,\n    \"humidity\": 63.79,\n    \"pressure\": 951.82\n  },\n  \"wind\": {\n    \"speed\": 0.9,\n    \"direction\": 225\n  },\n  \"rain\": {\n    \"actual\": 43,\n    \"hour\": 0.0,\n    \"day\": 5.03,\n    \"week\": 11.73\n  }\n}<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Install packages<\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">sudo apt update\nsudo apt upgrade\n\nsudo apt install python3-dev\nsudo apt install python3-pip\n\nsudo pip3 install jedi\nsudo pip3 install Flask \nsudo pip3 install adafruit-circuitpython-ahtx0\nsudo pip3 install adafruit-circuitpython-bmp280\n\nsudo raspi-config\n#interface options - activate IC2\n#interface - spi - enable\n#reboot raspberry\nshutdown -r now\n\n\n# test run\nsudo python -m flask --app serv.py run  --host=0.0.0.0 --port=80\n\n# install as service\nsudo cp rest-api@pi.service \/etc\/systemd\/system\/\nsudo systemctl --system daemon-reload\nsudo systemctl enable rest-api@pi\nsudo systemctl start rest-api@pi\n<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">rest-api@pi.service<\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">[Unit]\nDescription=Rest API Service for Raspberry Pi\nAfter=network.target\n\n[Service]\nType=simple\nUser=root\nExecStart=\/usr\/bin\/python -m flask --app \/home\/username\/serv.py run  --host=0.0.0.0 --port=80\n\n[Install]\nWantedBy=multi-user.target<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">The python code serv.py<\/h2>\n\n\n\n<pre class=\"wp-block-preformatted\">from flask import Flask\nimport adafruit_ahtx0, adafruit_bmp280\nimport json, math, time, board, pickle\nimport RPi.GPIO as GPIO\nfrom gpiozero import MCP3008, Button\nfrom datetime import datetime\nfrom collections import deque\n\nGPIO.setwarnings(False) # Ignore warning for now\nGPIO.setmode(GPIO.BCM)\n\nfile_path = \"bucket_tips\"\n\n######################## air\n\ni2c = board.I2C()\nsensor_aht = adafruit_ahtx0.AHTx0(i2c)\nsensor_bmp = adafruit_bmp280.Adafruit_BMP280_I2C(i2c)\n\ndef getAir():\n    return {\n        \"temperature\": round(sensor_aht.temperature, 2),\n        \"humidity\": round(sensor_aht.relative_humidity, 2),\n        \"pressure\": round(sensor_bmp.pressure, 2)\n    }\n\n######################## wind\n\nGPIO.setup(12, GPIO.IN, pull_up_down=GPIO.PUD_DOWN) #wind speed\nhalf_turns = deque([])\nread_span_sec = 10\nanenometer_radius_meters = 0.09\nanemometer_factor = 1.18\nhalf_turns_multiplier = math.pi * anenometer_radius_meters * anemometer_factor\n\ndef spin(channel):\n    global half_turns\n    now = datetime.timestamp(datetime.now())\n    half_turns.append(now)\n    #print(f\"anenometer turned half way {now}\")\n    #avoid the list filling too much\n    start_read_span = now-read_span_sec\n    if len(half_turns) &gt; 0 and half_turns[0] &lt; start_read_span:\n        half_turns.popleft()\n\nGPIO.add_event_detect(12, GPIO.RISING, callback=spin) \n\ndef getWindspeed():\n    global half_turns\n    start_read_span = datetime.timestamp(datetime.now()) - read_span_sec\n    while len(half_turns) &gt; 0 and half_turns[0] &lt; start_read_span:\n        half_turns.popleft()\n    #print(f\"after removing {half_turns}\")\n    #print(f\"turns in the past {read_span_sec} sec {len(half_turns)\/2}\")\n    windspeed_meter_per_sec = ( half_turns_multiplier * len(half_turns) ) \/ read_span_sec\n    return windspeed_meter_per_sec\n\nadc = MCP3008(channel=0)\nwind_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}\n\ndef getWinddirection():\n    wind_direction_measurement = int(adc.value * 100)\n    #print(f\"wind_direction_measurement {wind_direction_measurement}\")\n    for wind_direction_code in wind_direction_codes:\n        if wind_direction_measurement-1 &lt;= wind_direction_code and wind_direction_code &lt;= wind_direction_measurement+1:\n            return wind_direction_codes[wind_direction_code]\n\ndef getWind():\n    return {\n        \"speed\": round(getWindspeed(), 2),\n        \"direction\": getWinddirection()\n    }\n\n######################## rain\n\n\nbucket_tips = deque([])\nbucket_volume_mm = 0.2794\none_hour_sec = 60 * 60\none_day_sec = one_hour_sec * 24\none_week_sec = one_day_sec * 7\n\ndef bucket_tipped(channel):\n    now_sec = datetime.timestamp(datetime.now())\n    bucket_tips.append(now_sec)\n    #print(f\"added bucket tip {now_sec}\")\n    \n    one_week_ago = now_sec - one_week_sec\n    while len(bucket_tips) &gt; 0 and bucket_tips[0] &lt; one_week_ago:\n        bucket_tips.popleft()\n    #print(f\"removed bucket tips older than a week. \")\n\n    with open(file_path, 'wb') as file:\n        pickle.dump(bucket_tips, file)\n    #print(f\"saved bucket tips to file: {file_path}\")\n\n\nrain_actual = MCP3008(channel=1)\nrain_amount = Button(23)\nrain_amount.when_pressed = bucket_tipped\n\ndef getRain():\n    now_sec = datetime.timestamp(datetime.now())\n    one_hour_ago = now_sec - one_hour_sec\n    one_day_ago = now_sec - one_day_sec\n    one_week_ago = now_sec - one_week_sec\n    one_hour_tips = 0\n    one_day_tips = 0\n    one_week_tips = 0\n    for bucket_tip in bucket_tips:\n        if bucket_tip &gt; one_hour_ago:\n            one_hour_tips += 1\n        if bucket_tip &gt; one_day_ago:\n            one_day_tips += 1\n        if bucket_tip &gt; one_week_ago:\n            one_week_tips += 1\n    #print(f\"tips in the past hour: {one_hour_tips}, day: {one_day_tips}, week: {one_week_tips}\")\n\n    rain = {\n        \"actual\": round((1-rain_actual.value)*100),\n        \"hour\": round(one_hour_tips * bucket_volume_mm, 2),\n        \"day\": round(one_day_tips * bucket_volume_mm, 2),\n        \"week\": round(one_week_tips * bucket_volume_mm, 2),\n    }\n\n    return rain\n\n######################## init stuff\n\ndef initialize():\n    global bucket_tips\n    try:\n        with open(file_path, 'rb') as file:\n            bucket_tips = pickle.load(file)\n    except:\n        print(f\"could not read file {file_path}\")\n    print(\"initialize finished\")\n    \ninitialize()    \napp = Flask(__name__)\n\n######################## REST controller\n\n@app.get(\"\/values\")\ndef values():\n    air = getAir()\n    rain = getRain()\n    wind = getWind()\n    updated = datetime.now().strftime(\"%Y-%m-%d, %H:%M:%S\")\n    print(f\"values - updated: {updated}, air: {air}, rain: {rain}, wind: {wind}, \")\n\n    x = {\n      \"updated\": updated,\n      \"air\": air,\n      \"wind\": wind,\n      \"rain\": rain\n    }\n    return json.dumps(x)\n<\/pre>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"768\" src=\"https:\/\/www.gressly.ch\/wp-content\/uploads\/2024\/01\/P_20231206_185805-1024x768.jpg\" alt=\"\" class=\"wp-image-1314\" srcset=\"https:\/\/www.gressly.ch\/wp-content\/uploads\/2024\/01\/P_20231206_185805-1024x768.jpg 1024w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2024\/01\/P_20231206_185805-300x225.jpg 300w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2024\/01\/P_20231206_185805-768x576.jpg 768w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2024\/01\/P_20231206_185805-1536x1152.jpg 1536w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2024\/01\/P_20231206_185805-2048x1536.jpg 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Die Elektronik habe ich auf ein Perma-Proto Board gel\u00f6tet, 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.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"768\" src=\"https:\/\/www.gressly.ch\/wp-content\/uploads\/2024\/01\/P_20231206_185737-1024x768.jpg\" alt=\"\" class=\"wp-image-1315\" srcset=\"https:\/\/www.gressly.ch\/wp-content\/uploads\/2024\/01\/P_20231206_185737-1024x768.jpg 1024w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2024\/01\/P_20231206_185737-300x225.jpg 300w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2024\/01\/P_20231206_185737-768x576.jpg 768w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2024\/01\/P_20231206_185737-1536x1152.jpg 1536w, https:\/\/www.gressly.ch\/wp-content\/uploads\/2024\/01\/P_20231206_185737-2048x1536.jpg 2048w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">So habe ich alle Sensoren mit dem Board verbunden und f\u00fcr die Anschl\u00fcsse ausserhalb sind zwei RJ11 Buchsen montiert. Diese sind so platziert, dass kein Wasser eindringen kann.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Damit alle Messwerte einer Wetterstation mit openHAB angezeigt werden k\u00f6nnen, m\u00fcssen diese irgendwie maschinell lesbar sein. Die Ger\u00e4te, die man im Handel kaufen kann, haben aber keine Schnittstelle, die sowas erm\u00f6glicht. Auch funktionieren die allermeisten Systeme mit einer Batterie, die man periodisch auswechseln muss. Daher meine F\u00fchler auf dem Dach montiert werden sollen, kommt diese L\u00f6sung 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\u00fcr Ger\u00e4te. Daher habe ich mir nun eine Wetterstation gebaut, welche via REST-API die Daten im Heimnetz zur Verf\u00fcgung stellt und mit POE gespiesen wird. Als Basis dient ein Raspberry Pi Zero mit POE-Hat. Daran habe ich die n\u00f6tigen 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 { &#8220;updated&#8221;: &#8220;2023-11-07, 14:25:08&#8221;, &#8220;air&#8221;: { &#8220;temperature&#8221;: 11.04, &#8220;humidity&#8221;: 63.79, &#8220;pressure&#8221;: 951.82 }, &#8220;wind&#8221;: { &#8220;speed&#8221;: 0.9, &#8220;direction&#8221;: 225 }, &#8220;rain&#8221;: { &#8220;actual&#8221;: 43, &#8220;hour&#8221;: 0.0, &#8220;day&#8221;: 5.03, &#8220;week&#8221;: 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 &#8211; activate IC2 #interface &#8211; spi &#8211; enable #reboot raspberry shutdown -r now # test run sudo python -m flask &#8211;app serv.py run &#8211;host=0.0.0.0 &#8211;port=80 # install as service sudo cp rest-api@pi.service \/etc\/systemd\/system\/ sudo systemctl &#8211;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 &#8211;app \/home\/username\/serv.py run &#8211;host=0.0.0.0 &#8211;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 = &#8220;bucket_tips&#8221; ######################## air i2c = board.I2C() sensor_aht = adafruit_ahtx0.AHTx0(i2c) sensor_bmp = adafruit_bmp280.Adafruit_BMP280_I2C(i2c) def getAir(): return { &#8220;temperature&#8221;: round(sensor_aht.temperature, 2), &#8220;humidity&#8221;: round(sensor_aht.relative_humidity, 2), &#8220;pressure&#8221;: 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&#8221;anenometer turned half way {now}&#8221;) #avoid the list filling too much start_read_span = now-read_span_sec if len(half_turns) &gt; 0 and half_turns[0] &lt; 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()) &#8211; read_span_sec while len(half_turns) &gt; 0 and half_turns[0] &lt; start_read_span: half_turns.popleft() #print(f&#8221;after removing {half_turns}&#8221;) #print(f&#8221;turns in the past {read_span_sec} sec {len(half_turns)\/2}&#8221;) 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,&#46;&#46;&#46;<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":1202,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-1269","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/www.gressly.ch\/index.php\/wp-json\/wp\/v2\/pages\/1269","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.gressly.ch\/index.php\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.gressly.ch\/index.php\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.gressly.ch\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.gressly.ch\/index.php\/wp-json\/wp\/v2\/comments?post=1269"}],"version-history":[{"count":15,"href":"https:\/\/www.gressly.ch\/index.php\/wp-json\/wp\/v2\/pages\/1269\/revisions"}],"predecessor-version":[{"id":1353,"href":"https:\/\/www.gressly.ch\/index.php\/wp-json\/wp\/v2\/pages\/1269\/revisions\/1353"}],"up":[{"embeddable":true,"href":"https:\/\/www.gressly.ch\/index.php\/wp-json\/wp\/v2\/pages\/1202"}],"wp:attachment":[{"href":"https:\/\/www.gressly.ch\/index.php\/wp-json\/wp\/v2\/media?parent=1269"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}