Bisher konnte man Automatiken nur anlegen - ausgefuehrt hat sie niemand. restricted/autoActions/autoaction_runner.py holt das nach. Dauerlaeufer statt Cronjob, aus zwei Gruenden: Schwellwert-Ausloeser sollen greifen, wenn die MQTT-Nachricht hereinkommt, und actor_states.current_value wird sonst von niemandem fortgeschrieben - beim Discovery einmal gesetzt und danach nie wieder. Ein zustandsloser Lauf haette gar nichts, womit er vergleichen koennte. Der Runner pflegt den Wert nebenbei mit, wovon auch der Editor profitiert: er zeigt neben jedem Messwert den aktuellen Stand. Ausgeloest wird nur auf der steigenden Flanke (automations.cond_met), sonst wuerde "Temperatur ueber 22 Grad" bei jedem Takt erneut feuern. Aus demselben Grund heissen Zeit-Ausloeser jetzt "ab 16:30" statt "gleich 16:30": ein Gleichheitsvergleich waere nur in einer einzigen Minute wahr, und ein Ausfall in genau dieser Minute kostet den ganzen Tag. Dieselbe Ueberlegung steht hinter den breiten Zeitfenstern in auto_watering.py. Der Weg zum Geraet haengt an der URL des Aktors: mqtt:// abonniert und publiziert, http:// pollt und haengt Parameter an, io:// spricht mit der Tahoma-Box, Logic rechnet Uhrzeit, Datum und Sonnenzeiten. Alle vier stehen in transports.py; eine fuenfte Geraeteart ist eine weitere Klasse mit passt(), zustaende_lesen() und senden(). fetch_calendar.py fuellt calendar_days aus openholidaysapi.org, damit "in den Ferien" und "an Feiertagen" eine Grundlage haben. Einmal jaehrlich per Cron. Die Uebersicht blendet auf schmalen Schirmen Spalten aus, statt sie wegzuschieben - auf dem Handy waren die Knoepfe zum Pausieren und Loeschen sonst nur per Seitwaertsscrollen erreichbar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
103 lines
3.5 KiB
Python
103 lines
3.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Fuellt die Tabelle `calendar_days` mit Feiertagen und Schulferien.
|
|
|
|
Die Automatiken koennen mit "an Feiertagen ausfuehren" und "in den Ferien
|
|
ausfuehren" auf besondere Tage reagieren; woher das Wissen kommt, steht hier.
|
|
Nur besondere Tage werden eingetragen - ein fehlendes Datum ist ein
|
|
gewoehnlicher Tag.
|
|
|
|
Quelle ist openholidaysapi.org: ein offenes Verzeichnis der EU-Kommission, das
|
|
gesetzliche Feiertage und Schulferien fuer alle deutschen Bundeslaender
|
|
liefert. Ein Aufruf je Jahr genuegt, deshalb ist das ein Cronjob und kein
|
|
Dauerlaeufer:
|
|
|
|
0 4 1 1 * /usr/bin/python3 /pfad/zu/fetch_calendar.py
|
|
|
|
Ein zusaetzlicher Lauf im Herbst schadet nicht - die Ferientermine des
|
|
uebernaechsten Schuljahres stehen erst spaeter fest.
|
|
|
|
Bereits eingetragene Tage werden ueberschrieben, nie doppelt angelegt.
|
|
"""
|
|
|
|
import argparse
|
|
import logging
|
|
import sys
|
|
from datetime import date, timedelta
|
|
|
|
import pymysql
|
|
import requests
|
|
|
|
from autoaction_runner import Config, verbinden
|
|
|
|
logger = logging.getLogger("kalender")
|
|
|
|
BASIS = "https://openholidaysapi.org"
|
|
|
|
|
|
def zeitraum(api, land, region, von, bis):
|
|
"""Eintraege einer Art (PublicHolidays oder SchoolHolidays) abholen."""
|
|
antwort = requests.get(
|
|
BASIS + "/" + api,
|
|
params={"countryIsoCode": land, "subdivisionCode": region,
|
|
"languageIsoCode": "DE", "validFrom": von.isoformat(),
|
|
"validTo": bis.isoformat()},
|
|
headers={"Accept": "application/json"}, timeout=20)
|
|
antwort.raise_for_status()
|
|
return antwort.json()
|
|
|
|
|
|
def name(eintrag):
|
|
for teil in eintrag.get("name", []):
|
|
if teil.get("text"):
|
|
return teil["text"][:80]
|
|
return "?"
|
|
|
|
|
|
def tage(eintrag):
|
|
"""Ferien erstrecken sich ueber Wochen; hier wird daraus Tag fuer Tag."""
|
|
start = date.fromisoformat(eintrag["startDate"])
|
|
ende = date.fromisoformat(eintrag["endDate"])
|
|
while start <= ende:
|
|
yield start
|
|
start += timedelta(days=1)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Feiertage und Ferien in calendar_days schreiben.")
|
|
parser.add_argument("--jahr", type=int, default=date.today().year)
|
|
parser.add_argument("--jahre", type=int, default=2, help="wie viele Jahre ab --jahr")
|
|
parser.add_argument("--config", default="config.ini")
|
|
args = parser.parse_args()
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
config = Config(args.config)
|
|
land = config.text("calendar", "country", "DE")
|
|
region = config.text("calendar", "subdivision", "DE-BY")
|
|
|
|
von = date(args.jahr, 1, 1)
|
|
bis = date(args.jahr + args.jahre - 1, 12, 31)
|
|
logger.info("Hole %s bis %s fuer %s", von, bis, region)
|
|
|
|
gesammelt = {}
|
|
for eintrag in zeitraum("PublicHolidays", land, region, von, bis):
|
|
for tag in tage(eintrag):
|
|
gesammelt.setdefault(tag, {})["holiday"] = name(eintrag)
|
|
for eintrag in zeitraum("SchoolHolidays", land, region, von, bis):
|
|
for tag in tage(eintrag):
|
|
gesammelt.setdefault(tag, {})["vacation"] = name(eintrag)
|
|
|
|
db = verbinden(config)
|
|
with db.cursor() as c:
|
|
c.executemany(
|
|
"""INSERT INTO calendar_days (date, holiday, vacation) VALUES (%s, %s, %s)
|
|
ON DUPLICATE KEY UPDATE holiday = VALUES(holiday), vacation = VALUES(vacation)""",
|
|
[(tag, eintrag.get("holiday"), eintrag.get("vacation"))
|
|
for tag, eintrag in sorted(gesammelt.items())])
|
|
logger.info("%d Tage eingetragen", len(gesammelt))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|