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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user