Tide weiterprogrammiert

This commit is contained in:
Thomas Hooge 2025-10-09 20:12:54 +02:00
parent 7613a5b7cb
commit 9ffb28d65b
4 changed files with 159 additions and 35 deletions

View File

@ -19,7 +19,6 @@ class AppData():
self.navtex = None
if cfg['tide']:
self.web = WebInterface(logger, cfg)
self.web.refresh()
else:
self.web = None
self.track = Tracker(logger, cfg)

View File

@ -66,6 +66,7 @@ name = My boat
sailno = GER 815
class = One off
handicap = 100.0
draft = 1.90
club = NONE
team = OBP

View File

@ -6,6 +6,7 @@ Tide Vorausschau
import cairo
from .page import Page
from datetime import datetime
class Tide(Page):
@ -13,6 +14,8 @@ class Tide(Page):
super().__init__(pageno, cfg, appdata, boatdata)
self.station = "f3c6ee73-5561-4068-96ec-364016e7d9ef" # Schulau
self.app.web.set_pegel('List, Sylt')
self.app.web.refresh()
def draw(self, ctx):
# Title
@ -20,23 +23,70 @@ class Tide(Page):
ctx.set_font_size(24)
ctx.move_to(8, 40)
ctx.show_text("Tide prediction")
ctx.set_font_size(10)
ctx.set_font_size(16)
ctx.set_line_width(2)
# Daten holen
self.app.web.set_timestamp(2, 48)
ymin, ymax = self.app.web.get_tide_minmax()
rawdata = self.app.web.get_tide()
ctx.move_to(220, 40)
ctx.show_text("Schulau, Elbe")
# scale_y_step
scale_y_step = 50
ymin = min(ymin, self.app.web.tide['MNW']) - 10
ymin = (ymin // scale_y_step - 1) * scale_y_step
ymax = max(ymax, self.app.web.tide['MHW']) + 10
ymax = (ymax // scale_y_step + 1) * scale_y_step
#self.tide['warning'] = wvdata["warning"]
#self.tide['forecast_ts'] = wvdata["creation_forecast"]
x0 = 40 # links unten
y0 = 250
x1 = 380 # rechts oben
y1 = 60
ctx.set_line_width(1)
ctx.set_dash((2, 2), 0)
y = (self.app.web.tide['MNW'] - ymin) / (ymax - ymin) * (y0 - y1)
ctx.move_to(x0 - 8, y0 - y)
ctx.line_to(x1 + 8, y0 - y)
y = (self.app.web.tide['MHW'] - ymin) / (ymax - ymin) * (y0 - y1)
ctx.move_to(x0 - 8, y0 - y)
ctx.line_to(x1 + 8, y0 - y)
ctx.stroke()
ctx.set_dash([])
ctx.set_font_size(16)
ctx.set_line_width(2)
ctx.move_to(220, 40)
self.draw_text_ralign(ctx, 392, 36, self.app.web.tide['station'])
ctx.move_to(8, 50)
calc_ts = datetime.fromisoformat(self.app.web.tide['forecast_ts'])
self.draw_text_ralign(ctx, 392, 52, "calc: " + calc_ts.strftime('%H:%M'))
#self.tide['area'] = wvdata["area"]
# X-Achse
ctx.move_to(x0 + 0.5, y0 + 0.5)
ctx.line_to(x0 + 0.5, y1 + 0.5)
ctx.stroke()
# Pfeilspitze Y
ctx.move_to(x1 + 0.5, y0 + 0.5 - 4)
ctx.line_to(x1 + 0.5 + 12, y0 + 0.5)
ctx.line_to(x1 + 0.5, y0 + 0.5 + 4)
ctx.close_path()
ctx.fill()
# Pfeilspitze Y
ctx.move_to(x0 + 0.5 - 4, y1 + 0.5)
ctx.line_to(x0 + 0.5, y1 + 0.5 - 12)
ctx.line_to(x0 + 0.5 + 4, y1 + 0.5)
ctx.close_path()
ctx.fill()
# self.draw_text_center(ctx, x0 - 20, y0 + (y1 -y0) / 2, "Höhe, cm", rotate=True)
self.draw_text_center(ctx, 60, 150, "Höhe, cm", rotate=True)
self.draw_text_center(ctx, 60, 150, "height, cm", rotate=True)
#self.draw_text_center(ctx, 100, 100, "Höhe, cm", rotate=False)
#ctx.move_to(90, 90) # Rotationsursprung
#ctx.line_to(110, 110)
@ -47,31 +97,30 @@ class Tide(Page):
# Y-Achse
ctx.move_to(x0 + 0.5, y0 + 0.5)
ctx.line_to(x1 + 0.5, y0 + 0.5)
self.draw_text_center(ctx, x0 + (x1 - x0) / 2, y0 + 12, "Zeit, h")
self.draw_text_center(ctx, x0 + (x1 - x0) / 2, y0 + 12, "time, h")
ctx.stroke()
ymin, ymax = self.app.web.get_tide_minmax()
rawdata = self.app.web.get_tide()
ctx.move_to(x0 - 32, y0 + 8)
ctx.show_text(str(ymin))
ctx.move_to(x0 - 32, y1 + 8)
ctx.show_text(str(ymax))
ctx.move_to(x0 + 16, y0 + 16)
ctx.show_text(f"n = {len(rawdata)}")
# Anzahl Meßwerte für die X-Achse
#ctx.move_to(x0 + 16, y0 + 16)
#ctx.show_text(f"n = {len(rawdata)}")
dx = (x1 - x0) / len(rawdata)
x = x0
while rawdata[0][0] is None:
x += dx
del rawdata[0]
y = (rawdata[0][0] - ymin) / (ymax - ymin) * (y0 - y1)
ctx.move_to(x, y0 - (rawdata[0][0] - ymin))
del rawdata[0]
x += dx
prev_valid = False
for val in rawdata:
y = (val[0] - ymin) / (ymax - ymin) * (y0 - y1)
if val is not None:
y = (val - ymin) / (ymax - ymin) * (y0 - y1)
if prev_valid:
ctx.line_to(x, y0 - y)
else:
ctx.move_to(x, y0 - y)
prev_valid = True
else:
prev_valid = False
x += dx

103
web.py
View File

@ -6,20 +6,48 @@ Regelmäßiges Abfragen von Daten aus dem Internet
- DWD
- Pegelstände
- pegelonline
- WSV
- BSH
- Wetterinformationen
TODO
- Datenbanktabelle um feld bshid erweitern um mehrere Pegel gleichzeitig
verarbeiten zu können
"""
import os
import http.client
import ssl
import json
import sqlite3
import datetime
from datetime import datetime, timedelta
from gi.repository import GLib
class WebInterface():
pegeldata = {
'Amrum': '631P',
'Borkum': '101P',
'Büsum': '505P',
'Cuxhaven': '506P',
'Dagebüll': '635P',
'Dwarsgat': '737P',
'Eider Sperrwerk': '664P',
'Emden': '507P',
'Helgoland': '509A',
'Hörnum, Sylt': '624P',
'Hooksiel': '764B',
'Hooge': '636F',
'Husum': '510P',
'Leer': '806P',
'List, Sylt': '617P',
'Norderney': '111P',
'Papenburg': '814P',
'Scharhörn': '677C',
'Schulau': '714P',
'St. Pauli': '508P',
'Wangerooge': '754P'
}
def __init__(self, logger, cfg):
self.log = logger
dbpath = os.path.join(cfg['histpath'], "tidedata.db")
@ -47,6 +75,19 @@ class WebInterface():
self.cur.execute(sql)
self.log.info(f"Created tide database: {dbpath}")
self.pegelid = 'Schulau'
self.bshid = self.pegeldata[self.pegelid]
# Tidendaten (Rahmen)
self.tide = {
'station': None,
'area': None,
'MHW': None,
'MNW': None,
'warning': None,
'forecast_ts': None
}
# In der Konfiguration werden Minuten angegeben
#GLib.timeout_add_seconds(cfg['tide_refresh'] * 60, self.on_timer)
GLib.timeout_add_seconds(60 * 60, self.on_timer)
@ -70,12 +111,12 @@ class WebInterface():
"""
if local:
# Für Tests um nicht permanent die Webseite abzufragen
with open("/tmp/DE__714P.json", "r") as fh:
with open(f"/tmp/DE__{self.bshid}.json", "r") as fh:
content = fh.read()
else:
ssl_context = ssl.create_default_context()
conn = http.client.HTTPSConnection("wasserstand-nordsee.bsh.de", 443, context=ssl_context)
url = "https://wasserstand-nordsee.bsh.de/data/DE__714P.json"
url = f"https://wasserstand-nordsee.bsh.de/data/DE__{self.bshid}.json"
try:
conn.request("GET", url)
@ -91,31 +132,65 @@ class WebInterface():
except ssl.SSLError as ssl_error:
self.log.warning(f"SSL error occurred: {ssl_error}")
return []
finally:
conn.close()
return content
def refresh(self):
self.log.info("Data refresh")
data = self.bsh_get_data(False)
#tmpfile = open("/tmp/DE__714P.json", "w")
#tmpfile.write(data)
#tmpfile.close()
tmpfile = open(f"/tmp/DE__{self.bshid}.json", "w")
tmpfile.write(data)
tmpfile.close()
wvdata = json.loads(data)
sql = "INSERT OR REPLACE INTO data (timestamp, astro, forecast, measurement) VALUES (?, ?, ?, ?)"
for wv in wvdata["curve_forecast"]["data"]:
self.cur.execute(sql, (wv['timestamp'], wv['astro'], wv['curveforecast'], wv['measurement']))
self.conn.commit()
self.tide['station'] = wvdata["station_name"]
self.tide['area'] = wvdata["area"]
self.tide['MHW'] = wvdata["MHW"] # Mittleres Hochwasser
self.tide['MNW'] = wvdata["MNW"] # Mittleres Niedrigwasser
self.tide['warning'] = wvdata["warning"]
self.tide['forecast_ts'] = wvdata["creation_forecast"]
# Zeitpunkte und Höhen der beiden nächsten Hoch- und Niedrigwasser
# wvdata["hwnwforecast"]["data"]["event"] HW / NW
# wvdata["hwnwforecast"]["data"]["timestamp"]
# wvdata["hwnwforecast"]["data"]["value"]
def housekeeping(self):
self.log.info("Housekeeping")
def set_pegel(self, pegelid):
self.pegelid = pegelid
self.bshid = self.pegeldata[self.pegelid]
def set_timestamp(self, vorlauf, dauer):
"""
Manuell setzen, damit verschiedene Aufrufe nacheinander auch
auf den selben Zeitstempel zurückgreifen
"""
self.current_ts = datetime.now().replace(minute=0, second=0, microsecond=0)
self.start_ts = self.current_ts - timedelta(hours=vorlauf)
self.end_ts = self.current_ts + timedelta(hours=dauer)
# TODO
# Stationsdaten
# Berechnungszeitpunkt
# Warnhinweise (z.B. Sturmflut)
def get_tide(self):
# 12 Stunden
sql = "select forecast from data where timestamp > '2025-10-05 12:00' and timestamp < '2025-10-06 22:00'"
self.cur.execute(sql)
return self.cur.fetchall()
# 12 Stunden Vorhersage ab der aktuelle Stunde
# Vorlauf sind 4 Stunden
# TODO Aufbereiten, daß nur eine einfache Liste zurückgeliefert wird
sql = "select forecast from data where timestamp > ? and timestamp < ?"
self.cur.execute(sql, (self.start_ts, self.end_ts))
return [x[0] for x in self.cur.fetchall()]
def get_tide_minmax(self):
sql = "select min(forecast), max(forecast) from data where timestamp > '2025-10-05 12:00' and timestamp < '2025-10-06 22:00'"
self.cur.execute(sql)
# Liefert ein Tupel mit min, max zurück
sql = "select min(forecast), max(forecast) from data where timestamp > ? and timestamp < ?"
self.cur.execute(sql, (self.start_ts, self.end_ts))
return self.cur.fetchone()