Compare commits

...
38 Commits
Author SHA1 Message Date
eneuber 5b0f9013aa modbus polling Intervall vergrößert
Build Docker Image (Podman) / build (push) Has been cancelled
2026-05-26 22:46:35 +02:00
eneuber 9d33984c97 CONFIG_PATH für yaml Datei 2026-03-23 22:01:44 +01:00
eneuber 89f02422e7 VERISON in Dockerfile abhandeln 2026-03-23 22:01:10 +01:00
eneuber 58f5522442 Speichern von Holding und Coils ging nicht mehr 2026-03-23 21:13:04 +01:00
eneuber e6c60d0707 Ausführen /Debuggen in vscode nun mit Version 2026-03-23 20:48:56 +01:00
eneuber e96af3ac7e Pipeline triggern
Build Docker Image (Podman) / build (push) Failing after 33m16s
2026-03-20 10:55:02 +01:00
eneuber 87343e354b Pipeline triggern
Build Docker Image (Podman) / build (push) Failing after 32m55s
2026-03-19 22:01:25 +01:00
eneuber ca43d6b9a5 neueres rust verwenden
Build Docker Image (Podman) / build (push) Failing after 14m29s
2026-03-18 17:57:26 +01:00
eneuber 68140a0c45 Daten für InfluxDB ohne type 2026-03-18 17:28:21 +01:00
eneuber 3ed80f4b68 Schreibe MQTT.Set Werte, wenn das Leitsystem aktiv ist
Build Docker Image (Podman) / build (push) Failing after 15m52s
2026-03-13 22:56:06 +01:00
eneuber 497a0e48e3 write-Eigenschaft nun auch an holding-Register 2026-03-11 21:26:41 +01:00
eneuber 825547889c Anpassungen an der paramod.yaml 2026-03-11 17:59:05 +01:00
eneuber 923a35cad1 Lokal debuggen ermöglichen 2026-03-11 17:40:13 +01:00
eneuber 0c10ff4051 Seitenbezeichnung geändert 2026-03-07 21:24:12 +01:00
eneuber 451008b895 Pipeline triggern
Build Docker Image (Podman) / build (push) Failing after 20m44s
2026-02-22 20:42:31 +01:00
eneuber 2ca79a7b9f Pipeline triggern
Build Docker Image (Podman) / build (push) Failing after 5m52s
2026-02-22 20:29:27 +01:00
eneuber a78d49ef7e Pipeline triggern
Build Docker Image (Podman) / build (push) Failing after 1m8s
2026-02-22 20:21:32 +01:00
eneuber 56982cc26a Pipeline triggern
Build Docker Image (Podman) / build (push) Failing after 38s
2026-02-22 19:44:29 +01:00
eneuber 3168d21c5f Pipeline triggern
Build Docker Image (Podman) / build (push) Failing after 5m1s
2026-02-22 19:26:57 +01:00
eneuber 5b7b4fa57e Pipeline triggern
Build Docker Image (Podman) / build (push) Failing after 13m7s
2026-02-22 19:03:06 +01:00
eneuber ee2c69b4c2 Pipeline triggern
Build Docker Image (Podman) / build (push) Failing after 3m31s
2026-02-22 18:48:41 +01:00
eneuber cb7e8d19e4 Pipeline triggern
Build Docker Image (Podman) / build (push) Failing after 55s
2026-02-22 18:43:20 +01:00
eneuber 59de54e80a neue Checkout-Version beim Job
Build Docker Image (Podman) / build (push) Failing after 56s
2026-02-22 18:08:32 +01:00
eneuber 10bdc8fb5a Pipeline triggern
Build Docker Image (Podman) / build (push) Failing after 48s
2026-02-22 17:47:48 +01:00
eneuber c134a20da8 doch nicht privileged
Build Docker Image (Podman) / build (push) Failing after 49s
2026-02-21 22:34:19 +01:00
eneuber 0d696a44e4 Pipeline triggern
Build Docker Image (Podman) / build (push) Failing after 1m14s
2026-02-21 22:19:32 +01:00
eneuber 839fd24b4e Build-Job ist privileged
Build Docker Image (Podman) / build (push) Failing after 47s
2026-02-21 16:58:32 +01:00
eneuber 1c8dd77946 Pipeline tringgern
Build Docker Image (Podman) / build (push) Failing after 58s
2026-02-21 16:54:23 +01:00
eneuber 60823714a6 Pipeline triggern
Build Docker Image (Podman) / build (push) Failing after 1m18s
2026-02-20 00:13:12 +01:00
eneuber 4dd58ecaaa Pipeline triggern
Build Docker Image (Podman) / build (push) Failing after 26s
2026-02-20 00:06:05 +01:00
eneuber e15484c460 Benutze Podman im runner
Build Docker Image (Podman) / build (push) Failing after 55s
2026-02-19 23:57:23 +01:00
eneuber 4673ff3ff1 Pipeline triggern
Build Docker Image (Podman) / build (push) Failing after 2m0s
2026-02-19 23:21:18 +01:00
eneuber e15b1566f0 Pipeline triggern
Build Docker Image (Podman) / build (push) Failing after 47s
2026-02-18 23:20:05 +01:00
eneuber e75764da43 Pipeline triggern
Build Docker Image (Podman) / build (push) Failing after 48s
2026-02-18 22:18:20 +01:00
eneuber 41b88d9e3e Pipeline triggern
Build Docker Image (Podman) / build (push) Failing after 49s
2026-02-18 20:34:54 +01:00
eneuber 241a9ec240 Pipeline Anpassung
Build Docker Image (Podman) / build (push) Failing after 45s
2026-02-18 18:59:12 +01:00
eneuber 55e7260ee5 Änderungen am Trigger der Pipeline
Build Docker Image (Podman) / build (push) Failing after 2m19s
2026-02-18 18:46:07 +01:00
eneuber f47cd3a97b Pipeline umbenannt 2026-02-18 18:23:22 +01:00
22 changed files with 537 additions and 435 deletions

No files matched your search

@@ -2,25 +2,22 @@ name: Build Docker Image (Podman)
on:
push:
branches: [ main ]
branches: [ master ]
pull_request:
# workflow_call:
jobs:
build:
runs-on: ubuntu-latest
runs-on: podman-latest
steps:
- name: Install Podman
run: sudo apt-get update && sudo apt-get install -y podman
- name: Checkout code
uses: actions/checkout@v3
- name: Set version in Cargo.toml
run: |
VERSION=$(cat VERSION)
sed -i "s/^version = \"__VERSION__\"/version = \"$VERSION\"/" Cargo.toml
uses: actions/checkout@v4
- name: Build image with Podman
env:
BUILDAH_ISOLATION: chroot
run: |
VERSION=$(cat VERSION)
podman build -t paramod:latest -t git.skaville.rocks/eneuber/paramod:$VERSION .
podman build --network=host -t paramod:latest -t git.skaville.rocks/eneuber/paramod:$VERSION .
- name: Login to Gitea Registry
run: echo ${{ secrets.REGISTRY_TOKEN }} | podman login -u ${{ secrets.USER }} --password-stdin git.skaville.rocks
- name: Push image with version
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
set -e
# Patch Cargo.toml version from VERSION file before build
VERSION=$(cat VERSION)
sed -i 's/^version = ".*"/version = "'"$VERSION"'"/' Cargo.toml
exec cargo build
+3
View File
@@ -0,0 +1,3 @@
#!/bin/bash
set -e
sed -i 's/^version = ".*"/version = "__VERSION__"/' Cargo.toml
+12
View File
@@ -0,0 +1,12 @@
#!/bin/bash
set -e
reset_version() {
sed -i 's/^version = ".*"/version = "__VERSION__"/' Cargo.toml
}
trap reset_version EXIT
VERSION=$(cat VERSION)
sed -i 's/^version = ".*"/version = "'"$VERSION"'"/' Cargo.toml
cargo run
+2 -1
View File
@@ -9,7 +9,8 @@
"args": [],
"cwd": "${workspaceFolder}",
"sourceLanguages": ["rust"],
"preLaunchTask": "cargo build"
"preLaunchTask": "cargo build",
"postDebugTask": "cargo reset version placeholder"
}
]
}
+14 -2
View File
@@ -4,12 +4,24 @@
{
"label": "cargo build",
"type": "shell",
"command": "cargo build",
"command": ".vscode/cargo-build-version.sh",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": ["$rustc"]
"problemMatcher": ["$codelldb-rustc"]
},
{
"label": "cargo reset version placeholder",
"type": "shell",
"command": "bash .vscode/cargo-reset-version.sh",
"problemMatcher": []
},
{
"label": "cargo run",
"type": "shell",
"command": "bash .vscode/cargo-run-version.sh",
"problemMatcher": ["$codelldb-rustc"]
}
]
}
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "paramod"
version = "__VERSION__"
edition = "2021"
edition = "2024"
[dependencies]
serde_yaml = "0.9"
+5 -2
View File
@@ -1,16 +1,19 @@
# Build Stage
FROM rust:1.81 as builder
FROM rust:1.85 as builder
WORKDIR /usr/src/app
COPY Cargo.toml ./
COPY Cargo.lock ./
COPY VERSION ./
COPY src ./src
COPY templates ./templates
COPY static ./static
RUN cargo build --release
RUN VERSION=$(cat VERSION) \
&& sed -i 's/^version = "__VERSION__"/version = "'"$VERSION"'"/' Cargo.toml \
&& cargo build --release
# Runtime Stage
FROM debian:bookworm-slim
+51 -138
View File
@@ -1,190 +1,103 @@
# Tabellen Webserver
# Paramod
Ein einfacher Rust-Webserver, der eine editierbare 3x3-Tabelle bereitstellt und in einer JSON-Konfigurationsdatei persistiert.
## Projektstruktur
```
paramod/
├── src/
│ └── main.rs
├── templates/
│ ├── index.html
│ └── settings.html
├── static/
│ ├── style.css
│ ├── script.js
│ └── settings.js
├── Cargo.toml
├── Dockerfile
├── .gitignore
└── README.md
```
Rust-Webserver zur Anzeige und Bearbeitung von Modbus-Tabellen mit Persistierung in `paramod.yaml`.
## Funktionen
- **3 separate Tabellen** für verschiedene Sensor-Gruppen
- **Navigation** mit aktivem Status-Indikator
- **Header mit Logo** für professionelles Erscheinungsbild
- **Zeilen hinzufügen/löschen** dynamisch zur Laufzeit
- **Einstellungsseite** für MQTT und InfluxDB Konfiguration
- Editierbare Textfelder (Bezeichnung, Adresse, Type, Faktor)
- Toggle-Schalter für Boolean-Werte (MQTT, InfluxDB)
- **Zentrale JSON-Persistierung** für alle Tabellen und Einstellungen
- REST-API für Daten-Management
- Docker-Unterstützung
- Responsive Design
- Tabellen für `modbus_input_register`, `modbus_holding_register` und `modbus_coils`
- Web-UI zum Bearbeiten und Speichern der Tabellen
- Einstellungsseite für `default`, `modbus`, `mqtt` und `influxdb`
- MQTT/Influx-Integration gemäß Konfiguration
- Speicherung aller Änderungen in `paramod.yaml`
## Lokale Entwicklung
### Voraussetzungen
- Rust (Version 1.75 oder höher)
- Cargo
- Rust + Cargo
### Installation und Start
### Start
```bash
# Projekt erstellen
cargo new paramod
cd paramod
# Dependencies installieren und starten
cargo run
```
Der Server läuft dann auf `http://localhost:8080`
Server: `http://localhost:8080`
## Docker
## Docker (Podman)
### Container bauen
### Image bauen
```bash
podman build -t paramod .
podman build -t paramod:latest .
```
### Container starten
```bash
poddman run -p 8080:8080 -v $(pwd)/data:/app/data paramod
podman run --rm -p 8080:8080 -v "$(pwd)/paramod.yaml:/app/paramod.yaml" paramod:latest
```
Mit Volume-Mount bleibt die Konfigurationsdatei auch nach Container-Neustarts erhalten.
## Verwendung
1. Öffne `http://localhost:8080` im Browser
2. Navigiere zwischen den Tabellen über das Menü:
- **Tabelle 1, 2, 3**: Verschiedene Sensor-Gruppen
- **⚙️ Einstellungen**: MQTT und InfluxDB Konfiguration
3. In den Tabellen:
- **➕ Zeile hinzufügen**: Neue Sensor-Einträge erstellen
- **🗑️ Löschen**: Einzelne Zeilen entfernen
- **Felder bearbeiten**:
- Bezeichnung: Name des Sensors
- Adresse: IP-Adresse oder Identifier
- Type: Sensor-Typ (z.B. Temperatur, Luftfeuchtigkeit)
- Faktor: Numerischer Korrekturfaktor
- MQTT: Toggle-Schalter für MQTT-Aktivierung
- InfluxDB: Toggle-Schalter für InfluxDB-Aktivierung
4. **💾 Speichern**: Änderungen persistieren
5. Alle Daten werden zentral in `paramod.yaml` gespeichert
## API Endpoints
- `GET /` - Zeigt Tabelle 1
- `GET /table/table2` - Zeigt Tabelle 2
- `GET /table/table3` - Zeigt Tabelle 3
- `GET /settings` - Zeigt Einstellungsseite
- `POST /api/save` - Speichert eine Tabelle
- `POST /api/save-settings` - Speichert die Einstellungen
- `GET /static/*` - Statische Dateien (CSS, JS)
- `GET /`
- `GET /table/modbus_input_register`
- `GET /table/modbus_holding_register`
- `GET /table/modbus_coils`
- `GET /settings`
- `GET /api/config`
- `POST /api/save`
- `POST /api/save-settings`
### Beispiel API-Request (Tabelle speichern)
## API-Beispiel: Tabelle speichern
Beispiel für Holding-Register:
```bash
curl -X POST http://localhost:8080/api/save \
-H "Content-Type: application/json" \
-d '{
"table_id": "table1",
"table_id": "modbus_holding_register",
"rows": [
{
"bezeichnung": "Sensor 1",
"adresse": "192.168.1.100",
"type": "Temperatur",
"faktor": "1.0",
"mqtt": true,
"influxdb": false
"TVsoll": {
"addr": 2,
"type": "INT16",
"factor": 0.1,
"write": true,
"mqtt": true,
"influxdb": true,
"comment": "Vorlauf Soll"
}
}
]
}'
```
### Beispiel API-Request (Einstellungen speichern)
Beispiel für Coils:
```bash
curl -X POST http://localhost:8080/api/save-settings \
curl -X POST http://localhost:8080/api/save \
-H "Content-Type: application/json" \
-d '{
"mqtt_broker": "localhost",
"mqtt_port": "1883",
"influxdb_url": "http://localhost:8086",
"influxdb_token": "your-token-here"
"table_id": "modbus_coils",
"rows": [
{
"MgtSystem": {
"addr": 0,
"write": true,
"mqtt": true,
"influxdb": false,
"comment": "Leitsystem aktiv"
}
}
]
}'
```
## Konfigurationsdatei
## Konfiguration
Die komplette Anwendungskonfiguration wird in `table_config.json` gespeichert:
```json
{
"table1": [
{
"bezeichnung": "Temp Sensor 1",
"adresse": "192.168.1.100",
"type": "Temperatur",
"faktor": "1.0",
"mqtt": true,
"influxdb": false
},
{
"bezeichnung": "Temp Sensor 2",
"adresse": "192.168.1.101",
"type": "Temperatur",
"faktor": "1.0",
"mqtt": false,
"influxdb": true
}
],
"table2": [
{
"bezeichnung": "Humidity Sensor 1",
"adresse": "192.168.1.200",
"type": "Luftfeuchtigkeit",
"faktor": "0.5",
"mqtt": true,
"influxdb": true
}
],
"table3": [
{
"bezeichnung": "Pressure Sensor 1",
"adresse": "192.168.1.300",
"type": "Druck",
"faktor": "2.0",
"mqtt": true,
"influxdb": false
}
],
"settings": {
"mqtt_broker": "localhost",
"mqtt_port": "1883",
"influxdb_url": "http://localhost:8086",
"influxdb_token": ""
}
}
```
Die komplette Konfiguration liegt in `paramod.yaml`.
## Lizenz
-140
View File
@@ -1,140 +0,0 @@
[default]
loglevel = "DEBUG"
[mqtt]
broker = "192.168.178.2"
button_circulation = "zigbee2mqtt/WirelessButton"
password = "97sm3pHNSMZ4M5qUj0x8"
port = 1883
user = "admin"
[influxdb]
bucket = "Paradigma"
location = "Radebeul"
measurement = "ParadigmaModbus"
org = "skaville"
token = "i-sXFQbEkSC1XVzqFEaFwXwzasbsEIciVlK4SaAUOEvk0VjQPkD3fr8d7_3SPeyseTZkqj7ZMZU78b3n2F6_SQ=="
url = "192.168.178.2:8086"
[modbus]
host = "192.168.178.10"
max_coils_addr = 8
max_holding_addr = 61
max_input_addr = 45
port = 502
[[modbus_coils]]
MgtSystem = {addr = 0, write = false, mqtt = false, influxdb = false, comment = "Leitsystem aktiv"}
HK1pres = {addr = 1, write = false, mqtt = false, influxdb = false, comment = "HK1 vorhanden"}
HK2pres = {addr = 2, write = false, mqtt = false, influxdb = false, comment = "HK2 vorhanden"}
HK3pres = {addr = 3, write = false, mqtt = false, influxdb = false, comment = "HK3 vorhanden"}
TWrelease = {addr = 4, write = true, mqtt = false, influxdb = false, comment = "Trinkwassererwärmung freigegeben"}
TWlock = {addr = 5, write = true, mqtt = false, influxdb = false, comment = "Trinkwassererwärmung gesprerrt"}
Zrelease = {addr = 6, write = true, mqtt = false, influxdb = false, comment = "Zirkulation freigegeben"}
Zlock = {addr = 7, write = true, mqtt = false, influxdb = false, comment = "Zirkulation gesperrt"}
SHKpres = {addr = 8, write = false, mqtt = false, influxdb = false, comment = "Schwimmbadheizkrei vorhanden"}
[[modbus_input_register]]
TA = {addr = 0, type = "INT16", factor = 0.1, mqtt = true, influxdb = true}
TV = {addr = 1, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TR = {addr = 2, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TWO = {addr = 3, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TPO = {addr = 4, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TPU = {addr = 5, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TZR = {addr = 6, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TV2 = {addr = 7, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TR2 = {addr = 8, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
RI1 = {addr = 9, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
RI2 = {addr = 10, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TSA = {addr = 11, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
FATV = {addr = 12, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
FATR = {addr = 13, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TVKH = {addr = 14, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TRKH = {addr = 15, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TPOKH = {addr = 16, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TV3 = {addr = 17, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TR3 = {addr = 18, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TSB = {addr = 19, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TVSB = {addr = 20, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TRSB = {addr = 21, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TWE = {addr = 22, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TWA = {addr = 23, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TWS = {addr = 24, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TVSI = {addr = 25, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TK = {addr = 26, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
FATV1 = {addr = 27, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
FATV2 = {addr = 28, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
FATV3 = {addr = 29, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
FATV4 = {addr = 30, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TSE = {addr = 31, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TW = {addr = 32, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TSV = {addr = 33, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TW2 = {addr = 34, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
S = {addr = 35, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TAM = {addr = 36, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TAM2 = {addr = 37, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TSA1 = {addr = 38, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TSA2 = {addr = 39, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TSP = {addr = 40, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TWW = {addr = 41, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TKW = {addr = 42, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
VKW = {addr = 43, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
VSPm = {addr = 44, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
[[modbus_holding_register]]
nothing = {addr = 0, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
ErrLS = {addr = 1, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
TVsoll = {addr = 2, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TV2soll = {addr = 3, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TV3soll = {addr = 4, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
HK1soll = {addr = 5, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
HK2soll = {addr = 6, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
HK3soll = {addr = 7, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
TWWsoll = {addr = 8, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TV1max = {addr = 9, type = "INT16", factor = 0.1, mqtt = false, influxdb = true}
TV2max = {addr = 10, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TV3max = {addr = 11, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
ErrHR = {addr = 12, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
ErrSR = {addr = 13, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
ErrWE1_1 = {addr = 14, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
ErrWE1_2 = {addr = 15, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
ErrWE1_3 = {addr = 16, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
ErrWE1_4 = {addr = 17, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
ErrWE1_5 = {addr = 18, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
KollLei = {addr = 19, type = "UINT16", factor = 0.1, mqtt = false, influxdb = true}
TagesS = {addr = 20, type = "UINT16", factor = 0.1, mqtt = false, influxdb = true}
GesS = {addr = 21, type = "UINT32", factor = 0.1, mqtt = false, influxdb = true}
GesWW = {addr = 23, type = "UINT32", factor = 0.1, mqtt = false, influxdb = true}
GesZ = {addr = 25, type = "UINT32", factor = 0.1, mqtt = false, influxdb = true}
HGesK1 = {addr = 27, type = "UINT32", factor = 1, mqtt = false, influxdb = true}
StartK1 = {addr = 29, type = "UINT32", factor = 1, mqtt = false, influxdb = true}
HGesPel = {addr = 31, type = "UINT32", factor = 1, mqtt = false, influxdb = true}
VGesPel = {addr = 33, type = "UINT16", factor = 0.1, mqtt = false, influxdb = true}
StatWW = {addr = 34, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
StatZ = {addr = 35, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
StatHK1 = {addr = 36, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
StatHK2 = {addr = 37, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
StatHK3 = {addr = 38, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
StatS = {addr = 39, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
StatSB = {addr = 40, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
StatK1 = {addr = 41, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
StatPel = {addr = 42, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
StatKH = {addr = 43, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
TPOsoll = {addr = 44, type = "UINT16", factor = 0.1, mqtt = false, influxdb = true}
FATVsoll = {addr = 45, type = "UINT16", factor = 0.1, mqtt = false, influxdb = true}
TSBsollHK = {addr = 46, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
TSBsollS = {addr = 47, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
BetrHK1 = {addr = 48, type = "UINT16", factor = 1, mqtt = false, influxdb = true}
BetrHK2 = {addr = 49, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
BetrHK3 = {addr = 50, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
BetrSB = {addr = 51, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
GesKKsoll = {addr = 52, type = "INT16", factor = 0.1, mqtt = false, influxdb = false}
KKsollWE1 = {addr = 53, type = "UINT16", factor = 0.1, mqtt = false, influxdb = false}
KKsollWE2 = {addr = 54, type = "UINT16", factor = 0.1, mqtt = false, influxdb = false}
KKsollWE3 = {addr = 55, type = "UINT16", factor = 0.1, mqtt = false, influxdb = false}
KKsollWE4 = {addr = 56, type = "UINT16", factor = 0.1, mqtt = false, influxdb = false}
ErrWE1 = {addr = 57, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
ErrWE2 = {addr = 58, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
ErrWE3 = {addr = 59, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
ErrWE4 = {addr = 60, type = "UINT16", factor = 1, mqtt = false, influxdb = false}
+61 -60
View File
@@ -7,22 +7,23 @@ mqtt:
user: admin
password: 97sm3pHNSMZ4M5qUj0x8
path: heizung/paradigma
button_circulation: zigbee2mqtt/WirelessButton
leitsystem_path: heizung/leitsystem2
set_write_interval_ms: 2000
influxdb:
bucket: Paradigma
location: Radebeul
measurement: ParadigmaModbus
org: skaville
token: i-sXFQbEkSC1XVzqFEaFwXwzasbsEIciVlK4SaAUOEvk0VjQPkD3fr8d7_3SPeyseTZkqj7ZMZU78b3n2F6_SQ==
url: http://192.168.178.2:8086
location: Radebeul
measurement: ParadigmaModbus
modbus:
host: 192.168.178.10
port: 502
max_coils_addr: 8
max_holding_addr: 61
max_input_addr: 45
max_holding_addr: 61
modbus_coils:
- MgtSystem: {addr: 0, write: false, mqtt: false, influxdb: false, comment: Leitsystem aktiv}
- MgtSystem: {addr: 0, write: false, mqtt: true, influxdb: false, comment: Leitsystem aktiv}
- HK1pres: {addr: 1, write: false, mqtt: false, influxdb: false, comment: HK1 vorhanden}
- HK2pres: {addr: 2, write: false, mqtt: false, influxdb: false, comment: HK2 vorhanden}
- HK3pres: {addr: 3, write: false, mqtt: false, influxdb: false, comment: HK3 vorhanden}
@@ -78,58 +79,58 @@ modbus_input_register:
- VKW: {addr: 43, type: INT16, factor: 0.1, mqtt: false, influxdb: true}
- VSPm: {addr: 44, type: INT16, factor: 0.1, mqtt: false, influxdb: true}
modbus_holding_register:
- nothing: {addr: 0, type: UINT16, factor: 1, mqtt: false, influxdb: false}
- ErrLS: {addr: 1, type: UINT16, factor: 1, mqtt: false, influxdb: true}
- TVsoll: {addr: 2, type: INT16, factor: 0.1, mqtt: false, influxdb: true}
- TV2soll: {addr: 3, type: INT16, factor: 0.1, mqtt: false, influxdb: false}
- TV3soll: {addr: 4, type: INT16, factor: 0.1, mqtt: false, influxdb: false}
- HK1soll: {addr: 5, type: UINT16, factor: 1, mqtt: false, influxdb: true}
- HK2soll: {addr: 6, type: UINT16, factor: 1, mqtt: false, influxdb: false}
- HK3soll: {addr: 7, type: UINT16, factor: 1, mqtt: false, influxdb: false}
- TWWsoll: {addr: 8, type: INT16, factor: 0.1, mqtt: false, influxdb: true}
- TV1max: {addr: 9, type: INT16, factor: 0.1, mqtt: false, influxdb: true}
- TV2max: {addr: 10, type: INT16, factor: 0.1, mqtt: false, influxdb: false}
- TV3max: {addr: 11, type: INT16, factor: 0.1, mqtt: false, influxdb: false}
- ErrHR: {addr: 12, type: UINT16, factor: 1, mqtt: false, influxdb: true}
- ErrSR: {addr: 13, type: UINT16, factor: 1, mqtt: false, influxdb: true}
- ErrWE1_1: {addr: 14, type: UINT16, factor: 1, mqtt: false, influxdb: true}
- ErrWE1_2: {addr: 15, type: UINT16, factor: 1, mqtt: false, influxdb: false}
- ErrWE1_3: {addr: 16, type: UINT16, factor: 1, mqtt: false, influxdb: false}
- ErrWE1_4: {addr: 17, type: UINT16, factor: 1, mqtt: false, influxdb: false}
- ErrWE1_5: {addr: 18, type: UINT16, factor: 1, mqtt: false, influxdb: false}
- KollLei: {addr: 19, type: UINT16, factor: 0.1, mqtt: false, influxdb: true}
- TagesS: {addr: 20, type: UINT16, factor: 0.1, mqtt: true, influxdb: true}
- GesS: {addr: 21, type: UINT32, factor: 0.1, mqtt: false, influxdb: true}
- GesWW: {addr: 23, type: UINT32, factor: 0.1, mqtt: false, influxdb: true}
- GesZ: {addr: 25, type: UINT32, factor: 0.1, mqtt: false, influxdb: true}
- HGesK1: {addr: 27, type: UINT32, factor: 1, mqtt: false, influxdb: true}
- StartK1: {addr: 29, type: UINT32, factor: 1, mqtt: false, influxdb: true}
- HGesPel: {addr: 31, type: UINT32, factor: 1, mqtt: false, influxdb: true}
- VGesPel: {addr: 33, type: UINT16, factor: 0.1, mqtt: false, influxdb: true}
- StatWW: {addr: 34, type: UINT16, factor: 1, mqtt: false, influxdb: true}
- StatZ: {addr: 35, type: UINT16, factor: 1, mqtt: false, influxdb: true}
- StatHK1: {addr: 36, type: UINT16, factor: 1, mqtt: false, influxdb: true}
- StatHK2: {addr: 37, type: UINT16, factor: 1, mqtt: false, influxdb: false}
- StatHK3: {addr: 38, type: UINT16, factor: 1, mqtt: false, influxdb: false}
- StatS: {addr: 39, type: UINT16, factor: 1, mqtt: false, influxdb: true}
- StatSB: {addr: 40, type: UINT16, factor: 1, mqtt: false, influxdb: false}
- StatK1: {addr: 41, type: UINT16, factor: 1, mqtt: false, influxdb: true}
- StatPel: {addr: 42, type: UINT16, factor: 1, mqtt: false, influxdb: true}
- StatKH: {addr: 43, type: UINT16, factor: 1, mqtt: false, influxdb: false}
- TPOsoll: {addr: 44, type: UINT16, factor: 0.1, mqtt: false, influxdb: true}
- FATVsoll: {addr: 45, type: UINT16, factor: 0.1, mqtt: false, influxdb: true}
- TSBsollHK: {addr: 46, type: INT16, factor: 0.1, mqtt: false, influxdb: false}
- TSBsollS: {addr: 47, type: INT16, factor: 0.1, mqtt: false, influxdb: false}
- BetrHK1: {addr: 48, type: UINT16, factor: 1, mqtt: false, influxdb: true}
- BetrHK2: {addr: 49, type: UINT16, factor: 1, mqtt: false, influxdb: false}
- BetrHK3: {addr: 50, type: UINT16, factor: 1, mqtt: false, influxdb: false}
- BetrSB: {addr: 51, type: UINT16, factor: 1, mqtt: false, influxdb: false}
- GesKKsoll: {addr: 52, type: INT16, factor: 0.1, mqtt: false, influxdb: false}
- KKsollWE1: {addr: 53, type: UINT16, factor: 0.1, mqtt: false, influxdb: false}
- KKsollWE2: {addr: 54, type: UINT16, factor: 0.1, mqtt: false, influxdb: false}
- KKsollWE3: {addr: 55, type: UINT16, factor: 0.1, mqtt: false, influxdb: false}
- KKsollWE4: {addr: 56, type: UINT16, factor: 0.1, mqtt: false, influxdb: false}
- ErrWE1: {addr: 57, type: UINT16, factor: 1, mqtt: false, influxdb: false}
- ErrWE2: {addr: 58, type: UINT16, factor: 1, mqtt: false, influxdb: false}
- ErrWE3: {addr: 59, type: UINT16, factor: 1, mqtt: false, influxdb: false}
- ErrWE4: {addr: 60, type: UINT16, factor: 1, mqtt: false, influxdb: false}
- nothing: {addr: 0, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: false}
- ErrLS: {addr: 1, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: true}
- TVsoll: {addr: 2, type: INT16, factor: 0.1, write: true, mqtt: true, influxdb: true}
- TV2soll: {addr: 3, type: INT16, factor: 0.1, write: false, mqtt: false, influxdb: false}
- TV3soll: {addr: 4, type: INT16, factor: 0.1, write: false, mqtt: false, influxdb: false}
- HK1soll: {addr: 5, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: true}
- HK2soll: {addr: 6, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: false}
- HK3soll: {addr: 7, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: false}
- TWWsoll: {addr: 8, type: INT16, factor: 0.1, write: false, mqtt: false, influxdb: true}
- TV1max: {addr: 9, type: INT16, factor: 0.1, write: false, mqtt: false, influxdb: true}
- TV2max: {addr: 10, type: INT16, factor: 0.1, write: false, mqtt: false, influxdb: false}
- TV3max: {addr: 11, type: INT16, factor: 0.1, write: false, mqtt: false, influxdb: false}
- ErrHR: {addr: 12, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: true}
- ErrSR: {addr: 13, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: true}
- ErrWE1_1: {addr: 14, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: true}
- ErrWE1_2: {addr: 15, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: false}
- ErrWE1_3: {addr: 16, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: false}
- ErrWE1_4: {addr: 17, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: false}
- ErrWE1_5: {addr: 18, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: false}
- KollLei: {addr: 19, type: UINT16, factor: 0.1, write: false, mqtt: false, influxdb: true}
- TagesS: {addr: 20, type: UINT16, factor: 0.1, write: false, mqtt: true, influxdb: true}
- GesS: {addr: 21, type: UINT32, factor: 0.1, write: false, mqtt: false, influxdb: true}
- GesWW: {addr: 23, type: UINT32, factor: 0.1, write: false, mqtt: false, influxdb: true}
- GesZ: {addr: 25, type: UINT32, factor: 0.1, write: false, mqtt: false, influxdb: true}
- HGesK1: {addr: 27, type: UINT32, factor: 1, write: false, mqtt: false, influxdb: true}
- StartK1: {addr: 29, type: UINT32, factor: 1, write: false, mqtt: false, influxdb: true}
- HGesPel: {addr: 31, type: UINT32, factor: 1, write: false, mqtt: false, influxdb: true}
- VGesPel: {addr: 33, type: UINT16, factor: 0.1, write: false, mqtt: false, influxdb: true}
- StatWW: {addr: 34, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: true}
- StatZ: {addr: 35, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: true}
- StatHK1: {addr: 36, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: true}
- StatHK2: {addr: 37, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: false}
- StatHK3: {addr: 38, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: false}
- StatS: {addr: 39, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: true}
- StatSB: {addr: 40, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: false}
- StatK1: {addr: 41, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: true}
- StatPel: {addr: 42, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: true}
- StatKH: {addr: 43, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: false}
- TPOsoll: {addr: 44, type: UINT16, factor: 0.1, write: false, mqtt: false, influxdb: true}
- FATVsoll: {addr: 45, type: UINT16, factor: 0.1, write: false, mqtt: false, influxdb: true}
- TSBsollHK: {addr: 46, type: INT16, factor: 0.1, write: false, mqtt: false, influxdb: false}
- TSBsollS: {addr: 47, type: INT16, factor: 0.1, write: false, mqtt: false, influxdb: false}
- BetrHK1: {addr: 48, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: true}
- BetrHK2: {addr: 49, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: false}
- BetrHK3: {addr: 50, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: false}
- BetrSB: {addr: 51, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: false}
- GesKKsoll: {addr: 52, type: INT16, factor: 0.1, write: false, mqtt: false, influxdb: false}
- KKsollWE1: {addr: 53, type: UINT16, factor: 0.1, write: false, mqtt: false, influxdb: false}
- KKsollWE2: {addr: 54, type: UINT16, factor: 0.1, write: false, mqtt: false, influxdb: false}
- KKsollWE3: {addr: 55, type: UINT16, factor: 0.1, write: false, mqtt: false, influxdb: false}
- KKsollWE4: {addr: 56, type: UINT16, factor: 0.1, write: false, mqtt: false, influxdb: false}
- ErrWE1: {addr: 57, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: false}
- ErrWE2: {addr: 58, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: false}
- ErrWE3: {addr: 59, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: false}
- ErrWE4: {addr: 60, type: UINT16, factor: 1, write: false, mqtt: false, influxdb: false}
+2 -1
View File
@@ -24,7 +24,8 @@ pub struct MqttConfig {
pub user: Option<String>,
pub password: Option<String>,
pub path: Option<String>,
pub button_circulation: Option<String>,
pub leitsystem_path: Option<String>,
pub set_write_interval_ms: Option<u64>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
+3 -3
View File
@@ -31,7 +31,7 @@ pub fn start_influx_thread(config: Arc<Mutex<AppConfig>>, values: Arc<Mutex<Modb
if should_write(&config, name, "input_register") {
let point = WriteQuery::new(Timestamp::from(Utc::now()), measurement.clone())
.add_field(name, *v)
.add_tag("type", "input_register")
// .add_tag("type", "input_register")
.add_tag("location", location.as_str())
.to_owned();
points.push(point);
@@ -44,7 +44,7 @@ pub fn start_influx_thread(config: Arc<Mutex<AppConfig>>, values: Arc<Mutex<Modb
if should_write(&config, name, "holding_register") {
let point = WriteQuery::new(Timestamp::from(Utc::now()), measurement.clone())
.add_field(name, *v)
.add_tag("type", "holding_register")
// .add_tag("type", "holding_register")
.add_tag("location", location.as_str())
.to_owned();
points.push(point);
@@ -57,7 +57,7 @@ pub fn start_influx_thread(config: Arc<Mutex<AppConfig>>, values: Arc<Mutex<Modb
if should_write(&config, name, "coils") {
let point = WriteQuery::new(Timestamp::from(Utc::now()), measurement.clone())
.add_field(name, *v)
.add_tag("type", "coils")
// .add_tag("type", "coils")
.add_tag("location", location.as_str())
.to_owned();
points.push(point);
+31 -33
View File
@@ -16,6 +16,10 @@ use crate::config::{AppConfig, ModbusValueMaps};
use crate::modbus_types::{ModbusInputRegisterConfig, ModbusHoldingRegisterConfig, ModbusCoilsConfig};
use crate::app_state::AppState;
fn get_config_path() -> String {
std::env::var("CONFIG_PATH").unwrap_or_else(|_| "paramod.yaml".to_string())
}
async fn index(data: web::Data<AppState>) -> Result<HttpResponse> {
let config = data.config.lock().unwrap();
let value_maps = data.value_maps.lock().unwrap();
@@ -127,18 +131,10 @@ async fn settings_page(data: web::Data<AppState>) -> Result<HttpResponse> {
#[derive(Serialize, Deserialize, Clone)]
#[serde(untagged)]
enum SaveTableRows {
InputRegister(Vec<HashMap<String, ModbusInputRegisterConfig>>),
HoldingRegister(Vec<HashMap<String, ModbusHoldingRegisterConfig>>),
Coils(Vec<HashMap<String, ModbusCoilsConfig>>),
}
#[derive(Serialize, Deserialize)]
struct SaveTableRequest {
table_id: String,
rows: SaveTableRows,
rows: serde_json::Value,
}
async fn save_table(
@@ -146,28 +142,31 @@ async fn save_table(
req: web::Json<SaveTableRequest>,
) -> Result<HttpResponse> {
let mut config = data.config.lock().unwrap();
let conf_path = "paramod.yaml";
let conf_path = get_config_path();
match req.table_id.as_str() {
"modbus_input_register" => {
if let SaveTableRows::InputRegister(rows) = req.rows.clone() {
config.modbus_input_register = Some(rows);
} else {
return Ok(HttpResponse::BadRequest().body("Falscher Typ für input_register"));
}
let rows: Vec<HashMap<String, ModbusInputRegisterConfig>> =
match serde_json::from_value(req.rows.clone()) {
Ok(v) => v,
Err(e) => return Ok(HttpResponse::BadRequest().body(format!("Falscher Typ für input_register: {}", e))),
};
config.modbus_input_register = Some(rows);
}
"modbus_holding_register" => {
if let SaveTableRows::HoldingRegister(rows) = req.rows.clone() {
config.modbus_holding_register = Some(rows);
} else {
return Ok(HttpResponse::BadRequest().body("Falscher Typ für holding_register"));
}
let rows: Vec<HashMap<String, ModbusHoldingRegisterConfig>> =
match serde_json::from_value(req.rows.clone()) {
Ok(v) => v,
Err(e) => return Ok(HttpResponse::BadRequest().body(format!("Falscher Typ für holding_register: {}", e))),
};
config.modbus_holding_register = Some(rows);
}
"modbus_coils" => {
if let SaveTableRows::Coils(rows) = req.rows.clone() {
config.modbus_coils = Some(rows);
} else {
return Ok(HttpResponse::BadRequest().body("Falscher Typ für coils"));
}
let rows: Vec<HashMap<String, ModbusCoilsConfig>> =
match serde_json::from_value(req.rows.clone()) {
Ok(v) => v,
Err(e) => return Ok(HttpResponse::BadRequest().body(format!("Falscher Typ für coils: {}", e))),
};
config.modbus_coils = Some(rows);
}
_ => return Ok(HttpResponse::BadRequest().body("Invalid table_id")),
}
@@ -175,7 +174,7 @@ async fn save_table(
Ok(s) => s,
Err(e) => return Ok(HttpResponse::InternalServerError().body(format!("Serialisierungsfehler: {}", e))),
};
if let Err(e) = fs::write(conf_path, yaml_str) {
if let Err(e) = fs::write(&conf_path, yaml_str) {
return Ok(HttpResponse::InternalServerError().body(format!("Fehler beim Schreiben: {}", e)));
}
Ok(HttpResponse::Ok().body("success"))
@@ -200,16 +199,16 @@ async fn save_settings(
// Value-Maps neu initialisieren
let mut value_maps = data.value_maps.lock().unwrap();
*value_maps = ModbusValueMaps::from_config(&new_config);
let conf_path = "paramod.yaml";
let conf_path = get_config_path();
let yaml_str = match serde_yaml::to_string(&new_config) {
Ok(s) => s,
Err(e) => return Ok(HttpResponse::InternalServerError().body(format!("Serialisierungsfehler: {}", e))),
};
if let Err(e) = fs::write(conf_path, yaml_str) {
if let Err(e) = fs::write(&conf_path, yaml_str) {
return Ok(HttpResponse::InternalServerError().body(format!("Fehler beim Schreiben: {}", e)));
}
// Reload config from file
let conf_str = match std::fs::read_to_string(conf_path) {
let conf_str = match std::fs::read_to_string(&conf_path) {
Ok(s) => s,
Err(e) => return Ok(HttpResponse::InternalServerError().body(format!("Fehler beim Lesen: {}", e))),
};
@@ -231,9 +230,8 @@ async fn get_config(data: web::Data<AppState>) -> HttpResponse {
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let config_path = "paramod.yaml";
let app_state = web::Data::new(AppState::load_from_conf(config_path));
let config_path = get_config_path();
let app_state = web::Data::new(AppState::load_from_conf(&config_path));
// Starte Modbus-Polling-Thread
{
@@ -245,7 +243,7 @@ async fn main() -> std::io::Result<()> {
&config.modbus_holding_register,
&config.modbus_coils,
value_maps,
std::time::Duration::from_secs(2), // Poll-Intervall
std::time::Duration::from_secs(10), // Poll-Intervall
);
}
+107
View File
@@ -1,5 +1,6 @@
use crate::config::{ModbusValueMaps, ModbusConfig};
use crate::modbus_types::{ModbusInputRegisterConfig, ModbusHoldingRegisterConfig, ModbusCoilsConfig};
use crate::config::AppConfig;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::thread;
@@ -9,6 +10,112 @@ use tokio_modbus::prelude::*;
use tokio_modbus::client::tcp;
use std::net::SocketAddr;
pub fn write_value_by_name(config: &AppConfig, name: &str, payload: &str) -> Result<(), String> {
let addr: SocketAddr = format!("{}:{}", config.modbus.host, config.modbus.port)
.parse()
.map_err(|e| format!("Ungültige Modbus-Adresse: {}", e))?;
if let Some(ref coils) = config.modbus_coils {
for map in coils {
if let Some(coil) = map.get(name) {
if !coil.write.unwrap_or(false) {
return Err(format!("Schreiben für '{}' nicht erlaubt", name));
}
let bool_value = parse_bool_payload(payload)?;
let rt = tokio::runtime::Runtime::new()
.map_err(|e| format!("Tokio Runtime Fehler: {}", e))?;
rt.block_on(async {
let mut client = tcp::connect_slave(addr, 1u8.into())
.await
.map_err(|e| format!("Modbus Verbindung fehlgeschlagen: {}", e))?;
client
.write_single_coil(coil.addr, bool_value)
.await
.map_err(|e| format!("Coil schreiben fehlgeschlagen: {}", e))
})?;
return Ok(());
}
}
}
if let Some(ref holding_registers) = config.modbus_holding_register {
for map in holding_registers {
if let Some(reg) = map.get(name) {
if !reg.write.unwrap_or(false) {
return Err(format!("Schreiben für '{}' nicht erlaubt", name));
}
let numeric_value = payload
.trim()
.parse::<f64>()
.map_err(|e| format!("Ungültiger Zahlenwert '{}': {}", payload, e))?;
let factor = reg.factor.unwrap_or(1.0);
let raw = if factor == 0.0 {
numeric_value
} else {
numeric_value / factor
};
let rt = tokio::runtime::Runtime::new()
.map_err(|e| format!("Tokio Runtime Fehler: {}", e))?;
return rt.block_on(async {
let mut client = tcp::connect_slave(addr, 1u8.into())
.await
.map_err(|e| format!("Modbus Verbindung fehlgeschlagen: {}", e))?;
match reg.r#type.as_deref().unwrap_or("UINT16") {
"INT16" => {
let int_value = raw.round();
if int_value < i16::MIN as f64 || int_value > i16::MAX as f64 {
return Err(format!("Wert außerhalb INT16-Bereich für '{}': {}", name, numeric_value));
}
let register = int_value as i16 as u16;
client
.write_multiple_registers(reg.addr, &[register])
.await
.map_err(|e| format!("Holding Register schreiben fehlgeschlagen: {}", e))?;
}
"UINT32" => {
let int_value = raw.round();
if int_value < 0.0 || int_value > u32::MAX as f64 {
return Err(format!("Wert außerhalb UINT32-Bereich für '{}': {}", name, numeric_value));
}
let val = int_value as u32;
let high = ((val >> 16) & 0xFFFF) as u16;
let low = (val & 0xFFFF) as u16;
client
.write_multiple_registers(reg.addr, &[high, low])
.await
.map_err(|e| format!("Holding Register (UINT32) schreiben fehlgeschlagen: {}", e))?;
}
_ => {
let int_value = raw.round();
if int_value < 0.0 || int_value > u16::MAX as f64 {
return Err(format!("Wert außerhalb UINT16-Bereich für '{}': {}", name, numeric_value));
}
client
.write_multiple_registers(reg.addr, &[int_value as u16])
.await
.map_err(|e| format!("Holding Register schreiben fehlgeschlagen: {}", e))?;
}
}
Ok(())
});
}
}
}
Err(format!("Kein schreibbares Modbus-Mapping für '{}' gefunden", name))
}
fn parse_bool_payload(payload: &str) -> Result<bool, String> {
match payload.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "on" => Ok(true),
"0" | "false" | "off" => Ok(false),
_ => Err(format!("Ungültiger bool-Wert '{}', erwartet 0/1 oder true/false", payload)),
}
}
/// Startet einen Thread, der zyklisch Modbus-Register abfragt und ModbusValueMaps aktualisiert
pub fn start_modbus_polling_thread(
modbus_config: &ModbusConfig,
+1
View File
@@ -24,6 +24,7 @@ pub struct ModbusHoldingRegisterConfig {
pub addr: u16,
pub r#type: Option<String>,
pub factor: Option<f64>,
pub write: Option<bool>,
pub mqtt: Option<bool>,
pub influxdb: Option<bool>,
pub comment: Option<String>,
+157 -31
View File
@@ -1,8 +1,11 @@
use std::sync::{Arc, Mutex};
use std::process;
use std::collections::HashMap;
use std::thread;
use std::time::Duration;
use std::time::{Duration, Instant};
use crate::config::{AppConfig, ModbusValueMaps};
use rumqttc::{MqttOptions, Client, QoS};
use crate::modbus;
use rumqttc::{Event, MqttOptions, Packet, Client, QoS, RecvTimeoutError};
pub fn start_mqtt_thread(config: Arc<Mutex<AppConfig>>, values: Arc<Mutex<ModbusValueMaps>>) {
let mqtt_config = {
@@ -12,59 +15,182 @@ pub fn start_mqtt_thread(config: Arc<Mutex<AppConfig>>, values: Arc<Mutex<Modbus
let broker = if mqtt_config.broker.is_empty() { "localhost".to_string() } else { mqtt_config.broker.clone() };
let port = if mqtt_config.port == 0 { 1883 } else { mqtt_config.port };
let path = if let Some(ref p) = mqtt_config.path { p.clone() } else { "paramod/values".to_string() };
let leitsystem_path = mqtt_config
.leitsystem_path
.clone()
.unwrap_or_else(|| format!("{}/leitsystem", path));
let set_write_interval = Duration::from_millis(mqtt_config.set_write_interval_ms.unwrap_or(2000).max(100));
let user = mqtt_config.user.clone().unwrap_or_default();
let password = mqtt_config.password.clone().unwrap_or_default();
let user_is_empty = user.is_empty();
thread::spawn(move || {
let mut mqttoptions = MqttOptions::new("paramod-client", broker, port);
let client_id = format!("paramod-client-{}", process::id());
let mut mqttoptions = MqttOptions::new(client_id, broker, port);
mqttoptions.set_keep_alive(Duration::from_secs(5));
if !user_is_empty {
mqttoptions.set_credentials(user, password);
}
let (mut client, mut connection) = Client::new(mqttoptions, 10);
let set_topic = format!("{}/+/set", path);
if let Err(e) = client.subscribe(set_topic, QoS::AtLeastOnce) {
eprintln!("MQTT Subscribe fehlgeschlagen: {}", e);
}
let leitsystem_state_topic = format!("{}/active", leitsystem_path.trim_end_matches('/'));
if let Err(e) = client.subscribe(leitsystem_state_topic.clone(), QoS::AtLeastOnce) {
eprintln!("MQTT Subscribe Leitsystem fehlgeschlagen: {}", e);
}
let publish_interval = Duration::from_secs(5);
let mut last_publish = Instant::now() - publish_interval;
let mut last_set_write = Instant::now() - set_write_interval;
let mut pending_set_values: HashMap<String, String> = HashMap::new();
let mut leitsystem_enabled = false;
loop {
{
let values = values.lock().unwrap();
// Input Register
for (name, val) in &values.modbus_input_register_values {
if let Some(v) = val {
if should_publish(&config, name, "input_register") {
let topic = format!("{}/{}", path, name);
let payload = format!("{}", v);
let _ = client.publish(topic, QoS::AtLeastOnce, false, payload);
if last_publish.elapsed() >= publish_interval {
{
let values = values.lock().unwrap();
// Input Register
for (name, val) in &values.modbus_input_register_values {
if let Some(v) = val {
if should_publish(&config, name, "input_register") {
let topic = format!("{}/{}/state", path, name);
let payload = format!("{}", v);
let _ = client.publish(topic, QoS::AtLeastOnce, false, payload);
}
}
}
// Holding Register
for (name, val) in &values.modbus_holding_register_values {
if let Some(v) = val {
if should_publish(&config, name, "holding_register") {
let topic = format!("{}/{}/state", path, name);
let payload = format!("{}", v);
let _ = client.publish(topic, QoS::AtLeastOnce, false, payload);
}
}
}
// Coils
for (name, val) in &values.modbus_coils_values {
if let Some(v) = val {
if should_publish(&config, name, "coils") {
let topic = format!("{}/{}/state", path, name);
let payload = format!("{}", v);
let _ = client.publish(topic, QoS::AtLeastOnce, false, payload);
}
}
}
}
// Holding Register
for (name, val) in &values.modbus_holding_register_values {
if let Some(v) = val {
if should_publish(&config, name, "holding_register") {
let topic = format!("{}/{}", path, name);
let payload = format!("{}", v);
let _ = client.publish(topic, QoS::AtLeastOnce, false, payload);
let state_payload = if leitsystem_enabled { "ON" } else { "OFF" };
let _ = client.publish(
leitsystem_state_topic.clone(),
QoS::AtLeastOnce,
true,
state_payload,
);
last_publish = Instant::now();
}
if last_set_write.elapsed() >= set_write_interval {
let cfg = {
let lock = config.lock().unwrap();
lock.clone()
};
if leitsystem_enabled {
for (name, payload) in &pending_set_values {
if let Err(e) = modbus::write_value_by_name(&cfg, name, payload) {
eprintln!("MQTT set -> Modbus Fehler ({}): {}", name, e);
} else {
mirror_set_value(&values, &cfg, name, payload);
}
}
}
// Coils
for (name, val) in &values.modbus_coils_values {
if let Some(v) = val {
if should_publish(&config, name, "coils") {
let topic = format!("{}/{}", path, name);
let payload = format!("{}", v);
let _ = client.publish(topic, QoS::AtLeastOnce, false, payload);
last_set_write = Instant::now();
}
match connection.recv_timeout(Duration::from_millis(200)) {
Ok(Ok(Event::Incoming(Packet::Publish(publish)))) => {
if publish.topic == leitsystem_state_topic {
let payload = String::from_utf8_lossy(&publish.payload).trim().to_string();
match parse_boolish(&payload) {
Some(v) => leitsystem_enabled = v,
None => eprintln!("Ungültiger Leitsystem state '{}', erwartet ON/OFF", payload),
}
continue;
}
if let Ok(name) = extract_name_from_set_topic(&path, &publish.topic) {
let payload = String::from_utf8_lossy(&publish.payload).trim().to_string();
pending_set_values.insert(name.clone(), payload.clone());
}
}
Ok(Ok(_)) => {}
Ok(Err(e)) => {
eprintln!("MQTT Connection Fehler: {}", e);
eprintln!("Hinweis: Häufige Ursache ist eine zweite MQTT-Session mit derselben Client-ID.");
}
Err(RecvTimeoutError::Timeout) => {}
Err(RecvTimeoutError::Disconnected) => {
eprintln!("MQTT Verbindung getrennt (request channel geschlossen)");
thread::sleep(Duration::from_millis(500));
}
}
// Handle MQTT connection events
for _event in connection.iter().take(1) {
// Optionally log or handle events
}
thread::sleep(Duration::from_secs(5));
}
});
}
fn parse_boolish(payload: &str) -> Option<bool> {
match payload.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "on" => Some(true),
"0" | "false" | "off" => Some(false),
_ => None,
}
}
fn mirror_set_value(values: &Arc<Mutex<ModbusValueMaps>>, cfg: &AppConfig, name: &str, payload: &str) {
if let Ok(mut maps) = values.lock() {
if let Some(ref coils) = cfg.modbus_coils {
for map in coils {
if map.contains_key(name) {
if let Some(v) = parse_boolish(payload) {
maps.modbus_coils_values.insert(name.to_string(), Some(if v { 1.0 } else { 0.0 }));
}
return;
}
}
}
if let Some(ref holding) = cfg.modbus_holding_register {
for map in holding {
if map.contains_key(name) {
if let Ok(v) = payload.parse::<f64>() {
maps.modbus_holding_register_values.insert(name.to_string(), Some(v));
}
return;
}
}
}
}
}
fn extract_name_from_set_topic(base_path: &str, topic: &str) -> Result<String, String> {
let prefix = format!("{}/", base_path.trim_end_matches('/'));
if !topic.starts_with(&prefix) || !topic.ends_with("/set") {
return Err("Topic passt nicht zum /set-Schema".to_string());
}
let without_prefix = &topic[prefix.len()..];
let name = without_prefix.strip_suffix("/set").unwrap_or_default();
if name.is_empty() || name.contains('/') {
return Err("Ungültiger Variablenname im Topic".to_string());
}
Ok(name.to_string())
}
fn should_publish(config: &Arc<Mutex<AppConfig>>, name: &str, reg_type: &str) -> bool {
let cfg = config.lock().unwrap();
match reg_type {
+47 -4
View File
@@ -56,6 +56,34 @@ function addRow() {
<button class="delete-btn" onclick="deleteRow(this)">🗑️</button>
</td>
`;
} else if (typeof tableId !== 'undefined' && tableId === 'modbus_holding_register') {
newRow.innerHTML = `
<td><input type='text' class='text-input' data-field='bezeichnung' value='' /></td>
<td><input type='text' class='text-input' data-field='adresse' value='' /></td>
<td><input type='text' class='text-input' data-field='type' value='' /></td>
<td><input type='text' class='text-input' data-field='faktor' value='1.0' /></td>
<td>
<label class='switch'>
<input type='checkbox' class='bool-input' data-field='write' />
<span class='slider'></span>
</label>
</td>
<td>
<label class='switch'>
<input type='checkbox' class='bool-input' data-field='mqtt' />
<span class='slider'></span>
</label>
</td>
<td>
<label class='switch'>
<input type='checkbox' class='bool-input' data-field='influxdb' />
<span class='slider'></span>
</label>
</td>
<td>
<button class="delete-btn" onclick="deleteRow(this)">🗑️</button>
</td>
`;
} else {
newRow.innerHTML = `
<td><input type='text' class='text-input' data-field='bezeichnung' value='' /></td>
@@ -119,13 +147,27 @@ async function saveTable() {
influxdb: influxdb,
comment: comment
};
} else {
const rtype = row.querySelector("input[data-field='type']")?.value || null;
} else if (typeof tableId !== 'undefined' && tableId === 'modbus_holding_register') {
const write = row.querySelector("input[data-field='write']")?.checked || false;
const type = row.querySelector("input[data-field='type']")?.value || null;
const factor = parseFloat(row.querySelector("input[data-field='factor']")?.value || row.querySelector("input[data-field='faktor']")?.value || '1.0');
const comment = row.querySelector("input[data-field='comment']")?.value || null;
value = {
addr: addr,
rtype: rtype,
type: type,
factor: factor,
write: write,
mqtt: mqtt,
influxdb: influxdb,
comment: comment
};
} else {
const type = row.querySelector("input[data-field='type']")?.value || null;
const factor = parseFloat(row.querySelector("input[data-field='factor']")?.value || row.querySelector("input[data-field='faktor']")?.value || '1.0');
const comment = row.querySelector("input[data-field='comment']")?.value || null;
value = {
addr: addr,
type: type,
factor: factor,
mqtt: mqtt,
influxdb: influxdb,
@@ -154,8 +196,9 @@ async function saveTable() {
messageDiv.className = 'message success';
messageDiv.textContent = '✓ Erfolgreich gespeichert!';
} else {
const errorText = (await response.text()) || 'Unbekannter Fehler';
messageDiv.className = 'message error';
messageDiv.textContent = '✗ Fehler beim Speichern!';
messageDiv.textContent = `✗ Fehler beim Speichern: ${errorText}`;
}
setTimeout(() => {
+2 -1
View File
@@ -55,7 +55,8 @@ async function saveSettings() {
user: document.getElementById('mqtt_user').value || null,
password: document.getElementById('mqtt_password').value || null,
path: document.getElementById('mqtt_path')?.value || null,
button_circulation: document.getElementById('mqtt_button_circulation').value || null
leitsystem_path: document.getElementById('mqtt_leitsystem_path')?.value || null,
set_write_interval_ms: parseInt(document.getElementById('mqtt_set_write_interval_ms')?.value, 10) || 2000
};
// InfluxDB
+2 -2
View File
@@ -96,8 +96,8 @@ body {
}
.logo-text {
font-size: 20px;
font-weight: 700;
font-size: 40px;
font-weight: 800;
color: var(--text-light);
}
+16 -3
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sensor Konfiguration</title>
<title>Konfiguration</title>
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
@@ -14,7 +14,7 @@
<rect width="40" height="40" rx="8" fill="#667eea"/>
<path d="M12 20L18 26L28 14" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span class="logo-text">Sensor Manager</span>
<span class="logo-text">Paramod</span>
</div>
<nav class="nav">
<a href="/table/modbus_input_register" class="nav-link {% if active_page == 'modbus_input_register' %}active{% endif %}">Input Register</a>
@@ -26,7 +26,7 @@
</header>
<div class="container">
<h1>🔧 Sensor Konfiguration - {{ table_id | upper }}</h1>
<h1>🔧 Konfiguration - {{ table_id | upper }}</h1>
<div id="message" class="message"></div>
<div class="table-wrapper">
<table id="sensorTable">
@@ -37,6 +37,10 @@
<th>Adresse</th>
{% if table_id == "modbus_coils" %}
<th>Write</th>
{% elif table_id == "modbus_holding_registers" or table_id == "modbus_holding_register" %}
<th>Type</th>
<th>Faktor</th>
<th>Write</th>
{% else %}
<th>Type</th>
<th>Faktor</th>
@@ -63,6 +67,15 @@
<span class='slider'></span>
</label>
</td>
{% elif table_id == "modbus_holding_registers" or table_id == "modbus_holding_register" %}
<td><input type='text' class='text-input' data-field='type' value='{{ row.type | default(value="") }}' /></td>
<td><input type='text' class='text-input' data-field='factor' value='{{ row.factor | default(value="") }}' /></td>
<td>
<label class='switch'>
<input type='checkbox' class='bool-input' data-field='write' {% if row.write %}checked{% endif %} />
<span class='slider'></span>
</label>
</td>
{% else %}
<td><input type='text' class='text-input' data-field='type' value='{{ row.type | default(value="") }}' /></td>
<td><input type='text' class='text-input' data-field='factor' value='{{ row.factor | default(value="") }}' /></td>
+7 -3
View File
@@ -14,7 +14,7 @@
<rect width="40" height="40" rx="8" fill="#667eea"/>
<path d="M12 20L18 26L28 14" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span class="logo-text">Sensor Manager</span>
<span class="logo-text">Paramod</span>
</div>
<nav class="nav">
<a href="/table/modbus_input_register" class="nav-link {% if active_page == 'modbus_input_register' %}active{% endif %}">Input Register</a>
@@ -98,8 +98,12 @@
<input type="text" id="mqtt_path" class="text-input" value="{{ mqtt.path | default(value="") }}" />
</div>
<div class="form-group">
<label for="mqtt_button_circulation">Button Circulation:</label>
<input type="text" id="mqtt_button_circulation" class="text-input" value="{{ mqtt.button_circulation | default(value="") }}" />
<label for="mqtt_leitsystem_path">Leitsystem Pfad:</label>
<input type="text" id="mqtt_leitsystem_path" class="text-input" value="{{ mqtt.leitsystem_path | default(value="") }}" />
</div>
<div class="form-group">
<label for="mqtt_set_write_interval_ms">Set-Sendeintervall (ms):</label>
<input type="number" min="100" id="mqtt_set_write_interval_ms" class="text-input" value="{{ mqtt.set_write_interval_ms | default(value="2000") }}" />
</div>
</div>