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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

s7_gateway-0.1.0.tar.gz (23.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

s7_gateway-0.1.0-py3-none-any.whl (17.0 kB view details)

Uploaded Python 3

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

Hashes for s7_gateway-0.1.0.tar.gz
Algorithm Hash digest
SHA256 050c279c92c9f00b8a22df72720437bc0b3947351b6a8554ccc978a6c69a5b3d
MD5 f4df814ad55b2a8d56b4878fa9e48cb3
BLAKE2b-256 97520a934e9b20b359eb3f1f75b9dc923091d7bd7706825663a9423157e026c0

See more details on using hashes here.

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

Hashes for s7_gateway-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a8abf3fe20b500b37320ef3e1c0d177af1bf01c9df37f4a711f2d92fd31f0b60
MD5 48cd5c0be560dae3c261b9e5e8c7a285
BLAKE2b-256 9ef1add607a9f131dccb7d97f8ce7e7c9d95ac869ba13797179bdff6bc1eafdb

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page