Automatiken koennen andere Automatiken ausloesen
Anlass: "zehn Minuten nach dem Wecker den Rollladen hoch, aber nur wenn es dann schon hell ist". Als Verzoegerung an der Aktion war das nicht zu haben - die Zusatzbedingung gilt erst zum spaeteren Zeitpunkt, und eine verzoegerte Aktion, die selbst noch Bedingungen prueft, braeuchte ein zweites Bedingungssystem neben dem ersten. Der zweite Schritt ist also eine eigene Automatik; was ihr fehlte, war nur ein Bezug auf die erste. Den gibt jetzt das gerechnete Geraet "Automatiken", Gegenstueck zum vorhandenen "Zeitpunkt": jede Automatik ist dort ein Messwert, ihr Wert der Zeitpunkt der letzten Ausloesung. Der Editor braucht dafuer keine Zeile - er listet Geraete und deren Messwerte. Der neue Datentyp `elapsed` verhaelt sich dazu wie `deltatime` zum Sonnenaufgang: "+ 00:10", "ab + 00:10", "vor + 00:10". Ein Minus gibt es nicht. Gerechnet wird mit dem echten Abstand statt mit der Uhrzeit innerhalb des Tages - sonst machte ein Lauf von vorgestern die Bedingung heute wahr. Die offene Form endet trotzdem am Tagesrand, genau wie "ab 16:30". Ausgewertet wird topologisch, Ausloeser vor Nachfolger; nur so wirkt ein Versatz von null noch im selben Takt. Eine pausierte Automatik haelt ihre Nachfolger mit an: geladen werden nur die aktiven, und der Transport liefert fuer alle uebrigen einen leeren Wert. Die Messwerte pflegt ausloeser_nachfuehren() - fuer JEDE Automatik, auch fuer pausierte. Sonst loeschte ein Pausieren ueber fk_cond_state ON DELETE CASCADE die Bedingung des Nachfolgers, still. Geloescht wird nur, was keine Bedingung mehr benutzt. automatik_ausloeser.sql legt Datentyp und Geraet an. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,9 @@ URL des Aktors in der Tabelle `actors`:
|
||||
Funkart des Geraets, deshalb wird dort nicht danach
|
||||
entschieden, sondern an der Box-Kennung in der URL
|
||||
Logic das gerechnete Geraet "Zeitpunkt" (Uhrzeit, Datum, Sonne)
|
||||
Automatik das gerechnete Geraet "Automatiken" - jede Automatik ist
|
||||
dort ein Messwert, ihr Wert der Zeitpunkt der letzten
|
||||
Ausloesung
|
||||
|
||||
Eine Ausnahme gibt es beim Lesen: meldet ein Geraet seine Messwerte an den
|
||||
Broker, obwohl es ueber HTTP geschaltet wird, so steht in `actor_states.url`
|
||||
@@ -21,7 +24,7 @@ Runner.transport_fuer_messwert(). Das betrifft die Shellys der zweiten
|
||||
Generation; die aelteren koennen kein MQTT und bleiben ganz bei HTTP.
|
||||
|
||||
Alle liegen in einer Datei statt in einem Paket wie bei deviceDiscovery: es
|
||||
sind fuenf kurze Klassen, und wer eine sechste Geraeteart anschliesst, sieht
|
||||
sind sechs kurze Klassen, und wer eine siebte Geraeteart anschliesst, sieht
|
||||
hier auf einen Blick, was dafuer zu tun ist.
|
||||
|
||||
Jeder Transport hat zwei Haelften:
|
||||
@@ -707,3 +710,88 @@ class LogicTransport(Transport):
|
||||
|
||||
def senden(self, aktion):
|
||||
raise RuntimeError("Das Geraet \"Zeitpunkt\" kann nichts schalten")
|
||||
|
||||
|
||||
AUTOMATIK_URL = "Automatik"
|
||||
|
||||
|
||||
def ausloeser_url(automation_id):
|
||||
"""
|
||||
Wie eine Automatik in actor_states.url steht: "auto:15".
|
||||
|
||||
Die Kennung und nicht der Name, damit ein Umbenennen die Bedingungen der
|
||||
abhaengigen Automatiken nicht ins Leere zeigen laesst. Kein Schema und
|
||||
kein Schraegstrich - sonst hielte ist_topic() das fuer ein MQTT-Topic und
|
||||
der MQTT-Transport waere zustaendig.
|
||||
"""
|
||||
return "auto:%d" % int(automation_id)
|
||||
|
||||
|
||||
def ausloeser_kennung(state_url):
|
||||
"""Die Kennung zurueck aus "auto:15". None, wenn es keine ist."""
|
||||
text = str(state_url or "")
|
||||
if not text.startswith("auto:"):
|
||||
return None
|
||||
try:
|
||||
return int(text[5:])
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
class AutomatikTransport(Transport):
|
||||
"""
|
||||
Automatiken als Ausloeser fuer andere Automatiken.
|
||||
|
||||
Jede Automatik ist hier ein Messwert, und ihr Wert ist der Zeitpunkt, zu
|
||||
dem sie zuletzt gelaufen ist. Eine Bedingung darauf liest sich dann als
|
||||
"zehn Minuten nach dem Wecker"; den Abstand rechnet der Datentyp
|
||||
`elapsed`, siehe bedingung_erfuellt() im Runner.
|
||||
|
||||
Warum ueberhaupt ein Transport und keine eigene Art von Bedingung: so
|
||||
braucht der Editor keine Zeile Aenderung, um das anzubieten. Er listet
|
||||
Geraete und deren Messwerte - "Automatiken" ist dann ein Geraet wie jedes
|
||||
andere, die Automatiken sind dessen Messwerte. Dieselbe Ueberlegung steht
|
||||
hinter dem gerechneten Geraet "Zeitpunkt".
|
||||
|
||||
Geschaltet wird hier nichts. Eine Automatik, die eine andere aufruft, ist
|
||||
ausdruecklich nicht vorgesehen: die Verkettung laeuft immer ueber die
|
||||
Bedingung, also von hinten nach vorn. Sonst gaebe es zwei Wege zum selben
|
||||
Ziel, und einen davon koennte der Editor nicht anzeigen.
|
||||
"""
|
||||
|
||||
schema = AUTOMATIK_URL
|
||||
|
||||
def __init__(self, ausloesezeiten):
|
||||
"""ausloesezeiten: Funktion() -> {automation_id: datetime oder None}."""
|
||||
self.ausloesezeiten = ausloesezeiten
|
||||
self.states = []
|
||||
|
||||
def passt(self, actor_url):
|
||||
return actor_url == AUTOMATIK_URL
|
||||
|
||||
def zustaende_anmelden(self, states):
|
||||
self.states = states
|
||||
logger.info("Automatik: %d Ausloeser", len(states))
|
||||
|
||||
def zustaende_lesen(self):
|
||||
"""
|
||||
Leerer Text heisst "noch nicht gelaufen" - und gilt jeder Bedingung
|
||||
als unerfuellt.
|
||||
|
||||
Er steht auch dann da, wenn es die Automatik nicht mehr gibt oder sie
|
||||
pausiert ist: der Runner laedt nur die aktiven, und eine pausierte
|
||||
Automatik soll ihre Nachfolger mit anhalten. Genau deshalb wird hier
|
||||
jeder Messwert bei jedem Takt gesetzt und nicht nur der geaenderte -
|
||||
sonst bliebe nach dem Pausieren der letzte bekannte Zeitpunkt stehen
|
||||
und der Nachfolger liefe noch einmal.
|
||||
"""
|
||||
zeiten = self.ausloesezeiten()
|
||||
werte = {}
|
||||
for s in self.states:
|
||||
kennung = ausloeser_kennung(s["state_url"])
|
||||
letzter = zeiten.get(kennung) if kennung is not None else None
|
||||
werte[s["id"]] = letzter.strftime("%Y-%m-%d %H:%M:%S") if letzter else ""
|
||||
return werte
|
||||
|
||||
def senden(self, aktion):
|
||||
raise RuntimeError("Das Geraet \"Automatiken\" kann nichts schalten")
|
||||
|
||||
Reference in New Issue
Block a user