Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc96e990fa | ||
|
|
1e7b1d8b21 |
@@ -68,7 +68,7 @@ import requests
|
|||||||
import paho.mqtt.client as mqtt
|
import paho.mqtt.client as mqtt
|
||||||
|
|
||||||
from transports import (HTTPTransport, LogicTransport, MQTTTransport,
|
from transports import (HTTPTransport, LogicTransport, MQTTTransport,
|
||||||
TahomaTransport, WLEDTransport)
|
TahomaTransport, WLEDTransport, ist_topic)
|
||||||
|
|
||||||
logger = logging.getLogger("autoaction")
|
logger = logging.getLogger("autoaction")
|
||||||
|
|
||||||
@@ -163,8 +163,13 @@ class Regelwerk:
|
|||||||
drin: eine geaenderte Zielhoehe einer Jalousie schlug also ebenso
|
drin: eine geaenderte Zielhoehe einer Jalousie schlug also ebenso
|
||||||
wenig durch.
|
wenig durch.
|
||||||
|
|
||||||
Die Zeilenzahlen der Geraetetabellen bleiben, damit ein
|
Von den Geraetetabellen zaehlt nicht nur, wie viele Zeilen es gibt,
|
||||||
Discovery-Lauf weiterhin ein Neuladen ausloest.
|
sondern auch, wohin sie zeigen. Ein Discovery-Lauf legt naemlich nicht
|
||||||
|
nur an - er schreibt auch bestehende Zeilen um: bei den neueren
|
||||||
|
Shellys ist aus der HTTP-Adresse ein Topic geworden, bei gleicher
|
||||||
|
Zeilenzahl. Ohne url und value_path im Fingerabdruck haette der Runner
|
||||||
|
das erst beim naechsten Neustart bemerkt und bis dahin an einer
|
||||||
|
Adresse gefragt, an der niemand mehr antwortet.
|
||||||
"""
|
"""
|
||||||
with db.cursor() as c:
|
with db.cursor() as c:
|
||||||
c.execute("""SELECT (SELECT COUNT(*) FROM automations) AS a,
|
c.execute("""SELECT (SELECT COUNT(*) FROM automations) AS a,
|
||||||
@@ -183,7 +188,13 @@ class Regelwerk:
|
|||||||
action_id, parameter_id, value))), 0)
|
action_id, parameter_id, value))), 0)
|
||||||
FROM automation_action_params) AS ps,
|
FROM automation_action_params) AS ps,
|
||||||
(SELECT COUNT(*) FROM actor_states) AS d,
|
(SELECT COUNT(*) FROM actor_states) AS d,
|
||||||
(SELECT COUNT(*) FROM actor_commands) AS e""")
|
(SELECT COALESCE(SUM(CRC32(CONCAT_WS(':',
|
||||||
|
id, url, value_path))), 0)
|
||||||
|
FROM actor_states) AS ds,
|
||||||
|
(SELECT COUNT(*) FROM actor_commands) AS e,
|
||||||
|
(SELECT COALESCE(SUM(CRC32(CONCAT_WS(':',
|
||||||
|
id, command_url))), 0)
|
||||||
|
FROM actor_commands) AS es""")
|
||||||
return tuple(sorted(c.fetchone().items()))
|
return tuple(sorted(c.fetchone().items()))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -679,6 +690,26 @@ class Runner:
|
|||||||
return transport
|
return transport
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def transport_fuer_messwert(self, state):
|
||||||
|
"""
|
||||||
|
Wo ein einzelner Messwert gelesen wird.
|
||||||
|
|
||||||
|
Normalerweise sagt das Geraet es an: ein Shelly haengt an HTTP, eine
|
||||||
|
Jalousie an der Tahoma-Box. Bei den Shellys der zweiten Generation
|
||||||
|
faellt beides auseinander - sie schicken ihre Messwerte von selbst an
|
||||||
|
den Broker, geschaltet werden sie weiter ueber HTTP. Ein Messwert mit
|
||||||
|
einem Topic wird deshalb dort gelesen, wo er ankommt, und nicht dort,
|
||||||
|
wo sein Geraet sonst zu erreichen ist.
|
||||||
|
|
||||||
|
Fuer die aelteren Shellys aendert sich nichts: in ihren Messwerten
|
||||||
|
steht ein Feldname, und der fuehrt weiter zum HTTP-Transport.
|
||||||
|
"""
|
||||||
|
if ist_topic(state["state_url"]):
|
||||||
|
for transport in self.transporte:
|
||||||
|
if isinstance(transport, MQTTTransport):
|
||||||
|
return transport
|
||||||
|
return self.transport_fuer(state["actor_url"])
|
||||||
|
|
||||||
def regelwerk_laden(self):
|
def regelwerk_laden(self):
|
||||||
self.regelwerk = Regelwerk.laden(self.db)
|
self.regelwerk = Regelwerk.laden(self.db)
|
||||||
|
|
||||||
@@ -692,13 +723,19 @@ class Runner:
|
|||||||
transport.kombigeraete_setzen(kombi)
|
transport.kombigeraete_setzen(kombi)
|
||||||
|
|
||||||
# Jeder Transport bekommt die Messwerte, fuer die er zustaendig ist.
|
# Jeder Transport bekommt die Messwerte, fuer die er zustaendig ist.
|
||||||
for transport in self.transporte:
|
# Zugeteilt wird je Messwert und nicht je Geraet, weil beides
|
||||||
passende = [{"id": s["id"], "actor_url": s["actor_url"],
|
# auseinanderfallen kann - siehe transport_fuer_messwert().
|
||||||
"state_url": s["state_url"], "value_path": s["value_path"],
|
listen = [(transport, []) for transport in self.transporte]
|
||||||
"wertetabelle": s["wertetabelle"]}
|
for s in self.regelwerk.states.values():
|
||||||
for s in self.regelwerk.states.values()
|
zustaendig = self.transport_fuer_messwert(s)
|
||||||
if transport.passt(s["actor_url"])]
|
for transport, liste in listen:
|
||||||
transport.zustaende_anmelden(passende)
|
if transport is zustaendig:
|
||||||
|
liste.append({"id": s["id"], "actor_url": s["actor_url"],
|
||||||
|
"state_url": s["state_url"], "value_path": s["value_path"],
|
||||||
|
"wertetabelle": s["wertetabelle"]})
|
||||||
|
break
|
||||||
|
for transport, liste in listen:
|
||||||
|
transport.zustaende_anmelden(liste)
|
||||||
# Der zuletzt bekannte Wert aus der Datenbank ist besser als gar
|
# Der zuletzt bekannte Wert aus der Datenbank ist besser als gar
|
||||||
# keiner: nach einem Neustart steht sonst jede Bedingung auf "unklar",
|
# keiner: nach einem Neustart steht sonst jede Bedingung auf "unklar",
|
||||||
# bis das Geraet zufaellig etwas schickt.
|
# bis das Geraet zufaellig etwas schickt.
|
||||||
@@ -718,6 +755,7 @@ class Runner:
|
|||||||
return self._sonne[1], self._sonne[2]
|
return self._sonne[1], self._sonne[2]
|
||||||
auf, unter = "00:00", "00:00"
|
auf, unter = "00:00", "00:00"
|
||||||
try:
|
try:
|
||||||
|
self.solar.ping(reconnect=True) # re-establish connection if it dropped
|
||||||
with self.solar.cursor() as c:
|
with self.solar.cursor() as c:
|
||||||
c.execute("SELECT sunrise, sunset FROM daylight WHERE date = %s", (heute,))
|
c.execute("SELECT sunrise, sunset FROM daylight WHERE date = %s", (heute,))
|
||||||
zeile = c.fetchone()
|
zeile = c.fetchone()
|
||||||
@@ -727,7 +765,7 @@ class Runner:
|
|||||||
else:
|
else:
|
||||||
logger.warning("Kein Eintrag in daylight fuer %s", heute)
|
logger.warning("Kein Eintrag in daylight fuer %s", heute)
|
||||||
except Exception as fehler:
|
except Exception as fehler:
|
||||||
logger.warning("Sonnenzeiten nicht lesbar: %s", fehler)
|
logger.warning("Sonnenzeiten nicht lesbar: %r", fehler)
|
||||||
self._sonne = (heute, auf, unter)
|
self._sonne = (heute, auf, unter)
|
||||||
return auf, unter
|
return auf, unter
|
||||||
|
|
||||||
@@ -752,7 +790,7 @@ class Runner:
|
|||||||
if zeile:
|
if zeile:
|
||||||
stand = {"feiertag": bool(zeile["holiday"]), "ferien": bool(zeile["vacation"])}
|
stand = {"feiertag": bool(zeile["holiday"]), "ferien": bool(zeile["vacation"])}
|
||||||
except Exception as fehler:
|
except Exception as fehler:
|
||||||
logger.warning("Kalender nicht lesbar: %s", fehler)
|
logger.warning("Kalender nicht lesbar: %r", fehler)
|
||||||
self._kalender = (heute, stand)
|
self._kalender = (heute, stand)
|
||||||
return stand
|
return stand
|
||||||
|
|
||||||
@@ -816,7 +854,7 @@ class Runner:
|
|||||||
self.geschrieben[state_id] = wert
|
self.geschrieben[state_id] = wert
|
||||||
self.geschrieben_um[state_id] = jetzt
|
self.geschrieben_um[state_id] = jetzt
|
||||||
except Exception as fehler:
|
except Exception as fehler:
|
||||||
logger.warning("current_value nicht schreibbar: %s", fehler)
|
logger.warning("current_value nicht schreibbar: %r", fehler)
|
||||||
|
|
||||||
# --- Ausfuehren ------------------------------------------------------
|
# --- Ausfuehren ------------------------------------------------------
|
||||||
|
|
||||||
@@ -865,7 +903,7 @@ class Runner:
|
|||||||
c.execute("""INSERT INTO automation_log (automation_id, result, detail)
|
c.execute("""INSERT INTO automation_log (automation_id, result, detail)
|
||||||
VALUES (%s, %s, %s)""", (automatik["id"], ergebnis, detail))
|
VALUES (%s, %s, %s)""", (automatik["id"], ergebnis, detail))
|
||||||
except Exception as fehler:
|
except Exception as fehler:
|
||||||
logger.warning("Protokoll nicht schreibbar: %s", fehler)
|
logger.warning("Protokoll nicht schreibbar: %r", fehler)
|
||||||
automatik["last_run"] = datetime.now()
|
automatik["last_run"] = datetime.now()
|
||||||
self.lief_im_fenster[automatik["id"]] = True
|
self.lief_im_fenster[automatik["id"]] = True
|
||||||
|
|
||||||
@@ -899,7 +937,7 @@ class Runner:
|
|||||||
c.execute("""UPDATE automations SET cond_met = %s, changed = changed
|
c.execute("""UPDATE automations SET cond_met = %s, changed = changed
|
||||||
WHERE id = %s""", (automatik["cond_met"], automatik["id"]))
|
WHERE id = %s""", (automatik["cond_met"], automatik["id"]))
|
||||||
except Exception as fehler:
|
except Exception as fehler:
|
||||||
logger.warning("cond_met nicht schreibbar: %s", fehler)
|
logger.warning("cond_met nicht schreibbar: %r", fehler)
|
||||||
|
|
||||||
def durchlauf(self, fenster=NACHHOLFENSTER):
|
def durchlauf(self, fenster=NACHHOLFENSTER):
|
||||||
jetzt = datetime.now()
|
jetzt = datetime.now()
|
||||||
@@ -979,7 +1017,7 @@ class Runner:
|
|||||||
c.execute("DELETE FROM automation_log WHERE ts < NOW() - INTERVAL %s DAY",
|
c.execute("DELETE FROM automation_log WHERE ts < NOW() - INTERVAL %s DAY",
|
||||||
(LOG_AUFBEWAHRUNG_TAGE,))
|
(LOG_AUFBEWAHRUNG_TAGE,))
|
||||||
except Exception as fehler:
|
except Exception as fehler:
|
||||||
logger.warning("Protokoll nicht aufraeumbar: %s", fehler)
|
logger.warning("Protokoll nicht aufraeumbar: %r", fehler)
|
||||||
|
|
||||||
# --- Hauptschleife ---------------------------------------------------
|
# --- Hauptschleife ---------------------------------------------------
|
||||||
|
|
||||||
@@ -1012,7 +1050,7 @@ class Runner:
|
|||||||
logger.info("Regelwerk hat sich geaendert, wird neu geladen")
|
logger.info("Regelwerk hat sich geaendert, wird neu geladen")
|
||||||
self.regelwerk_laden()
|
self.regelwerk_laden()
|
||||||
except Exception as fehler:
|
except Exception as fehler:
|
||||||
logger.warning("Regelwerk nicht pruefbar: %s", fehler)
|
logger.warning("Regelwerk nicht pruefbar: %r", fehler)
|
||||||
|
|
||||||
if nur_einmal:
|
if nur_einmal:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ URL des Aktors in der Tabelle `actors`:
|
|||||||
entschieden, sondern an der Box-Kennung in der URL
|
entschieden, sondern an der Box-Kennung in der URL
|
||||||
Logic das gerechnete Geraet "Zeitpunkt" (Uhrzeit, Datum, Sonne)
|
Logic das gerechnete Geraet "Zeitpunkt" (Uhrzeit, Datum, Sonne)
|
||||||
|
|
||||||
|
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`
|
||||||
|
ein Topic. Solche Werte liest der MQTT-Transport - siehe ist_topic() und
|
||||||
|
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
|
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 fuenf kurze Klassen, und wer eine sechste Geraeteart anschliesst, sieht
|
||||||
hier auf einen Blick, was dafuer zu tun ist.
|
hier auf einen Blick, was dafuer zu tun ist.
|
||||||
@@ -46,6 +52,23 @@ logger = logging.getLogger("autoaction.transport")
|
|||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
|
||||||
|
def ist_topic(state_url):
|
||||||
|
"""
|
||||||
|
Sieht diese Zustands-URL nach einem MQTT-Topic aus?
|
||||||
|
|
||||||
|
Gebraucht fuer die neueren Shellys: sie melden ihre Messwerte von selbst
|
||||||
|
an den Broker, geschaltet werden sie aber weiter ueber HTTP. Ihr Geraet
|
||||||
|
steht deshalb mit einer http://-Adresse in `actors`, waehrend in ihren
|
||||||
|
Messwerten ein Topic steht ("Power_EG/status/em:0"). Wer nur auf das
|
||||||
|
Geraet schaut, wuerde solche Werte im Poll-Takt suchen - und nie finden.
|
||||||
|
|
||||||
|
Ein Topic hat Schraegstriche und kein Schema davor. Feldnamen der anderen
|
||||||
|
Transporte haben beides nicht: "a_voltage", "core:ClosureState",
|
||||||
|
"seg[0].col[0]".
|
||||||
|
"""
|
||||||
|
return bool(state_url) and "/" in state_url and "://" not in state_url
|
||||||
|
|
||||||
|
|
||||||
def wert_aus_pfad(daten, pfad):
|
def wert_aus_pfad(daten, pfad):
|
||||||
"""
|
"""
|
||||||
Einen Teilwert aus einer Nutzlast holen: "[3]", "ssid", "seg[0].col[0]".
|
Einen Teilwert aus einer Nutzlast holen: "[3]", "ssid", "seg[0].col[0]".
|
||||||
|
|||||||
+2
-2
@@ -24,14 +24,14 @@
|
|||||||
/volume1/homes/wagner/SolarManager/wsMQTTbridge.log
|
/volume1/homes/wagner/SolarManager/wsMQTTbridge.log
|
||||||
/volume1/homes/wagner/SolarManager/wecker.log
|
/volume1/homes/wagner/SolarManager/wecker.log
|
||||||
{
|
{
|
||||||
daily
|
monthly
|
||||||
# Zwei Wochen zurueck. Weiter zurueck hat noch nie jemand gesucht, und
|
# Zwei Wochen zurueck. Weiter zurueck hat noch nie jemand gesucht, und
|
||||||
# was aelter ist, steht ohnehin als Messwert in der Datenbank.
|
# was aelter ist, steht ohnehin als Messwert in der Datenbank.
|
||||||
rotate 14
|
rotate 14
|
||||||
# Zusaetzlich zur Tagesgrenze: was ueber 20 MB geht, wird sofort
|
# Zusaetzlich zur Tagesgrenze: was ueber 20 MB geht, wird sofort
|
||||||
# rotiert. Sonst koennte ein Prozess, der in eine Fehlerschleife
|
# rotiert. Sonst koennte ein Prozess, der in eine Fehlerschleife
|
||||||
# geraet, an einem einzigen Tag das Volume fuellen.
|
# geraet, an einem einzigen Tag das Volume fuellen.
|
||||||
maxsize 20M
|
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 - der Wecker meldet an den meisten Tagen gar nichts.
|
||||||
|
|||||||
Reference in New Issue
Block a user