s7-gateway
An HTTP interface to Siemens S7 controllers. It reads and writes the tags you declare in a YAML file, and exposes them for Prometheus.
It exists because getting a value out of an S7 PLC from anything that is not TIA Portal usually means writing snap7 code, and every plant writes that code again. This is that code, written once, with the addresses in a file instead of in a script.
$ curl localhost:8080/plcs/line1/tags/motor_temp
{"plc":"line1","tag":"motor_temp","value":72.5,"type":"real","unit":"°C"}
Status
First public version, and it should be read as one.
Every path is tested against a real S7 server over the actual protocol, but on
localhost — it has not yet been run against physical hardware. The protocol
work is python-snap7's and is well travelled; what is new here is the layer
above it, and that is what needs your plant to find the rest of. Reports from a
real line are the most useful thing anyone can send.
Read paths first, and read Before you point it at a real line.
Install
pip install git+https://github.com/Poseidonas/s7-gateway
Python 3.11+. python-snap7 3.1+ speaks S7 in Python over a plain socket, so
there is no snap7 C library to install and nothing to compile.
Configure
Nothing is discovered: a tag exists if you declare it, and it is read-only unless you say otherwise.
plcs:
line1:
host: 192.168.0.10
rack: 0 # default
slot: 1 # default; 2 on some S7-300 CPUs
port: 102 # default
tags:
motor_temp:
db: 10
offset: 0
type: real
unit: "°C"
description: Motor winding temperature
line_speed:
db: 10
offset: 4
type: int
unit: m/min
writable: true
running:
db: 10
offset: 8
type: bool
bit: 0 # required for bool: which bit of the byte
Check it before starting anything. This reads the file only — it does not contact a controller:
$ s7-gateway --config config.yml --check
line1 192.168.0.10:102 rack 0 slot 1
line_speed int DB10.DBX4 rw m/min
motor_temp real DB10.DBX0 r- °C
running bool DB10.DBX8.0 r-
Run
s7-gateway --config config.yml
It listens on 127.0.0.1:8080. Pass --host 0.0.0.0 to accept connections from
the network — deliberately not the default, since a config with a writable tag
would otherwise become reachable from the plant network the moment someone tries
it out.
Interactive documentation is at /docs.
To run it under your own ASGI host instead, point S7GATEWAY_CONFIG at the
file and use the factory:
S7GATEWAY_CONFIG=config.yml uvicorn --factory s7gateway.api:create_app
Endpoints
GET /health |
the gateway is up. Does not touch a controller |
GET /plcs |
the configured controllers |
GET /plcs/{plc}/tags |
the tags declared on one, with type and address |
GET /plcs/{plc}/values |
every tag in one request |
POST /plcs/{plc}/values |
write several tags in one request |
GET /plcs/{plc}/tags/{tag} |
one tag |
PUT /plcs/{plc}/tags/{tag} |
write one tag — {"value": 1500} |
GET /plcs/{plc}/status |
whether the controller answers, and its CPU state |
GET /plcs/{plc}/info |
CPU type, serial number, PDU length, and clock drift |
GET /metrics |
every tag in the Prometheus text format |
GET /plcs/{plc}/values reports a failing tag individually rather than failing
the whole response, so one bad address does not cost you the other readings:
{"plc": "line1", "readings": [
{"tag": "motor_temp", "type": "real", "value": 72.5, "unit": "°C", "error": null},
{"tag": "line_speed", "type": "int", "value": null, "unit": null,
"error": "could not read 'line_speed' from PLC 'line1' (DB10.DBX4): ..."}
]}
GET /plcs/{plc}/status answers 200 even when the controller is unreachable,
and says so in the body. A monitoring system needs to tell "the plant is down"
apart from "the gateway is down", which a 502 makes harder.
POST /plcs/{plc}/values takes [{"tag": "line_speed", "value": 1500}, ...]
and reports each tag separately. There is no transaction and the protocol
offers none, so a write that fails halfway leaves the earlier ones in place —
the response says which of them landed rather than implying all or nothing.
GET /plcs/{plc}/info reports what the CPU says it is, and
clock_drift_seconds — the controller's clock minus the gateway's. Drift is
the thing plants go hunting for after the fact, when timestamps in a PLC log
will not line up with anything else.
Status codes elsewhere: 404 unknown PLC or tag, 403 writing a read-only tag,
400 a value the tag cannot hold, 502 the controller could not be reached.
Every message names the PLC and the tag.
Prometheus
scrape_configs:
- job_name: plc
static_configs:
- targets: ["localhost:8080"]
Tag names travel in labels, not in metric names, because a tag may be called whatever the plant calls it and a Prometheus metric name may not.
s7_up{plc="line1"} 1
s7_tag_value{plc="line1",tag="motor_temp"} 72.5
s7_tag_read_ok{plc="line1",tag="motor_temp"} 1
s7_tag_value carries numbers only; a char tag has no numeric form and is
reported through s7_tag_read_ok alone. Scraping reads every tag on every
controller, so keep the scrape interval away from the controller's cycle time.
Types
bool byte char sint usint int uint word dint udint dword
real lreal lint ulint lword
Two values snap7 accepts and quietly converts are refused instead, because a wrong number in a controller is worse than a rejected request:
1.5into anintwould be written as1"off"into aboolwould be written as true, sincebool("off")isTrue
A whole number arriving as 1500.0 is accepted — JSON from a browser has no
integers.
Before you point it at a real line
- Write access is opt-in per tag. Nothing is writable unless the config says
writable: true. Declare the tags you monitor and leave it at that until you have a reason not to. - There is no authentication. Put it behind a reverse proxy or keep it on a host only your monitoring system can reach. Do not expose it to the plant network with writable tags declared.
- Writing a
boolwrites the byte it lives in. The gateway reads that byte first and puts the other seven bits back, but that is a read-modify-write: if the PLC program changes a neighbouring bit in between, that change is lost. Do not use it for a bit the program also writes. - One connection per controller, reused and reopened after a drop. An S7 CPU allows only a handful of connections, so the gateway does not open more.
Development
pip install -e ".[test]"
pytest
The tests run against a real S7 server on localhost, over the actual protocol — no mocking of snap7. Exactly one test injects a failure, because the test server answers an unregistered data block with fabricated bytes rather than an error and so cannot be made to fail on demand.
That last point is worth knowing if you write your own tests against
snap7.server: a read from a DB that does not exist returns 42 FF 12 34
instead of raising, so a test can pass while reading an address that is not
there. Verified against python-snap7 3.1.2.
Licence
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file s7_gateway-0.1.0.tar.gz.
File metadata
- Download URL: s7_gateway-0.1.0.tar.gz
- Upload date:
- Size: 23.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
050c279c92c9f00b8a22df72720437bc0b3947351b6a8554ccc978a6c69a5b3d
|
|
| MD5 |
f4df814ad55b2a8d56b4878fa9e48cb3
|
|
| BLAKE2b-256 |
97520a934e9b20b359eb3f1f75b9dc923091d7bd7706825663a9423157e026c0
|
File details
Details for the file s7_gateway-0.1.0-py3-none-any.whl.
File metadata
- Download URL: s7_gateway-0.1.0-py3-none-any.whl
- Upload date:
- Size: 17.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a8abf3fe20b500b37320ef3e1c0d177af1bf01c9df37f4a711f2d92fd31f0b60
|
|
| MD5 |
48cd5c0be560dae3c261b9e5e8c7a285
|
|
| BLAKE2b-256 |
9ef1add607a9f131dccb7d97f8ce7e7c9d95ac869ba13797179bdff6bc1eafdb
|