Mini-Dashboard für PV-Anlage
Sinn und Zweck

Aus Fernost gab es gerade sehr billig ein Gadget in Form eines Mini-Fernsehers, der im wesentlichen aus einem ESP32 mit einem 240x240 Pixel großen Farb-LCD besteht: das GIFTV von GeekMagic. Ich habe mir damit ein Mini-Dashboard für meine Photovoltaik-Anlage gebaut.
Komponenten
Übersicht
Das Dashboard wird als JPG-Bild mit dem Modul RSS on-the-fly erstellt. Ein at-Kommando ruft jede Minute ein Bild vom Modul ab und schiebt es über eine Perl-Funktion auf das GIFTV. Das GIFTV arbeitet dabei mit seiner Standard-Firmware im Bilderalbum-Modus.
Die Werte für das Dashboard stammen aus einem readingsProxy (neue Version ab April 2026), der die angezeigten Werte aus mehreren Geräten (SMA Energy Meter, SMA Inverter, Electricity Calculator) zusammenführt. Die Anzeige im Dashboard bedient sich der Widgets von DOIF (siehe DOIF/uiTable Schnelleinstieg).
readingsProxy
Einleitung
Der readingsProxy wird benötigt, um aus verschiedenen Geräten Werte zusammenzuziehen, diese verständlich zu benennen, und weitere Werte mit userReadings zu berechnen.
Ich möchte im Mini-Dashboard gerne folgende Werte sehen:
- Momentane Leistung der PV-Anlage in Watt: PVPowerSourceW
- Tageserzeugung Solarstrom in kWh: PVEnergySourcekWh
- Momentaner Stromverbrauch des Hauses in Watt: HousePowerSinkW
- Tagesverbrauch des Hauses in kWh: HouseEnergySinkkWh
- Momentane Netzeinspeisung oder Netzbezug in W: GridPowerW = GridPowerSourceW + GridPowerSinkW
- Netzbezug des Tages in kWh: GridEnergySourcekWh
- Momentane Batterieladung oder -entladung in W: BatteryPowerW = BatteryPowerSourceW + BatteryPowerSinkW
- Ladezustand der Batterie in %: BatteryPercent
Die Benennung der Werte und deren Vorzeichen folgen einer Logik:
- Es gibt Quellen (Source) und Senken (Sink) von Strom.
- Leistung mit positivem Vorzeichen wird einer Komponente entnommen (Quellen: PV-Anlage, Netzbezug, Batterieentladung).
- Leistung mit negativem Vorzeichen wird einer Komponente hinzugefügt (Senken: Netzeinspeisung, Hausstromverbrauch, Batterieladung).
- Die Energiemengen (PV-Anlage, Netzbezug, Netzeinspeisung, Batterieladung, Batterieentladung, Hausstromverbrauch) sind immer positiv.
Definition
Inspiration fand ich auf der Webseite Jürgens Technikwelt.
define aggr.PV readingsProxy \
smainverter:SPOT_PDC_SUM:PVPowerSourceW \
smainverter:SPOT_EPVTODAY:PVEnergySourceWh \
smaem:SMAEM1234567890_Bezug_Wirkleistung:GridPowerSourceW \
smaem:SMAEM1234567890_Einspeisung_Wirkleistung:GridPowerSinkW \
calc.Netzbezug:smaem_SMAEM1234567890_Bezug_Wirkleistung_Zaehler_EnergyDay:GridEnergySourcekWh \
calc.Netzeinspeisung:smaem_SMAEM1234567890_Einspeisung_Wirkleistung_Zaehler_EnergyDay:GridEnergySinkkWh \
smainverter:BAT_P_CHARGE:BatteryPowerSinkW \
smainverter:BAT_P_DISCHARGE:BatteryPowerSourceW \
smainverter:ChargeStatus:BatteryPercent \
smainverter:BAT_LOADTODAY:BatteryEnergySinkWh \
smainverter:BAT_UNLOADTODAY:BatteryEnergySourceWh \
smaem:SMAEM1234567890_GridFreq:GridFrequencyHz
attr aggr.PV stateFormat PV: PVPowerSourceW W < PVEnergySourcekWh kWh | Haus: HousePowerSinkW W > HouseEnergySinkkWh kWh | Netz: GridPowerW W < GridEnergySourcekWh kWh > GridEnergySinkkWh kWh | Batterie: BatteryPowerW W BatteryPercent % < BatteryEnergySourcekWh kWh > BatteryEnergySinkkWh kWh
attr aggr.PV userReadings \
GridPowerW { ReadingsNum("aggr.PV", "GridPowerSourceW", 0) - ReadingsNum("aggr.PV", "GridPowerSinkW", 0) }, \
BatteryPowerW { ReadingsNum("aggr.PV", "BatteryPowerSourceW", 0) - ReadingsNum("aggr.PV", "BatteryPowerSinkW", 0) }, \
PVEnergySourcekWh { ReadingsNum("aggr.PV", "PVEnergySourceWh", 0)/1000.0 }, \
BatteryEnergySourcekWh { ReadingsNum("aggr.PV", "BatteryEnergySourceWh", 0)/1000.0 }, \
BatteryEnergySinkkWh { ReadingsNum("aggr.PV", "BatteryEnergySinkWh", 0)/1000.0 }, \
HousePowerSinkW { -( ReadingsNum("aggr.PV", "PVPowerSourceW", 0)+ReadingsNum("aggr.PV", "GridPowerW", 0)+ReadingsNum("aggr.PV", "BatteryPowerW", 0) ) }, \
HouseEnergySinkkWh { ReadingsNum("aggr.PV", "PVEnergySourcekWh", 0)-ReadingsNum("aggr.PV","GridEnergySinkkWh", 0)+ReadingsNum("aggr.PV", "GridEnergySourcekWh", 0)-ReadingsNum("aggr.PV", "BatteryEnergySinkkWh",0)+ReadingsNum("aggr.PV", "BatteryEnergySourcekWh", 0) \
}
RSS
Definition
define sensorfeed2 RSS jpg has-1.example.com /opt/fhem/conf/sensorfeed2.layout
attr sensorfeed2 bg /opt/fhem/PictureFrame/sensorfeed2
attr sensorfeed2 size 240x240
Das KI-generierte Hintergrundbild /opt/fhem/PictureFrame/sensorfeed2 dient als Symbolbild.
Layout
Das Modul 98_DOIF.pm wird benötigt, damit RSS die darin enthaltenen Widgets verwenden kann. Wenn man kein DOIF benutzt, muss man das Modul mit require 98_DOIF.pm im Perl-Kode laden - das erfolgt unprätentiös in dem automatisch geladenen 99_GIFTV.pm.
Datei: /opt/fhem/conf/sensorfeed2.layout
# ------------------
# font
# ------------------
font /opt/fhem/conf/DroidSans.ttf
# ------------------
# bottom left corner
# ------------------
#
# date/time
#
rgb "c0c0c0"
pt 10
date 10 230
time 100 230
#
# links oben: Erzeugung
#
img 10 10 100w svg data { ui_Table::ring2( \
main::ReadingsVal('aggr.PV','PVPowerSourceW',0.0), 0.0, 4.0, 0, 240, "PV", 100, undef, "0,,,W", \
main::ReadingsVal('aggr.PV','PVEnergySourcekWh',0.0), 0, 50, 0, 120, undef, undef, "0,,,kWh") \
}
#
# rechts oben: Verbrauch
#
img 130 10 100w svg data { ui_Table::ring2( \
main::ReadingsVal('aggr.PV','HousePowerSinkW',0.0), 0.0, 4.0, 0, 240, "Haus", 100, undef, "0,,,W", \
main::ReadingsVal('aggr.PV','HouseEnergySinkkWh',0.0), 0, 50, 0, 120, undef, undef, "0,,,kWh") \
}
#
# links unten: Netzbezug (+)/Netzeinspeisung (-)
#
img 10 130 100w svg data { ui_Table::ring2( \
main::ReadingsVal('aggr.PV','GridPowerW',0.0), -4.0, 4.0, 0, 240, "Netz", 100, undef, "0,,,W", \
main::ReadingsVal('aggr.PV','GridEnergySourcekWh',0.0), 0, 100, 0, 120, undef, undef, "0,,,kWh") \
}
#
# rechts unten: Batterie
#
img 130 130 100w svg data { ui_Table::ring2( \
main::ReadingsVal('aggr.PV','BatteryPowerW',0.0), -4.0, 4.0, 0, 240, "Batt", 100, undef, "0,,,W", \
main::ReadingsVal('aggr.PV','BatteryPercent',0.0), 0, 100, 0, 120, undef, undef, "0,,,%") \
}
Bespielung des GIFTV
at
Der at-Befehl schiebt durch Aufruf von push2giftv() in Sekunde 1 jeder Minute das Bild vom RSS-Gerät sensorfeed2 auf zwei im Haus verteilte GIFTVs.
define at.sensorfeed at +*00:01:00 { push2giftv( { "sensorfeed2" => ["giftv-1.example.com","giftv-3.example.com"] } ) }
attr at.sensorfeed alignTime 01:01:01
push2giftv()
Es wird ein automatisch von FHEM geladenes Modul 99_GIFTV.pm mit folgendem Inhalt im Ordner FHEM (unter Linux üblicherweise /opt/fhem/FHEM/99_GIFTV.pm) bei den anderen Modulen angelegt:
require '98_DOIF.pm';
sub push2giftv {
my $uploadsref = shift;
# wrap code in eval to avoid lethal errors crashing FHEM
eval {
my $type = "jpg";
while( my ($rssname, $hostnamesref) = each(%$uploadsref) ) {
# make RSS generate the image
my ($mimetype, $image) = RSS_returnIMG($rssname, $type);
my $targetfilename = "$rssname.$type";
foreach my $hostname (@$hostnamesref) {
# parameters for GIFTV
my $url = "http://$hostname/doUpload?dir=%2Fimage%2F";
my $param = {
loglevel => 4,
url => $url,
method => "POST",
hideurl => 0,
noshutdown => 0,
callback => sub($$$) { Log3 $name, 1,"ERR:$_[1] DATA:".length($_[2]) },
};
# add image as multipart form data
HttpUtils_AddMultipartData($param, $image,
{"Content-Disposition" => "form-data; name=\"file\"; filename=\"$targetfilename\"", "Content-Type" => $mimetype }
);
# upload to GIFTV $hostname
HttpUtils_NonblockingGet($param);
}
}
};
if ($@) {
my $msg = $@;
chomp $msg;
Log3 $name, 2, $msg;
}
}
Das Modul 98_DOIF.pm wird benötigt, damit RSS die darin enthaltenen Widgets verwenden kann.
push2giftv() nimmt als Argument ein Hash, das jedem RSS-Gerät (hier sensorfeed2) ein Array mit den Hostnamen oder IP-Adressen der GIFTVs zuordnet, auf die das Bild vom RSS-Gerät geschoben wird.
Beispielaufruf, um mehrere Dashboards (sensorfeed1 und sensorfeed2) auf zwei GIFTVs zu schieben:
push2giftv( { "sensorfeed1" => ["giftv-1.example.com","giftv-3.example.com"], "sensorfeed2" => ["giftv-1.example.com","giftv-3.example.com"] } ) }
Das GIFTV zeigt dann die beiden Dashboards im Wechsel an.