Jalousien: Zu heisst zweifach schwenken, Versand laeuft im Hintergrund

Die Kugelschreiber-Mechanik betrifft jedes Kommando, nicht nur
setOrientation, und sie betrifft auch das Schliessen: ein "Zu" faehrt die
Jalousie zwar herunter, die Lamellen bleiben aber bei etwa 30 % offen
stehen. Dicht wird sie erst durch das zweifache Schwenken.

Die Regel lautet jetzt einheitlich: derselbe Befehl zweimal - erst mit
Neigung 0, dann warten, bis die Fahrt steht, dann mit dem gewuenschten Wert.
Die Position bleibt dabei erhalten, die Jalousie faehrt also nur einmal.

    Zu                     -> [100,0] warten [100,100]
    Neigung 20             -> direkt, unter der Schwelle
    Neigung 80             -> [0] warten [80]
    Position 40 Neigung 50 -> [40,0] warten [40,50]

"Zu" wird dabei nur umgeschrieben, wenn das Geraet Position und Neigung
zusammen setzen kann - der einfache Rollladen "Terasse" hat keine Lamellen
und bekommt weiter das rohe down.

Gewartet wird auf zwei Auskuenfte zusammen, weil einzeln keine traegt:
core:MovingState trug die lange Fahrt (gemessen 61 s), wird bei kurzen
Neigungsfahrten aber nie gesetzt; die Zustandswerte sind die Wahrheit,
zeigen direkt nach dem Kommando aber noch den alten Stand. Fertig heisst:
nichts faehrt mehr, die Ziele stimmen, und es wurde entweder ein "faehrt"
gesehen oder der Vorlauf von acht Sekunden ist um.

Und weil ein "Zu" damit ueber eine Minute dauert, schickt der Runner nicht
mehr selbst: der neue Versand nimmt die Kommandos entgegen und arbeitet sie
in eigenen Faeden ab - je Geraet der Reihe nach, ueber Geraete hinweg
nebeneinander. Sonst haette eine einzige Jalousie die ganze Auswertung fuer
eine Minute angehalten: keine Zeit-Ausloeser, keine Messwerte, und mehrere
Rollladen in einer Automatik haetten sich aufaddiert. Die Datenbank bleibt
dabei im Hauptfaden - die Faeden melden nur ihr Ergebnis zurueck, eine
pymysql-Verbindung ist nicht fuer mehrere Faeden gedacht.

Nachgemessen: Einreihen kehrt sofort zurueck, zwei Kommandos an dasselbe
Geraet laufen nacheinander (6 s, dann 12 s), drei an verschiedene Geraete
gleichzeitig, Fehler kommen zurueck, und die Schleife lief in 20 Sekunden
20 Takte durch.

Die Bedienung im Modal wartet weiterhin - dort sitzt ein Mensch vor einem
Fortschritt und klickt einmal. ajax/room.php hebt dafuer die Zeitgrenze auf
180 Sekunden an, wie es ajax/tahoma.php an derselben Stelle auch tut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-31 20:29:40 +02:00
co-authored by Claude Opus 5
parent d557a1651f
commit 131d7cc46d
4 changed files with 333 additions and 77 deletions
+126 -9
View File
@@ -47,7 +47,9 @@ import configparser
import json
import logging
import os
import queue
import sys
import threading
import time
from datetime import date, datetime, timedelta
@@ -367,6 +369,79 @@ def tag_passt(automatik, jetzt, kalender):
return True
# ===========================================================================
# Versand
# ===========================================================================
class Versand:
"""
Schickt Kommandos im Hintergrund - je Geraet der Reihe nach, ueber
Geraete hinweg nebeneinander.
Ohne das blockiert ein einziges Kommando die ganze Auswertung: eine
Jalousie zuzufahren dauert ueber eine Minute, weil zwischen den beiden
Schwenkbefehlen auf das Ende der Fahrt gewartet werden muss (siehe
TahomaTransport). So lange kaeme kein Zeit-Ausloeser mehr durch, kein
Messwert wuerde zurueckgeschrieben, und mehrere Rollladen in einer
Automatik wuerden sich aufaddieren.
Je Geraet eine Warteschlange mit einem eigenen Faden: zwei Kommandos an
dieselbe Jalousie duerfen sich nicht ueberholen - "Neigung 0" und
"Neigung 100" sind sonst wirkungslos oder vertauscht -, zwei Kommandos an
verschiedene Jalousien duerfen ruhig gleichzeitig laufen.
Die Datenbank bleibt aussen vor: die Faeden melden ihr Ergebnis nur
zurueck, geschrieben wird im Hauptfaden. Eine pymysql-Verbindung ist
nicht fuer mehrere Faeden gedacht.
"""
def __init__(self):
self.warteschlangen = {} # actor_url -> Queue
self.ergebnisse = queue.Queue()
self.laeuft = True
def einreihen(self, automation_id, beschreibung, transport, auftrag):
schlange = self.warteschlangen.get(auftrag["actor_url"])
if schlange is None:
schlange = queue.Queue()
self.warteschlangen[auftrag["actor_url"]] = schlange
faden = threading.Thread(target=self._arbeiten, args=(schlange,),
name="versand", daemon=True)
faden.start()
schlange.put((automation_id, beschreibung, transport, auftrag))
def _arbeiten(self, schlange):
while self.laeuft:
posten = schlange.get()
if posten is None:
return
automation_id, beschreibung, transport, auftrag = posten
try:
transport.senden(auftrag)
self.ergebnisse.put((automation_id, beschreibung, None))
except Exception as fehler:
self.ergebnisse.put((automation_id, beschreibung, str(fehler)))
finally:
schlange.task_done()
def abholen(self):
"""Alles, was seit dem letzten Mal fertig geworden ist."""
fertig = []
while True:
try:
fertig.append(self.ergebnisse.get_nowait())
except queue.Empty:
return fertig
def offen(self):
return sum(s.unfinished_tasks for s in self.warteschlangen.values())
def beenden(self):
self.laeuft = False
for schlange in self.warteschlangen.values():
schlange.put(None)
# ===========================================================================
# Der Runner
# ===========================================================================
@@ -387,6 +462,7 @@ class Runner:
self._kalender = (None, {"feiertag": False, "ferien": False})
self._letzte_saeuberung = None
self.versand = Versand()
self.mqtt = self._mqtt_verbinden()
self.transporte = [
MQTTTransport(self.mqtt, self.dry_run),
@@ -427,6 +503,16 @@ class Runner:
def regelwerk_laden(self):
self.regelwerk = Regelwerk.laden(self.db)
# Welche Geraete Position und Neigung zusammen koennen. Nur die haben
# die Kugelschreiber-Mechanik, und nur bei ihnen wird ein "Zu" zum
# zweifachen Schwenken - siehe TahomaTransport.
kombi = {k["actor_url"] for k in self.regelwerk.kommandos.values()
if k["command_url"] == "setClosureAndOrientation"}
for transport in self.transporte:
if isinstance(transport, TahomaTransport):
transport.kombigeraete_setzen(kombi)
# Jeder Transport bekommt die Messwerte, fuer die er zustaendig ist.
for transport in self.transporte:
passende = [{"id": s["id"], "actor_url": s["actor_url"],
@@ -523,7 +609,15 @@ class Runner:
# --- Ausfuehren ------------------------------------------------------
def ausloesen(self, automatik, anlass):
"""
Die Aktionen einer Automatik in den Versand geben.
Geschickt wird im Hintergrund - eine Jalousie zuzufahren dauert ueber
eine Minute, und so lange darf die Auswertung nicht stehen. Was dabei
schiefgeht, kommt spaeter ueber ergebnisse_verbuchen() ins Protokoll.
"""
fehlerText = []
eingereiht = 0
for aktion in automatik["aktionen"]:
kommando = self.regelwerk.kommandos.get(aktion["command_id"])
if kommando is None:
@@ -544,17 +638,14 @@ class Runner:
"wert": aktion["werte"].get(p["id"], "")}
for p in kommando["params"]],
}
try:
transport.senden(auftrag)
logger.info("%s: %s -> %s", automatik["name"], kommando["actor_name"],
kommando["command_name"])
except Exception as fehler:
fehlerText.append("%s: %s" % (kommando["command_name"], fehler))
logger.error("%s: %s konnte nicht geschickt werden: %s",
automatik["name"], kommando["command_name"], fehler)
beschreibung = "%s: %s" % (kommando["actor_name"], kommando["command_name"])
logger.info("%s: %s (unterwegs)", automatik["name"], beschreibung)
self.versand.einreihen(automatik["id"], beschreibung, transport, auftrag)
eingereiht += 1
ergebnis = "error" if fehlerText else anlass
detail = "; ".join(fehlerText)[:255]
detail = "; ".join(fehlerText)[:255] if fehlerText \
else ("%d Kommando(s) unterwegs" % eingereiht)
try:
with self.db.cursor() as c:
c.execute("""UPDATE automations SET last_run = NOW(), changed = changed
@@ -566,6 +657,27 @@ class Runner:
automatik["last_run"] = datetime.now()
self.lief_im_fenster[automatik["id"]] = True
def ergebnisse_verbuchen(self):
"""
Was der Versand inzwischen erledigt hat ins Protokoll schreiben.
Nur Fehlschlaege bekommen eine eigene Zeile - der Lauf selbst steht
schon drin, und ein Protokoll, das jedes gelungene Kommando einzeln
auffuehrt, findet niemand mehr etwas darin.
"""
for automation_id, beschreibung, fehler in self.versand.abholen():
if fehler is None:
logger.debug("erledigt: %s", beschreibung)
continue
logger.error("%s konnte nicht geschickt werden: %s", beschreibung, fehler)
try:
with self.db.cursor() as c:
c.execute("""INSERT INTO automation_log (automation_id, result, detail)
VALUES (%s, 'error', %s)""",
(automation_id, ("%s: %s" % (beschreibung, fehler))[:255]))
except Exception as schreibfehler:
logger.warning("Protokoll nicht schreibbar: %s", schreibfehler)
def flanke_merken(self, automatik, erfuellt):
if bool(automatik["cond_met"]) == bool(erfuellt):
return
@@ -674,6 +786,7 @@ class Runner:
self.werte_einsammeln(mit_pollen)
self.durchlauf()
self.ergebnisse_verbuchen()
self.protokoll_saeubern()
if jetzt - letztes_pruefen >= neuladen:
@@ -690,6 +803,10 @@ class Runner:
time.sleep(takt)
def beenden(self):
offen = self.versand.offen()
if offen:
logger.info("%d Kommando(s) noch unterwegs - wird nicht abgewartet", offen)
self.versand.beenden()
try:
self.mqtt.loop_stop()
self.mqtt.disconnect()