ReST-API

ReST-API (Representational State Transfer-Application Programming Interface) meint eine Programmierschnittstelle, die auf HTTP(S) und anderen vorhandenen Webtechniken aufbaut.

Damit ist auch hier der Webserver des Geräts zu aktivieren. Client ist eine andere  Software auf einer anderen Machine.

JSON

JSON (JavaScript Object Notation) ist ein programmunabhängiges Datenformat in Textform, dass dem einfachen Datenaustausch zwischen Programmen dient. Für die Verarbeitung von JSON-Daten sind in allen Hochsprachen fertige Programmbibliotheken vorhanden.

Hier wird mittels einer Python3-Anwendung demonstriert, wie ein SPS-Programm auch ohne Web-Browser gesteuert werden kann. Der Datenaustausch erfolgt im JSON-Format.

In diesem Programmbeispiel soll die SPS mit folgendem JSON-Text antworten:

{ 
    "Motorschütz":0,
    "Motorschutzschalter":1
}

JSON-Daten werden immer von den Zeichen { und } umschlossen. In diesem einfachen Fall bestehen die Daten aus kommaseparierten Name/Wert-Paaren.

Programmierung der Anwenderseite

Hierfür wird eine Anwenderseite mit dem Namen api.io programmiert und als Baustein in die SPS geladen:

1
2
3
4
5
6
<!-- AWP_In_Variable Name='"Datenbaustein_Motorschaltung".WebStart' -->
<!-- AWP_In_Variable Name='"Datenbaustein_Motorschaltung".WebStop' -->
{ 
	"Motorschütz"::="Motorschütz":, 
	"Motorschutzschalter"::="Motorschutzschalter":
}

Datei api.io

In Zeilen 1-2 wird genau wie bei einer Anwenderwebseite in der Automation Web Programming-Konvention die Variablen in HTML-Kommentaren bezeichnet, auf die schreibend zugegriffen werden soll.

In den Zeilen 3-4 folgen dann im JSON-Format die Name/Werte-Paare, wobei die SPS die Variablen

:="Motorschütz":
:="Motorschutzschalter":

mit den entsprechenden Werten befüllen soll.

Die Datei api.io wird als Anwenderseite im Webserver des Geräts eingetragen.
Zusätzlich muss die Dateiendung .io unter "Erweitert" als Datei mit dynamischen Inhalt eingetragen werden, denn die SPS soll ja die Variablen dynamisch mit die Variablen mit Werten befüllen.
Bei der folgenden Pythonprogrammierung muss der Applikationsname (hier: Motorsteuerung) berücksichtigt werden.
Abschließend werden die Bausteine erzeugt.

Konfiguration des Webserver und Bausteine erzeugen

Konfiguration des Webserver und Bausteine erzeugen

Falls noch nicht - wie in Anwenderseiten beschrieben - geschehen, muss noch die WWW-Funktion vom SPS-Programm aufgerufen werden.

In das Gerät laden nicht vergessen!

Testweise kann mit dem Browser die Webseite des Geräts aufgerufen werden.
Einloggen und danach kann die neue Anwenderseite io.api heruntergeladen werden:

Herunterladen von api.io

Herunterladen von api.io

Mit einem Editor können wir den Inhalt überprüfen:

Heruntergeladene Datei api.io im Editor

Heruntergeladene Datei api.io im Editor


Nun kann ein Python-Programm geschrieben werden, dass mit dem SPS-Programm kommuniziert, bzw. dieses steuert.


Python3-Programm

Für den Aufbau der HTTPS-Verbindung wird noch das Zertifikat benötigt, es kann von der Startseite der SPS (Intro) heruntergeladen werden:

Zertifikat der SPS herunterladen

Zertifikat der SPS herunterladen


Es müssen eventuell noch fehlende Bibliotheken installiert werden:

pip3 install requests beautifulsoup4 json lxml

Das Python3-Programm WebClient.py

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# Source:   https://johanneskinzig.de 15.10.2020
# modified: W. Eick 13.02.2025

import requests
from bs4 import BeautifulSoup
import json
import settings  # login data and more

# Daten anfordern
def requestData():
    S7PLC.login(settings.WEB_USERNAME, settings.PASSWORD)
    Motorschütz, Motorschutzschalter = S7PLC.getData()
    print("Motorschütz=" + str(Motorschütz), "Motorschutzschalter=" + str(Motorschutzschalter))
    S7PLC.logout()

# Daten senden, um den Motor zu schalten
def sendData(start, stop):
    S7PLC.login(settings.WEB_USERNAME, settings.PASSWORD)
    S7PLC.postData(start,stop)
    Motorschütz, Motorschutzschalter = S7PLC.getData()
    print("Motorschütz=" + str(Motorschütz), "Motorschutzschalter=" + str(Motorschutzschalter))
    S7PLC.logout()

class S7ApiClient():
    def __init__(self, host, uri_api, path_to_certfile):
        # S7ApiClient initialisieren
        self.url_post_login = 'https://' + str(host) + '/FormLogin'
        self.url_post_logout = self.url_post_login + '?LOGOUT'
        self.url_api = str(uri_api)
        self.s7certfile = str(path_to_certfile)
        self.auth_cookie = None
        self.s7auth_cookie = None
	# http header initialisieren, sonst weist die S7 die Anfrage ab
        self.http_headers = {
            'Host': '',
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0',
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language': 'de,en-US;q=0.7,en;q=0.3',
            'Accept-Encoding': 'gzip, deflate, br',
            'Referer': '',
            'Content-Type': 'application/x-www-form-urlencoded',
            'Content-Length': '45',
            'DNT': '1',
            'Connection': 'keep-alive',
            'Upgrade-Insecure-Requests': '1'
            }
        self.http_headers['Host'] = str(host)
        self.http_headers['Referer'] = 'https://' + str(host) + '/Portal/Portal.mwsl'

    # einloggen vor der Anforderung
    def login(self, username, password):
        print("Logging in ...")
        session = requests.Session()
        payload_login = {'Login': username, 'Password': password, 'Redirection': ''}
        response = session.post(self.url_post_login, data=payload_login, headers=self.http_headers, 
                                verify=self.s7certfile, proxies=settings.proxies)
        print('Status Code: ' + str(response.status_code))
        webpage_html = response.content
        
        # Authentication cookie aus der Antwort extrahieren (in payload anstatt in http header)
        webpage_soup = BeautifulSoup(webpage_html,features="lxml")
        auth_cookie_part = webpage_soup.find('input', attrs={'name': 'Cookie'})
        auth_cookie_part = str(auth_cookie_part)
        try:
            self.auth_cookie = auth_cookie_part.split('"')[5]
            print("Authentication cookie: ", self.auth_cookie)
            print("Login Done")
        except:
            print("Login failed!")
        
        # Cookie für Anforderung vorbereiten
        self.s7auth_cookie = dict(siemens_ad_session=self.auth_cookie, coming_from_login='true')

    # nach der Anforderung ausloggen
    def logout(self):
        payload_logout = {'Cookie': self.auth_cookie, 'Redirection': ''}
        session = requests.Session()
        logout = session.post(self.url_post_logout, cookies=self.s7auth_cookie, headers=self.http_headers, 
                              data=payload_logout, proxies=settings.proxies, verify=self.s7certfile)
        print('Status Code: ' + str(logout.status_code))
        print('Logout Done')

    # Anforderung der Daten 
    def getData(self):
        session = requests.Session()
        payload = session.get(self.url_api, cookies=self.s7auth_cookie, verify=self.s7certfile, proxies=settings.proxies)
        content_json = json.loads(payload.text)
        print("Empfangen: " + str(content_json))
        return content_json['Motorschütz'], content_json['Motorschutzschalter']

    # Daten senden
    def postData(self, start, stop):
        session = requests.Session()
        payload = {'"Datenbaustein_Motorschaltung".WebStart': str(start).lower(), '"Datenbaustein_Motorschaltung".WebStop': str(stop).lower()}
        action = session.post(self.url_api, data=payload, cookies=self.s7auth_cookie, verify=self.s7certfile, proxies=settings.proxies)
        print("Status Code: " + str(action.status_code))


S7PLC = S7ApiClient(settings.PLC_IP, settings.PLC_API_URI, settings.CERT_PATH)

Quellcode des Python3-Programms WebClient.py


Zeile 24-94: Klasse S7ApiClient

Zeile 25-48: Konstruktor

Der Konstruktor wird ausgeführt, wenn ein Objekt dieser Klasse erzeugt wird (Zeile 105). Hier werden die Eigenschaften des Objektes initialisiert.

Zeile 51-72: Methode login()

Der Anmeldevorgang auf der Simatic S7 ist nur über das Webformular auf der Startseite möglich.
Da hier ein Python-Programm als Client und kein Web-Browser benutzt wird, müssen die Daten an den Web-Server direkt gesendet werden. Der Server antwortet dann mit einem Session-Cookie, in dem der Stand der Authentifizierung gespeichert ist. Dieses Session-Cookie muss bei jeder folgenden Anfrage an den Server mit gesendet werden. Hierfür wird in:

  • Zeile 53: ein Session-Objekt session erzeugt.
  • Zeile 54: eine JSON-Wörterbuch payload_login mit den Anmeldedaten angelegt.
  • Zeile 55-56: die Anmeldedaten zusammen an die gleiche Seite gesendet, die normalerweise die Anmeldedaten von der Startseite entgegen nimmt. Die Headerdaten (Kopfdaten), die im Konstruktor initialisiert wurden, müssen dabei vorangestellt werden, sonst antwortet der Webserver mit 400 - Bad Request. Die Verschlüsselung erfolgt mit dem Zertifikat, das wie oben beschrieben heruntergeladen werden muss.
  • Zeile 57: der Statuscode der Antwort wird zur Kontrolle ausgegeben (200 OK).
  • Zeile 58: der Inhalt der Antwort wird in webpage_html gespeichert (die erste Seite nach dem erfolgreichen Anmeldevorgang).
  • Zeile 61: da das Authentifizierungs-Cookie nicht wie normalerweise im Header der Antwort sondern im Inhaltsbereich zurückgesendet wird, wird versucht, es mit mit Hilfe der Programmbibliothek Beautiful Soup zu extrahieren.
  • Zeile 69: falls das nicht gelang, wird eine Fehlermeldung ausgegeben und das Programm mit einer Exception beendet.
  • Zeile 72: falls das Cookie extrahiert werden konnte, wird es für weitere Anfragen vorbereitet.

Zeile 75-81: Methode logout()

Nach einer erfolgreichen Abfrage muss eine Abmeldung erfolgen, sonst schlägt eine Neuanmeldung fehl. Hierfür wird in:

  • Zeile 78-79: Die Daten, die in Zeile payload_logout definiert wurden, werden zusammen mit dem Cookie an genau die Seite gesendet, die normalerweise beim Anklicken der "Abmelden"-Schaltfläche aufgerufen wird. Der Anfragekopf (Header) wird auch mitgesendet. Verschlüsselt wird wieder mit dem Zertifikat.

Zeile 84-89: Methode getData()

Die Methode getData() fordert Daten von der SPS an.

  • Zeile 86: Die dynamische Webseite api.io wird mit der HTTP-Methode GET angefordert, von der SPS mit entsprechenden Werten befüllt und zurückgesendet.
  • Zeile 87: Der JSON-Anteil wird aus der der Antwort extrahiert.
  • Zeile 88: Der JSON-Antwort wird ausgegeben
  • Zeile 89: Die Werte für den Motorschütz und den Motorschutzschalter werden zurückgegeben (In Python kann mehr als ein Return-Wert zurückgegeben werden).

Zeile 92-96: Methode postData()

Die Methode postData() sendet Daten zur SPS.

  • Zeile 94: Der Variablen payload wird eine JSON-Zeichenkette zugewiesen. Die Werte der Parameter start und stop werden dabei eingefügt.
  • Zeile 95: Hier wird die HTTP-Methode POST aufgerufen. session.post ruft hierfür sendet payload an api.io.
  • Zeile 96: Zur Kontrolle wird der Statuscode der Antwort des Webservers ausgegeben (200 ok)

Zeile 99: Objekt S7PLC wird erzeugt

Dabei werden drei Parameter: die IP-Adresse des Geräts, die URL der ReST-API api.io und der Pfad der zuvor heruntergeladene Zertifikatsdatei übergeben. Außerdem muss beachtet werden, dass der Name der Webapplikation Bestandteil der URL ist.

Die Einstellungen werden in settings.py vorgenommen.  settings-template.py ist dabei die Vorlage. DIe GET- und POST-Aufrufe müssen dabei an der Werner-von Siemens-Schule ohne Proxy vorgenommen werden, weil das Experimentalnetz 172.17.10.0/24 lokal ist.

Zeile 10-22: Definition der Prozeduren zum Senden und Anfordern von Daten

Die Prozedur requestData()

  • meldet den oben spezifizierten Benutzer mit Kennwort an,
  • ruft die Methode getData() des PLC-Objekts S7PLC auf und weist die beiden Rückgabewerte den Motorschutz und Motorschutzschalter zu,
  • gibt beides aus und
  • meldet den Benutzer ab.



Benutzung des Programms

Nach dem Programmstart in der Python Shell steht das erzeugte Pythonobjekt S7PLC zur Verfügung und die Prozeduren requestData() und sendData() können aufgerufen werden:

Steuerung des SPS-Programms mit der Python Shell

Steuerung des SPS-Programms mit der Python Shell

Die Python Shell kann mit Strg+z wieder verlassen werden.

Das Programm kann auch in der Python Shell im Terminalfenster von Visual Studio Code gestartet werden durch:

python -i WebClient.py

Programmstart von WebClient.py in der Python Shell vom Terminalfenster von Visual Studio Code
Programmstart von WebClient.py in der Python Shell vom Terminalfenster von Visual Studio Code




Quellen:

https://johanneskinzig.de/index.php/software-development/19-logging-into-simatic-s7-1200-web-api-using-python-client (03.11.2020)

https://github.com/ateachment/RemoteControlPLC/tree/main/PythonWebClient  (11.02.2025)

Last modified: Thursday, 13 February 2025, 9:52 PM