diff --git a/README.md b/README.md index ea51085..d9a8f30 100644 --- a/README.md +++ b/README.md @@ -85,10 +85,8 @@ flowchart TB BROKER{{"MQTT-Broker"}} SOLARLOG[("solarLog
Verlauf")] HOMEMESH[("homeMesh
Geräte · Automatiken")] - ALARM[("alarm
Weckzeiten")] RUNNER["autoaction_runner.py
SolarManager"] - WECKER["wecker.py
SolarManager"] KALENDER["fetch_calendar.py
SolarManager · Cronjob, jährlich"] DISCOVERY["device_discovery.py
Web · von Hand gestartet"] AJAX["ajax/*.php
Web"] @@ -103,10 +101,8 @@ flowchart TB RUNNER -.->|"HTTP · WLED · Tahoma"| HAUS BROKER -.->|"Gartenwasser/…/set"| VENTILE - ALARM --> WECKER FERIEN --> KALENDER KALENDER -->|"calendar_days"| HOMEMESH - WECKER -.->|HTTP| HAUS BROKER -->|"homeassistant/#"| DISCOVERY HAUS -->|"mDNS · Tahoma"| DISCOVERY @@ -121,8 +117,8 @@ flowchart TB classDef skript fill:#fff6e5,stroke:#d0a548 classDef speicher fill:#eaf5ee,stroke:#6fa981 classDef geraet fill:#eef4fb,stroke:#7f9dc0 - class RUNNER,WECKER,DISCOVERY,AJAX,BROWSER,KALENDER skript - class BROKER,SOLARLOG,HOMEMESH,ALARM speicher + class RUNNER,DISCOVERY,AJAX,BROWSER,KALENDER skript + class BROKER,SOLARLOG,HOMEMESH speicher class HAUS,VENTILE,FERIEN geraet ``` @@ -152,7 +148,6 @@ von Hand, wenn sich am Bestand etwas geändert hat. | `homeassistant/#` | die Geräte selbst | `device_discovery.py` | | **`solarLog`** | `solarManager.py` | `ajax/*.php`, `gatherWaterData` | | **`homeMesh`** | `device_discovery.py`, Runner, Web-Editor | Runner, `ajax/*.php` | -| **`alarm`** | Web-Oberfläche | `wecker.py` | ## Was wann startet @@ -163,7 +158,6 @@ von Hand, wenn sich am Bestand etwas geändert hat. | `gatherRainData.py` | dito | | `wsMQTTbridge.py` | `startMQTTbridge.sh` | | `wattpilot_bruecke.py` | `startWattpilotMQTT.sh` | -| `wecker.py` | `startWecker.sh` | | `autoActions/fetch_calendar.py` | Cronjob, einmal im Jahr | | `device_discovery.py` (Web-Repo) | von Hand | @@ -171,6 +165,42 @@ von Hand, wenn sich am Bestand etwas geändert hat. startet — beim Runner ist das wichtig, zwei Instanzen würden jedes Kommando doppelt schicken. +## Der Wecker ist keiner mehr + +`wecker.py` gibt es nicht mehr. Es hat genau das getan, was der +AutoAction-Runner ohnehin kann — nur mit allem doppelt: eigener Datenbank +`alarm`, eigenem Feiertags- und Ferienkalender, eigenem UDP-Log auf Port +13377, eigener Endlosschleife und einem eigenen Startskript samt nächtlichem +Neustart um drei. + +Punkt für Punkt hatte der Runner die bessere Fassung schon: + +| `wecker.py` | Automatik | +|---|---| +| `alarmtime.time_1` | Bedingung „Uhrzeit um 05:50" | +| Wochentagsspalten `mo` … `so` | `weekdays`, eine Bitmaske | +| `is_holiday()` gegen `alarm.feiertage` | `on_holiday`, gegen `calendar_days` | +| `is_school_holiday()` gegen `alarm.ferien` | `on_vacation`, ebenso | +| `alarmtime.wecked = heute` | `cond_met`, die steigende Flanke | +| fünf HTTP-Versuche im Abstand von vier Sekunden | der `Versand`, ein Faden je Gerät | +| `send_log()` per UDP | `automation_log` und `autoActions.log` | + +Der Kalender war der auffälligste Teil davon: `alarm.feiertage` und +`alarm.ferien` standen neben `calendar_days`, das `fetch_calendar.py` einmal +im Jahr von openholidaysapi.org holt. Zwei Kalender, die dasselbe wissen +müssen, gehen früher oder später auseinander. + +Umgezogen ist die eine Weckzeit, die scharf war: 05:50, Montag bis Freitag, +nicht in den Ferien, nicht an Feiertagen, WLED-Preset 5 („Wakeup") auf +`MenasHimmel`. Die drei anderen Zeilen in `alarmtime` standen auf +`onoff = -1`. `wecker_zu_automatik.sql` legt die Automatik an und beschreibt +im Kopf, wie dasselbe im Dashboard von Hand geht. + +Damit ist die Weckzeit dort einstellbar, wo alles andere auch eingestellt +wird — unter „Automatismen", Etage EG. Die Datenbank `alarm` liest niemand +mehr; sie steht noch da, mitsamt dem Abschnitt `[alarm]` in der `config.ini`, +und kann weg, sobald der erste Morgen ohne `wecker.py` durch ist. + ## Konfiguration Zugangsdaten und Standort stehen in `config.ini` (Vorlage: diff --git a/logrotate.conf b/logrotate.conf index 8c9c947..ae67bf9 100644 --- a/logrotate.conf +++ b/logrotate.conf @@ -22,7 +22,6 @@ /volume1/homes/wagner/SolarManager/rainOutput.log /volume1/homes/wagner/SolarManager/wattpilotshell.log /volume1/homes/wagner/SolarManager/wsMQTTbridge.log -/volume1/homes/wagner/SolarManager/wecker.log { monthly # Zwei Wochen zurueck. Weiter zurueck hat noch nie jemand gesucht, und @@ -34,7 +33,8 @@ maxsize 10M compress # Eine leere Datei zu rotieren bringt nichts ausser vierzehn leeren - # Archiven - der Wecker meldet an den meisten Tagen gar nichts. + # Archiven, und mehrere dieser Prozesse melden an den meisten Tagen + # gar nichts. notifempty # Fehlt eine Datei, ist das kein Fehler: nicht jeder Prozess laeuft # auf jedem System. diff --git a/startWecker.sh b/startWecker.sh deleted file mode 100755 index 94e11df..0000000 --- a/startWecker.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -echo "Starting..." -# Change the name of the script here -SCRIPT_NAME="wecker.py" -LOG_FILE="wecker.log" - -# Find the process ID of any running instance of the script -PID=$(ps aux | grep "$SCRIPT_NAME" | grep -v grep | awk '{print $2}') - -# If a running instance was found, kill it -if [[ -n "$PID" ]]; then - echo "Killing process $PID" - kill "$PID" -fi - - -# Start the script -cd "/volume1/homes/wagner/SolarManager/" -# Angehaengt statt ueberschrieben. Dieses Skript laeuft jede Nacht um drei, -# und mit "&>" war die Datei danach jedes Mal leer - was der Wecker am -# Vortag gemeldet hat, war morgens nicht mehr nachzulesen. Ausserdem braucht -# logrotate den Anhaengemodus: es leert die Datei mit copytruncate, waehrend -# der Prozess sie offen haelt. -/usr/bin/python3 "$SCRIPT_NAME" >> "$LOG_FILE" 2>&1 & \ No newline at end of file diff --git a/wecker.py b/wecker.py deleted file mode 100644 index b350640..0000000 --- a/wecker.py +++ /dev/null @@ -1,129 +0,0 @@ -import sys -import mysql.connector as pymysql -from mysql.connector import connect, Error -import requests -import datetime -import time -import socket -import traceback -import konfig - -# MySQL-Datenbankverbindung. Zugangsdaten stehen in config.ini, siehe konfig.py. -DB_CONFIG = konfig.datenbank("alarm") - -wled_url = "http://192.168.179.139/json" # URL zur WLED JSON API -LOG_SERVER = ("localhost", 13377) # UDP Logserver - -# UDP Logging Funktion -def send_log(message): - sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - log_message = f"{datetime.datetime.now().strftime('%b %d %H:%M:%S')} alarm_scheduler: {message}" - sock.sendto(log_message.encode(), LOG_SERVER) - sock.close() - -def is_holiday(): - today_str = datetime.datetime.today().strftime("%Y-%m-%d") - - conn = pymysql.connect(**DB_CONFIG) - cursor = conn.cursor() - - query = "SELECT COUNT(*) FROM feiertage WHERE datetime = %s" - cursor.execute(query, (today_str,)) - result = cursor.fetchone() - conn.close() - - return result[0] > 0 - -def is_school_holiday(): - - today_str = datetime.datetime.today().strftime("%Y-%m-%d") - - conn = pymysql.connect(**DB_CONFIG) - cursor = conn.cursor() - - query = "SELECT COUNT(*) FROM ferien WHERE date_from <= %s AND date_to >= %s" - cursor.execute(query, (today_str, today_str)) - result = cursor.fetchone() - conn.close() - - return result[0] > 0 - -def get_active_alarms(): - today = datetime.datetime.today() - today_str = today.strftime("%Y-%m-%d") - weekday_map = {0: "mo", 1: "di", 2: "mi", 3: "do", 4: "fr", 5: "sa", 6: "so"} - weekday_col = weekday_map[today.weekday()] - holiday = is_holiday() - school_holiday = is_school_holiday() - - conn = pymysql.connect(**DB_CONFIG) - cursor = conn.cursor() - - query = f""" - SELECT id, TIME_FORMAT(time_1, '%H:%i:%s'), mode FROM alarmtime - WHERE onoff != -1 AND {weekday_col} != -1 AND (wecked IS NULL OR wecked != '{today_str}') - """ - - if holiday: - query += " AND mode NOT IN ('arbeit', 'schule')" - elif school_holiday: - query += " AND mode != 'schule'" - - cursor.execute(query) - alarms = cursor.fetchall() - conn.close() - - return [(alarm[0], alarm[1]) for alarm in alarms] - -def trigger_wled_playlist(): - data = {"ps": 5} # Beispiel: Playlist 1 aktivieren - headers = {"Content-Type": "application/json"} - - for attempt in range(5): - try: - response = requests.post(wled_url, json=data, headers=headers) - send_log(f"WLED Response (Attempt {attempt + 1}): {response.text}") - try: - if response.json().get("success"): - return - except ValueError: - send_log(f"Value error: {response.text}") # Falls die Antwort kein gültiges JSON ist, weitermachen - except: - send_log(f"No response (Attempt {attempt + 1}): {traceback.print_exc()}") - time.sleep(4) # Kurze Wartezeit zwischen Versuchen - -def mark_alarm_as_executed(alarm_id): - today_str = datetime.datetime.today().strftime("%Y-%m-%d") - conn = pymysql.connect(**DB_CONFIG) - cursor = conn.cursor() - - query = "UPDATE alarmtime SET wecked = %s WHERE id = %s" - cursor.execute(query, (today_str, alarm_id)) - conn.commit() - conn.close() - -def main(): - last_db_check = 0 - active_alarms = [] - - while True: - now = time.time() - - # Nur alle 5 Minuten die Datenbankabfrage ausführen - if now - last_db_check >= 300: - active_alarms = get_active_alarms() - last_db_check = now - - current_time = datetime.datetime.now().strftime("%H:%M:%S") - - for alarm_id, alarm_time in active_alarms: - if current_time >= alarm_time: - send_log(f"Triggering WLED Playlist at {alarm_time}") - trigger_wled_playlist() - mark_alarm_as_executed(alarm_id) - active_alarms = get_active_alarms() - - time.sleep(20) # Überprüfung alle 20 sec - -if __name__ == "__main__": - main() diff --git a/wecker_zu_automatik.sql b/wecker_zu_automatik.sql new file mode 100644 index 0000000..d1268e0 --- /dev/null +++ b/wecker_zu_automatik.sql @@ -0,0 +1,76 @@ +-- =========================================================================== +-- Der Wecker als Automatik +-- +-- Ersetzt wecker.py. Das Skript hat dasselbe getan, was der AutoAction-Runner +-- ohnehin kann, nur mit eigener Datenbank (`alarm`), eigenem Feiertags- und +-- Ferienkalender, eigenem UDP-Log und einer eigenen Endlosschleife: +-- +-- wecker.py Automatik +-- --------------------------------- ---------------------------------- +-- alarmtime.time_1 = 05:50:00 Bedingung "Uhrzeit = 05:50" +-- alarmtime.mo..fr != -1 weekdays = 31 (Bit 0..4, Mo bis Fr) +-- mode = schule, is_holiday() on_holiday = 0 +-- mode = schule, is_school_holiday() on_vacation = 0 +-- alarmtime.wecked = heute cond_met, die steigende Flanke +-- POST {"ps": 5} an 192.168.179.139 Kommando "Preset" des Aktors +-- MenasHimmel, Parameter preset = 5 +-- +-- Preset 5 heisst am Geraet "Wakeup". +-- +-- Von den vier Zeilen in `alarmtime` war nur eine scharf (id 3, 05:50, Modus +-- schule); die drei anderen standen auf onoff = -1. Nur diese eine zieht hier +-- also um. +-- +-- Einspielen: +-- mysql -h 127.0.0.1 -P 3310 -u homeMesh -p homeMesh < wecker_zu_automatik.sql +-- +-- Dasselbe geht auch ohne SQL: im Dashboard unter "Automatismen", Etage EG, +-- neue Automatik anlegen - Bedingung "Uhrzeit um 05:50", Aktion +-- "MenasHimmel / Preset" mit Wakeup, Wochentage Mo-Fr, die beiden Haken +-- "auch in den Ferien" und "auch an Feiertagen" heraus. Der Runner liest das +-- Regelwerk alle 30 Sekunden neu, ein Neustart ist so oder so nicht noetig. +-- =========================================================================== + +INSERT INTO automations + (name, floor, enabled, window_from, window_to, weekdays, + on_vacation, on_holiday, force_once, lockout_secs) +SELECT Wecker Magdalena, EG, 1, 00:00:00, 23:59:00, 31, 0, 0, 0, 900 + FROM DUAL + WHERE NOT EXISTS (SELECT 1 FROM automations WHERE name = Wecker Magdalena); + +SET @auto = (SELECT id FROM automations WHERE name = Wecker Magdalena); + +-- Bedingung: state_id 1 ist der gerechnete Messwert "Uhrzeit" des +-- Logic-Aktors. Operator = heisst "um", also ab dieser Minute noch +-- catchup_minutes lang wahr - ausgeloest wird trotzdem nur einmal, dafuer +-- sorgt die Flanke. +INSERT INTO automation_conditions + (automation_id, group_no, position, state_id, operator, value) +SELECT @auto, 0, 0, 1, =, 05:50 + FROM DUAL + WHERE NOT EXISTS (SELECT 1 FROM automation_conditions + WHERE automation_id = @auto AND state_id = 1); + +-- Aktion: command_id 20 ist "Preset" des WLED-Aktors MenasHimmel +-- (wled://192.168.179.139), Vorlage {"ps":%preset%}. +INSERT INTO automation_actions (automation_id, position, command_id) +SELECT @auto, 0, 20 + FROM DUAL + WHERE NOT EXISTS (SELECT 1 FROM automation_actions + WHERE automation_id = @auto AND command_id = 20); + +SET @aktion = (SELECT id FROM automation_actions + WHERE automation_id = @auto AND command_id = 20); + +-- parameter_id 16 ist "preset" zu diesem Kommando. +INSERT INTO automation_action_params (action_id, parameter_id, value) +VALUES (@aktion, 16, "5") +ON DUPLICATE KEY UPDATE value = VALUES(value); + +SELECT a.id, a.name, a.weekdays, a.on_vacation, a.on_holiday, + c.operator, c.value AS uhrzeit, p.value AS preset + FROM automations a + JOIN automation_conditions c ON c.automation_id = a.id + JOIN automation_actions ak ON ak.automation_id = a.id + JOIN automation_action_params p ON p.action_id = ak.id + WHERE a.name = "Wecker Magdalena";