From 73188fcb7819f576b6ca82427958b3196e1a7353 Mon Sep 17 00:00:00 2001 From: Hofei90 <29521028+hofei90@users.noreply.github.com> Date: Mon, 6 Jan 2020 12:11:36 +0100 Subject: [PATCH] Initial Commit --- .gitattributes | 1 + .gitignore | 132 +++++ README.md | 101 +++- db/create_function_set_intervall.sql | 40 ++ db_postgrest.py | 73 +++ doc/pg_datenbankstruktur.pdf | Bin 0 -> 59678 bytes eigene_wetterdaten.py | 31 + feinstaub.py | 50 ++ grafana_export.py | 169 ++++++ messwerte_umrechner.py | 134 +++++ requirements.txt | 5 + setup_logging.py | 82 +++ systemd_files/feinstaub.service | 11 + systemd_files/feinstaub.timer | 10 + systemd_files/grafana_export.service | 11 + systemd_files/grafana_export.timer | 10 + vorlage_wetterconfig.toml | 50 ++ weewx_db_model.py | 850 +++++++++++++++++++++++++++ 18 files changed, 1759 insertions(+), 1 deletion(-) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 db/create_function_set_intervall.sql create mode 100644 db_postgrest.py create mode 100644 doc/pg_datenbankstruktur.pdf create mode 100644 eigene_wetterdaten.py create mode 100644 feinstaub.py create mode 100644 grafana_export.py create mode 100644 messwerte_umrechner.py create mode 100644 requirements.txt create mode 100644 setup_logging.py create mode 100644 systemd_files/feinstaub.service create mode 100644 systemd_files/feinstaub.timer create mode 100644 systemd_files/grafana_export.service create mode 100644 systemd_files/grafana_export.timer create mode 100644 vorlage_wetterconfig.toml create mode 100644 weewx_db_model.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ed41ae6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.py text eol=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fef32fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,132 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Smartmeter Konfigurationsfile +wetterconfig.toml diff --git a/README.md b/README.md index feb6216..d027eb4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,101 @@ -# weewx_to_grafanaserver +# Vorbereitungen +## Koordinaten vorbereiten +[Bayernatlas](https://geoportal.bayern.de "Bayernatlas") besuchen, Adresse der +Station eingeben, Koordinaten werden später benötigt. +Kooardinaten müssen dem Administrator bei der Stationsregistrierung mittgeteilt +werden. + +# Installation Client Skripte + +## Python +Es wird mindestens Python 3.7+ vorausgesetzt + +## Benötigte Python Module installieren + +```console +apt install python3-systemd +git clone https://git.pertl-ing.de/Hofei/weewx_to_grafanaserver.git /home/pi/weewx_to_grafanaserver +pip3 install -r requirements.txt +``` + +Bei der Installation ist gegenfalls zu achten die Module für den passenden User +zu installieren (bsp. sudo pip3 install ... oder pip3 install --user). +Die beiliegenden Service Units sind für den User root ausgelegt + + + +Es empiehlt sich folgenden Pfad zu verwenden, da hierfür die Service Units schon vorbereitet sind, +Abweichungen müssen angepasst werden +`/home/pi/weewx_to_grafanaserver`. + +## Konfiguration anpassen + +```console +cp vorlage_wetterconfig.toml wetterconfig.toml +nano wetterconfig.toml +``` + + +## Inbetriebnahme +Da WeeWx in der Standardinstallation mit root Rechten läuft, so benötigen auch diese Skripte root Rechte +um auf die Datenbank von WeeWx zugreifen zu können. Somit empfiehlt es sich die root Crontab zu verwenden +(`sudo crontab -e`) + +Anstelle der Verwendung der Crontab liegen im Repository für jedes Exportskript eine Systemd Service Unit mit Timer Unit +bei. +Dies stellt den **moderneren** Weg dar. Hierfür die universelle Anleitung unter dem Punkt Systemd Unit beachten + +### grafana_export.py + +Datei erstmals manuell ausführen. Erste Ausführung wird je nach Weewx Datenbank- +größe viel Zeit in Anspruch nehmen. --> Geduldig abwarten! +Nach der erfolgreichen ersten Übertragung der Wetterdaten aus der Weewx +Datenbank einen Cronjob mit dem gewünschten Ausführintervall erstellen +(Standardmäßig alle 5 Minuten) + +#### Cronjob + +````console +crontab -e +```` +`*/5 * * * * python3 /home/pi/weewx_to_grafanaserver/grafana_export.py` + + +### Feinstaubsensor (Optional) + +Wer einen Feinstaubsensor hat, die URL in der Konfigurationsdatei hinterlegen und `feinstaub.py` direkt aufrufen, +entweder mit Cronjob oder Systemd Service Unit + +#### Cronjob + +````console +crontab -e +```` +`*/5 * * * * python3 /home/pi/weewx_to_grafanaserver/feinstaub.py` + +Hinweis: Grafana kann nur nach erfolgreicher Stationsregistrierung +funktionieren. Hierfür den Administrator kontaktieren mit Besitzer, Stationsname +und Koordinaten + +### Systemd Unit +Alternativ zur Crontab kann eine Systemd Unit verwendet werden. +Hierfür die zugehörigen Dateien mit der Endung .service und .timer im systemd_files Ordner nach +`/etc/systemd/system` kopieren und die Rechte anpassen + +```console +cp /etc/systemd/system/ +chmod 644 /etc/systemd/system/ +``` + +Anschließend kann das Skript manuell getestet werden mit: +```console +systemctl start .service +``` + +Verläuft dieser Test positiv, kann der Timer gestartet werden: +```console +systemctl start .timer +systemctl enable .timer +``` + diff --git a/db/create_function_set_intervall.sql b/db/create_function_set_intervall.sql new file mode 100644 index 0000000..2b95a49 --- /dev/null +++ b/db/create_function_set_intervall.sql @@ -0,0 +1,40 @@ +-- Funktion für PostgreSQL Server, für Clientskripte nicht relevant +-- FUNCTION: public.set_intervall(interval, timestamp with time zone, timestamp with time zone) + +-- DROP FUNCTION public.set_intervall(interval, timestamp with time zone, timestamp with time zone); + +CREATE OR REPLACE FUNCTION public.set_intervall( + min_intervall interval, + von_timestamp timestamp with time zone, + bis_timestamp timestamp with time zone) + RETURNS interval + LANGUAGE 'sql' + + COST 100 + VOLATILE +AS $BODY$ +SELECT CASE + WHEN (min_intervall <= '1 seconds'::interval and '1 seconds'::interval > (bis_timestamp::timestamp - von_timestamp::timestamp) / 2000) THEN ('1 seconds'::interval) + WHEN (min_intervall <= '10 seconds'::interval and '10 seconds'::interval > (bis_timestamp::timestamp - von_timestamp::timestamp) / 2000) THEN ('10 seconds'::interval) + WHEN (min_intervall <= '30 seconds'::interval and '30 seconds'::interval > (bis_timestamp::timestamp - von_timestamp::timestamp) / 2000) THEN ('30 seconds'::interval) + WHEN (min_intervall <= '1 minutes'::interval and '1 minutes'::interval > (bis_timestamp::timestamp - von_timestamp::timestamp) / 2000) THEN ('1 minutes'::interval) + WHEN (min_intervall <= '5 minutes'::interval and '5 minutes'::interval > (bis_timestamp::timestamp - von_timestamp::timestamp) / 2000) THEN ('5 minutes'::interval) + WHEN (min_intervall <= '15 minutes'::interval and '15 minutes'::interval > (bis_timestamp::timestamp - von_timestamp::timestamp) / 2000) THEN ('15 minutes'::interval) + WHEN (min_intervall <= '30 minutes'::interval and '30 minutes'::interval > (bis_timestamp::timestamp - von_timestamp::timestamp) / 2000) THEN ('30 minutes'::interval) + WHEN (min_intervall <= '60 minutes'::interval and '60 minutes'::interval > (bis_timestamp::timestamp - von_timestamp::timestamp) / 2000) THEN ('60 minutes'::interval) + WHEN (min_intervall <= '120 minutes'::interval and '120 minutes'::interval > (bis_timestamp::timestamp - von_timestamp::timestamp) / 2000) THEN ('120 minutes'::interval) + WHEN (min_intervall <= '240 minutes'::interval and '240 minutes'::interval > (bis_timestamp::timestamp - von_timestamp::timestamp) / 2000) THEN ('240 minutes'::interval) + WHEN (min_intervall <= '480 minutes'::interval and '480 minutes'::interval > (bis_timestamp::timestamp - von_timestamp::timestamp) / 2000) THEN ('480 minutes'::interval) + WHEN (min_intervall <= '720 minutes'::interval and '720 minutes'::interval > (bis_timestamp::timestamp - von_timestamp::timestamp) / 2000) THEN ('720 minutes'::interval) + WHEN (min_intervall <= '1440 minutes'::interval and '1440 minutes'::interval > (bis_timestamp::timestamp - von_timestamp::timestamp) / 2000) THEN ('1440 minutes'::interval) + WHEN (min_intervall <= '2880 minutes'::interval and '2880 minutes'::interval > (bis_timestamp::timestamp - von_timestamp::timestamp) / 2000) THEN ('2880 minutes'::interval) + WHEN (min_intervall <= '10080 minutes'::interval and '10080 minutes'::interval > (bis_timestamp::timestamp - von_timestamp::timestamp) / 2000) THEN ('10080 minutes'::interval) + WHEN (min_intervall <= '43200 minutes'::interval and '43200 minutes'::interval > (bis_timestamp::timestamp - von_timestamp::timestamp) / 2000) THEN ('43200 minutes'::interval) + ELSE ('43200 minutes'::interval) +END; + +$BODY$; + +ALTER FUNCTION public.set_intervall(interval, timestamp with time zone, timestamp with time zone) + OWNER TO hofei; + diff --git a/db_postgrest.py b/db_postgrest.py new file mode 100644 index 0000000..317b406 --- /dev/null +++ b/db_postgrest.py @@ -0,0 +1,73 @@ +from dataclasses import dataclass +import datetime +from dataclasses_json import dataclass_json +import requests +import json +import pprint + + +@dataclass_json +@dataclass +class Basiswetterdaten: + ts: datetime.datetime + stationsname: str + outtemp: float = None + outluftfeuchte: float = None + heatindex: float = None + taupunkt: float = None + luftdruck: float = None + regenaktuell: float = None + regenrate: float = None + wind: float = None + windrichtung: str = None + windboe: float = None + windboe_richtung: str = None + windrichtung_grad: float = None + out_abs_luftfeuchte: float = None + windchill: float = None + + +@dataclass_json +@dataclass +class Zusatzwetterdaten: + ts: datetime.datetime + stationsname: str + wertname: str + wert: float + public: bool + + +def status_auswerten(r, logger, daten): + if not (r.status_code == 200 or r.status_code == 201): + logger.error(f"Statuscode: {r.status_code}\n Message: {r.text}") + logger.error(pprint.pprint(daten)) + + +def sende_daten(url, table, headers, daten, logger): + url = f"{url}{table}" + for data in daten: + data.ts = datetime.datetime.fromtimestamp(int(data.ts)).strftime("%Y-%m-%d %H:%M:%S") + logger.debug(f"Folgende Daten werden gesendet an {table}:\n {daten}") + r = requests.post(url, headers=headers, json=[data.to_dict() for data in daten]) + status_auswerten(r, logger, daten) + if r.status_code == 409: + logger.error("Primary Key Verletzung. Beginne Datensätze einzeln zu übertragen") + for data in daten: + r = requests.post(url, headers=headers, json=data.to_dict()) + status_auswerten(r, logger, data) + + +def hole_letzten_ts(url, table, headers, username): + query = f"select=ts&stationsname=eq.{username}&limit=1&order=ts.desc" + url = f"{url}{table}" + r = requests.get(url, headers=headers, params=query) + if r.status_code == 200: + inhalt = json.loads(r.text) + try: + ts_str = inhalt[0]["ts"] + ts = datetime.datetime.strptime(ts_str, "%Y-%m-%dT%H:%M:%S%z") + except IndexError: + ts = datetime.datetime(1970, 1, 1) + return ts + else: + raise TypeError(r.text) diff --git a/doc/pg_datenbankstruktur.pdf b/doc/pg_datenbankstruktur.pdf new file mode 100644 index 0000000000000000000000000000000000000000..21529da93a5e6abb997da10e1672ac85bd6a6ac7 GIT binary patch literal 59678 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

x|30}?cBm1g15^n3Of6GI8wd98i<@y}xoxn<=q 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 timestamp)\ + .order_by(db_weewx.Archive.date_time.asc()).limit(int(SENDELIMIT * 1.5)) + gesamtzahl = (len(query)) + if gesamtzahl > SENDELIMIT: + gesamtzahl = SENDELIMIT + LOGGER.info(f"Gesamtanzahl an Datensätze: {gesamtzahl} wird geladen") + basiswetterdaten_liste = [] + zusatzwetterdaten_liste = [] + for nr, datensatz in enumerate(query): + LOGGER.debug(f"Datensatz {nr + 1} von {gesamtzahl}") + rohdaten = {"ts": datensatz.date_time} + if datensatz.us_units == 1: + rohdaten["outtemp"] = mwu.temperaturumrechner(datensatz.out_temp) + rohdaten["inTemp"] = mwu.temperaturumrechner(datensatz.in_temp) + rohdaten["luftdruck"] = mwu.druckumrechner(datensatz.barometer) + rohdaten["wind"] = mwu.windumrechner(datensatz.wind_speed) + rohdaten["windboe"] = mwu.windumrechner(datensatz.wind_gust) + rohdaten["regenrate"] = mwu.regen_rate(datensatz.rain_rate) + rohdaten["regenaktuell"] = mwu.regen_menge(datensatz.rain) + rohdaten["taupunkt"] = mwu.temperaturumrechner(datensatz.dewpoint) + rohdaten["heatindex"] = mwu.temperaturumrechner(datensatz.heatindex) + rohdaten["windchill"] = mwu.temperaturumrechner(datensatz.windchill) + else: + rohdaten["outtemp"] = datensatz.out_temp + rohdaten["inTemp"] = datensatz.in_temp + rohdaten["luftdruck"] = datensatz.barometer + rohdaten["wind"] = datensatz.wind_speed + rohdaten["windboe"] = datensatz.wind_gust + rohdaten["regenrate"] = datensatz.rain_rate + rohdaten["regenaktuell"] = datensatz.rain + rohdaten["taupunkt"] = datensatz.dewpoint + rohdaten["heatindex"] = datensatz.heatindex + rohdaten["wincchill"] = datensatz.windchill + if isinstance(datensatz.out_humidity, (int, float)): + rohdaten["outluftfeuchte"] = float(round(datensatz.out_humidity, 0)) + else: + rohdaten["outluftfeuchte"] = None + if isinstance(datensatz.in_humidity, (int, float)): + rohdaten["inLuftfeuchte"] = float(round(datensatz.in_humidity, 0)) + else: + rohdaten["inLuftfeuchte"] = None + if isinstance(datensatz.wind_dir, (int, float)): + rohdaten["windrichtung_grad"] = float(round(datensatz.wind_dir, 1)) + else: + rohdaten["windrichtung_grad"] = None + if isinstance(datensatz.wind_gust_dir, (int, float)): + rohdaten["windboe_richtung"] = float(round(datensatz.wind_gust_dir, 1)) + else: + rohdaten["windboe_richtung"] = None + rohdaten["windrichtung"] = mwu.himmelsrichtungwandler(datensatz.wind_dir) + rohdaten["out_abs_luftfeuchte"] = mwu.absolute_luftfeuchtigkeit(rohdaten["outtemp"], rohdaten["outluftfeuchte"]) + + rohdaten = eigene_wetterdaten.eigene_wetterdaten(rohdaten) + + basiswetterdaten_liste.append( + db_postgrest.Basiswetterdaten(stationsname=stationsname, + **{key: value for key, value in rohdaten.items() if + key in db_postgrest.Basiswetterdaten.__dataclass_fields__})) + zusatzdatenliste = [{key: value} for key, value in rohdaten.items() if + key not in db_postgrest.Basiswetterdaten.__dataclass_fields__] + + for zusatzdaten in zusatzdatenliste: + for key, value in zusatzdaten.items(): + if key is None or value is None: + continue + zusatzwetterdaten_liste.append( + db_postgrest.Zusatzwetterdaten(ts=rohdaten["ts"], stationsname=stationsname, wertname=key, + wert=value, public=False)) + if nr >= SENDELIMIT - 1: + aufruf_wiederholen = True + break + + return basiswetterdaten_liste, zusatzwetterdaten_liste, aufruf_wiederholen + + +def freigabe_setzen(zusatzwetterdaten_liste): + for zusatzdaten in zusatzwetterdaten_liste: + if zusatzdaten.wertname in CONFIG["grafana"]["public"]: + zusatzdaten.public = CONFIG["grafana"]["public"][zusatzdaten.wertname] + else: + zusatzdaten.public = False + return zusatzwetterdaten_liste + + +def main(): + laufende_prozesse = check_process() + if laufende_prozesse > 1: + print("EXIT aufgrund laufender Prozesse") + sys.exit() + # Verzögerung aufgrund vom Cronjob, >>alle 5Minute, damit es nicht mit der Erstellung von Weewx kolidiert + time.sleep(CONFIG["weewx"]["sleeptime"]) + + db_adapter = CONFIG["weewx"]["db"] + db = db_weewx.init_db(CONFIG["weewx"][db_adapter]["database"], db_adapter, CONFIG["weewx"].get(db_adapter)) + db_weewx.database.initialize(db) + + headers = {f"Authorization": "{user} {token}".format(user=CONFIG["zieldb"]["postgrest"]["user"], + token=CONFIG["zieldb"]["postgrest"]["token"])} + url = CONFIG["zieldb"]["postgrest"]["url"] + if not url.endswith("/"): + url = f"{url}/" + + while True: + letzter_ts_server = db_postgrest.hole_letzten_ts(url, + CONFIG["zieldb"]["postgrest"]["tablename_basiswetterdaten"], + headers, CONFIG["grafana"]["grafana_name"]) + basiswetterdaten, zusatzwetterdaten, aufruf_wiederholen = rohdaten_laden(letzter_ts_server.timestamp(), + CONFIG["grafana"]["grafana_name"]) + + zusatzwetterdaten = freigabe_setzen(zusatzwetterdaten) + db_postgrest.sende_daten(url, CONFIG["zieldb"]["postgrest"]["tablename_basiswetterdaten"], headers, + basiswetterdaten, LOGGER) + db_postgrest.sende_daten(url, CONFIG["zieldb"]["postgrest"]["tablename_zusatzwetterdaten"], headers, + zusatzwetterdaten, LOGGER) + + if not aufruf_wiederholen: + break + + +if __name__ == "__main__": + start = datetime.datetime.now() + LOGGER.debug(f"Start: {start}") + try: + main() + + except KeyboardInterrupt: + LOGGER.info("Durch Benutzer abgebrochen") + else: + LOGGER.info("Export erfolgreich") + ende = datetime.datetime.now() + LOGGER.debug(f"Ende: {ende}") + LOGGER.debug(f"Dauer: {ende - start}") diff --git a/messwerte_umrechner.py b/messwerte_umrechner.py new file mode 100644 index 0000000..e4201d1 --- /dev/null +++ b/messwerte_umrechner.py @@ -0,0 +1,134 @@ +""" +Modul zum Umrechnen der US Einheiten aus Weewx in das Metrische System +Version 0.1 +""" + +import math +from typing import Optional, Union + + +def temperaturumrechner(fahrenheit: Optional[Union[int, float]]) -> Optional[float]: + """ + Umrechnung von Fahrenheit in Celsius + :param fahrenheit: int, float or None + :return: float or None + """ + if isinstance(fahrenheit, (int, float)): + celsius = (fahrenheit - 32) / 1.8 + celsius = round(celsius, 2) + return celsius + else: + return None + + +def druckumrechner(inHG): + """ + Umwandlung von inHG in mBar + :param inHG: int, float or None + :return: float or None + """ + if isinstance(inHG, (int, float)): + mbar = inHG * 33.86389 + mbar = round(mbar, 2) + return mbar + else: + return None + + +def himmelsrichtungwandler(grad): + """ + Umwandlung von Grad in Himmelsrichtung + von (>) bis (<=) + N 348,75 11,25 + NNO 11,25 33,75 + NO 33,75 56,25 + ONO 56,25 78,75 + O 78,75 101,25 + OSO 101,25 123,75 + SO 123,75 146,25 + SSO 146,25 168,75 + S 168,75 191,25 + SSW 191,25 213,75 + SW 213,75 236,25 + WSW 236,25 258,75 + W 258,75 281,25 + WNW 281,25 303,75 + NW 303,75 326,25 + NNW 326,25 348,75 + :param grad: float or None + :return: String or None + """ + if isinstance(grad, (int, float)): + richtung = ("N", "NNO", "NO", "ONO", "O", "OSO", "SO", "SSO", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW", + "N") + return richtung[int((int(grad / (360 / 32)) + 1) / 2)] + else: + return None + + +def windumrechner(wind): + """ + Umwandlung von milen pro std in km/h + :param wind: Int, float or None + :return: Float or None + """ + if isinstance(wind, (int, float)): + kmh = wind * 1.609346 + kmh = round(kmh, 2) + return kmh + else: + return None + + +def regen_rate(wert): + """ + Umwandlung von Inch/h in mm/h + :param wert: Int, float or None + :return: Float or None + """ + if isinstance(wert, (int, float)): + regenrate = wert * 25.4 + regenrate = round(regenrate, 2) + return regenrate + else: + return None + + +def regen_menge(wert): + """ + Umwandlung von Inch in mm + :param wert: Int, float or None + :return: Float or None + """ + if isinstance(wert, (int, float)): + regenmenge = wert * 25.4 + regenmenge = round(regenmenge, 2) + return regenmenge + else: + return None + + +def magnus_formel_wasser(temperatur): + sattdampfdruck_wasser = 611.2 * math.e ** ((17.62 * temperatur) / (243.12 + temperatur)) + return sattdampfdruck_wasser + + +def wasserdampf_partialdruck_berechnen(relative_feuchte, sattdampfdruck_wasser): + wasserdampf_partialdruck = (relative_feuchte/100) * sattdampfdruck_wasser + return wasserdampf_partialdruck + + +def celsius_in_kelvin(temperatur): + kelvin = temperatur + 273.15 + return kelvin + + +def absolute_luftfeuchtigkeit(temperatur, relative_feuchte): + """g/m³""" + if temperatur is None or relative_feuchte is None: + return + sattdampfdruck_wasser = magnus_formel_wasser(temperatur) + wasserdampf_partialdruck = wasserdampf_partialdruck_berechnen(relative_feuchte, sattdampfdruck_wasser) + kelvin = celsius_in_kelvin(temperatur) + abs_feuchte = round((wasserdampf_partialdruck / (461.51 * kelvin)) * 1000, 3) + return abs_feuchte diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..dbaf208 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +requests +peewee +psutil +toml +dataclasses-json \ No newline at end of file diff --git a/setup_logging.py b/setup_logging.py new file mode 100644 index 0000000..543cb69 --- /dev/null +++ b/setup_logging.py @@ -0,0 +1,82 @@ +#!/usr/bin/python3 +""" +Beschreibung: +Hilfskript zum Erstellen einer logging Instanz in Abhängigkeit, ob das Skript manuell gestartet worden ist oder +ob das Skript per Service Unit gestartet wurde +Abhängig dessen wird entweder die Logging Instanz mit einem JournalHandler erstellt (Service Unit) +oder mit einem StreamHandler (manuell) + +Author: Hofei +Datum: 03.08.2018 +Version: 0.1 +""" +import logging +import os +import shlex +import subprocess + +from systemd import journal + + +def __setup_logging(loglevel, frm, startmethode, unitname): + """ + Erstellt die Logger Instanz für das Skript + """ + logger = logging.getLogger() + logger.setLevel(loglevel) + logger.handlers = [] + if startmethode == "auto": + log_handler = journal.JournalHandler(SYSLOG_IDENTIFIER=unitname) + + else: + log_handler = logging.StreamHandler() + log_handler.setLevel(loglevel) + log_handler.setFormatter(frm) + logger.addHandler(log_handler) + return logger + + +def __get_service_unit_pid(unitname): + """Ermittelt ob das ausführende Skript mit einer Service Unit gestartet worden ist, wenn ja so ist das + Ergebnis (pid_service_unit) != 0""" + cmd = "systemctl show -p MainPID {}".format(unitname) + cmd = shlex.split(cmd) + antwort = subprocess.run(cmd, stdout=subprocess.PIPE) + ausgabe = antwort.stdout + # strip entfernt \n, split teilt am = in eine Liste und [1] weißt die Zahl in die Variable zu + pid_service_unit = int(ausgabe.decode().strip().split("=")[1]) + return pid_service_unit + + +def __get_startmethode(unitname): + """Verglicht die PID vom skript mit der pid Service Unit Prüfung + wenn die Nummern gleich sind wird auf auto gestellt, wenn nicht auf manuell""" + pid_service_unit = __get_service_unit_pid(unitname) + pid_skript = os.getpid() + if pid_service_unit == pid_skript: + startmethode = "auto" + else: + startmethode = "manuell" + return startmethode + + +def __set_loggerformat(startmethode): + """Stellt die passende Formattierung ein""" + if startmethode == "auto": + frm = logging.Formatter("%(levelname)s: %(message)s", "%d.%m.%Y %H:%M:%S") + else: + frm = logging.Formatter("%(asctime)s %(levelname)s: %(message)s", "%d.%m.%Y %H:%M:%S") + return frm + + +def create_logger(unitname, loglevel): + """Dies ist die aufzurufende Funktion bei der Verwendung des Moduls von außen + Liefert die fertige Logging Instanz zurück""" + startmethode = __get_startmethode(unitname) + frm = __set_loggerformat(startmethode) + return __setup_logging(loglevel, frm, startmethode, unitname) + + +if __name__ == "__main__": + logger = create_logger("testunit", 10) + logger.debug("Testnachricht") diff --git a/systemd_files/feinstaub.service b/systemd_files/feinstaub.service new file mode 100644 index 0000000..d5f8fda --- /dev/null +++ b/systemd_files/feinstaub.service @@ -0,0 +1,11 @@ +# Pfad zum speichern: /etc/systemd/system/feinstaub.service +[Unit] +Description=ServiceUnit zum starten des Feinstaub Skriptes +After=network.target + +[Service] +Type=simple +ExecStart=/usr/bin/python3 /home/pi/weewx_to_grafanaserver/feinstaub.py + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/systemd_files/feinstaub.timer b/systemd_files/feinstaub.timer new file mode 100644 index 0000000..2c9cd96 --- /dev/null +++ b/systemd_files/feinstaub.timer @@ -0,0 +1,10 @@ +# Pfad zum speichern: /etc/systemd/system/feinstaub.timer +[Unit] +Description=Timer zum Aufruf der Feinstaub Service Unit + +[Timer] +OnCalendar=OnCalendar=*:0/5 +Unit=feinstaub.service + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/systemd_files/grafana_export.service b/systemd_files/grafana_export.service new file mode 100644 index 0000000..47157e4 --- /dev/null +++ b/systemd_files/grafana_export.service @@ -0,0 +1,11 @@ +# Pfad zum speichern: /etc/systemd/system/grafana_export.service +[Unit] +Description=ServiceUnit zum starten des Grafana Export Skriptes +After=network.target + +[Service] +Type=simple +ExecStart=/usr/bin/python3 /home/pi/weewx_to_grafanaserver/grafana_export.py + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/systemd_files/grafana_export.timer b/systemd_files/grafana_export.timer new file mode 100644 index 0000000..4683dcd --- /dev/null +++ b/systemd_files/grafana_export.timer @@ -0,0 +1,10 @@ +# Pfad zum speichern: /etc/systemd/system/grafana_export.timer +[Unit] +Description=Timer zum Aufruf der Grafana Export Service Unit + +[Timer] +OnCalendar=OnCalendar=*:0/5 +Unit=grafana_export.service + +[Install] +WantedBy=multi-user.target \ No newline at end of file diff --git a/vorlage_wetterconfig.toml b/vorlage_wetterconfig.toml new file mode 100644 index 0000000..d11579e --- /dev/null +++ b/vorlage_wetterconfig.toml @@ -0,0 +1,50 @@ +# vorlage_wetterconfig.toml umbennen in wetterconfig.toml +loglevel = 20 # (0=NOTSET 10=DEBUG 20=INFO 30=WARNING 40=ERROR 50=CRITICAL) Standard = 20 + +[weewx] +sleeptime = 60 # Zeitverzögerung wie lange Skript bis zur Ausführung wartet damit es nicht kolidiert mit Weewx +db = "mysql" # "mysql" | "sqlite" + + [weewx.mysql] + database = "" + user = "" + password = "" + host= "localhost" + + [weewx.sqlite] + database = ":memory:" # Path to database file or :memory: + +[zieldb] + + [zieldb.postgrest] + url = "" + user = "" + token = "" + tablename_basiswetterdaten = "" + tablename_zusatzwetterdaten = "" + + +# Grafana Konfiguration +[grafana] +pfad_ausgabe = "/PFAD/WOHIN/AUSGABE/FÜR/GRAFANA/" +grafana_name = "STATIONSNAME" # Wie Station bei Admin gemeldet +server_url = "GRAFANA_SERVER_URL" # Erhältlich bei Admin von Grafana +token = "GRAFANA_TOKEN" # Erhältlich bei Admin von Grafana +dateiname = "DATEINAME_FÜR_ABHOLUNG" # Standard: "daten_grafana.toml" +server_info_url = "GRAFANA_SERVER_INFO_URL" # Erhältlich bei Admin von Grafana +longitude = "STATIONSKOORDINATEN" +latitude = "STATIONSKOORDINATEN" + + # Hier alle Zusatzdaten eintragen welche (nicht) öffentlich sichtbar sein sollen + [grafana.public] + inTemp = false + inLuftfeuchte = false + bad_temp = false + bad_feuchte = false + bad_abs_feuchte = false + + +[feinstaub] +url = "" + + diff --git a/weewx_db_model.py b/weewx_db_model.py new file mode 100644 index 0000000..705f14d --- /dev/null +++ b/weewx_db_model.py @@ -0,0 +1,850 @@ +from peewee import * + +database = Proxy() + + +class UnknownField(object): + def __init__(self, *_, **__): pass + + +class BaseModel(Model): + class Meta: + database = database + + +class Archive(BaseModel): + et = FloatField(column_name='ET', null=True) + uv = FloatField(column_name='UV', null=True) + altimeter = FloatField(null=True) + barometer = FloatField(null=True) + cons_battery_voltage = FloatField(column_name='consBatteryVoltage', null=True) + date_time = AutoField(column_name='dateTime') + dewpoint = FloatField(null=True) + extra_humid1 = FloatField(column_name='extraHumid1', null=True) + extra_humid2 = FloatField(column_name='extraHumid2', null=True) + extra_temp1 = FloatField(column_name='extraTemp1', null=True) + extra_temp2 = FloatField(column_name='extraTemp2', null=True) + extra_temp3 = FloatField(column_name='extraTemp3', null=True) + hail = FloatField(null=True) + hail_rate = FloatField(column_name='hailRate', null=True) + heatindex = FloatField(null=True) + heating_temp = FloatField(column_name='heatingTemp', null=True) + heating_voltage = FloatField(column_name='heatingVoltage', null=True) + in_humidity = FloatField(column_name='inHumidity', null=True) + in_temp = FloatField(column_name='inTemp', null=True) + in_temp_battery_status = FloatField(column_name='inTempBatteryStatus', null=True) + interval = IntegerField() + leaf_temp1 = FloatField(column_name='leafTemp1', null=True) + leaf_temp2 = FloatField(column_name='leafTemp2', null=True) + leaf_wet1 = FloatField(column_name='leafWet1', null=True) + leaf_wet2 = FloatField(column_name='leafWet2', null=True) + out_humidity = FloatField(column_name='outHumidity', null=True) + out_temp = FloatField(column_name='outTemp', null=True) + out_temp_battery_status = FloatField(column_name='outTempBatteryStatus', null=True) + pressure = FloatField(null=True) + radiation = FloatField(null=True) + rain = FloatField(null=True) + rain_battery_status = FloatField(column_name='rainBatteryStatus', null=True) + rain_rate = FloatField(column_name='rainRate', null=True) + reference_voltage = FloatField(column_name='referenceVoltage', null=True) + rx_check_percent = FloatField(column_name='rxCheckPercent', null=True) + soil_moist1 = FloatField(column_name='soilMoist1', null=True) + soil_moist2 = FloatField(column_name='soilMoist2', null=True) + soil_moist3 = FloatField(column_name='soilMoist3', null=True) + soil_moist4 = FloatField(column_name='soilMoist4', null=True) + soil_temp1 = FloatField(column_name='soilTemp1', null=True) + soil_temp2 = FloatField(column_name='soilTemp2', null=True) + soil_temp3 = FloatField(column_name='soilTemp3', null=True) + soil_temp4 = FloatField(column_name='soilTemp4', null=True) + supply_voltage = FloatField(column_name='supplyVoltage', null=True) + tx_battery_status = FloatField(column_name='txBatteryStatus', null=True) + us_units = IntegerField(column_name='usUnits') + wind_battery_status = FloatField(column_name='windBatteryStatus', null=True) + wind_dir = FloatField(column_name='windDir', null=True) + wind_gust = FloatField(column_name='windGust', null=True) + wind_gust_dir = FloatField(column_name='windGustDir', null=True) + wind_speed = FloatField(column_name='windSpeed', null=True) + windchill = FloatField(null=True) + + class Meta: + table_name = 'archive' + + +class ArchiveDayEt(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_ET' + + +class ArchiveDayUv(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_UV' + + +class ArchiveDayMetadata(BaseModel): + name = CharField(primary_key=True) + value = TextField(null=True) + + class Meta: + table_name = 'archive_day__metadata' + + +class ArchiveDayAltimeter(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_altimeter' + + +class ArchiveDayBarometer(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_barometer' + + +class ArchiveDayConsBatteryVoltage(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_consBatteryVoltage' + + +class ArchiveDayDewpoint(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_dewpoint' + + +class ArchiveDayExtraHumid1(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_extraHumid1' + + +class ArchiveDayExtraHumid2(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_extraHumid2' + + +class ArchiveDayExtraTemp1(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_extraTemp1' + + +class ArchiveDayExtraTemp2(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_extraTemp2' + + +class ArchiveDayExtraTemp3(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_extraTemp3' + + +class ArchiveDayHail(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_hail' + + +class ArchiveDayHailRate(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_hailRate' + + +class ArchiveDayHeatindex(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_heatindex' + + +class ArchiveDayHeatingTemp(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_heatingTemp' + + +class ArchiveDayHeatingVoltage(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_heatingVoltage' + + +class ArchiveDayInHumidity(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_inHumidity' + + +class ArchiveDayInTemp(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_inTemp' + + +class ArchiveDayInTempBatteryStatus(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_inTempBatteryStatus' + + +class ArchiveDayLeafTemp1(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_leafTemp1' + + +class ArchiveDayLeafTemp2(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_leafTemp2' + + +class ArchiveDayLeafWet1(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_leafWet1' + + +class ArchiveDayLeafWet2(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_leafWet2' + + +class ArchiveDayOutHumidity(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_outHumidity' + + +class ArchiveDayOutTemp(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_outTemp' + + +class ArchiveDayOutTempBatteryStatus(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_outTempBatteryStatus' + + +class ArchiveDayPressure(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_pressure' + + +class ArchiveDayRadiation(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_radiation' + + +class ArchiveDayRain(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_rain' + + +class ArchiveDayRainBatteryStatus(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_rainBatteryStatus' + + +class ArchiveDayRainRate(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_rainRate' + + +class ArchiveDayReferenceVoltage(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_referenceVoltage' + + +class ArchiveDayRxCheckPercent(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_rxCheckPercent' + + +class ArchiveDaySoilMoist1(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_soilMoist1' + + +class ArchiveDaySoilMoist2(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_soilMoist2' + + +class ArchiveDaySoilMoist3(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_soilMoist3' + + +class ArchiveDaySoilMoist4(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_soilMoist4' + + +class ArchiveDaySoilTemp1(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_soilTemp1' + + +class ArchiveDaySoilTemp2(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_soilTemp2' + + +class ArchiveDaySoilTemp3(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_soilTemp3' + + +class ArchiveDaySoilTemp4(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_soilTemp4' + + +class ArchiveDaySupplyVoltage(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_supplyVoltage' + + +class ArchiveDayTxBatteryStatus(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_txBatteryStatus' + + +class ArchiveDayWind(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + dirsumtime = IntegerField(null=True) + max = FloatField(null=True) + max_dir = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + squaresum = FloatField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsquaresum = FloatField(null=True) + wsum = FloatField(null=True) + xsum = FloatField(null=True) + ysum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_wind' + + +class ArchiveDayWindBatteryStatus(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_windBatteryStatus' + + +class ArchiveDayWindDir(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_windDir' + + +class ArchiveDayWindGust(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_windGust' + + +class ArchiveDayWindGustDir(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_windGustDir' + + +class ArchiveDayWindSpeed(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_windSpeed' + + +class ArchiveDayWindchill(BaseModel): + count = IntegerField(null=True) + date_time = AutoField(column_name='dateTime') + max = FloatField(null=True) + maxtime = IntegerField(null=True) + min = FloatField(null=True) + mintime = IntegerField(null=True) + sum = FloatField(null=True) + sumtime = IntegerField(null=True) + wsum = FloatField(null=True) + + class Meta: + table_name = 'archive_day_windchill' + + +def init_db(name, type_="sqlite", config=None): + config = config or {} + drivers = { + "sqlite": SqliteDatabase, + "mysql": MySQLDatabase, + } + + try: + cls = drivers[type_] + except KeyError: + raise ValueError("Unknown database type: {}".format(type_)) from None + del config["database"] + db = cls(name, **config) + return db