Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 10 additions & 7 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: fix-byte-order-marker
- id: check-ast
Expand All @@ -10,36 +10,39 @@ repos:
- id: debug-statements
- id: end-of-file-fixer
- id: trailing-whitespace
- id: fix-encoding-pragma
- id: requirements-txt-fixer
- id: mixed-line-ending
args: ['--fix=lf']
description: Forces to replace line ending by the UNIX 'lf' character
- id: detect-aws-credentials
args: ['--allow-missing-credentials']
- id: detect-private-key
- repo: https://github.com/asottile/pyupgrade
rev: v3.21.2
hooks:
- id: pyupgrade
- repo: https://github.com/myint/autoflake
rev: v2.3.1
rev: v2.3.3
hooks:
- id: autoflake
args:
- --in-place
- --remove-unused-variables
- --remove-all-unused-imports
- repo: https://github.com/hadolint/hadolint
rev: v2.12.0
rev: v2.14.0
hooks:
- id: hadolint-docker
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.7.0
rev: v0.15.2
hooks:
- id: ruff
args:
- '--line-length=120'
- '--fix'
- '--exit-non-zero-on-fix'
- repo: https://github.com/pycqa/isort
rev: 5.13.2
rev: 8.0.0
hooks:
- id: isort
name: isort (python)
Expand All @@ -48,7 +51,7 @@ repos:
- black
- '--filter-files'
- repo: https://github.com/psf/black
rev: 24.10.0
rev: 26.1.0
hooks:
- id: black
args:
Expand Down
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FROM alpine:3.23.3

LABEL maintainer="Michael Oberdorf IT-Consulting <info@oberdorf-itc.de>"
LABEL site.local.program.version="2.1.0"
LABEL site.local.program.version="2.2.0"

RUN apk upgrade --available --no-cache --update \
&& apk add --no-cache --update \
Expand All @@ -19,6 +19,7 @@ RUN pip3 install --no-cache-dir -r /requirements.txt --break-system-packages

EXPOSE 5020/tcp
EXPOSE 5020/udp
EXPOSE 9090/tcp

USER 1434:1434

Expand Down
3 changes: 2 additions & 1 deletion Dockerfile.test
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FROM alpine:3.23.2

LABEL maintainer="Michael Oberdorf IT-Consulting <info@oberdorf-itc.de>"
LABEL site.local.program.version="2.1.0"
LABEL site.local.program.version="2.2.0"

RUN apk upgrade --available --no-cache --update \
&& apk add --no-cache --update \
Expand All @@ -20,6 +20,7 @@ RUN pip3 install --no-cache-dir -r /requirements.txt --break-system-packages

EXPOSE 5020/tcp
EXPOSE 5020/udp
EXPOSE 9090/tcp

USER 1434:1434

Expand Down
207 changes: 138 additions & 69 deletions README.md

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions examples/abb_coretec_example.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@
"logLevel": "DEBUG"
}
},
"persistence": {
"enabled": false,
"file": "/data/modbus_registers.json",
"saveInterval": 30
},
"metrics": {
"enabled": false,
"address": "0.0.0.0",
"port": 9090,
"path": "/metrics"
},
"registers": {
"description": "initial values for the register types",
"initializeUndefinedRegisters": true,
Expand Down
103 changes: 103 additions & 0 deletions examples/metrics_example.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# HELP memory_total_bytes Total available memory in bytes
# TYPE memory_total_bytes gauge
memory_total_bytes 4093587456

# HELP memory_available_bytes Current available memory in bytes
# TYPE memory_available_bytes gauge
memory_available_bytes 325832704

# HELP memory_consumption_bytes Current memory consumption in bytes
# TYPE memory_consumption_bytes gauge
memory_consumption_bytes 3524468736

# HELP memory_consumption_percentage Current memory consumption percentage
# TYPE memory_consumption_percentage gauge
memory_consumption_percentage 92.0

# HELP cpu_usage_percentage Current CPU usage percentage
# TYPE cpu_usage_percentage gauge
cpu_usage_percentage 0.0

# HELP cpu_count Number of CPU cores
# TYPE cpu_count gauge
cpu_count 2

# HELP cpu_load1 Load average over 1 minute
# TYPE cpu_load1 gauge
cpu_load1 0.085

# HELP cpu_load5 Load average over 5 minutes
# TYPE cpu_load5 gauge
cpu_load5 0.215

# HELP cpu_load15 Load average over 15 minutes
# TYPE cpu_load15 gauge
cpu_load15 0.22

# HELP cpu_load1_percentage Load average percentage over 1 minute
# TYPE cpu_load1_percentage gauge
cpu_load1_percentage 8.5

# HELP cpu_load5_percentage Load average percentage over 5 minutes
# TYPE cpu_load5_percentage gauge
cpu_load5_percentage 21.5

# HELP cpu_load15_percentage Load average percentage over 15 minutes
# TYPE cpu_load15_percentage gauge
cpu_load15_percentage 22.0

# HELP modbus_requests_total Total number of Modbus requests received, by function code
# TYPE modbus_requests_total counter
modbus_requests_total{function_code="01",function_name="read_coils"} 43
modbus_requests_total{function_code="02",function_name="read_discrete_inputs"} 1
modbus_requests_total{function_code="03",function_name="read_holding_registers"} 34
modbus_requests_total{function_code="05",function_name="write_single_coil"} 3
modbus_requests_total{function_code="06",function_name="write_single_register"} 2

# HELP modbus_register_reads_total Total number of read operations per register
# TYPE modbus_register_reads_total counter
modbus_register_reads_total{address="2",type="coil"} 37
modbus_register_reads_total{address="3",type="coil"} 34
modbus_register_reads_total{address="4",type="coil"} 34
modbus_register_reads_total{address="5",type="coil"} 34
modbus_register_reads_total{address="6",type="coil"} 37
modbus_register_reads_total{address="7",type="coil"} 34
modbus_register_reads_total{address="8",type="coil"} 34
modbus_register_reads_total{address="9",type="coil"} 34
modbus_register_reads_total{address="10",type="coil"} 37
modbus_register_reads_total{address="11",type="coil"} 34
modbus_register_reads_total{address="2",type="discrete_input"} 1
modbus_register_reads_total{address="3",type="discrete_input"} 1
modbus_register_reads_total{address="4",type="discrete_input"} 1
modbus_register_reads_total{address="5",type="discrete_input"} 1
modbus_register_reads_total{address="6",type="discrete_input"} 1
modbus_register_reads_total{address="7",type="discrete_input"} 1
modbus_register_reads_total{address="8",type="discrete_input"} 1
modbus_register_reads_total{address="9",type="discrete_input"} 1
modbus_register_reads_total{address="10",type="discrete_input"} 1
modbus_register_reads_total{address="11",type="discrete_input"} 1
modbus_register_reads_total{address="2",type="holding"} 29
modbus_register_reads_total{address="3",type="holding"} 29
modbus_register_reads_total{address="4",type="holding"} 29
modbus_register_reads_total{address="5",type="holding"} 29
modbus_register_reads_total{address="6",type="holding"} 29
modbus_register_reads_total{address="7",type="holding"} 31
modbus_register_reads_total{address="8",type="holding"} 29
modbus_register_reads_total{address="9",type="holding"} 29
modbus_register_reads_total{address="10",type="holding"} 29
modbus_register_reads_total{address="11",type="holding"} 32

# HELP modbus_register_writes_total Total number of write operations per register
# TYPE modbus_register_writes_total counter
modbus_register_writes_total{address="2",type="coil"} 1
modbus_register_writes_total{address="6",type="coil"} 1
modbus_register_writes_total{address="10",type="coil"} 1
modbus_register_writes_total{address="7",type="holding"} 1
modbus_register_writes_total{address="11",type="holding"} 1

# HELP modbus_errors_total Total number of Modbus errors returned, by exception code
# TYPE modbus_errors_total counter

# HELP modbus_server_uptime_seconds Total uptime of the mock server in seconds
# TYPE modbus_server_uptime_seconds counter
modbus_server_uptime_seconds 90.30
16 changes: 11 additions & 5 deletions examples/test.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@
"logLevel": "DEBUG"
}
},
"persistence": {
"enabled": true,
"file": "/data/modbus_registers.json",
"saveInterval": 30
},
"persistence": {
"enabled": true,
"file": "/data/modbus_registers.json",
"saveInterval": 30
},
"metrics": {
"enabled": true,
"address": "0.0.0.0",
"port": 9090,
"path": "/metrics"
},
"registers": {
"description": "initial values for the register types",
"initializeUndefinedRegisters": true,
Expand Down
11 changes: 11 additions & 0 deletions examples/udp.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@
"logLevel": "DEBUG"
}
},
"persistence": {
"enabled": false,
"file": "/data/modbus_registers.json",
"saveInterval": 30
},
"metrics": {
"enabled": false,
"address": "0.0.0.0",
"port": 9090,
"path": "/metrics"
},
"registers": {
"description": "initial values for the register types",
"initializeUndefinedRegisters": true,
Expand Down
18 changes: 13 additions & 5 deletions src/app/lib/register_persistence/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
###############################################################################
# Library to make register writes persistent across restarts of the modbus server.
Expand All @@ -7,14 +6,14 @@
# Author: Michael Oberdorf
# Date: 2026-02-07
# Last modified by: Michael Oberdorf
# Last modified at: 2026-02-08
# Last modified at: 2026-02-21
###############################################################################\n
"""

__author__ = "Michael Oberdorf <info@oberdorf-itc.de>"
__status__ = "production"
__date__ = "2026-02-08"
__version_info__ = ("1", "0", "2")
__date__ = "2026-02-21"
__version_info__ = ("1", "1", "0")
__version__ = ".".join(__version_info__)

__all__ = ["RegisterPersistence"]
Expand Down Expand Up @@ -73,7 +72,7 @@ def load_registers(self) -> Optional[dict]:
return None

try:
with open(self.persistence_file, "r", encoding="utf-8") as f:
with open(self.persistence_file, encoding="utf-8") as f:
data = json.load(f)
self.logger.info(f"Successfully loaded register data from {self.persistence_file}")
return data
Expand Down Expand Up @@ -147,6 +146,15 @@ def _extract_register_values(self, slave_context: ModbusServerContext, register_
else:
return result

# Unwrap any wrapper blocks (e.g. metrics wrappers) to access the underlying store
try:
# unwrap multiple layers if necessary
while hasattr(store, "wrapped_block"):
store = getattr(store, "wrapped_block")
except Exception:
# if unwrapping fails, log and continue with original store
self.logger.debug("Failed to unwrap store wrapper, continuing with original store")

# Check if it's a sparse or sequential block
if isinstance(store, ModbusSparseDataBlock):
# Sparse blocks have a values dict
Expand Down
Empty file.
Loading
Loading