How it started...

Für welche Projekte verwendet Ihr OpenHAB? Was habt Ihr automatisiert? Stellt eure Projekte hier vor.

Moderatoren: Cyrelian, seppy

Antworten
Benutzeravatar
udo1toni
Beiträge: 13986
Registriert: 11. Apr 2018 18:05
Answers: 222
Wohnort: Darmstadt

How it started...

Beitrag von udo1toni »

...how it ends

Ich habe mein Netzwerk schon seit Jahrzehnten in Betrieb :) und entsprechend bin ich auch etwas nerdig, was mein Equipment betrifft.
Ich habe z.B. keine FRITZ!Box und auch sonst keinen "gewöhnlichen" Router, stattdessen erledigt hier OPNsense die Routingaufgaben, und zwar auf einer APU3D4, sparsam und komplett ohne Videoausgang :)
Als VDSL Modem setze ich dabei ein DrayTek Vigor 165 ein, eines der wenigen Modelle am Markt, die direkt als "echtes" Modem verwendet werden können, d.h. die OPNsense nutzt PPPoE, um die Verbindung ins Internet aufzubauen.

Nun möchte man ja zumindest ein paar Informationen über den Zustand des Modems haben, denn trotz Großstadt mit Telekom-Vergangenheit kommt es auch hier immer wieder mal zu Ausfällen auf VDSL-Seite.

Mein Ansatz war in diesem Fall auch eher unkonventionell, ich wollte nicht über das Webinterface zugreifen.
Stattdessen habe ich im Modem einen Syslog Server eingetragen, den ich über OPNsense im Verwaltungsnetz verfügbar gemacht habe. (Ja, das Modem kann das von Haus aus...)
Der Syslog Dienst läuft gewöhnlich auf Port 514 mit UDP, es reicht dazu, in der rsyslog.conf des Hosts das Modul imudp zu aktivieren.
Die Standardkonfiguration schreibt für jedes externe System die Logs in ein eigenes Verzeichnis, pro Programm eine eigene Datei, perfekt...
Nun schreibt das Vigor Modem also brav etwa einmal pro Minute eine Logzeile, welche in einer Datei /var/log/meinModem/DrayTek.log landet (wobei meinModem dank unbound dem fqdn des Modems entspricht, der spielt hier aber keine Rolle :) )

Das Doofe: Der Inhalt der Datei sieht etwa so aus:

Code: Alles auswählen

Jul 15 08:20:59 meinModem DrayTek: ADSL_Status:[Mode=17A States=SHOWTIME UpSpeed=31999000 DownSpeed=69998000 SNR=17 Atten=14 ]
Jul 15 08:21:34 meinModem DrayTek: 9:13:44    [DSL] Status was switched: Showtime(7) to Exception(8)#015
Jul 15 08:21:36 meinModem DrayTek: 9:13:46    [DSL] Status was switched: Exception(8) to Restart(10)#015
Jul 15 08:21:38 meinModem DrayTek: 9:13:48    [DSL] Status was switched: Restart(10) to FirmwareRequest(1)#015
Jul 15 08:21:46 meinModem DrayTek: 9:13:56    [DSL] Entering VDSL2 mode#015
Jul 15 08:21:47 meinModem DrayTek: 9:13:57    [DSL] modem code: [08-0B-02-06-00-07]#015
Jul 15 08:21:47 meinModem DrayTek: 9:13:57    [DSL] Status was switched: FirmwareRequest(1) to firmwareReady(3)#015
Jul 15 08:21:48 meinModem DrayTek: 9:13:58    [DSL] Status was switched: firmwareReady(3) to Init(5)#015
Jul 15 08:21:51 meinModem DrayTek: 9:14:01    [DSL] Status was switched: Init(5) to Train(6)#015
Jul 15 08:23:18 meinModem DrayTek: 9:15:28    [DSL] Status was switched: Train(6) to Showtime(7)#015
Jul 15 08:23:23 meinModem DrayTek: ADSL_Status:[Mode=----- States=SHOWTIME UpSpeed=31999000 DownSpeed=69998000 SNR=15 Atten=14 ]
Jul 15 08:23:23 meinModem DrayTek: 9:15:34    [DSL] VDSL2: UP rate=0, DOWN rate=69992 Kbps, Mode=17A#015
Jul 15 08:23:24 meinModem DrayTek: [DSL] G.Vectoring Status: ON (Bidirection)
Jul 15 08:24:36 meinModem DrayTek: ADSL_Status:[Mode=17A States=SHOWTIME UpSpeed=31999000 DownSpeed=69998000 SNR=17 Atten=14 ]
Jul 15 08:25:47 meinModem DrayTek: ADSL_Status:[Mode=17A States=SHOWTIME UpSpeed=31999000 DownSpeed=69998000 SNR=17 Atten=14 ]
Jul 15 08:26:59 meinModem DrayTek: ADSL_Status:[Mode=17A States=SHOWTIME UpSpeed=31999000 DownSpeed=69998000 SNR=17 Atten=14 ]
Ich hätte die Daten ja gerne hübsch in openHAB...

Meine "faule" Lösung mit bullseye war, über crontab diese Zeile auszuführen:

Code: Alles auswählen

@reboot /usr/bin/tail -f /var/log/meinModem/DrayTek.log | /usr/bin/mosquitto_pub -h 192.168.178.55 -t draytek/log -l
tail liest also beim Start des Host Systems die letzten 10 Zeilen aus und schiebt diese, und ab dann alle neu hinzu kommenden Zeilen über mosquitto-pub als Payload in das topic draytek/log, so komme ich recht bequem von openHAB dran, muss aber dafür in openHAB recht aufwändig parsen.
Das muss besser gehen, aber... keine Zeit...

Bis nun mit debian bookworm die aktuelle Version von cron die Funktion @reboot verlernt hat. :shock:
Und was ich auch angestellt habe, es lief darauf hinaus, bei jedem Systemstart daran denken zu müssen, das tail-Kommando manuell zu starten. Nö.

Also muss doch eine Kanone herangerollt werden, mit der ich auf das Problem schießen kann :)
Da ich schon auf einigen Systemen einen kleinen Python mqtt Client programmiert habe, war es naheliegend, auch hier einen spezialisierten Client zu schreiben. :)

Code: Alles auswählen

#!/usr/bin/python

import configparser
import logging
from paho.mqtt import client as mqtt
import time
import json
from typing import Iterator

config = configparser.ConfigParser()
config.read('/etc/default/mqttclient')

log_file = (config['logging']).get('filename','/var/log/pymqtt.log')
log_level = int((config['logging']).get('level','30'))
logging.basicConfig(filename=log_file,format='%(asctime)s %(levelname)s:%(message)s', datefmt='%Y/%m/%d/ %H:%M:%S', level=log_level)

MQTT_SERVER = (config['mqtt']).get('url','localhost')
MQTT_PORT = int((config['mqtt']).get('port','1883'))
MQTT_RECONNECT = int((config['mqtt']).get('reconnect','60'))
MQTT_PATH = (config['mqtt']).get('path','draytek/')
FILEPATH = (config['file']).get('path','/var/log/test.log')

# Sobald die Verbindung zum Broker steht
def on_connect(client, userdata, flags, rc):
    client.publish(MQTT_PATH+"LWT", payload="Online", qos=0, retain=True)
    logging.info("Connected with result code "+str(rc))
    if rc: logging.warning("Connecting resulted with" +str(rc))

# Routine zum Lesen und zeilenweisen Ausgeben einer Datei
def follow(file, sleep_sec=0.1) -> Iterator[str]:
    """ Yield each line from a file as they are written.
    `sleep_sec` is the time to sleep after empty reads. """
    line = ''
    while True:
        tmp = file.readline()
        if tmp is not None:
            line += tmp
            if line.endswith("\n"):
                yield line.rstrip()
                line = ''
        elif sleep_sec:
            time.sleep(sleep_sec)

# Los geht's
client = mqtt.Client("draytek")
client.on_connect = on_connect
client.will_set(MQTT_PATH+"LWT", payload="Offline", qos=0, retain=True)
client.connect(MQTT_SERVER, MQTT_PORT, MQTT_RECONNECT)

client.loop_start()
with open(FILEPATH, 'r') as file:
    for line in follow(file):
        thelog = ''
# falls die Zeichenfolge 'DrayTek: ' vorkommt, alles dahinter übernehmen
        if line.find('DrayTek: ') > 1:
            thelog = line.split('DrayTek: ')[1]
        logging.debug(thelog)
# falls die Zeichenfolge 'ADSL_Status:' vorkommt, Zeile für das Topic adsl aufbereiten
        if thelog.find('ADSL_Status:') > -1:
            payload = {}
            payload['mode'] = line.split('Mode=')[1].split(' ')[0]
            payload['state'] = line.split('States=')[1].split(' ')[0]
            payload['upSpeed'] = int(line.split('UpSpeed=')[1].split(' ')[0])
            payload['downSpeed'] = int(line.split('DownSpeed=')[1].split(' ')[0])
            payload['snr'] = int(line.split('SNR=')[1].split(' ')[0])
            payload['atten'] = int(line.split('Atten=')[1].split(' ')[0])
            payload['timestamp'] = time.time()
            payload=json.dumps(payload)
            logging.info(payload)
            client.publish(MQTT_PATH+"adsl", payload, qos=0, retain=False)
# falls die Zeichenfolge '[DSL]' vorkommt, Zeile für das Topic dsl aufbereiten
        if thelog.find('[DSL]') > -1:
            payload = {}
            payload['DSL'] = line.split('[DSL] ')[1]
            payload['timestamp'] = time.time()
            payload=json.dumps(payload)
            logging.info(payload)
            client.publish(MQTT_PATH+"dsl", payload, qos=0, retain=False)
# wenn im DSL-Block die Zeichenfolge 'ON (Bidi' auftaucht, das Topic state auf ONLINE setzen, ansonsten auf OFFLINE
            if thelog.find('ON (Bidi'):
                client.publish(MQTT_PATH+"state", "ONLINE", qos=0, retain=False)
            else:
                client.publish(MQTT_PATH+"state", "OFFLINE", qos=0, retain=False)
Der Code ist nicht schön, aber er funktioniert :) Man kann sehr schön sehen, dass ich keine Ahnung davon habe, was ich da tue ;) aber mein Ziel habe ich dennoch (vorerst) erreicht, ich habe nun drei Topics der Form

Code: Alles auswählen

draytek/dsl {"G.Vectoring Status: ON (Bidirection)", "timestamp": 1689444854.9230878}
draytek/adsl {"mode": "17A", "state": "SHOWTIME", "upSpeed": 31999000, "downSpeed": 69998000, "snr": 18, "atten": 14, "timestamp": 1689444854.9233086}
draytek/state ONLINE
Es gibt eine Datei mit der konkreten Konfiguration:

Code: Alles auswählen

[mqtt]
url = 192.168.178.33
port = 1883
reconnect = 60
path = draytek/
lwt = LWT

[file]
path = /var/log/meinModem/DrayTek.log

[logging]
filename = /var/log/pymqtt.log
# CRITICAL=50 ERROR=40 WARNING=30 INFO=20 DEBUG=10 NOTSET=0
level = 30
Wenn ein Wert hier nicht gesetzt ist, wird er im Programm mit einem Default Wert gesetzt, unnötig, aber so habe ich mehr Optionen Fehler zu machen :)

Und natürlich darf eine service-Datei zum Start als Dienst nicht fehlen:

Code: Alles auswählen

[Install]
WantedBy=multi-user.target

[Service]
ExecStart=/usr/bin/python3 /usr/local/bin/mqttclient.py
User=mqtt
Restart=always
RestartSec=60
Dazu kommen in openHAB dann noch ein Thing und eine Handvoll Items:

Code: Alles auswählen

     Thing topic draytek "Vigor 165" @ "mqtt" {
          Type string : adsl      "ADSL"      [ stateTopic="draytek/adsl" ]
          Type string : dsl       "DSL"       [ stateTopic="draytek/dsl" ]
          Type switch : state     "Status"    [ stateTopic="draytek/state", on="ONLINE", off="OFFLINE" ]
      }

Code: Alles auswählen

 String   DraytekMode    "Mode"          <network> (gModem) ["Status"]    {channel="mqtt:topic:mymqtt:draytek:adsl"[profile="transform:JSONPATH",function="$.mode"]} 
 Number   DraytekUp      "Upload"        <network> (gModem) ["Status"]    {channel="mqtt:topic:mymqtt:draytek:adsl"[profile="transform:JSONPATH",function="$.upSpeed"]}
 Number   DraytekDown    "Download"      <network> (gModem) ["Status"]    {channel="mqtt:topic:mymqtt:draytek:adsl"[profile="transform:JSONPATH",function="$.downSpeed"]}
 Number   DraytekSNR     "SNR"           <network> (gModem) ["Status"]    {channel="mqtt:topic:mymqtt:draytek:adsl"[profile="transform:JSONPATH",function="$.snr"]}
 Number   DraytekAtten   "Atten"         <network> (gModem) ["Status"]    {channel="mqtt:topic:mymqtt:draytek:adsl"[profile="transform:JSONPATH",function="$.atten"]}
 Switch   DraytekOnline  "Online"        <network> (gModem) ["Status"]    {channel="mqtt:topic:mymqtt:draytek:state"}
 Number   DraytekTime    "Zeit"          <network> (gModem) ["Status"]    {channel="mqtt:topic:mymqtt:draytek:adsl"[profile="transform:JSONPATH",function="$.timestamp"]}
Und schon habe ich mit unverhältnismäßigem Aufwand die wichtigsten Kenndaten meines Modems in openHAB.

Vielleicht baue ich noch weitere Parseroptionen in den Client, um die DSL-Zeilen weiter zu zerlegen, aber für's erste spielt es jetzt so, wie ich es mir wünsche :) das dsl-Topic nutze ich jetzt erst mal nicht aktiv, online/offline reicht mir hier.
openHAB4.1.2 stable in einem Debian-Container (bookworm) (Proxmox 8.1.5, LXC), mit openHABian eingerichtet

Benutzeravatar
udo1toni
Beiträge: 13986
Registriert: 11. Apr 2018 18:05
Answers: 222
Wohnort: Darmstadt

Re: How it started...

Beitrag von udo1toni »

Und weil's so schön nicht sein kann:

Obwohl follow() an vielen Stellen zu finden ist, ist diese Lösung geradezu katastrophal. Ein Glück habe ich noch mal auf die Systemlast geschaut und festgestellt, dass mein Prozess einen Kern komplett auslastet.
Deshalb hier eine andere Lösung:

Code: Alles auswählen

#!/usr/bin/python

import configparser
import logging
import time
import json
import subprocess
import select
from paho.mqtt import client as mqtt

config = configparser.ConfigParser()
config.read('/etc/default/mqttclient')

log_file = (config['logging']).get('filename','/var/log/pymqtt.log')
log_level = int((config['logging']).get('level','30'))
logging.basicConfig(filename=log_file,format='%(asctime)s %(levelname)s:%(message)s', datefmt='%Y/%m/%d/ %H:%M:%S', level=log_level)

MQTT_SERVER = (config['mqtt']).get('url','localhost')
MQTT_PORT = int((config['mqtt']).get('port','1883'))
MQTT_RECONNECT = int((config['mqtt']).get('reconnect','60'))
MQTT_PATH = (config['mqtt']).get('path','draytekmodem/')
#MQTT_COMMAND = (config['mqtt']).get('command','cmnd')
MQTT_STATE = (config['mqtt']).get('state','state')
#MY_COMMAND = MQTT_PATH+MQTT_COMMAND
MY_STATE = MQTT_PATH+MQTT_STATE
FILEPATH = (config['file']).get('path','/var/log/meinModem/DrayTek.log')

# The callback for when the client receives a CONNACK response from the server.
def on_connect(client, userdata, flags, rc):
    client.publish(MQTT_PATH+"LWT", payload="Online", qos=0, retain=True)
    logging.info("Connected with result code "+str(rc))
    if rc: logging.warning("Connecting resulted with" +str(rc))

def adsl(line):
    payload = {}
    payload['mode'] = line.split('Mode=')[1].split(' ')[0]
    payload['state'] = line.split('States=')[1].split(' ')[0]
    payload['upSpeed'] = int(line.split('UpSpeed=')[1].split(' ')[0])
    payload['downSpeed'] = int(line.split('DownSpeed=')[1].split(' ')[0])
    payload['snr'] = int(line.split('SNR=')[1].split(' ')[0])
    payload['atten'] = int(line.split('Atten=')[1].split(' ')[0])
    payload['timestamp'] = time.time()
    payload=json.dumps(payload)
    logging.info(payload)
    return payload

def dsl(line):
    payload = {}
    payload['DSL'] = line.split('[DSL] ')[1]
    payload['timestamp'] = time.time()
    payload=json.dumps(payload)
    logging.info(payload)
    return payload

# Los geht'S
client = mqtt.Client("draytek")
client.on_connect = on_connect
client.will_set(MQTT_PATH+"LWT", payload="Offline", qos=0, retain=True)
client.connect(MQTT_SERVER, MQTT_PORT, MQTT_RECONNECT)

client.loop_start()

f = subprocess.Popen(['tail','-F',FILEPATH], stdout=subprocess.PIPE,stderr=subprocess.PIPE)
p = select.poll()
p.register(f.stdout)

while True:
    if p.poll(1):

        thelog = ''
        line = f.stdout.readline().decode("utf-8").rstrip()
        if line.find('DrayTek: ') > 1:
            thelog = line.split('DrayTek: ')[1]
        logging.debug(thelog)
        if thelog.find('ADSL_Status:') > -1:
            client.publish(MQTT_PATH+"adsl", adsl(thelog), qos=0, retain=False)

        if thelog.find('[DSL]') > -1:
            client.publish(MQTT_PATH+"dsl", dsl(thelog), qos=0, retain=False)
            if thelog.find('ON (Bidi'):
                client.publish(MQTT_PATH+"state", "ONLINE", qos=0, retain=False)
            else:
                client.publish(MQTT_PATH+"state", "OFFLINE", qos=0, retain=False)
    time.sleep(.5)
Ist nur auf die Schnelle reingezimmert...
Nun ruft python tatsächlich den Befehl tail als Subprozess auf. tail schreibt in eine Pipe, aus der das Script wiederum liest. Das hat zum Einen den Vorteil, dass es nun keine Rolle mehr spielt, wie groß die zu lesende Datei ist (oder zumindest spielt es keine große Rolle ;) )
Zum Anderen ist die Systemlast nun wieder bei etwa 0 für diesen Service.

Bei Gelegenheit muss ich das noch hübsch machen...
openHAB4.1.2 stable in einem Debian-Container (bookworm) (Proxmox 8.1.5, LXC), mit openHABian eingerichtet

Antworten