6 Commits

Author SHA1 Message Date
Nils Reiners
5319a299be inverter was included 2025-09-16 12:52:27 +02:00
Nils Reiners
2186c4d7db wechselrichter zum tesent eingebunden 2025-09-14 10:52:50 +02:00
Nils Reiners
7df61fd6c1 shelly upgedatet 2025-05-26 21:31:28 +02:00
Nils Reiners
0734f7a810 shelly hinzugefügt 2025-05-26 21:08:16 +02:00
Nils Reiners
65a75e061b läuft 2025-04-26 22:31:14 +01:00
Nils Reiners
974ec43f10 influx data base added 2025-04-26 23:13:22 +02:00
23 changed files with 623 additions and 52585 deletions

38
README
View File

@@ -11,10 +11,42 @@ Was needs to be done on the Raspberry pi before the tool can run.
- pip install -r requirements.txt
How to run the script:
3) How to run the script for testing:
- nohup python main.py > terminal_log 2>&1 &
nohup python main.py > terminal_log 2>&1 &
For reading out the terminal_log while script is runing:
- tail -f terminal_log
tail -f terminal_log
4) Implement and run the ems as systemd service:
create:
/etc/systemd/system/allmende_ems.service
insert:
[Unit]
Description=Allmende EMS Python Script
After=network.target
[Service]
WorkingDirectory=/home/pi/projects/allmende_ems
ExecStart=/home/pi/allmende_ems/bin/python3.11 /home/pi/projects/allmende_ems/main.py
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
manage the service with the following commands:
Once:
sudo systemctl daemon-reload
sudo systemctl start allmende_ems.service
sudo systemctl enable allmende_ems.service
While running:
sudo systemctl status allmende_ems.service
sudo systemctl restart allmende_ems.service
sudo systemctl stop allmende_ems.service
journalctl -u allmende_ems.service

31
SG_ready_schalten.py Normal file
View File

@@ -0,0 +1,31 @@
from pymodbus.client import ModbusTcpClient
def write_coils(ip):
# IP und Port der Wärmepumpe
port = 502
client = ModbusTcpClient(ip, port=port)
if not client.connect():
print("Verbindung zur Wärmepumpe fehlgeschlagen.")
return
try:
# Coil 300 = Kommunikation über Bus aktivieren (1)
response_300 = client.write_coil(300, True)
# Coil 301 = SG Ready Stufe 1 aktivieren (1)
response_301 = client.write_coil(301, False)
# Coil 302 = SG Ready Stufe 2 deaktivieren (0)
response_302 = client.write_coil(302, False)
# Optional: Rückmeldungen prüfen
for addr, resp in zip([300, 301, 302], [response_300, response_301, response_302]):
if resp.isError():
print(f"Fehler beim Schreiben von Coil {addr}: {resp}")
else:
print(f"Coil {addr} erfolgreich geschrieben.")
finally:
client.close()
# Testaufruf mit IP-Adresse deiner Wärmepumpe
write_coils("10.0.0.10") # <-- IP-Adresse hier anpassen

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -18,7 +18,7 @@ class DataBaseCsv:
writer.writerow(data)
return
# If file exists → read existing header and data
# If file exists → read existing header and modbus_registers
with open(self.filename, mode='r', newline='') as csv_file:
reader = csv.DictReader(csv_file)
existing_fields = reader.fieldnames
@@ -39,7 +39,7 @@ class DataBaseCsv:
for row in existing_data:
writer.writerow({field: row.get(field, '') for field in all_fields})
# Write new data row
# Write new modbus_registers row
writer.writerow({field: data.get(field, '') for field in all_fields})
# Replace original file with updated temporary file

28
data_base_influx.py Normal file
View File

@@ -0,0 +1,28 @@
from influxdb_client import InfluxDBClient, Point, WritePrecision
from datetime import datetime
class DataBaseInflux:
def __init__(self, url: str, token: str, org: str, bucket: str):
self.url = url
self.token = token
self.org = org
self.bucket = bucket
self.client = InfluxDBClient(url=self.url, token=self.token, org=self.org)
self.write_api = self.client.write_api()
def store_data(self, device_name: str, data: dict):
measurement = device_name # Fest auf "messungen" gesetzt
point = Point(measurement)
# Alle Key/Value-Paare als Fields speichern
for key, value in data.items():
point = point.field(key, value)
# Zeitstempel automatisch auf jetzt setzen
point = point.time(datetime.utcnow(), WritePrecision.NS)
# Punkt in InfluxDB schreiben
self.write_api.write(bucket=self.bucket, org=self.org, record=point)

View File

@@ -3,15 +3,17 @@ import pandas as pd
import time
class HeatPump:
def __init__(self, ip_address: str):
def __init__(self, device_name: str, ip_address: str, port: int=502):
self.device_name = device_name
self.ip = ip_address
self.port = port
self.client = None
self.connect_to_modbus()
self.registers = None
self.get_registers()
def connect_to_modbus(self):
port = 502
port = self.port
self.client = ModbusTcpClient(self.ip, port=port)
try:
if not self.client.connect():
@@ -25,7 +27,7 @@ class HeatPump:
def get_registers(self):
# Excel-Datei mit den Input-Registerinformationen
excel_path = "data/ModBus TCPIP 1.17(1).xlsx"
excel_path = "modbus_registers/heat_pump_registers.xlsx"
xls = pd.ExcelFile(excel_path)
df_input_registers = xls.parse('04 Input Register')
@@ -42,7 +44,7 @@ class HeatPump:
for _, row in df_clean.iterrows()
}
def get_data(self):
def get_state(self):
data = {}
data['Zeit'] = time.strftime('%Y-%m-%d %H:%M:%S')
for address, info in self.registers.items():

30
main.py
View File

@@ -1,17 +1,35 @@
import time
from datetime import datetime
from data_base_csv import DataBaseCsv
from data_base_influx import DataBaseInflux
from heat_pump import HeatPump
from pv_inverter import PvInverter
from shelly_pro_3m import ShellyPro3m
interval = 10 # z.B. alle 10 Sekunden
# For dev-System run in terminal: ssh -N -L 127.0.0.1:8111:10.0.0.10:502 pi@192.168.1.146
# For productive-System change port in heatpump to 502
db = DataBaseCsv('modbus_log.csv')
hp = HeatPump(ip_address='10.0.0.10')
interval_seconds = 10
db = DataBaseInflux(
url="http://192.168.1.146:8086",
token="Cw_naEZyvJ3isiAh1P4Eq3TsjcHmzzDFS7SlbKDsS6ZWL04fMEYixWqtNxGThDdG27S9aW5g7FP9eiq5z1rsGA==",
org="allmende",
bucket="allmende_db"
)
hp = HeatPump(device_name='hp_master', ip_address='localhost', port=8111)
shelly = ShellyPro3m(device_name='wohnung_2_6', ip_address='192.168.1.121')
wr = PvInverter(device_name='wr_master', ip_address='192.168.1.112')
#controller = SgReadyController(hp, wr)
while True:
now = datetime.now()
if now.second % interval == 0 and now.microsecond < 100_000:
db.store_data(hp.get_data())
if now.second % interval_seconds == 0 and now.microsecond < 100_000:
db.store_data(hp.device_name, hp.get_state())
db.store_data(shelly.device_name, shelly.get_state())
db.store_data(wr.device_name, wr.get_state())
#controller.perform_action()
time.sleep(0.1)

22
make_tunnel.py Normal file
View File

@@ -0,0 +1,22 @@
from sshtunnel import SSHTunnelForwarder
# ---- KONFIG ----
SSH_HOST = "192.168.1.146" # Raspberry Pi im 192.168.1.x Netz
SSH_PORT = 22
SSH_USER = "pi"
PASSWORD = 'raspberry' # oder Passwort als String
REMOTE_IP = "10.0.0.10" # Wärmepumpe im 10.0.0.x Netz
REMOTE_PORT = 502 # Modbus/TCP Port
def make_tunnel(port):
tunnel = SSHTunnelForwarder(
(SSH_HOST, SSH_PORT),
ssh_username=SSH_USER,
ssh_password=PASSWORD,
remote_bind_address=(REMOTE_IP, REMOTE_PORT),
local_bind_address=("127.0.0.1", port),
)
tunnel.start()
return tunnel

Binary file not shown.

Binary file not shown.

41
plot_data.py Normal file
View File

@@ -0,0 +1,41 @@
# Neu laden nach Code-Reset
import pandas as pd
import matplotlib.pyplot as plt
# Pfad zur neu hochgeladenen Datei
file_path = "modbus_log.csv"
df_new = pd.read_csv(file_path)
# Zeitstempel in datetime konvertieren
df_new['Zeit'] = pd.to_datetime(df_new['Zeit'])
# Spaltenbezeichnungen für den Plot
registers = [
'10 - Gebäudeseite Wärmepumpe Vorlauf/Austritt (Warm)',
'11 - Gebäudeseite Wärmepumpe Rücklauf/Eintritt (Kalt)',
'12 - Umweltseite/Quelle Wärmepumpe Eintritt (Warm)',
'13 - Umweltseite/Quelle Wärmepumpe Austritt (Kalt)',
'50 - Rücklauftemperatur Direkterheizkreis oder Puffertemperatur',
'70 - Vorlauftemperatur Mischerkreis 1',
'150 - Trinkwarmwasserspiecher oben (Ein)',
'153 - Trinkwarmwasserspiecher unten (Aus)'
]
all_registers = ['300 - Aussentemperatur'] + registers
# Plot erzeugen
plt.figure(figsize=(14, 8))
for reg in all_registers:
plt.plot(df_new['Zeit'], df_new[reg], label=reg)
plt.title("Temperaturverläufe inkl. Außentemperatur (neue Daten)")
plt.xlabel("Zeit")
plt.ylabel("Temperatur (°C)")
plt.grid(True)
plt.tight_layout()
# Legende außerhalb des Plots platzieren
plt.legend(loc='center left', bbox_to_anchor=(1.0, 0.5))
plt.subplots_adjust(right=0.75)
plt.show()

68
pv_inverter.py Normal file
View File

@@ -0,0 +1,68 @@
import time
import pandas as pd
from pymodbus.client import ModbusTcpClient
class PvInverter:
def __init__(self, device_name: str, ip_address: str, port: int = 502, unit: int = 1):
self.device_name = device_name
self.ip = ip_address
self.port = port
self.unit = unit
self.client = None
self.registers = None
self.connect_to_modbus()
self.get_registers()
def connect_to_modbus(self):
# Timeout & retries optional, aber hilfreich:
self.client = ModbusTcpClient(self.ip, port=self.port, timeout=3.0, retries=3)
if not self.client.connect():
print("Verbindung zu Wechselrichter fehlgeschlagen.")
raise SystemExit(1)
print("Verbindung zu Wechselrichter erfolgreich.")
# WICHTIG: NICHT hier schließen!
# finally: self.client.close() <-- entfernen
def close(self):
if self.client:
self.client.close()
self.client = None
def get_registers(self):
excel_path = "modbus_registers/pv_inverter_registers.xlsx"
xls = pd.ExcelFile(excel_path)
df_input_registers = xls.parse()
df_clean = df_input_registers[['MB Adresse', 'Beschreibung', 'Variabel Typ']].dropna()
df_clean['MB Adresse'] = df_clean['MB Adresse'].astype(int)
self.registers = {
row['MB Adresse']: {
'desc': row['Beschreibung'],
'type': 'REAL' if str(row['Variabel Typ']).upper() == 'REAL' else 'INT'
}
for _, row in df_clean.iterrows()
}
def get_state(self):
data = {'Zeit': time.strftime('%Y-%m-%d %H:%M:%S')}
for address, info in self.registers.items():
reg_type = info['type']
# Unit-ID mitgeben (wichtig bei pymodbus>=3)
result = self.client.read_holding_registers(
address=address,
count=2 if reg_type == 'REAL' else 1,
slave=self.unit # pymodbus 2.x -> 'slave', nicht 'unit'
)
if result.isError():
print(f"Fehler beim Lesen von Adresse {address}: {result}")
continue
# Minimal invasiv: wie bei dir erstes Register verwenden
value = result.registers[0]
print(f"Adresse {address} - {info['desc']}: {value}")
data[f"{address} - {info['desc']}"] = value
return data

View File

@@ -1,3 +1,4 @@
pymodbus~=3.8.6
pandas
openpyxl
sshtunnel

64
shelly_pro_3m.py Normal file
View File

@@ -0,0 +1,64 @@
import struct
from pymodbus.client import ModbusTcpClient
import pandas as pd
import time
class ShellyPro3m:
def __init__(self, device_name: str, ip_address: str, port: int=502):
self.device_name = device_name
self.ip = ip_address
self.port = port
self.client = None
self.connect_to_modbus()
self.registers = None
self.get_registers()
def connect_to_modbus(self):
port = self.port
self.client = ModbusTcpClient(self.ip, port=port)
try:
if not self.client.connect():
print("Verbindung zum Shelly-Logger fehlgeschlagen.")
exit(1)
print("Verbindung zum Shelly-Logger erfolgreich.")
except KeyboardInterrupt:
print("Beendet durch Benutzer (Ctrl+C).")
finally:
self.client.close()
def get_registers(self):
# Excel-Datei mit den Input-Registerinformationen
excel_path = "modbus_registers/shelly_pro_3m_registers.xlsx"
xls = pd.ExcelFile(excel_path)
df_input_registers = xls.parse()
# Relevante Spalten bereinigen
df_clean = df_input_registers[['MB Adresse', 'Beschreibung', 'Variabel Typ']].dropna()
df_clean['MB Adresse'] = df_clean['MB Adresse'].astype(int)
# Dictionary aus Excel erzeugen
self.registers = {
row['MB Adresse']: {
'desc': row['Beschreibung'],
'type': 'REAL' if row['Variabel Typ'] == 'REAL' else 'INT'
}
for _, row in df_clean.iterrows()
}
def get_state(self):
data = {}
data['Zeit'] = time.strftime('%Y-%m-%d %H:%M:%S')
for address, info in self.registers.items():
reg_type = info['type']
result = self.client.read_input_registers(address, count=2 if reg_type == 'REAL' else 1)
if result.isError():
print(f"Fehler beim Lesen von Adresse {address}: {result}")
continue
packed = struct.pack(">HH", result.registers[1], result.registers[0])
value = round(struct.unpack(">f", packed)[0], 2)
print(f"Adresse {address} - {info['desc']}: {value}")
data[f"{address} - {info['desc']}"] = value
return data

52759
terminal_log

File diff suppressed because it is too large Load Diff

110
test_wr.py Normal file
View File

@@ -0,0 +1,110 @@
from pymodbus.client import ModbusTcpClient
import struct
import sys
from typing import Optional
# === Verbindungseinstellungen ===
MODBUS_IP = "192.168.1.112"
MODBUS_PORT = 502 # SetApp: 1502; LCD-Menü: 502 -> ggf. anpassen
UNIT_ID = 1 # Default laut Doku: 1
client = ModbusTcpClient(MODBUS_IP, port=MODBUS_PORT)
if not client.connect():
print("Verbindung fehlgeschlagen.")
sys.exit(1)
def read_regs(addr: int, count: int):
"""Hilfsfunktion: liest 'count' Holding-Register ab base-0 'addr'."""
rr = client.read_holding_registers(address=addr, count=count)
if rr.isError():
return None
return rr.registers
def read_string(addr: int, words: int) -> Optional[str]:
"""
SunSpec-Strings: ASCII, Big-Endian, 2 Bytes pro Register, 0x00 gepadded.
"""
regs = read_regs(addr, words)
if regs is None:
return None
b = b"".join(struct.pack(">H", r) for r in regs)
# SunSpec Strings sind meist mit \x00 und Spaces gepadded:
s = b.decode("ascii", errors="ignore").rstrip("\x00 ").strip()
return s or None
def to_int16(u16: int) -> int:
"""unsigned 16 -> signed 16"""
return struct.unpack(">h", struct.pack(">H", u16))[0]
def apply_sf(raw: int, sf: int) -> float:
return raw * (10 ** sf)
def read_scaled(value_addr: int, sf_addr: int) -> Optional[float]:
regs = read_regs(value_addr, 1)
sf = read_regs(sf_addr, 1)
if regs is None or sf is None:
return None
raw = to_int16(regs[0])
sff = to_int16(sf[0])
return apply_sf(raw, sff)
def read_u32_with_sf(value_addr: int, sf_addr: int) -> Optional[float]:
"""
Liest 32-bit Zähler (acc32, Big-Endian, 2 Register) + SF.
"""
regs = read_regs(value_addr, 2)
sf = read_regs(sf_addr, 1)
if regs is None or sf is None:
return None
# Big-Endian zusammenbauen:
u32 = (regs[0] << 16) | regs[1]
sff = to_int16(sf[0])
return apply_sf(u32, sff)
# ==== Common Block (base-0) ====
manufacturer = read_string(40004, 16) # C_Manufacturer
model = read_string(40020, 16) # C_Model
version = read_string(40044, 8) # C_Version
serial = read_string(40052, 16) # C_SerialNumber
print(f"Hersteller: {manufacturer}")
print(f"Modell: {model}")
print(f"Version: {version}")
print(f"Seriennummer: {serial}")
# ==== Inverter Block (base-0) ====
# AC Power + Scale Factor
ac_power = read_scaled(40083, 40084) # I_AC_Power, I_AC_Power_SF
if ac_power is not None:
print(f"AC Power: {ac_power} W")
else:
print("Fehler beim Lesen von AC Power")
# AC Spannung L-N Durchschnitt (falls 1ph/3ph mit N verfügbar) + SF
ac_voltage = read_scaled(40079, 40082) # I_AC_VoltageAN, I_AC_Voltage_SF
if ac_voltage is not None:
print(f"AC Spannung: {ac_voltage} V")
# AC Frequenz + SF
ac_freq = read_scaled(40085, 40086) # I_AC_Frequency, _SF
if ac_freq is not None:
print(f"Frequenz: {ac_freq} Hz")
# DC Power + SF
dc_power = read_scaled(40100, 40101) # I_DC_Power, _SF
if dc_power is not None:
print(f"DC Power: {dc_power} W")
# Lifetime Energy (AC_Energy_WH, acc32) + SF
lifetime_wh = read_u32_with_sf(40093, 40095) # I_AC_Energy_WH, _SF
if lifetime_wh is not None:
print(f"Lifetime Energy: {lifetime_wh} Wh")
# Status
status_regs = read_regs(40107, 2) # I_Status, I_Status_Vendor
if status_regs:
i_status = status_regs[0]
i_status_vendor = status_regs[1]
print(f"Status: {i_status} (Vendor: {i_status_vendor})")
client.close()