JSON-RPC über Python

Hallo Leute

Ich kämpfe mit der JSON-RPC-Schnittstelle. Genauer mit der Formatierung des „Param“-Feldes.

Ausgangslage:
Ein Raspberry-Pi hat ein 4x4 Keypad an einen I2C-Keyboard-controller angeschlossen und soll die gedrückte Taste via JSON-RPC an IPS übermitteln. Die gedrückte Taste ist eine 3-stellige Ziffer.

Auf dem Raspberry läuft ein Python-Script. Der JSON-RPC-Teil sieht so aus:

# JSON-RPS zu IPS definieren
def IpsRpc(methodIps, paramIps):
    url = "http://<username>:<password>@<IPS-IP>:82/api/"
    headers = {'content-type': 'application/json'}

    payload = {
        "method": methodIps,
        "params": paramIps,
        "jsonrpc": "2.0",
        "id": 0,
    }
    print json.dumps(payload)

    response = requests.post(url, data=json.dumps(payload), headers=headers).json

    print(response)

Der Aufruf geschieht mittels dem Befehl:

IpsRpc("SetValue", '[10900, '+str(key) +']')

und generiert folgenden JSON-String:

{"jsonrpc": "2.0", "params": "[10900, 183]", "method": "SetValue", "id": 0}

10900 ist dabei die zu aktualisierende Variablen-ID von IPS. 183 ist die 3-stellige Ziffer der gedrückten Taste. Soweit sogut, nur antwortet mir IPS mit folgender Fehlermeldung:

{u'jsonrpc': u'2.0', u'id': None, u'error': {u'message': u'Invalid request (params field invalid)', u'code': u'-32600'}}

Der Fehler liegt offensichtlich beim „Params“ -Feld.

Wenn ich stattdessen folgenden CURL-Aufruf mache, funktioniert alles bestens:

 curl -i -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0", "id": "0", "method": "SetValue", "params": [10900, 183]}' http://<username>:<password>@<IPS-IP>:82/api/

Mittels Wireshark sehe ich den Unterschied, dass der CURL-Aufruf die Daten des „Param“-Feldes als Array verschickt, der Python Aufruf als String.

Nun meine Fragen an euch:
Wie müssen die Daten des Param-Feldes genau übermittelt werden ? Als String oder als Array ? Wenn als String mit den eckigen Klammer oder ohne ? Ich habe diverse Beispiele gefunden, allerdings immer nur mit der Methode „IPS_RunScript“. Hat mir vielleicht jemand ein funktionierendes Beispiel mit „SetValue“ oder sieht wo ich mich vertan habe ?

Auf die Idee den JSON-RPC-Aufruf via dem Shell-Befehl CURL zu machen, kam ich natürlich auch schon. Nur habe ich mich da mit den doppelten und einfachen Hochkommas ziemlich vertan … :confused: Eleganter wäre es natürlich schon alles in einem Python-Script zu haben.

Danke und Gruss
Letraz

… ich habe mich in der Zwischenzeit etwas Schlau gemacht. Gemäss der „JSON-RPC 2.0 Spezifikation“ --> http://www.jsonrpc.org/specification MUSS das Feld „params“ ein Array sein.

Ich hatte einen Fehler in der Parameterübergabe meines Funktionsaufrufs. Korrekt muss der Aufruf lauten:

IpsRpc("SetValue", [10900, +(key)])

Dann ergibt sich folgender JSON-String:

{"jsonrpc": "2.0", "params": [10900, 183], "method": "SetValue", "id": "0"}

Der Output sieht fast identisch aus wie vorher, nur der Unterschied ist jetzt das Array beim Feld „params“.

… ich nochmals.

Es trat noch das Problem auf, dass mit dem Python-modul „python-requests“ Version 0.12.1-1 (Python Version 2.7.3) kein Authentication-header an IPS gesendet wurde und daher immer eine „Invalid Username/Password for remote access“ - Fehlermeldung zurückgeliefert wurde. (Wie ich Wireshark zum debuggen liebe :))
Dazu muss noch „HTTPBasicAuth“ importiert werden.

Mein Erster und jetzt zu meiner vollsten Zufriedenheit funktionierende Python-Script (der JSON-RPC relevante Teil) sieht wie folgt aus:


# notwendige Module importieren
import requests, json
from requests.auth import HTTPBasicAuth
...
# JSON-RPS zu IPS definieren
def IpsRpc(methodIps, paramIps):
    url = "http://<IPS-IP>:82>/api/"
    auth=HTTPBasicAuth('<username>', '<passwort>')   # username nicht codiert, bsp. johndoe@unknown.net 
    headers = {'content-type': 'application/json'}

    payload = {
        "method": methodIps,
        "params": paramIps,
        "jsonrpc": "2.0",
        "id": "0",
    }
    response = requests.post(url, auth=auth, data=json.dumps(payload), headers=headers).json
...
IpsRpc("SetValue", [10900, +(key)])  # 10900 = zu aktualisierende IPS-Variablen-ID, key = zu sendender Wert

# JSON-RPS zu IPS definieren
def IpsRpc(methodIps, paramIps):
    url = "http://<username>:<password>@<IPS-IP>:82/api/"
    headers = {'content-type': 'application/json'}

    payload = {
        "method": methodIps,
        "params": paramIps,
        "jsonrpc": "2.0",
        "id": 0,
    }
    print json.dumps(payload)

    response = requests.post(url, data=json.dumps(payload), headers=headers).json

    print(response)
IpsRpc("SetValue", [10900, +(key)]) 

Kannst du mir mal erklären was dieser Teil macht? Irgendwie steig ich da nicht ganz durch.
Ergeben all diese CODE-schnipsel ein komplettes Python-Script?

{"jsonrpc": "2.0", "params": [10900, 183], "method": "SetValue", "id": "0"} 

OK hierbei wird der Wert 183 in die IPS Variable 10900 geschrieben. Richtig?

Danke für dein Beispiel :slight_smile:

Habe das Thema mal ein wenig umbenannt und nach oben gehängt!

paresy

Hallo Stefan

Bitte entschuldige die späte Antwort.

IpsRpc("SetValue", [10900, +(key)])

Diese Zeile ruft die Funktion „IpsRpc“ auf und übergibt ihr ein Array mit zwei Werten. 10900 ist die zu aktualisierende Variable in IPS und die Python-Variable (key) beinhaltet den Tastencode der gedrückten Taste (den Wert „183“ im Beispiel).

Diese Zeilen sind zwar schon ein komplettes Python-Script, allerdings macht das so noch nicht viel. Die Funktion IpsRpc (also die Zeile „def IpsRpc(methodIps, paramIps):“ bis „print(response)“) definiert eine Funktion namens IpsRpc und sendet beim Aufruf die erhaltenen Daten an IPS. Die zitierte Zeile oben ruft eben diese Funktion „IpsRpc“ auf (= führt sie aus) und übergibt ihr Daten, welche dann an IPS gesendet werden.

Der Rest meines Python-Scriptes habe ich nicht gepostet, da es lediglich für meinen Anwendungsfall passt. Wie eingangs erwähnt frage ich via I2C einen 4x4 Keyboard-controller ab und übermittle die gedrückte Taste.

hi,

danke fuer deine Berichte zum posten der Variablen aus Python. Ich wuerde das gerne ein etwas erweitern, allerdings kaempfe ich ein bisschen mit den Funktionen zum Zusammenbauen der Variablenwerte/Strings/Nicht-Strings.

–EDIT–

Noch 5 Versuche mehr und jetzt hab ich’s auch geschafft, hier die Aenderung um beliebige Typen schreiben zu koennen:

Boolean:

variable=44718
value=True
IpsRpc("SetValue", [variable,value]) 

String:

variable=44713
value="abc"
IpsRpc("SetValue", [variable,value]) 

Int/Float:

variable=44711
value=123
IpsRpc("SetValue", [variable,value]) 

Hallo Letraz,

ja, auch ich muss mich bei Dir bedanken, für Dein tolles Konzept.

Obwohl ich JSON-RPC schon „lange“ benutze, um z.B. aus eigenen WEB-Seiten mittels JSON-RPC Werte aus IPS abzufragen, oder zu ändern (alles mittels PHP), war es mir bislang nicht möglich etwas ähnliches unter Python zu erledigen. (btw: ich bin blutiger Anfänger was Python betrifft.)

Ich suchte eine Möglichkeit, meinen Raspberry Pi dazu zu bringen irgendwelche Werte oder Ereignisse mittels Python an meine normale IPS Installation (Windows Home Server) zu übertragen. Jetzt geht das !:slight_smile:

Vielleicht noch eine Anmerkung dazu:
Ich hatte mir vorher schon - speziell für JSON-RPC - eine separate WebServer Instanz eingerichtet. (s.Bild)
Damit wird die Verbindung und Authentifizierung zu IPS vielleicht etwas übersichtlicher:

url = "http://192.168.xxx.xxx:8088/api/"
auth=HTTPBasicAuth('nnn','ppp')   

Gruß
mareng

Hallo,

jetzt muss ich doch nochmal auf das Thema zurückkommen.

Die Übertragung von Werten von einem Raspi aus in IPS-Variablen funktioniert nach den zuvor beschriebenen Beispielen problemlos.

Nun möchte ich gerne auf diese Weise Variableninhalte aus IPS in Python verwenden. Alle verzweifelten Versuche haben bisher keinen Erfolg gebracht. Vielleicht kann mir jemand ein Beispiel dazu nennen ? (Ich bin kein Python Experte !!)

Ich verwende die selbe Funktion „IpsRpc“ nur mit den angepassten Parametern
>>>>> IpsRpc(„GetValueFormatted“, „18447“) # 18447 ist die Variablen ID

und versuche mit
>>>> response = requests.post(url, auth=auth, data=json.dumps(payload), headers=headers).json
und
>>>> print response
auf das Ergebnis zuzugreifen.

Als Ergebnis erhalte ich:
<bound method Response.json of <Response [200]>>

Das ist sicherlich zu kurz gedacht, das mit dem „print“, aber wie gesagt ich bin leider kein Python Experte.

Kann mir trotzdem jemand sagen wie es richtig geht ?

Besten Dank im vorraus.

Gruß
mareng

So, nach vielen Recherchen im Netz habe ich eine Antwort auf die (meine) Frage erhalten. Ich möchte mein Ergebnis niemandem vorenthalten, weil ich inzwischen weiss, wie mühseelig eine solche Suche sein kann.

Zuerst der Hinweis auf die Webseite, die mich mich wirklich entscheidend weitergebracht hat:
http://www.python-requests.org/en/latest/user/quickstart/#json-response-content

Die ganze Problematik hing demnach nicht an einer fehlerhaften Verwendung der „json“-Library, sondern an der richtigen Nutzung von „requests“.

Ich habe mal die Einzelschritte in Python aufgeführt:

>>> import requests
>>> from requests.auth import HTTPBasicAuth
>>> import json
>>> url = "http://192.168.0.nnn:8088/api/"
>>> auth=HTTPBasicAuth('uuuu','pppp')
>>> headers = {'content-type': 'application/json'}
>>> payload = {"method": "GetValueFloat", "params": [18447], "jsonrpc": "2.0", "id": "0"}    # 18447 ist ID im IPS
>>> r = requests.post(url, auth=auth, data=json.dumps(payload), headers=headers, stream=True)  # Achtung ! kein ".json" am Ende
>>> r.text
u'{"result":6597.34000000006,"id":"0","jsonrpc":"2.0"}'
>>> s = r.text    # s:   nur zum Test ... !
>>> print s
{"result":6597.34000000006,"id":"0","jsonrpc":"2.0"}
>>> r.json()["result"]
6597.34000000006
>>> gc = r.json()["result"]    # gc ist eine gas_counter Variable 
>>> print gc
6597.34
>>>

Der entscheidende Punkt war die - offensichtlich unzulässige - Verwendung von .json am Ende des request Statements.

Ich habe die Einzelschritte selbstverständlich in eine Funktion integriert, so wie bereits oben beschrieben.

Damit kann ich nun meine Anforderung an die Kopplung raspberry <–> IPS erfüllen.

viele Grüße
mareng

Danke für die tolle Arbeit. Läuft das auch unter Symcon 4.0 auf eine Raspberry?

LG
Andreas

Ja. IP-Symcon 4.0 hat die selbe JSON-RPC Schnittstellen :slight_smile:

paresy

Gut zu wissen, dann liegt der Fehler also bei mir und kann es nicht auf jemand anderes schieben :mad:

mareng

ich habe die Kommunikation von Python zu IPS nach den Vorgaben hier am laufen - andersherum aber nicht.
Du hattest wohl das gleiche Problem - IpsRpc(„GetValueFormatted“, „18447“)

Leider verstehe ich Deine Lösung nicht. Kannst Du mir bitte ein PHP-Codeschnipsel schicken.

LG
Andreas

und wieder ein wenig schlauer

Hallo,

url = "http://192.168.0.20:82/api/"
    auth=HTTPBasicAuth('z.b.@meineadresse.de', 'unsichtbar')   # username nicht codiert, bsp. johndoe@unknown.net 
    headers = {'content-type': 'application/json'}

ich erhalte immer nur response [401]

ich habe das Kennwort eingegeben, welches ich unter IP-Symcon Tray -> Fernzugriff -> Ändern eingeben kann.
und User ist meine Mailadresse, die als Lizenz angezeigt bekomme.

was mache ich falsch ?

gruß
frank

Funktioniert die selbe Kombination wenn du sie in der Konsole nutzt? Sind im Kennwort evtl. Sonderzeichen die Probleme machen könnten?

paresy

also da hast Du mich ja als Anfänger enttarnt, ich weiß ehrlich gesagt garnicht, wie ich das Beispiel in der Konsole umsetzen soll. Ich hab mal nach Doku das json-beispiel ausprobiert und da kommt dann auch 401

$rpc = new JSONRPC("http://lizenz@name:passwort@192.168.0.20:82/api/");
$result = $rpc->IPS_GetKernelVersion();
echo "KernelVersion: ".$result;

ergibt

failed to open stream: HTTP request failed! HTTP/1.1 401 Unauthorized
 in C:\Haussteuerung\IP-Symcon\scripts\__rpc.inc.php on line 89

Hast du den Dienst nach dem Ändern neu gestartet? Sonderzeichen?

paresy

User und Passwort hast Du im Klartext? Hatte gestern abend den gleichen Fehler, als ich mein Python Skript geschrieben habe. Dort war´s Problem, dass mein Passwort noch base64 kodiert war.

Gruß
dfhome