Compare commits
2
Commits
f643e37676
...
cc96e990fa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc96e990fa | ||
|
|
1e7b1d8b21 |
@@ -68,7 +68,7 @@ import requests
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
from transports import (HTTPTransport, LogicTransport, MQTTTransport,
|
||||
TahomaTransport, WLEDTransport)
|
||||
TahomaTransport, WLEDTransport, ist_topic)
|
||||
|
||||
logger = logging.getLogger("autoaction")
|
||||
|
||||
@@ -163,8 +163,13 @@ class Regelwerk:
|
||||
drin: eine geaenderte Zielhoehe einer Jalousie schlug also ebenso
|
||||
wenig durch.
|
||||
|
||||
Die Zeilenzahlen der Geraetetabellen bleiben, damit ein
|
||||
Discovery-Lauf weiterhin ein Neuladen ausloest.
|
||||
Von den Geraetetabellen zaehlt nicht nur, wie viele Zeilen es gibt,
|
||||
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:
|
||||
c.execute("""SELECT (SELECT COUNT(*) FROM automations) AS a,
|
||||
@@ -183,7 +188,13 @@ class Regelwerk:
|
||||
action_id, parameter_id, value))), 0)
|
||||
FROM automation_action_params) AS ps,
|
||||
(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()))
|
||||
|
||||
@classmethod
|
||||
@@ -679,6 +690,26 @@ class Runner:
|
||||
return transport
|
||||
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):
|
||||
self.regelwerk = Regelwerk.laden(self.db)
|
||||
|
||||
@@ -692,13 +723,19 @@ class Runner:
|
||||
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"],
|
||||
"state_url": s["state_url"], "value_path": s["value_path"],
|
||||
"wertetabelle": s["wertetabelle"]}
|
||||
for s in self.regelwerk.states.values()
|
||||
if transport.passt(s["actor_url"])]
|
||||
transport.zustaende_anmelden(passende)
|
||||
# Zugeteilt wird je Messwert und nicht je Geraet, weil beides
|
||||
# auseinanderfallen kann - siehe transport_fuer_messwert().
|
||||
listen = [(transport, []) for transport in self.transporte]
|
||||
for s in self.regelwerk.states.values():
|
||||
zustaendig = self.transport_fuer_messwert(s)
|
||||
for transport, liste in listen:
|
||||
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
|
||||
# keiner: nach einem Neustart steht sonst jede Bedingung auf "unklar",
|
||||
# bis das Geraet zufaellig etwas schickt.
|
||||
@@ -718,6 +755,7 @@ class Runner:
|
||||
return self._sonne[1], self._sonne[2]
|
||||
auf, unter = "00:00", "00:00"
|
||||
try:
|
||||
self.solar.ping(reconnect=True) # re-establish connection if it dropped
|
||||
with self.solar.cursor() as c:
|
||||
c.execute("SELECT sunrise, sunset FROM daylight WHERE date = %s", (heute,))
|
||||
zeile = c.fetchone()
|
||||
@@ -727,7 +765,7 @@ class Runner:
|
||||
else:
|
||||
logger.warning("Kein Eintrag in daylight fuer %s", heute)
|
||||
except Exception as fehler:
|
||||
logger.warning("Sonnenzeiten nicht lesbar: %s", fehler)
|
||||
logger.warning("Sonnenzeiten nicht lesbar: %r", fehler)
|
||||
self._sonne = (heute, auf, unter)
|
||||
return auf, unter
|
||||
|
||||
@@ -752,7 +790,7 @@ class Runner:
|
||||
if zeile:
|
||||
stand = {"feiertag": bool(zeile["holiday"]), "ferien": bool(zeile["vacation"])}
|
||||
except Exception as fehler:
|
||||
logger.warning("Kalender nicht lesbar: %s", fehler)
|
||||
logger.warning("Kalender nicht lesbar: %r", fehler)
|
||||
self._kalender = (heute, stand)
|
||||
return stand
|
||||
|
||||
@@ -816,7 +854,7 @@ class Runner:
|
||||
self.geschrieben[state_id] = wert
|
||||
self.geschrieben_um[state_id] = jetzt
|
||||
except Exception as fehler:
|
||||
logger.warning("current_value nicht schreibbar: %s", fehler)
|
||||
logger.warning("current_value nicht schreibbar: %r", fehler)
|
||||
|
||||
# --- Ausfuehren ------------------------------------------------------
|
||||
|
||||
@@ -865,7 +903,7 @@ class Runner:
|
||||
c.execute("""INSERT INTO automation_log (automation_id, result, detail)
|
||||
VALUES (%s, %s, %s)""", (automatik["id"], ergebnis, detail))
|
||||
except Exception as fehler:
|
||||
logger.warning("Protokoll nicht schreibbar: %s", fehler)
|
||||
logger.warning("Protokoll nicht schreibbar: %r", fehler)
|
||||
automatik["last_run"] = datetime.now()
|
||||
self.lief_im_fenster[automatik["id"]] = True
|
||||
|
||||
@@ -899,7 +937,7 @@ class Runner:
|
||||
c.execute("""UPDATE automations SET cond_met = %s, changed = changed
|
||||
WHERE id = %s""", (automatik["cond_met"], automatik["id"]))
|
||||
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):
|
||||
jetzt = datetime.now()
|
||||
@@ -979,7 +1017,7 @@ class Runner:
|
||||
c.execute("DELETE FROM automation_log WHERE ts < NOW() - INTERVAL %s DAY",
|
||||
(LOG_AUFBEWAHRUNG_TAGE,))
|
||||
except Exception as fehler:
|
||||
logger.warning("Protokoll nicht aufraeumbar: %s", fehler)
|
||||
logger.warning("Protokoll nicht aufraeumbar: %r", fehler)
|
||||
|
||||
# --- Hauptschleife ---------------------------------------------------
|
||||
|
||||
@@ -1012,7 +1050,7 @@ class Runner:
|
||||
logger.info("Regelwerk hat sich geaendert, wird neu geladen")
|
||||
self.regelwerk_laden()
|
||||
except Exception as fehler:
|
||||
logger.warning("Regelwerk nicht pruefbar: %s", fehler)
|
||||
logger.warning("Regelwerk nicht pruefbar: %r", fehler)
|
||||
|
||||
if nur_einmal:
|
||||
return
|
||||
|
||||
@@ -14,6 +14,12 @@ URL des Aktors in der Tabelle `actors`:
|
||||
entschieden, sondern an der Box-Kennung in der URL
|
||||
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
|
||||
sind fuenf kurze Klassen, und wer eine sechste Geraeteart anschliesst, sieht
|
||||
hier auf einen Blick, was dafuer zu tun ist.
|
||||
@@ -46,6 +52,23 @@ logger = logging.getLogger("autoaction.transport")
|
||||
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):
|
||||
"""
|
||||
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/wecker.log
|
||||
{
|
||||
daily
|
||||
monthly
|
||||
# Zwei Wochen zurueck. Weiter zurueck hat noch nie jemand gesucht, und
|
||||
# was aelter ist, steht ohnehin als Messwert in der Datenbank.
|
||||
rotate 14
|
||||
# Zusaetzlich zur Tagesgrenze: was ueber 20 MB geht, wird sofort
|
||||
# rotiert. Sonst koennte ein Prozess, der in eine Fehlerschleife
|
||||
# geraet, an einem einzigen Tag das Volume fuellen.
|
||||
maxsize 20M
|
||||
maxsize 10M
|
||||
compress
|
||||
# Eine leere Datei zu rotieren bringt nichts ausser vierzehn leeren
|
||||
# Archiven - der Wecker meldet an den meisten Tagen gar nichts.
|
||||
|
||||
Reference in New Issue
Block a user