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:
@@ -85,10 +85,8 @@ flowchart TB
|
||||
BROKER{{"<b>MQTT-Broker</b>"}}
|
||||
SOLARLOG[("<b>solarLog</b><br/><small>Verlauf</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>"]
|
||||
WECKER["<b>wecker.py</b><br/><small>SolarManager</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>"]
|
||||
AJAX["<b>ajax/*.php</b><br/><small>Web</small>"]
|
||||
@@ -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:
|
||||
|
||||
+2
-2
@@ -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.
|
||||
|
||||
@@ -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 &
|
||||
@@ -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()
|
||||
@@ -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";
|
||||
Reference in New Issue
Block a user