Das Lastmanagement schaltet die Güllemixer - und pumpen während der Melkvorgänge aus, um eine Überlastung des elektrischen Hauptanschlusses zu verhindern.
Das Lastmanagement wurde für einen Milchviehbetrieb umgesetzt, auf dem ein neuer Milchviehstall gebaut wurde. Im Zuge dieser Baumaßnahmen sind viele neue elektrische Verbraucher und insbesondere leistungsstarke Güllemixer- und pumpen hinzugekommen. Im darauffolgenden Winter lösten während des Melkens die Sicherungen des elektrischen Hauptanschlusses des Betriebs aufgrund von Überlast aus. Deshalb wurde ein Lastmanagement umgesetzt, das während der Melkvorgänge die Gülletechnik sperrt.
Die nachfolgende Beschreibung ermöglicht den Nachbau des Lastmanagements. Dafür sind Änderungen und Erweiterungen an elektrischen Anlagen notwendig. Die DGUV Vorschrift 3 schreibt vor, dass elektrische Anlagen und Betriebsmittel nur von einer Elektrofachkraft oder unter Leitung und Aufsicht einer Elektrofachkraft errichtet, geändert und instandgehalten werden dürfen [1].
Die folgende Beschreibung richtet sich an Menschen, die Grundkenntnisse in den Themenbereichen Elektrotechnik, Netzwerk und Programmieren haben. Erfahrungen mit dem Einplatinencomputer Raspberry Pi sind von Vorteil. Um in das Thema einzusteigen bietet sich die Herstellerwebseite des Raspberry Pi an [2]. Einen guten Einstieg in die Programmiersprache Python ermöglicht die Seite python.org [3].
Unabhängig von der Gesamtumsetzung sollen aber auch einzelne Teile dieser Beschreibung Möglichkeiten aufzeigen, wie ein Lastmanagement auf dem eigenen Betrieb umgesetzt werden kann.
Zur Planung des Lastmanagements wurden die Ursachen der Überlastung des Hausanschlusses untersucht. Die Auswertung der Lastgänge in Abbildung 1 bis einschließlich 6 Tage vor der Sicherungsauslösung am 11.02. zeigte, dass an einigen Tagen die Hauptsicherungen überlastet wurden. Der Hauptanschluss ist mit NH-Sicherungen (Niederspannungs-Hochleistungssicherungen) mit einem Nennstrom von 63 A abgesichert.
NH-Sicherungen können Ströme bis zu ihrem Nennstrom dauerhaft führen. Höhere Ströme können zum Schmelzen des Schmelzleiters im Inneren der Sicherung und damit zu dessen Auslösung führen. Für jede Sicherung geben Zeit/Strom-Kennlinien an, welcher Strom nach welcher Zeit zur Sicherungsauslösung führt. Die Erwärmung des Schmelzleiters durch den Strom ist von vielen Umgebungsbedingungen und Fertigungstoleranzen abhängig. Daher Stellen die Zeit/Strom-Kennlinien keine scharfen Auslösegrenzen, sondern Mittelwerte mit statistischen Abweichungen dar. Eine Überlastung der Sicherung ohne Auslösung kann zu einer Vorschädigung des Schmelzleiters führen. Zukünftig kann die Auslösung dadurch schon bei einer geringeren Überlastung erfolgen [4].
Der Tageslastgang in Abbildung 2 des Gesamtbetriebs zeigt die Sicherungsauslösung am 11.02. um etwa 6:20 Uhr, nachdem der Strom eine knappe Stunde höher als der Nennstrom der Sicherung war. Die Sicherungsauslösung erfolgte während des Melkbetriebs an einem sehr kalten Tag, an dem mehrere elektrische Heizlüfter und Heizbetriebe für Tränkwasser aktiv waren. Aus dem Lastgang des Gesamtbetriebs ist aber nicht ersichtlich, welche weiteren Geräte zur Überlastung geführt haben. Weitere Auswertungen der Daten der betriebseigenen detaillierten Energieerfassung ergaben, dass die Hauptsicherungen immer dann überlastet wurden, wenn die Gülletechnik während eines Melkvorgangs aktiviert wurde. Der Tageslastgang in Abbildung 3 zeigt, aus welchen Verbrauchsgruppen sich der Gesamtstrom auf dem Betrieb zusammensetzt.
Die Mix- und Pumpvorgänge der Gülletechnik werden von einer automatischen Steuerung anhand der Füllstände in den Güllegruben gestartet und gestoppt. Daher kommt es immer wieder vor, dass die Gülletechnik während des Melkens aktiviert wird. Es ist bei dieser Steuerung nicht möglich Sperrzeiten zu programmieren. Die Erhöhung der Hauptanschlussleistung des Betriebs wird mittelfristig angestrebt, aber ist kurzfristig nicht umsetzbar, da hierfür neue Leitungen mit höheren Querschnitten vom Ortsnetztransformator zum Hausanschluss verlegt werden müssen.
Als Zwischenlösung wurde daher ein Lastmanagement umgesetzt, welches eine Aktivierung der Gülletechnik während des Melkens verhindert. Die Kapazität der Güllegrube ist ausreichend, sodass Pumpvorgänge gefahrlos einige Stunden verzögert werden können.
Die automatische Steuerung der Gülletechnik wird durch eine SPS (Speicherprogrammierbare Steuerung) realisiert. Die SPS fragt zyklisch den Pegelstand in den beiden Güllegruben ab. Übersteigt der Pegel den parametrierten Wert, wird die Gülle in der entsprechenden Grube zuerst gemixt und dann abgepumpt. Die SPS schließt und öffnet auch die Schieber der Güllegruben automatisch. Der vereinfachte Programmablauf ist in Abbildung 4 beschrieben.
Die einfachste Umsetzung eines Lastmanagements wäre die Parametrierung von Sperrzeiten, die den typischen Melkzeiten entsprechen, im Programm der SPS. Es könnte auch ein digitaler Eingang an der SPS programmiert werden, der je nach gemessenen Eingangssignal (z.B. 24 V Signal an/aus) die Aktivierung der Gülletechnik innerhalb des SPS Programms sperrt oder freigibt.
In diesem Fall war es nicht möglich das SPS Programm anzupassen und daher wurde eine Alternativlösung erarbeitet. Die Güllemixer und die Güllepumpe werden von der SPS über Leistungsschütze ein- und ausgeschaltet. Die Leistungsschütze werden mit 24 V Gleichspannung angesteuert. In Reihe zu den digitalen Ausgängen der SPS wurden Relais Platinen geschaltet, dessen Relais K4, K5 und K6 von einem Raspberry Pi gesteuert werden. Der Raspberry Pi kann die Ansteuerung der Mixer und der Pumpe durch öffnen der Relaiskontakte von K4, K5 und K6 unterbrechen. Abbildung 5 zeigt den vereinfachten Schaltplan dazu. Das SPS-Programm läuft unabhängig vom Schaltzustand von K4, K5 und K6 wie in Abbildung 4 dargestellt weiter.
Bei Aktivierung des Lastmanagements werden die Relaiskontakte von K4, K5 und K6 gleichzeitig geöffnet und damit beide Mixer und die Pumpe gesperrt. Bei der Deaktivierung werden zuerst die beiden Mixer über K4 und K5 wieder freigegeben und 10 Minuten später auch die Pumpe mit K6, denn es ist nicht klar in welchem Programmabschnitt die SPS ist, wenn das Lastmanagement deaktiviert wird. Die verzögerte Freigabe der Pumpe stellt sicher, dass keine ungemixte Gülle gepumpt wird, was andernfalls zu Verstopfungen der Gülleleitungen führen kann.
Abbildung 6 zeigt die Verdrahtung des Raspberry Pi mit dem Netzteil zur Spannungsversorgung, den Relais Platinen und einem Uhrzeitmodul (RTC - Real Time Clock). Durch das Uhrzeitmodul erhält der Raspberry Pi nach Neustart das korrekte Datum und die aktuelle Uhrzeit. Falls eine Internetverbindung besteht, wird das Datum und die Uhrzeit auch per NTP (Network Time Protokoll) synchronisiert.
Abbildung 7 zeigt ein Foto des Aufbaus auf einer 3D-gedruckten Montageplatte. Die Verbindungen zwischen den einzelnen Platinen sind mit der 0,25 mm² Schaltlitze, den Dupont Crimpkontakten und passenden Buchsengehäusen ausgeführt. Am Ende des Artikels ist eine Liste der verwendeten Bauteile aufgeführt. Die Verdrahtung der Relaiskontakte mit der SPS bzw. mit den Leistungsschützen wurde mit stärkerer Litze (1 mm² Querschnitt) und passenden Aderendhülsen ausgeführt.
Ist die Schaltung verdrahtet, kann zuerst die Funktion der Relais getestet werden. Als Hilfe kann die Pinbelegung des Raspberry Pi mit dem Befehl “pinout” im Terminal angezeigt werden (siehe Abbildung 8).
Das folgende Python Skript schaltet nacheinander die Relais an den GPIOs 17, 18, 23 und 22 für 3 Sekunden ein. Zu beachten ist, dass die Relais anziehen, wenn auf den entsprechenden GPIO der Spannungspegel “Low” geschaltet wird. Beim Spannungspegel “High” fällt das entsprechende Relais ab. Man spricht auch von negativer Logik, denn die logische 0 (Relais inaktiv) entspricht dem Spannungspegel “High” am GPIO und die logische 1 (Relais angezogen) entspricht dem Spannungspegel “Low” [5].
from time import sleep
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
gpio_ids = [17,18,23,22]
for gpio_id in gpio_ids:
GPIO.setup(gpio_id, GPIO.OUT, initial=GPIO.HIGH)
for gpio_id in gpio_ids:
GPIO.output(gpio_id,GPIO.LOW)
sleep(3)
GPIO.output(gpio_id,GPIO.HIGH)
GPIO.cleanup()
Das Kriterium zum Aktivieren und Deaktivieren des Lastmanagements ist der Gesamtstrom in der Elektroverteilung im Melkstand. Im Verteilerkasten ist ein Energiemanager EM420 verbaut. Der Energiemanager ist Teil der Energieerfassung vieler einzelner Verbraucher auf dem Betrieb. Eine am EM 420 angeschlossene Sensorbar misst auch die Gesamtströme des Melkstands in allen 3 Phasen des Drehstromnetzes. Der EM420 bietet die Möglichkeit der Datenabfrage über eine REST API (REST = Representational State Transfer, API = Application Programming Interface). Das Datenformat dieser Programmierschnittstelle ist JSON (Java Script Object Notation). Eine Ausführliche Beschreibung bietet die Herstellerdokumentation des EM420 [6].
Zunächst muss im Webinterface des EM420 ein Access Token (Zugriffsschlüssel) unter “Profile” erstellt und freigegeben werden. Der Access Token wird bei der Erstellung einmalig angezeigt und sollte kopiert und auf dem Raspberry Pi unter “/home/pi/access_token.txt” gespeichert werden. In diese Textdatei dürfen keine weiteren Zeichen, Zeilen oder Steuerzeichen wie dem Zeilenende Zeichen hinzugefügt werden.
Abweichend zur Herstellerdokumentation lauten die URL (Uniform Resource Locator) der Datenendpunkte für die Datenabfrage:
Smart-Meter (EM420 Grundgerät): /api/json/local/values/smart-meter
Sensorbars (falls angeschlossen): /api/json/local/values/sensors
Das Format der JSON-Daten vom Smart-Meter (Grundgerät EM 420) ist immer gleich. Das Format der Sensorbars ist von der Anzahl der angeschlossenen Sensorbars und dessen Konfiguration abhängig. Jeder Sensor einer Sensorbar wird einer elektrischen Phase zugeordnet. In den JSON-Daten ist die Sensor ID enthalten. Im folgenden Beispiel wird der Strom von Sensor “S6” dargestellt.
Für die Datenabfrage muss eine Netzwerkverbindung zwischen dem Raspberry Pi und dem EM420 bestehen. Im folgenden Beispiel wird das EM420 über den Hostnamen angesprochen (EM420-74636926), die sich aus “EM420-” und der Seriennummer des Geräts zusammensetzt. Ebenso kann das Gerät über dessen IP-Adresse erreicht werden.
Das Python Skript empfängt einmalig die Daten des Smart Meters und den Sensorbars und stellt sie im JSON-Format dar. Zum Schluss wird der Strom in Phase 1 vom Smart Meter und vom Stromsensor “S6” formatiert dargestellt.
import requests
host = "EM420-74636926"
# Authoriation
with open("access_token.txt") as fid:
access_token = "Bearer " + fid.read() # Read access token from text file
authorisation_header = {'Authorization' : access_token}
# Concatenate EM420 data url
url_smartmeter = "http://" + host + "/api/json/local/values/smart-meter" # url smartmeter data
url_sensor = "http://" + host + "/api/json/local/values/sensors" # url sensor data
# Read smartmeter data
resp_smartmeter = requests.get(url_smartmeter, headers=authorisation_header,timeout=3)
# Read sensor bar data
resp_sensor = requests.get(url_sensor, headers=authorisation_header,timeout=3)
# Print out all json data
print(resp_smartmeter.json())
print(resp_sensor.json())
# Print out current in Phase 1
current_L1 = resp_smartmeter.json()['smart-meter']['values']['current_L1'] / 1000 # data is in mA
print("Strom in Phase 1 (Smartmeter): %0.3f Ampere" % current_L1)
current_L1 = resp_sensor.json()['s6']['values']['current_L1'] / 1000 # data is in mA
print("Strom in Phase 1 (Sensor 6): %0.3f Ampere" % current_L1)
Beispielausgabe des Skripts (zur besseren Lesbarkeit wurden Zeilensprünge und Einrückungen hinzugefügt sowie einige Zeilen mit … ersetzt):
{
'smart-meter': {
'configuration_id': 'f8b0289fc46e6871cd30ecf5b0c61cbcddfc5c42',
'status': 'STATUS_OK',
'timestamp': {
'seconds': 1705582873,
'nanos': 994799871
},
'values': {
'active_energy_+': 616200,
...
'apparent_power_-_L3': 0,
'current_L1': 18,
'current_L2': 0,
'current_L3': 0,
'power_factor': 1000,
...
'voltage_L3': 0
}
}
}
{
's6': {
'configuration_id': '767d2f2a9c59cfd318d4cda9f2ccfe611f572555',
'status': 'STATUS_OK',
'timestamp': {
'seconds': 1705582874,
'nanos': 127439079
},
'values': {
'active_energy_+_L1': 0,
'active_power_+_L1': 0,
'apparent_energy_+_L1': 0,
'apparent_power_+_L1': 0,
'current_L1': 0,
'power_factor_L1': 0,
'voltage_L1': 0
}
},
's7': {
...
Strom in Phase 1 (Smartmeter): 0.018 Ampere
Strom in Phase 1 (Sensor 6): 0.000 Ampere
Die wichtigsten Parameter für das Lastmanagement können in der Textdatei “config.ini” eingesehen und angepasst werden. Die Parameter werden beim Start vom Python Skript eingelesen und verwendet, sodass der Raspberry Pi neu gestartet werden sollte, wenn Parameter in der Datei angepasst wurden. Für die eigene Umsetzung muss mindestens der Eintrag “ip_EM420” auf die IP-Adresse oder Hostname des verwendeten EM420 angepasst werden.
# config.ini
[GENERAL]
# switch off times as fallback if current measurement unavailable
# Format bgn-end HH:MM:SS-HH:MM:SS,HH:MM:SS-HH:MM:SS,HH:MM:SS-HH:MM:SS
switch_off_times = 05:00:00-08:00:00,16:00:00-19:00:00
# currents greater this value enable power management [A]
i_load_mgt_on = 35.0
# currents less this value enable power management [A]
i_load_mgt_off = 10.0
# time delay switching on pump after power management disabled [s]
t_delay_pump_on = 600.0
# sample time for program loop [s]
ts = 1.0
# average time measurement [s]
t_mean = 60.0
# time wait for network at start [s]
t_wait_for_network = 60.0
# time sleep if error accured [s]
t_sleep_on_error = 60.0
# timeout while reading from json API [s]
t_timeout_read_json = 3.0
# CLI output only for testing, default false
screen_output = false
# EM420's IP or hostname
ip_EM420 = 192.168.2.53
# file contents access token, avoid control pictures at end in this file
token_filename = access_token.txt
# Raspberry gpio settings
gpio_mixer1 = 17
gpio_mixer2 = 18
gpio_pump = 23
gpio_spare = 22
Im Folgenden wird der Programmablauf mit der zuvor dargestellten Parametrierung beschrieben. Die Gesamtströme der 3 Phasen im Melkstand werden etwa sekündlich abgefragt. Aus den Werten werden für jede Phase gleitende 60 Sekunden Mittelwerte gebildet. Übersteigt der Mittelwert in einer Phase 35 Ampere, wird das Lastmanagement aktiviert. Unterschreiten danach die Mittelwerte aller Phasen gleichzeitig 10 Ampere wird es wieder deaktiviert. Diese Stromgrenzen und die Mittelungszeit wurden empirisch von mehreren Tageslastgängen des Melkstands festgelegt. Ziel war es dabei, die Grenzen so festzulegen, dass bei jedem Melkvorgang das Lastmanagement nur einmalig aktiviert und deaktiviert wird. Ist die Strommessung nicht verfügbar oder gestört, wird das Lastmanagement zu festen Zeiten aktiviert (In der Beispielkonfiguration von 5:00 bis 9:00 Uhr und von 16:00 bis 19:00 Uhr).
Das Programm gibt die Strommittelwerte und den Lastmanagement Status in einer Textdatei aus, deren Inhalt im nächsten Kapitel näher beschrieben wird.
Python Programmcode:
#!/usr/bin/env python3
# Loadmanagement manure mixer and pump based on current EM420 measurement
#
#
# \|||/
# (o o)
# ,~~~~~ooO~~(_)~~~~~~~~~~,
# | CAU Kiel | **
# | Inst. f. Landwirtsch. | _____ **
# | Verfahrenstechnik | /|`O``\ *
# | Rainer Kock | |_|-/ \___$_
# | Version | / \-.\| |
# | 2023-11-24 | _ _ _ _/( () )=====/^\'
# '~~~~~~~~~~~~~~~~~ooO~~~' ~~~~~\_\_\_\_-\__/------\_/------
# | | | |
# |__| |__|
# || ||
# || ||
# ooO Ooo
# History
#
# 2023-10-16
# First Version
#
# 2023-11-24
# Create new data file each day
#import json
import requests
import logging
from time import time,sleep
from datetime import datetime, date
from RPi import GPIO
from configparser import ConfigParser
from datetime import datetime, timedelta
# REST API (JSON format) EM 420
class Json_EM420:
def __init__(self,ip,access_token,timeout = 30):
self.ip = ip
self.access_token = access_token
self.url_smartmeter = "http://" + self.ip + "/api/json/local/values/smart-meter"
self.url_sensor = "http://" + self.ip + "/api/json/local/values/sensors"
self.url_group = "http://" + self.ip + "/api/json/local/values/groups"
self.my_headers = {'Authorization' : self.access_token}
self.timeout = timeout
def read_smartmeter(self):
resp_smartmeter = requests.get(self.url_smartmeter, headers=self.my_headers,timeout=self.timeout)
resp_smartmeter.raise_for_status()
return resp_smartmeter
def read_groups(self):
resp_group = requests.get(self.url_group, headers=self.my_headers,timeout=self.timeout)
resp_group.raise_for_status()
return resp_group
def read_sensors(self):
resp_sensor = requests.get(self.url_sensor, headers=self.my_headers,timeout=self.timeout)
resp_sensor.raise_for_status()
return resp_sensor
# Current sensors of EM 420
class Sensor:
def __init__(self, sensor_id, sensorname, label, unit, lim_load_mgt_on ,lim_load_mgt_off):
self.sensor_id = sensor_id
self.sensorname = sensorname
self.label = label
self.unit = unit
self.data = []
self.lim_load_mgt_on = lim_load_mgt_on
self.lim_load_mgt_off = lim_load_mgt_off
self.exceeded = False
self.t_bgn_exceeded = 0
def check(self):
if not self.exceeded and self.lim_load_mgt_on is not None:
if self.avg > self.lim_load_mgt_on:
self.t_bgn_exceeded = time()
self.exceeded = True
elif self.lim_load_mgt_off is not None:
if self.avg < self.lim_load_mgt_off:
self.exceeded = False
def rm_data(self):
self.data = []
@property
def avg(self):
if not len(self.data):
return 0.0
else:
return sum(self.data)/len(self.data)
def time_exceeded(self):
if self.exceeded:
return time()-self.t_bgn_exceeded
else:
return 0
# Relay for power management
class Actuator:
def __init__(self, gpio_id, active_level = GPIO.LOW, initial_state = False, t_delay_on = 0):
self.gpio_id = gpio_id
self.active_level = active_level
self.state = initial_state
self.timer_delay_on_active = True # Must be true for first start
self.t_delay_on = t_delay_on
self.t_bgn_delay_on = time()-self.t_delay_on
if self.state:
self.t_bgn_off = 0
self.t_bgn_on = time()
GPIO.setup(self.gpio_id, GPIO.OUT, initial=self.active_level)
else:
self.t_bgn_off = time()
self.t_bgn_on = 0
GPIO.setup(self.gpio_id, GPIO.OUT, initial=not self.active_level)
def on(self):
if not self.state:
GPIO.output(self.gpio_id, self.active_level)
self.state = True
def off(self):
if self.state:
GPIO.output(self.gpio_id, not self.active_level)
self.state = False
def timer_delay_on(self):
return time() - self.t_bgn_delay_on > self.t_delay_on
def start_timer_delay_on(self):
self.timer_delay_on_active = True
self.t_bgn_delay_on = time()
def stop_timer_delay_on(self):
self.timer_delay_on_active = False
@property
def time_on(self):
if state:
return time() - self.t_bgn_on
else:
return 0
def time_off(self):
if not state:
return time() - self.t_bgn_off
else:
return 0
# Test if a time is within interval
def is_in_between(time_bgn_str, time_end_str, time_check):
# convert HH:MM:SS strings to datetime
bgn_datetime = datetime.strptime(time_bgn_str, "%H:%M:%S")
end_datetime = datetime.strptime(time_end_str, "%H:%M:%S")
check_datetime = datetime.strptime(time_check, "%H:%M:%S")
# if interval override midnight
if end_datetime < bgn_datetime:
return bgn_datetime >= check_datetime <= end_datetime
else:
return bgn_datetime <= check_datetime <= end_datetime
# Writes caption into data file
def filename_and_caption(sensors):
# Write Caption into data file
filename = datetime.now().strftime("/home/pi/data/data_%Y-%m-%d.csv")
str_caption = "Date and time,"
for sensor in sensors:
str_caption += sensor.sensorname + " [" + sensor.unit + "],"
str_caption += "Mixer 1, Mixer 2, Pumpe, Control by"
with open(filename, "a") as fid:
fid.write(str_caption + "\r\n")
if screen_output:
print(str_caption)
return filename
# Parameters (read from config.ini)
config = ConfigParser()
config.read('config.ini')
screen_output = config.getboolean('GENERAL','screen_output') # CLI output only for testing
ip_EM420 = config.get('GENERAL','ip_EM420') # EM420's IP or hostname
token_filename = config.get('GENERAL','token_filename') # file contents access token, avoid control pictures at end in this file
i_load_mgt_on = config.getfloat('GENERAL','i_load_mgt_on') # currents greater this value enable power management
i_load_mgt_off = config.getfloat('GENERAL','i_load_mgt_off') # currents less this value enable power management
t_delay_pump_on = config.getfloat('GENERAL','t_delay_pump_on') # time delay switching on pump after power management disabled
ts = config.getfloat('GENERAL','ts') # sample time for program loop
t_mean = config.getfloat('GENERAL','t_mean') # average time measurement
t_wait_for_network = config.getfloat('GENERAL','t_wait_for_network') # wait for network at start
t_sleep_on_error = config.getfloat('GENERAL','t_sleep_on_error') # time sleep if error accured
t_timeout_read_json = config.getfloat('GENERAL','t_timeout_read_json') # timeout while reading from json API
gpio_mixer1 = config.getint('GENERAL','gpio_mixer1')
gpio_mixer2 = config.getint('GENERAL','gpio_mixer2')
gpio_pump = config.getint('GENERAL','gpio_pump')
gpio_spare = config.getint('GENERAL','gpio_spare')
# table with switch of timings
switch_off_times = [x.strip() for x in config.get('GENERAL','switch_off_times').split(',')]
if screen_output:
print("Wait %i Seconds for network" % t_wait_for_network) ## better ping and wait some time?
sleep(t_wait_for_network) # wait some time for network after reboot
# Logging file
logging.basicConfig(filename='logfile.log',
format='%(levelname)-8s %(asctime)s,%(msecs)-3d [%(lineno)-4d] %(message)s',
datefmt='%Y-%m-%d %H:%M:%S', level=logging.WARNING)
# EM 420 with json interface
with open(token_filename) as fid:
access_token = "Bearer " + fid.read() # Read access token
em420 = Json_EM420(ip_EM420,access_token,t_timeout_read_json)
# Sensor IDs, names and current limits (Sensor IDs should be moved into config.ini)
sensors = [
Sensor("s6", "Summe_L1", "current_L1", "A", i_load_mgt_on, i_load_mgt_off),
Sensor("s7", "Summe_L2", "current_L2", "A", i_load_mgt_on, i_load_mgt_off),
Sensor("s8", "Summe_L3", "current_L3", "A", i_load_mgt_on, i_load_mgt_off),
#Sensor("s9", "Vakuumpumpe_1_L1", "current_L1", "A", None, None ),
#Sensor("s10", "Vakuumpumpe_1_L2", "current_L2", "A", None, None ),
#Sensor("s11", "Vakuumpumpe_1_L3", "current_L3", "A", None, None ),
]
# Actuatiors
GPIO.setmode(GPIO.BCM) # use BSM pin numbering
mixers = [Actuator(gpio_id = gpio_mixer1, active_level = GPIO.HIGH, initial_state = True),
Actuator(gpio_id = gpio_mixer2, active_level = GPIO.HIGH, initial_state = True)]
pump = Actuator(gpio_id = gpio_pump, active_level = GPIO.HIGH, initial_state = True, t_delay_on = t_delay_pump_on)
spare = Actuator(gpio_id = gpio_spare, active_level = GPIO.HIGH, initial_state = True) # set fourth relay off
# Loop
current_day = 0
t_start = time()
run = True
while(run):
sleep(ts) # sleep reduces CPU load
try:
# read all sensors from EM420
resp_sensor = em420.read_sensors()
# append list with measurement data for all sensors in list
for sensor in sensors:
current = resp_sensor.json()[sensor.sensor_id]["values"][sensor.label]
sensor.data.append(current/1000) # value in [mA]
except KeyboardInterrupt:
run = False
GPIO.cleanup()
except Exception as e:
logging.error(e)
if screen_output:
print(e)
sleep(t_sleep_on_error)
# t_mean exceeded
if time() - t_start >= t_mean:
# start time next interval
t_start += t_mean
# make new file each day
if current_day != date.today():
filename = filename_and_caption(sensors)
current_day = date.today()
# check if current measurement is available
if all([len(sensor.data) for sensor in sensors]):
# if current measurement is available
control = 'by_current'
else:
# if current measurement is unavailable control by fallback times
control = 'by_time'
# data file output
out_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
for sensor in sensors:
sensor.check()
out_str += ",% 4.1f" % sensor.avg
sensor.rm_data() # Erase data
# power management control
if control == 'by_current':
# activate load management if any current exceeds limit
if any(sensor.exceeded for sensor in sensors):
for mixer in mixers:
mixer.off()
out_str += ",0"
pump.off()
pump.stop_timer_delay_on()
out_str += ",0"
# deactivate load management if all currents fall below limit
if all(sensor.exceeded == False for sensor in sensors): # Load mgt off
for mixer in mixers:
mixer.on()
out_str += ",1"
if not pump.timer_delay_on_active:
pump.start_timer_delay_on()
if not pump.timer_delay_on():
out_str += ",0"
else:
pump.on()
out_str += ",1"
elif control == 'by_time':
switch_off_timings = [switch_off_time.split('-') for switch_off_time in switch_off_times]
[switch_off_timing.append(datetime.now().strftime('%H:%M:%S')) for switch_off_timing in switch_off_timings]
print(switch_off_timings)
if any([is_in_between(bgn,end,ctime) for bgn,end,ctime in switch_off_timings]):
for mixer in mixers:
mixer.off()
out_str += ",0"
pump.off()
pump.stop_timer_delay_on()
out_str += ",0"
else:
for mixer in mixers:
mixer.on()
out_str += ",1"
if not pump.timer_delay_on_active:
pump.start_timer_delay_on()
if not pump.timer_delay_on():
out_str += ",0"
else:
pump.on()
out_str += ",1"
# write data to file or screen
out_str += "," + control
if screen_output:
print(out_str)
with open(filename, "a") as fid:
fid.write(out_str + "\r\n")
# if raspberry time set externally, reset t_start
if abs(time() - t_start) > t_mean + 5:
t_start = time()
Nachfolgend ist ein Beispiel des Inhalts einer Datendatei dargestellt. Die Daten werden in einer Textdatei als csv (comma separated values) gespeichert. Jeden Tag wird eine neue Datei im Verzeichnis “/home/pi/data” angelegt. Im Namen ist das Datum nach dem Muster “data_yyyy-mm-dd.csv” (z.B. “data_2024-01-17.csv”) enthalten.
Die Spalten in der Datei bedeuten:
Date and time: Datum und Uhrzeit
Summe_L1 [A]: Strom in Phase 1 in Ampere
Summe_L2 [A]: Strom in Phase 2 in Ampere
Summe_L3 [A]: Strom in Phase 3 in Ampere
Mixer 1: Status Freigabe/Sperrung von Mixer 1 (0 = gesperrt, 1 = freigegeben)
Mixer 2: Status Freigabe/Sperrung von Mixer 2 (0 = gesperrt, 1 = freigegeben)
Pumpe: Status Freigabe/Sperrung von der Pumpe (0 = gesperrt, 1 = freigegeben)
Control by: “by_current” wenn das Lastmanagement über die Strommessung gesteuert wird, “by_time”
Lastmanagement über feste Zeiten als Backuplösung falls die Strommessung gestört ist
Im Beispiel wurde das Lastmanagement um 05:49:50 Uhr aktiviert und ab 09:54:50 Uhr deaktiviert. Die Pumpe wird es 10 min. später ebenfalls wieder freigegeben. Bei der verwendeten Parametrierung wird jede Minute ein Datensatz geschrieben. Für die Übersichtlichkeit wurden mehrere Zeilen der ursprünglichen Datendatei mit “…” ersetzt.
Date and time,Summe_L1 [A],Summe_L2 [A],Summe_L3 [A],Mixer 1, Mixer 2, Pumpe, Control by
2024-01-15 00:00:50, 3.0, 3.5, 0.4,1,1,1,by_current
...
2024-01-15 05:46:51, 30.7, 27.6, 23.4,1,1,1,by_current
2024-01-15 05:47:50, 30.1, 28.0, 22.9,1,1,1,by_current
2024-01-15 05:48:50, 31.3, 29.2, 23.9,1,1,1,by_current
2024-01-15 05:49:50, 36.0, 34.1, 28.8,0,0,0,by_current
2024-01-15 05:50:50, 40.1, 37.7, 32.6,0,0,0,by_current
2024-01-15 05:51:50, 31.6, 29.1, 24.7,0,0,0,by_current
...
2024-01-15 09:51:50, 13.4, 11.2, 9.1,0,0,0,by_current
2024-01-15 09:52:51, 13.4, 11.2, 9.1,0,0,0,by_current
2024-01-15 09:53:50, 13.3, 11.2, 9.1,0,0,0,by_current
2024-01-15 09:54:50, 6.1, 6.0, 3.3,1,1,0,by_current
2024-01-15 09:55:51, 3.0, 3.4, 0.4,1,1,0,by_current
2024-01-15 09:56:50, 3.0, 3.4, 0.4,1,1,0,by_current
2024-01-15 09:57:51, 3.0, 3.4, 0.4,1,1,0,by_current
2024-01-15 09:58:51, 3.0, 3.4, 0.4,1,1,0,by_current
2024-01-15 09:59:50, 3.0, 3.4, 0.4,1,1,0,by_current
2024-01-15 10:00:50, 3.0, 3.4, 0.4,1,1,0,by_current
2024-01-15 10:01:51, 3.0, 3.4, 1.2,1,1,0,by_current
2024-01-15 10:02:51, 3.0, 3.4, 1.3,1,1,0,by_current
2024-01-15 10:03:50, 2.9, 3.4, 0.6,1,1,0,by_current
2024-01-15 10:04:51, 2.9, 3.4, 0.4,1,1,1,by_current
2024-01-15 10:05:50, 2.9, 3.4, 0.4,1,1,1,by_current
2024-01-15 10:06:50, 2.9, 3.4, 0.4,1,1,1,by_current
...
Nach dem Booten sollte das Python Programm automatisch gestartet werden, um sicherzustellen, dass das Lastmanagement auch nach einem Stromausfall wieder funktioniert. Das Python Programm hat den Pfad “/home/pi/loadmanagement.py”.
Für den System- und Sitzungs-Manager “systemd” wird eine sogenannte “unit”-Datei “loadmanagement.service” mit den nötigen Informationen erstellt. Eine Beschreibung System- und Sitzungs-Managers “systemd” findet sich unter anderem im Debian-Wiki [7].
sudo nano /lib/systemd/system/loadmanagement.service
Folgender Inhalt wird in die Datei kopiert und gespeichert Tastenkombination “Strg + X” → Änderungen übernehmen “Y” für yes).
[Unit]
Description=Loadmanagement
After=multi-user.target
[Service]
Type=idle
ExecStart=/usr/bin/python3 -u loadmanagement.py
WorkingDirectory = /home/pi
StandardOutput=inherit
StandardError=inherit
Restart=always
RestartSec = 600
[Install]
WantedBy=multi-user.target
Anschließend wird die Datei ausführbar gemacht:
sudo chmod 644 /lib/systemd/system/loadmanagement.service
Der neue Dienst wird gestartet:
sudo systemctl daemon-reload
sudo systemctl enable loadmanagement.service
sudo systemctl start loadmanagement.service
Mit folgendem Befehl kann der Status des Service “loadmanagement” abgerufen werden
pi@raspi-loadmgt1:~ $ systemctl status loadmanagement
● loadmanagement.service - Loadmanagement
Loaded: loaded (/lib/systemd/system/loadmanagement.service; enabled; vendo>
Active: active (running) since Wed 2023-12-27 09:28:49 CET; 3 weeks 0 days>
Main PID: 1038 (python3)
Tasks: 1 (limit: 1599)
CPU: 5h 38min 23.361s
CGroup: /system.slice/loadmanagement.service
└─1038 /usr/bin/python3 -u loadmanagement.py
Dec 27 09:28:49 raspi-loadmgt1 systemd[1]: Started Loadmanagement.
Die folgende Konfiguration wurde mit einem Raspberry Pi 4 B, der Betriebssystemversion Raspberry Pi OS 11 (Bullseye) und dem RTC Modul Whadda WPM352 (I2C, DS3231) getestet. Ein ausführliches Tutorial ist auf der Webseite von Adafruit zu finden [8]. Als erstes muss die I2C-Schnittestelle des Raspberry Pi aktiviert werden. Im Terminal wird dazu folgender Befehl ausgeführt. Alle folgenden Befehle werden ebenfalls im Terminalfenster ausgeführt.
sudo raspi-config
Im geöffneten Konfigurationsmenü wählt man “3 - Interface Options”, dann “I5 - I2C” und beantwortet die Frage “Would you like the ARM I2C interface to be enabled?” mit “Yes”. Nun beendet man das Menü mit der Schaltfläche “Finish”. Mit dem folgenden Befehl wird der Raspberry Pi neu gestartet
sudo reboot
Nach dem Neustart wird zunächst nötige Software installiert und anschließend getestet, ob das RTC-Modul verbunden ist. Wenn das RTC-Modul korrekt nach Abbildung 6 und 7 verdrahtet ist, wird unter Adresse #68 im I2C Bus ein Gerät angezeigt. Für die Installation der Software ist eine Internet-Verbindung notwendig. Je nach installiertem Betriebssystem ist es auch möglich, dass die benötigte Software schon vorhanden ist.
sudo apt-get install python-smbus i2c-tools
sudo i2cdetect -y 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- 68 -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
Für die Funktion des Uhrzeitmoduls wird die Datei “/boot/config.txt” editiert
sudo nano /boot/config.txt
Am Ende der Datei folgende Zeile anfügen
dtoverlay=i2c-rtc,ds3231
Änderungen übernehmen (Tatenkombination “Strg + X” → “Save modified buffer?” mit “Y” für yes)
Nach einem weiteren Neustart sollte die Abfrage des I2C Bus folgende Ausgabe haben. Statt “68” wird “UU” angezeigt.
sudo i2cdetect -y 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- UU -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
Ohne Uhrzeitmodul speichert der Raspberry die aktuelle Uhrzeit in einer Textdatei und zählt nach einem Neustart die Zeit von diesem Punkt weiter. Diese sogenannte Fake-Clock muss mit den folgenden Befehlen deaktiviert werden.
sudo apt -y remove fake-hwclock
sudo update-rc.d -f fake-hwclock remove
sudo systemctl disable fake-hwclock
Danach muss die Datei “/lib/udev/hwclock-set" editiert und angepasst werden.
sudo nano /lib/udev/hwclock-set
Es werden 3 Zeilen mit “#” auskommentiert und die letzten beiden Zeilen angepasst. Die Änderungen werden gespeichert (Tatenkombination “Strg + X” → “Save modified buffer?” mit “Y” für yes)
Angepasste Datei “/lib/udev/hwclock-set":
#!/bin/sh
# Reset the System Clock to UTC if the hardware clock from which it
# was copied by the kernel was in localtime.
dev=$1
#if [ -e /run/systemd/system ] ; then
# exit 0
#fi
/sbin/hwclock --rtc=$dev --systz
/sbin/hwclock --rtc=$dev --hctosys
Nun muss noch einmalig die richtige Uhrzeit auf dem RTC-Modul gesetzt werden, da das Modul im Auslieferungszustand keine gültige Uhrzeit besitzt. Mit dem Befehl “date” wird geprüft, ob der Raspberry bereits die korrekte Systemzeit hat.
pi@raspi-loadmgt1:~ $ date
Wed 17 Jan 10:20:47 CET 2024
Ist die Zeit nicht korrekt, kann sie manuell gesetzt werden. Im Beispiel wird das Datum 18.01.2024 und der Uhrzeit 8:15 (Mitteleuropäische Zeit, Central European Time - CET) gesetzt.
sudo date -s 'Mon Jan 18 08:15:00 CET 2024'
Wenn die Systemzeit korrekt ist, wird sie in das RTC-Modul geschrieben.
sudo hwclock -w
Anschließend werden die Zeit im RTC-Modul, die Systemzeit und die Funktion der Hardware-Clock nochmals geprüft.
pi@raspi-loadmgt1:~ $ sudo hwclock -r
2024-01-17 10:24:50.135137+01:00
pi@raspi-loadmgt1:~ $ date
Wed 17 Jan 10:24:54 CET 2024
pi@raspi-loadmgt1:~ $ timedatectl
Local time: Wed 2024-01-17 10:24:57 CET
Universal time: Wed 2024-01-17 09:24:57 UTC
RTC time: Wed 2024-01-17 09:24:57
Time zone: Europe/Berlin (CET, +0100)
System clock synchronized: yes
NTP service: active
RTC in local TZ: no
Nachfolgend geschieht die Synchronisation der Hardware-Clock automatisch.
Dipl. Ing. (FH) Rainer Kock, Technischer Mitarbeiter im Experimentierfeld Betriebsleitung und Stoffstrommanagement - Vernetzte Agrarwirtschaft in Schleswig-Holstein (BeSt-SH)