Wecker als Automatik statt als eigener Prozess

wecker.py hat genau das getan, was der AutoAction-Runner ohnehin kann -
nur mit allem doppelt: eigener Datenbank alarm, eigenem Feiertags- und
Ferienkalender neben calendar_days, eigenem UDP-Log auf Port 13377,
eigener Endlosschleife und einem Startskript samt naechtlichem Neustart
um drei.

Punkt fuer Punkt hatte der Runner die bessere Fassung schon: die Zeit
als Bedingung "um 05:50" mit Nachholfenster statt eines Vergleichs im
20-Sekunden-Takt, weekdays als Bitmaske statt sieben Spalten,
on_holiday/on_vacation gegen calendar_days statt gegen einen zweiten
Kalender, die steigende Flanke statt einer wecked-Spalte, und den
Versand mit einem Faden je Geraet statt fuenf HTTP-Versuchen im Abstand
von vier Sekunden.

Umgezogen ist die eine Weckzeit, die scharf war: 05:50, Mo-Fr, nicht in
den Ferien, nicht an Feiertagen, WLED-Preset 5 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.

Die Datenbank alarm und der Abschnitt [alarm] in der config.ini bleiben
vorerst stehen - sie liest nur niemand mehr.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 21:12:20 +02:00
co-authored by Claude Opus 5
parent cc96e990fa
commit 1f1700391d
5 changed files with 116 additions and 163 deletions
+38 -8
View File
@@ -85,10 +85,8 @@ flowchart TB
BROKER{{"<b>MQTT-Broker</b>"}} BROKER{{"<b>MQTT-Broker</b>"}}
SOLARLOG[("<b>solarLog</b><br/><small>Verlauf</small>")] SOLARLOG[("<b>solarLog</b><br/><small>Verlauf</small>")]
HOMEMESH[("<b>homeMesh</b><br/><small>Geräte · Automatiken</small>")] HOMEMESH[("<b>homeMesh</b><br/><small>Geräte · Automatiken</small>")]
ALARM[("<b>alarm</b><br/><small>Weckzeiten</small>")]
RUNNER["<b>autoaction_runner.py</b><br/><small>SolarManager</small>"] RUNNER["<b>autoaction_runner.py</b><br/><small>SolarManager</small>"]
WECKER["<b>wecker.py</b><br/><small>SolarManager</small>"]
KALENDER["<b>fetch_calendar.py</b><br/><small>SolarManager · Cronjob, jährlich</small>"] KALENDER["<b>fetch_calendar.py</b><br/><small>SolarManager · Cronjob, jährlich</small>"]
DISCOVERY["<b>device_discovery.py</b><br/><small>Web · von Hand gestartet</small>"] DISCOVERY["<b>device_discovery.py</b><br/><small>Web · von Hand gestartet</small>"]
AJAX["<b>ajax/*.php</b><br/><small>Web</small>"] AJAX["<b>ajax/*.php</b><br/><small>Web</small>"]
@@ -103,10 +101,8 @@ flowchart TB
RUNNER -.->|"HTTP · WLED · Tahoma"| HAUS RUNNER -.->|"HTTP · WLED · Tahoma"| HAUS
BROKER -.->|"Gartenwasser/…/set"| VENTILE BROKER -.->|"Gartenwasser/…/set"| VENTILE
ALARM --> WECKER
FERIEN --> KALENDER FERIEN --> KALENDER
KALENDER -->|"calendar_days"| HOMEMESH KALENDER -->|"calendar_days"| HOMEMESH
WECKER -.->|HTTP| HAUS
BROKER -->|"homeassistant/#"| DISCOVERY BROKER -->|"homeassistant/#"| DISCOVERY
HAUS -->|"mDNS · Tahoma"| DISCOVERY HAUS -->|"mDNS · Tahoma"| DISCOVERY
@@ -121,8 +117,8 @@ flowchart TB
classDef skript fill:#fff6e5,stroke:#d0a548 classDef skript fill:#fff6e5,stroke:#d0a548
classDef speicher fill:#eaf5ee,stroke:#6fa981 classDef speicher fill:#eaf5ee,stroke:#6fa981
classDef geraet fill:#eef4fb,stroke:#7f9dc0 classDef geraet fill:#eef4fb,stroke:#7f9dc0
class RUNNER,WECKER,DISCOVERY,AJAX,BROWSER,KALENDER skript class RUNNER,DISCOVERY,AJAX,BROWSER,KALENDER skript
class BROKER,SOLARLOG,HOMEMESH,ALARM speicher class BROKER,SOLARLOG,HOMEMESH speicher
class HAUS,VENTILE,FERIEN geraet 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` | | `homeassistant/#` | die Geräte selbst | `device_discovery.py` |
| **`solarLog`** | `solarManager.py` | `ajax/*.php`, `gatherWaterData` | | **`solarLog`** | `solarManager.py` | `ajax/*.php`, `gatherWaterData` |
| **`homeMesh`** | `device_discovery.py`, Runner, Web-Editor | Runner, `ajax/*.php` | | **`homeMesh`** | `device_discovery.py`, Runner, Web-Editor | Runner, `ajax/*.php` |
| **`alarm`** | Web-Oberfläche | `wecker.py` |
## Was wann startet ## Was wann startet
@@ -163,7 +158,6 @@ von Hand, wenn sich am Bestand etwas geändert hat.
| `gatherRainData.py` | dito | | `gatherRainData.py` | dito |
| `wsMQTTbridge.py` | `startMQTTbridge.sh` | | `wsMQTTbridge.py` | `startMQTTbridge.sh` |
| `wattpilot_bruecke.py` | `startWattpilotMQTT.sh` | | `wattpilot_bruecke.py` | `startWattpilotMQTT.sh` |
| `wecker.py` | `startWecker.sh` |
| `autoActions/fetch_calendar.py` | Cronjob, einmal im Jahr | | `autoActions/fetch_calendar.py` | Cronjob, einmal im Jahr |
| `device_discovery.py` (Web-Repo) | von Hand | | `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 startet — beim Runner ist das wichtig, zwei Instanzen würden jedes Kommando
doppelt schicken. 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 ## Konfiguration
Zugangsdaten und Standort stehen in `config.ini` (Vorlage: Zugangsdaten und Standort stehen in `config.ini` (Vorlage:
+2 -2
View File
@@ -22,7 +22,6 @@
/volume1/homes/wagner/SolarManager/rainOutput.log /volume1/homes/wagner/SolarManager/rainOutput.log
/volume1/homes/wagner/SolarManager/wattpilotshell.log /volume1/homes/wagner/SolarManager/wattpilotshell.log
/volume1/homes/wagner/SolarManager/wsMQTTbridge.log /volume1/homes/wagner/SolarManager/wsMQTTbridge.log
/volume1/homes/wagner/SolarManager/wecker.log
{ {
monthly monthly
# Zwei Wochen zurueck. Weiter zurueck hat noch nie jemand gesucht, und # Zwei Wochen zurueck. Weiter zurueck hat noch nie jemand gesucht, und
@@ -34,7 +33,8 @@
maxsize 10M maxsize 10M
compress compress
# Eine leere Datei zu rotieren bringt nichts ausser vierzehn leeren # 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 notifempty
# Fehlt eine Datei, ist das kein Fehler: nicht jeder Prozess laeuft # Fehlt eine Datei, ist das kein Fehler: nicht jeder Prozess laeuft
# auf jedem System. # auf jedem System.
-24
View File
@@ -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 &
-129
View File
@@ -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()
+76
View File
@@ -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";