Fragen zur Umgang mit Websockets.

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
dll-live
User
Beiträge: 32
Registriert: Dienstag 11. August 2020, 09:25
Wohnort: CH

Hallo zusammen

Ausgangslage:
Zuhause werden einige Lichter mit deCONZ (Zigbee Netzwerk) gesteuert. Nun ist in mir den Wunsch aufgekommen, die Zustände der gesteuerten Lichter in Echtzeit zu erfahren.

Unterlagen:
Dresden-elektronik (der Hersteller von deCONZ) hat auf github eine Beschreibung Ihrer REST-API bereit gestellt.
Im Abschnitt Websocket wird erklärt wie man auf "den" Websocket Server zugreifen kann.

Erledigte Schritte:
Die Unterlagen habe ich gelesen und soweit ich diese verstanden habe, habe ich es im folgendes Script umgesetzt:

Code: Alles auswählen

# reason for this script => I want the real-time state of my lights from Deconz.
# description deconz-api section webserver: https://dresden-elektronik.github.io/deconz-rest-doc/endpoints/websocket/

import websocket
# websocket is use from library install with "pip3 install websocket-client"
import json

i = 0
j = 0


def on_message(wsapp, message):
    # how can i use / print number of runs without global....
    global i
    global j
    # print(message)  # Python thinks "message" is a string
    data = json.loads(message)  # convert str to Python Dict
#    print(data)
    if (list(data.keys())[0] == "e"
            and data.get("e") == "changed"
            and data.get("r") == "lights"
            and data.get("state", None) is not None):
        print("++++++++++++++++++++++")
        state_value = data.get("state").get("on")
        light_id = data.get("id")
        print("ID: {}, Status: {}".format(light_id, state_value))
        i += 1
    j += 1
    print("Number of runs: {} // number of runs where the conditions are met: {}".format(j, i))


def main():
    server = "127.0.0.1"
    port = "8088"
    wsapp = websocket.WebSocketApp("ws://{}:{}".format(server, port),
                                   on_message=on_message)
    wsapp.run_forever()


if __name__ == "__main__":
    main()
Das Script funktioniert sauber anbei noch eine mögliche Ausgabe des Scriptes.

Code: Alles auswählen

Number of runs: 1 // number of runs where the conditions are met: 0
++++++++++++++++++++++
ID: 27, Status: True
Number of runs: 2 // number of runs where the conditions are met: 1
Number of runs: 3 // number of runs where the conditions are met: 1
++++++++++++++++++++++
ID: 26, Status: True
Number of runs: 4 // number of runs where the conditions are met: 2
Number of runs: 5 // number of runs where the conditions are met: 2
Number of runs: 6 // number of runs where the conditions are met: 2
Fragen / "Probleme":
Beim Script habe ich folgende Fragen / "Probleme" (teilweise schon im script (in englisch) notiert.
- Wie erhalte ich die gleiche Funktionalität vom Script ohne die globalen Variablen?
- Erklärt mir bitte jemand noch folgende Verständnisprobleme:
- Wieso muss in der Zeile: "def on_message(wsapp, message):" wsapp vorkommen? (ohne funktioniert das Script nicht - dies habe ich bereits probiert)
- Warum klappt der Aufruf der Funktion: on_message?

Für die Beantwortung einer oder aller Fragen / "Probleme" bedanke ich mich bereits im voraus herzlich.

Freundliche Grüsse
Daniel
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Das ist ja mal wieder eine schreckliche Art, wie man auf Wörterbücher zugreift. Manchmal frage ich mich schon, wie man etwas so kompliziert machen kann, wo doch die einfache Lösung viel einfacher ist. Warum muß in einem ungeordneten Wörterbuch der erste Eintrag unbedingt "e" sein?
Python denkt nicht nur, dass message ein String ist, es ist wirklich einer, und on_message erwartet halt als erstes Argument die app, das ist so implementiert.
Und das wäre auch der Weg, die globalen Variablen loszuwerden, indem man von WebSocketApp erbt und dort i und j als Attribute (mit besseren Namen) definiert.
Aber eigentlich ist es völlig Bohne, die Anzahl der Aufrufe zu zählen, würde ich einfach weglassen.

Das Abfangen des KeyErrors sollte eigentlich nicht nötig sein, weil alle Messages ein e und ein r definiert haben.

Code: Alles auswählen

# websocket is use from library install with "pip3 install websocket-client"
import websocket
import json

WS_URL = "ws://127.0.0.1:8080"


def on_message(wsapp, message):
    # print(message)
    data = json.loads(message)
    try:
        if (data["e"], data["r"]) == ("changed", "lights"):
            state = data["state"]["on"]
            light_id = data["id"]
            print(f"ID: {light_id}, Status: {state}")
    except KeyError:
        # ignore for missing keys
        pass


def main():
    wsapp = websocket.WebSocketApp(
        WS_URL,
       on_message=on_message)
    wsapp.run_forever()


if __name__ == "__main__":
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Der Test mit dem "e" ist auch falsch, denn auch wenn Python-Wörterbücher mittlerweile eine offiziell definierte Reihenfolge von Schlüssel/Wert-Paaren haben, gilt das für JSON nicht, auch wenn das so aussehen mag in den Daten. Verlassen kann man sich da nicht drauf.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
dll-live
User
Beiträge: 32
Registriert: Dienstag 11. August 2020, 09:25
Wohnort: CH

Hallo @Sirius3 und @__blackjack__

Besten Dank für euere Antworten.
Warum muß in einem ungeordneten Wörterbuch der erste Eintrag unbedingt "e" sein?
und
Das Abfangen des KeyErrors sollte eigentlich nicht nötig sein, weil alle Messages ein e und ein r definiert haben
Dies war "empirsche" Forschung :wink: => bei den Datensätzen die mich interessieren (Der Status eines Lichts hat sich geändert) ist das immer so (e am Anfang und r = lights).
"Leider" gibt der Webserver noch andere "message" retour, welche kein "e" am anfang haben und das r fehlt..
on_message erwartet halt als erstes Argument die app, das ist so implementiert.
Jetzt weiss ich wieder mehr, woher hast du dieses wissen? Wo hätte ich mir dieses Wissen selber aneignen können?
Und das wäre auch der Weg, die globalen Variablen loszuwerden, indem man von WebSocketApp erbt und dort i und j als Attribute (mit besseren Namen) definiert.
Ja i und j sind Zählervariablen (hab irgendwo mal gelernt, dass man i, j und so weiter für Zählervariablen nutzen kann (nicht die "Mühe" gemacht werden muss, für diese einen sprechenden Namen zu finden /nutzen)
Ob es nun Sinn macht, die Anzahl der Aufrufe zu zählen ist irrelevant - ich brauche (will) persistente Variablen haben, welche die Werte in der onmessage-Funktion ändern, aber auf vorherige Zustände zugreifen kann, für den nächsten Aufruf bereitgehalten werden.
Wie ich diese Variablen ins WebSocketApp integriert, weiss ich nicht, ebenso wenig wie dann der Aufruf in der onmessage Funktion ist.

Wenn ich dazu eine Anleitung bekomme, (am besten als Code, nicht "bloss" als prosa) bin ich sehr glücklich und komme meiner Vision (der Echtzeitzustände aufzeichnen) wieder ein Stück weiter.

Freundliche Grüsse
Daniel
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

Wenn Du nicht weißt, wie Du eine Klasse von WebSocketApp erben kannst, dann wäre der erste Schritt, mit einem Tutorial Deiner Wahl Vererbung zu lernen.

Welche Signatur ein Callback braucht, steht in der Dokumentation, da Du aber schon on_message erfolgreich einsetzt, brauch ich da gar nichts nachschauen, weil wie die Signatur aussieht, steht ja schon da.
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@dll-live: Es gibt bei JSON-Objekten diesen ”Anfang” nicht. Die Reihenfolge der Schlüssel/Wert-Paare ist nicht festgelegt. Ich sehe auch nicht warum dieser Test notwendig ist, denn wenn "e" und "r" mit "changed" und "lights" vorhanden ist, dann sollte das doch auch die Art von Nachrichten auf das einschränken was Dich interessiert.

Statt hier mit komplexeren Bedingungen zu hantieren, wäre das ein Fall wo ein ``match``-Konstrukt Code sparen kann ohne das die Lesbarkeit leidet, beziehungsweise man auch gleich schön sieht wie die erwartete Struktur aussieht.

`i` und `j` sind typische Namen für Laufvariablen in *Schleifen*. Oder manchmal auch in einer Funktion für Werte die für Indexzugriffe verwendet werden. Was in Python selten der Fall ist. Das sind aber begrenzte Gültigkeitsbereiche. Je weitreichender ein Gültigkeitsbereich ist, um so wichtiger ist ein Name, der verrät was das *bedeutet* was man da speichert, oder in diesem Fall zählt. Globale Variablen die etwas bestimmtes zählen sollte/muss man schon immer so benennen, das der Leser weiss was da gezählt wird. Also selbst bei Pascal zu DOS-Zeiten hätte man das gemusst. Einzige Ausnahme würde ich bei klassischen BASIC-Dialekten machen wo Namen auf ein bis zwei Zeichen beschränkt sind. Und selbst da wären I und J keine sinnvolle Wahl, denn die sind ja schon für Schleifen ”reserviert”.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
dll-live
User
Beiträge: 32
Registriert: Dienstag 11. August 2020, 09:25
Wohnort: CH

Hallo

@Sirius3 und @__blackjack__ Wieder um besten Dank für euere Antworten.

@Sirius3: Vererbung, ja da war mal was (ist ein Thema wo ich sehr schwach auf der Brust bin....). hab mein Script angepasst. jedoch komme ich leider noch nicht ganz weiter:

Anbei mein "neues"/ angepasstes" - sprich erweitertes Script (mit Vererbung, und besserer Bezeichnung der "Laufvariable" :wink: )

Code: Alles auswählen

# reason for this script => I want the real-time state of my lights from Deconz.
# description deconz-api section webserver: https://dresden-elektronik.github.io/deconz-rest-doc/endpoints/websocket/

import websocket
# websocket is use from library install with "pip3 install websocket-client"
import json


class my_websocket(websocket.WebSocketApp):
    new_number_of_messages = 0


number_of_messages = 0

def on_message(wsapp, message):
    # how it works without global???
    global number_of_messages
    data = json.loads(message)
    if (list(data.keys())[0] == "e"
            and data.get("e") == "changed"
            and data.get("r") == "lights"
            and data.get("state", None) is not None):
        print("++++++++++++++++++++++")
        state_value = data.get("state").get("on")
        light_id = data.get("id")
        print("ID: {}, Status: {}".format(light_id, state_value))
        number_of_messages += 1
    print("Number of interesting messages: {}".format(number_of_messages))


def main():
    server = "127.0.0.1"
    port = "8088"
    wsapp = my_websocket("ws://{}:{}".format(server, port),
                                   on_message=on_message)
    wsapp.run_forever()


if __name__ == "__main__":
    main()
    
Leider verstehe ich noch nicht was ich nun tun muss, damit statt meiner globalen Variable "number_of_messages" die variable der klasse verwendet wird (new_number_of_messages)
den in der funktion on_message() kann ich die neue variable (stand jetzt - sprich nach meinem jetzigen wissen) noch nicht verwenden.

Es wäre sehr toll, wenn mir da weiter geholfen würde, ich nochmals einen weiteren stups bekommen wurde, wie das geht.

@__blackjack__
Ich sehe auch nicht warum dieser Test notwendig ist, denn wenn "e" und "r" mit "changed" und "lights" vorhanden ist, dann sollte das doch auch die Art von Nachrichten auf das einschränken was Dich interessiert.
Leider nein, es kommt vor, das ich Meldungen vom Server erhalte, wo "e" und "r" mit "changed" und "lights" belegt sind, mir nichts nützen. als einziges Unterscheidungskriterium habe ich da den "anfang" der message gesehen....
``match``-Konstrukt
schaue ich mir mal an, evlt kann ich dass ja einbauen.
.... `i` und `j` sind typische Namen für Laufvariablen in *Schleifen* ....
Aha, was dazu gelernt und im angepassten Script auch umgesetzt......

Gruss Daniel
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@dll-live: Die Klasse macht so keinen Sinn. Das Klassenattribut ist ja auch wieder eine globale Variable. Und es sieht so aus wenn man in den Code schaut, das man doch nicht von dieser Klasse erben sollte, denn die hat gar nicht die Methoden die man da erwarten und überschreiben könnte, sondern speichert in der `__init__()` tatsächlich die ganzen Default-None-Werte als Attribute mit dem gleichen Namen wie das Argument.

Man könnte sich dann beispielsweise eine eigene Klasse schreiben die so ein WebSocketApp-Exemplar als Teil des Zustands speichert, also als Attribut. Neben den Zählern.

Du müsstest da am besten wirklich mal einen Schritt von dem konkreten Problem zurück gehen und ganz allgemein Klassen lernen, mit völlig anderen Beispielen, und *danach* dann das gewonnene Wissen auf dieses Problem übertragen.

Wenn "e" → "changed", "r" → "lights", und das Vorhandensein von "state" und darin dann "on" nicht ausreicht um die gewünschten Nachrichten zu filtern, dann hast Du verloren. Denn wie gesagt ist dieser Test auf "e" als erstes in der Nachricht vom Konzept her kaputt, weil JSON keine solche Garantie gibt, das der Inhalt von einem JSON-Objekt eine feste, erwartbare Reihenfolge hat/einhält.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
dll-live
User
Beiträge: 32
Registriert: Dienstag 11. August 2020, 09:25
Wohnort: CH

@ __blackjack__

Nun ein Versuch mit meiner Klasse wars wert. Wäre auch auch zu schön gewesen wenns geklappt hätte oder ich drauf gestossen worden wäre.....
Somit werde ich mich in Zukunft mal mit dem ganzen Klassen und Vererbung, überschrieben etc auseinander setzen....
In der Zwischenzeit werde ich (gezwungermassen) mit globalen Variablen arbeiten. (grummel - mag ich nicht - jedoch will ich mein Projekt demnächst erledigt haben.....)

das "match" Konstrukt, kann ich nicht verwenden, da meine Python Version noch kleiner ist als 3.10...., auch da werde ich mal in Zukunft updaten....
Wenn "e" → "changed", "r" → "lights", und das Vorhandensein von "state" und darin dann "on" nicht.....
Das habe ich verstanden und bedanke mich herzlich für deine Ausführungen.

Gruss Daniel
Benutzeravatar
__blackjack__
User
Beiträge: 13117
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Der Code von `on_message()` mit ``match``:

Code: Alles auswählen

def on_message(_app, message):
    match json.loads(message):
        case {
            "e": "changed",
            "r": "lights",
            "id": light_id,
            "state": {"on": state},
        }:
            print(f"ID: {light_id}, Status: {state}")
Man sieht da viel besser was als Struktur erwartet wird, als wenn das in ``if`` und mehreren Bedingungen und danach einzelnen Zugriffen auf die Werte die man da raus haben möchte, ausgedrückt wäre.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
snafu
User
Beiträge: 6744
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Bin nicht der TE, aber es ist auf jeden Fall beeindruckend, wie mächtig das "match-case"-Konstrukt ist. 👍
Antworten