diff --git a/appdata.py b/appdata.py index 8cc7256..a03a7e1 100644 --- a/appdata.py +++ b/appdata.py @@ -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) diff --git a/obp60v.conf-sample b/obp60v.conf-sample index 0ad6a85..40e23fd 100644 --- a/obp60v.conf-sample +++ b/obp60v.conf-sample @@ -66,6 +66,7 @@ name = My boat sailno = GER 815 class = One off handicap = 100.0 +draft = 1.90 club = NONE team = OBP diff --git a/pages/tide.py b/pages/tide.py index 2c089c1..29977c8 100644 --- a/pages/tide.py +++ b/pages/tide.py @@ -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) - ctx.line_to(x, y0 - y) + 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 diff --git a/web.py b/web.py index 751d4d3..f7053d3 100644 --- a/web.py +++ b/web.py @@ -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()