Skip to main content

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.5 into an int would be written as 1
  • "off" into a bool would be written as true, since bool("off") is True

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 bool writes 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

Metadata

Release files for s7-gateway 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for s7-gateway 0.1.0
File Size Uploaded
s7_gateway-0.1.0.tar.gz 23.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for s7-gateway 0.1.0
File Interpreter ABI Platform
s7_gateway-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 40.2 kB

Release files / s7_gateway-0.1.0.tar.gz

Download URL s7_gateway-0.1.0.tar.gz
Size 23.2 kB
Tags Source
SHA-256 checksum
How to use checksums
050c279c92c9f00b8a22df72720437bc0b3947351b6a8554ccc978a6c69a5b3d
BLAKE2b-256 checksum
How to use checksums
97520a934e9b20b359eb3f1f75b9dc923091d7bd7706825663a9423157e026c0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.4

Release files / s7_gateway-0.1.0-py3-none-any.whl

Download URL s7_gateway-0.1.0-py3-none-any.whl
Size 17.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a8abf3fe20b500b37320ef3e1c0d177af1bf01c9df37f4a711f2d92fd31f0b60
BLAKE2b-256 checksum
How to use checksums
9ef1add607a9f131dccb7d97f8ce7e7c9d95ac869ba13797179bdff6bc1eafdb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.4

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page