I tried to create a widget that is similar to the Solar.Web app, but with the advantage that the data is stored locally and is therefore recorded even if there is an internet outage.
For this I used the following addons:
- Fronius Binding
- influxdb
- JavaScript Scripting
What is needed for this? First of all, the items that are linked to the following channels (see official Fronius Binding documentation):
powerflowchannelpgrid
powerflowchannelpload
powerflowchannelpakku
powerflowchannelppv
powerflowinverterpower
Here are my items:
Number:Power Powerinverter_Grid_Power "Netzstromfluss [%.0f W]" <energy> { channel="fronius:powerinverter:mybridge:myinverter:powerflowchannelpgrid" }
Number:Power Powerinverter_Load_Power "Gesamtverbrauch [%.0f W]" <energy> { channel="fronius:powerinverter:mybridge:myinverter:powerflowchannelpload" }
Number:Power Powerinverter_Battery_Power "Ladestrom [%.0f W]" <energy> { channel="fronius:powerinverter:mybridge:myinverter:powerflowchannelpakku" }
Number:Power Powerinverter_Production_Power "Aktuelle Produktion [%.0f W]" <solarplant> { channel="fronius:powerinverter:mybridge:myinverter:powerflowchannelppv" }
Number:Power Powerinverter_Inverter1_Power { channel="fronius:powerinverter:mybridge:myinverter:powerflowinverterpower" }
plus Items without Channels:
String PV_Zeitraum "Berechnungszeitraum [%s]"
String PV_Erzeugung "PV Erzeugung [%s]"
String PV_Eigenverbrauch "Eigenverbrauch [%s]"
String PV_Direktverbrauch "Direktverbrauch [%s]"
String PV_Batterieladung "Batterieladung [%s]"
String PV_Netzeinspeisung "Netzeinspeisung [%s]"
String PV_Gesamtverbrauch "Gesamtverbrauch [%s]"
String PV_Eigenversorgung "Eigenversorgung [%s]"
String PV_EigenversorgungPV "Eigenversorgung PV [%s]"
String PV_EigenversorgungBatterie "Eigenversorgung Batterie [%s]"
String PV_Netzbezug "Netzbezug [%s]"
Number PV_EigenverbrauchProzent "Eigenverbrauch [%d %%]"
Number PV_DirektverbrauchProzent "Direktverbrauch [%d %%]"
Number PV_BatterieladungProzent "Batterieladung [%d %%]"
Number PV_NetzeinspeisungProzent "Netzeinspeisung [%d %%]"
Number PV_EigenversorgungProzent "Eigenversorgung [%d %%]"
Number PV_EigenversorgungPVProzent "Eigenversorgung PV [%d %%]"
Number PV_EigenversorgungBatterieProzent "Eigenversorgung Batterie [%d %%]"
Number PV_NetzbezugProzent "Netzbezug [%d %%]"
// Summen-Items fĂĽr PV-Anlage (Number:Energy verwendet kWh als Einheit)
Number:Energy PV_Sum_GridDraw "Netzbezug Summe [%.2f kWh]" <energy>
Number:Energy PV_Sum_GridFeed "Netzeinspeisung Summe [%.2f kWh]" <energy>
Number:Energy PV_Sum_Load "Verbrauch Summe [%.2f kWh]" <energy>
Number:Energy PV_Sum_BatteryCharge "Batterieladung Summe [%.2f kWh]" <energy>
Number:Energy PV_Sum_BatteryDischarge "Batterieentladung Summe [%.2f kWh]" <energy>
Number:Energy PV_Sum_Production "Produktion Summe [%.2f kWh]" <energy>
and my item to control extended logging, but you can leave this out, you just have to adjust the rules:
Switch Advanced_Logging "Erweiterte Logs"
The PV_Sum* items are stored by me in the influxdb, their values ​​are obtained from the corresponding rules.
Here is the entry in the influxdb:
PV_Sum_GridDraw : strategy = everyUpdate
PV_Sum_GridFeed : strategy = everyUpdate
PV_Sum_Load : strategy = everyUpdate
PV_Sum_BatteryCharge : strategy = everyUpdate
PV_Sum_BatteryDischarge : strategy = everyUpdate
PV_Sum_Production : strategy = everyUpdate
here are the corresponding rules to calculate the PV_Sum* items:
const { items, rules } = require('openhab');
// Konstanten
const MIN_DC_TO_AC_EFFICIENCY = 0.85; // Minimaler Wirkungsgrad
const DEFAULT_DC_TO_AC_EFFICIENCY = 0.95; // Standard-Wirkungsgrad
const BRIDGE_REFRESH_INTERVAL = 10; // muss identisch zum Refresh-Intervall der Fronius Bridge sein
// Summen-Items Konfiguration
const SUM_ITEMS = {
gridDraw: 'PV_Sum_GridDraw',
gridFeed: 'PV_Sum_GridFeed',
load: 'PV_Sum_Load',
batteryCharge: 'PV_Sum_BatteryCharge',
batteryDischarge: 'PV_Sum_BatteryDischarge',
production: 'PV_Sum_Production'
};
// Logger-Funktion
const log = (message, level = 'info') => {
const advancedLogging = items.getItem('Advanced_Logging').state === 'ON';
if (advancedLogging || level === 'error') {
// Alles als INFO oder höher loggen für bessere Sichtbarkeit
switch(level) {
case 'debug': console.log('[DEBUG] ' + message); break;
case 'warn': console.warn(message); break;
case 'error': console.error(message); break;
default: console.log(message);
}
}
};
// Hilfsfunktionen
const safeParseNumber = (value, defaultValue = 0) => {
if (value === null || value === undefined) return defaultValue;
const parsed = parseFloat(value.toString());
return isNaN(parsed) ? defaultValue : parsed;
};
const updateSum = (itemName, currentValue, increment) => {
try {
const current = safeParseNumber(items.getItem(itemName).state, 0);
const newValue = current + increment;
if (newValue >= 0) {
items.getItem(itemName).postUpdate(newValue);
log(`${itemName} Update: ${newValue.toFixed(4)} kWh`, 'debug');
}
} catch (error) {
log(`Fehler beim Update von ${itemName}: ${error}`, 'error');
}
};
const calculateEfficiency = () => {
try {
const inverterPowerAC = Math.abs(safeParseNumber(items.getItem('Powerinverter_Inverter1_Power').state));
const productionPowerDC = Math.abs(safeParseNumber(items.getItem('Powerinverter_Production_Power').state));
const batteryPower = safeParseNumber(items.getItem('Powerinverter_Battery_Power').state);
// Batterieladung (negativ) und -entladung (positiv) trennen
const batteryChargeDC = batteryPower < 0 ? Math.abs(batteryPower) : 0;
const batteryDischargeDC = batteryPower > 0 ? batteryPower : 0;
// VerfĂĽgbare DC-Leistung fĂĽr AC-Wandlung
const availableDCPower = (productionPowerDC - batteryChargeDC) + batteryDischargeDC;
if (availableDCPower > 100 && inverterPowerAC > 50) { // Nur bei relevanter Leistung
const efficiency = inverterPowerAC / availableDCPower;
// Debug-Ausgabe
log(`Effizienzberechnung:`, 'debug');
log(` Inverter AC: ${inverterPowerAC.toFixed(2)} W`, 'debug');
log(` Produktion DC: ${productionPowerDC.toFixed(2)} W`, 'debug');
log(` Batterie Ladung DC: ${batteryChargeDC.toFixed(2)} W`, 'debug');
log(` Batterie Entladung DC: ${batteryDischargeDC.toFixed(2)} W`, 'debug');
log(` VerfĂĽgbare DC: ${availableDCPower.toFixed(2)} W`, 'debug');
log(` Effizienz: ${(efficiency * 100).toFixed(1)}%`, 'debug');
if (efficiency >= MIN_DC_TO_AC_EFFICIENCY && efficiency <= 1) {
return efficiency;
}
}
return DEFAULT_DC_TO_AC_EFFICIENCY;
} catch (error) {
log(`Fehler bei Effizienzberechnung: ${error}`, 'warn');
return DEFAULT_DC_TO_AC_EFFICIENCY;
}
};
// Update Rules
// Grid Power Update Rule
const gridPowerRule = rules.JSRule({
name: "PV Grid Power Update",
description: "Aktualisiert Grid-Power Summen",
triggers: [triggers.ItemStateUpdateTrigger("Powerinverter_Grid_Power")],
execute: (event) => {
try {
const gridPower = safeParseNumber(items.getItem('Powerinverter_Grid_Power').state);
const gridDrawItem = items.getItem(SUM_ITEMS.gridDraw);
const gridFeedItem = items.getItem(SUM_ITEMS.gridFeed);
if (gridPower > 0) {
updateSum(
SUM_ITEMS.gridDraw,
null,
gridPower * BRIDGE_REFRESH_INTERVAL / 3600000
);
gridFeedItem.postUpdate(gridFeedItem.state);
} else if (gridPower < 0) {
updateSum(
SUM_ITEMS.gridFeed,
null,
Math.abs(gridPower) * BRIDGE_REFRESH_INTERVAL / 3600000
);
gridDrawItem.postUpdate(gridDrawItem.state);
} else {
gridDrawItem.postUpdate(gridDrawItem.state);
gridFeedItem.postUpdate(gridFeedItem.state);
}
} catch (error) {
log(`Fehler beim Grid Power Update: ${error}`, 'error');
}
}
});
// Load Power Update Rule
const loadPowerRule = rules.JSRule({
name: "PV Load Power Update",
description: "Aktualisiert Load-Power Summe",
triggers: [triggers.ItemStateUpdateTrigger("Powerinverter_Load_Power")],
execute: (event) => {
try {
const loadPower = Math.abs(safeParseNumber(items.getItem('Powerinverter_Load_Power').state));
const loadItem = items.getItem(SUM_ITEMS.load);
if (loadPower > 0) {
updateSum(
SUM_ITEMS.load,
null,
loadPower * BRIDGE_REFRESH_INTERVAL / 3600000
);
} else {
loadItem.postUpdate(loadItem.state);
}
} catch (error) {
log(`Fehler beim Load Power Update: ${error}`, 'error');
}
}
});
// Battery Power Update Regel
const batteryPowerRule = rules.JSRule({
name: "PV Battery Power Update",
description: "Aktualisiert Battery-Power Summen",
triggers: [triggers.ItemStateUpdateTrigger("Powerinverter_Battery_Power")],
execute: (event) => {
try {
const batteryPower = safeParseNumber(items.getItem('Powerinverter_Battery_Power').state);
const batteryChargeItem = items.getItem(SUM_ITEMS.batteryCharge);
const batteryDischargeItem = items.getItem(SUM_ITEMS.batteryDischarge);
if (batteryPower < 0) {
// Batterieladung (DC zu DC)
updateSum(
SUM_ITEMS.batteryCharge,
null,
Math.abs(batteryPower) * BRIDGE_REFRESH_INTERVAL / 3600000
);
batteryDischargeItem.postUpdate(batteryDischargeItem.state);
} else if (batteryPower > 0) {
// Batterieentladung (DC zu AC)
const currentEfficiency = calculateEfficiency();
updateSum(
SUM_ITEMS.batteryDischarge,
null,
batteryPower * currentEfficiency * BRIDGE_REFRESH_INTERVAL / 3600000
);
batteryChargeItem.postUpdate(batteryChargeItem.state);
} else {
batteryChargeItem.postUpdate(batteryChargeItem.state);
batteryDischargeItem.postUpdate(batteryDischargeItem.state);
}
} catch (error) {
log(`Fehler beim Battery Power Update: ${error}`, 'error');
}
}
});
// Production Power Update Rule
const productionPowerRule = rules.JSRule({
name: "PV Production Power Update",
description: "Aktualisiert Production-Power Summe",
triggers: [triggers.ItemStateUpdateTrigger("Powerinverter_Production_Power")],
execute: (event) => {
try {
const productionPower = safeParseNumber(items.getItem('Powerinverter_Production_Power').state);
const productionItem = items.getItem(SUM_ITEMS.production);
if (productionPower > 0) {
const currentEfficiency = calculateEfficiency();
updateSum(
SUM_ITEMS.production,
null,
productionPower * currentEfficiency * BRIDGE_REFRESH_INTERVAL / 3600000
);
} else {
productionItem.postUpdate(productionItem.state);
}
} catch (error) {
log(`Fehler beim Production Power Update: ${error}`, 'error');
}
}
});
// Module-Export
module.exports = {
SUM_ITEMS,
calculateEfficiency,
DEFAULT_DC_TO_AC_EFFICIENCY,
BRIDGE_REFRESH_INTERVAL,
log
};
and here are the rules to calculate the display values ​​for the widget:
const { items, rules, time } = require('openhab');
// Java-Zeitklassen importieren
const JavaZDT = Java.type('java.time.ZonedDateTime');
const ZoneId = Java.type('java.time.ZoneId');
const ChronoField = Java.type('java.time.temporal.ChronoField');
// Konstanten
const TIMEZONE = 'Europe/Berlin';
const MINIMAL_POWER_THRESHOLD = 0.001; // 1 Wh Schwelle
// Summen-Items Konfiguration
const SUM_ITEMS = {
gridDraw: 'PV_Sum_GridDraw',
gridFeed: 'PV_Sum_GridFeed',
load: 'PV_Sum_Load',
batteryCharge: 'PV_Sum_BatteryCharge',
batteryDischarge: 'PV_Sum_BatteryDischarge',
production: 'PV_Sum_Production'
};
// Logger-Funktion
const log = (message, level = 'info') => {
const advancedLogging = items.getItem('Advanced_Logging').state === 'ON';
if (advancedLogging || level === 'error') {
// Alles als INFO oder höher loggen für bessere Sichtbarkeit
switch(level) {
case 'debug': console.log('[DEBUG] ' + message); break;
case 'warn': console.warn(message); break;
case 'error': console.error(message); break;
default: console.log(message);
}
}
};
const formatDateTime = timestamp => {
const date = new Date(timestamp);
return date.toLocaleString('de-DE', {
timeZone: 'Europe/Berlin',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
};
// Zeit-Funktionen
const getTimeRange = (zeitraum) => {
const zoneId = ZoneId.of(TIMEZONE);
const now = JavaZDT.now(zoneId);
const dataCollectionStart = JavaZDT.of(2025, 2, 19, 14, 0, 0, 0, zoneId);
// Hilfsfunktion fĂĽr Tagesstart/-ende
const startOfDay = (date) => date.withHour(0).withMinute(0).withSecond(0).withNano(0);
const endOfDay = (date) => date.withHour(23).withMinute(59).withSecond(59).withNano(999999999);
let startTime, endTime;
switch(zeitraum.toLowerCase()) {
case 'heute':
startTime = startOfDay(now);
endTime = now;
break;
case 'gestern':
startTime = startOfDay(now.minusDays(1));
endTime = endOfDay(now.minusDays(1));
break;
case 'woche':
startTime = startOfDay(now.minusWeeks(1).with(ChronoField.DAY_OF_WEEK, 1));
endTime = now;
break;
case 'monat':
startTime = startOfDay(now.withDayOfMonth(1));
endTime = now;
break;
case 'jahr':
startTime = startOfDay(now.withDayOfYear(1));
endTime = now;
break;
default:
startTime = startOfDay(now);
endTime = now;
}
// Debug-Ausgabe vor der Anpassung
log(`UrsprĂĽnglicher Zeitraum fĂĽr ${zeitraum}:`, 'debug');
log(` Start: ${formatDateTime(startTime.toInstant().toEpochMilli())}`, 'debug');
log(` Ende: ${formatDateTime(endTime.toInstant().toEpochMilli())}`, 'debug');
// Startzeit nicht vor Datenerfassungsbeginn
if (startTime.isBefore(dataCollectionStart)) {
startTime = dataCollectionStart;
log(`Startzeit auf Datenerfassungsbeginn angepasst: ${formatDateTime(startTime.toInstant().toEpochMilli())}`, 'debug');
}
// Debug-Ausgabe nach der Anpassung
log(`Finaler Zeitraum ${zeitraum}:`, 'debug');
log(` Start: ${formatDateTime(startTime.toInstant().toEpochMilli())}`, 'debug');
log(` Ende: ${formatDateTime(endTime.toInstant().toEpochMilli())}`, 'debug');
return [startTime, endTime];
};
// Hilfsfunktion zum Aktualisieren der Anzeige-Items
const updateDisplayItems = (values) => {
try {
// Debug vor dem Update
log('Starte Update der Display-Items mit folgenden Werten:', 'debug');
Object.entries(values).forEach(([key, value]) => {
log(`${key}: ${value}`, 'debug');
});
// Formatierungsfunktion fĂĽr Energiewerte
const formatValue = value => value < 1
? `${(value * 1000).toFixed(1)} Wh`
: `${value.toFixed(2)} kWh`;
// Basis-Werte mit Formatierung
items.getItem('PV_Erzeugung').postUpdate(formatValue(values.erzeugung));
items.getItem('PV_Eigenverbrauch').postUpdate(formatValue(values.eigenverbrauch));
items.getItem('PV_Direktverbrauch').postUpdate(formatValue(values.direktverbrauch));
items.getItem('PV_Batterieladung').postUpdate(formatValue(values.batterieladung));
items.getItem('PV_Netzeinspeisung').postUpdate(formatValue(values.netzeinspeisung));
items.getItem('PV_Gesamtverbrauch').postUpdate(formatValue(values.gesamtverbrauch));
items.getItem('PV_Eigenversorgung').postUpdate(formatValue(values.eigenversorgung));
items.getItem('PV_EigenversorgungPV').postUpdate(formatValue(values.eigenversorgungPV));
items.getItem('PV_EigenversorgungBatterie').postUpdate(formatValue(values.eigenversorgungBatterie));
items.getItem('PV_Netzbezug').postUpdate(formatValue(values.netzbezug));
// Prozentwerte immer berechnen und mit 0 initialisieren
const prozentWerte = {
eigenverbrauchProzent: 0,
direktverbrauchProzent: 0,
batterieladungProzent: 0,
netzeinspeisungProzent: 0,
eigenversorgungProzent: 0,
eigenversorgungPVProzent: 0,
eigenversorgungBatterieProzent: 0,
netzbezugProzent: 0
};
// Berechnung basierend auf Erzeugung
if (values.erzeugung > MINIMAL_POWER_THRESHOLD) {
prozentWerte.eigenverbrauchProzent = Math.round((values.eigenverbrauch / values.erzeugung) * 100);
prozentWerte.direktverbrauchProzent = Math.round((values.direktverbrauch / values.erzeugung) * 100);
prozentWerte.batterieladungProzent = Math.round((values.batterieladung / values.erzeugung) * 100);
prozentWerte.netzeinspeisungProzent = Math.round((values.netzeinspeisung / values.erzeugung) * 100);
}
// Berechnung basierend auf Gesamtverbrauch
if (values.gesamtverbrauch > MINIMAL_POWER_THRESHOLD) {
// Effektive Werte berechnen
const effectiveNetzbezug = values.netzbezug < MINIMAL_POWER_THRESHOLD ? 0 : values.netzbezug;
const effectiveBatterie = values.eigenversorgungBatterie;
const effectivePV = values.eigenversorgungPV;
// Prozentwerte berechnen
let batterieProzent = Math.round((effectiveBatterie / values.gesamtverbrauch) * 100);
let pvProzent = Math.round((effectivePV / values.gesamtverbrauch) * 100);
let netzbezugProzent = Math.round((effectiveNetzbezug / values.gesamtverbrauch) * 100);
// Sicherstellen, dass die Summe 100% nicht ĂĽbersteigt
const total = batterieProzent + pvProzent + netzbezugProzent;
if (total > 100) {
// Proportionale Anpassung
const factor = 100 / total;
batterieProzent = Math.round(batterieProzent * factor);
pvProzent = Math.round(pvProzent * factor);
netzbezugProzent = 100 - batterieProzent - pvProzent;
}
// Wenn fast alles aus der Batterie kommt
if (batterieProzent > 95 && netzbezugProzent < 5) {
batterieProzent = 100;
netzbezugProzent = 0;
pvProzent = 0;
}
prozentWerte.eigenversorgungProzent = batterieProzent + pvProzent;
prozentWerte.eigenversorgungBatterieProzent = batterieProzent;
prozentWerte.eigenversorgungPVProzent = pvProzent;
prozentWerte.netzbezugProzent = netzbezugProzent;
}
// Update der Prozentwerte
items.getItem('PV_EigenverbrauchProzent').postUpdate(prozentWerte.eigenverbrauchProzent);
items.getItem('PV_DirektverbrauchProzent').postUpdate(prozentWerte.direktverbrauchProzent);
items.getItem('PV_BatterieladungProzent').postUpdate(prozentWerte.batterieladungProzent);
items.getItem('PV_NetzeinspeisungProzent').postUpdate(prozentWerte.netzeinspeisungProzent);
items.getItem('PV_EigenversorgungProzent').postUpdate(prozentWerte.eigenversorgungProzent);
items.getItem('PV_EigenversorgungPVProzent').postUpdate(prozentWerte.eigenversorgungPVProzent);
items.getItem('PV_EigenversorgungBatterieProzent').postUpdate(prozentWerte.eigenversorgungBatterieProzent);
items.getItem('PV_NetzbezugProzent').postUpdate(prozentWerte.netzbezugProzent);
log('Display-Items erfolgreich aktualisiert', 'debug');
} catch (error) {
log(`Fehler beim Update der Display-Items: ${error}`, 'error');
}
};
const calculateCurrentValues = async (startTime, endTime) => {
try {
log(`Berechne Werte von ${startTime} bis ${endTime}`, 'debug');
// Konvertiere zu DateTimeType fĂĽr die Persistence-Abfrage
const startDateTime = startTime;
const endDateTime = endTime;
log(`Konvertierte Zeiten - Start: ${startDateTime}, End: ${endDateTime}`, 'debug');
// Hole die Werte mit den konvertierten Zeiten
const itemStates = await Promise.all(
Object.values(SUM_ITEMS).flatMap(itemName => [
actions.PersistenceExtensions.persistedState(
items.getItem(itemName),
endDateTime,
'influxdb'
).state,
actions.PersistenceExtensions.persistedState(
items.getItem(itemName),
startDateTime,
'influxdb'
).state
])
);
// Differenzberechnung mit verbessertem Debugging
const getDifference = (index) => {
const endState = itemStates[index * 2];
const startState = itemStates[index * 2 + 1];
const itemName = Object.values(SUM_ITEMS)[index];
const parseValue = (state) => {
if (!state) return 0;
const rawValue = state.toString().split(' ')[0];
return parseFloat(rawValue);
};
const endValue = parseValue(endState);
const startValue = parseValue(startState);
const difference = Math.max(0, endValue - startValue);
log(`${itemName}: ${startValue} → ${endValue} = ${difference} kWh`, 'debug');
return difference;
};
// Werte extrahieren
const gridDraw = getDifference(0);
const gridFeed = getDifference(1);
const load = getDifference(2);
const batteryCharge = getDifference(3);
const batteryDischarge = getDifference(4);
const production = getDifference(5);
// Zwischen-Log der Rohdifferenzen
log('Rohe Differenzwerte:', 'debug');
log(`Load (Verbrauch): ${load} kWh`, 'debug');
log(`Production: ${production} kWh`, 'debug');
log(`Grid Feed: ${gridFeed} kWh`, 'debug');
log(`Battery Charge: ${batteryCharge} kWh`, 'debug');
log(`Battery Discharge: ${batteryDischarge} kWh`, 'debug');
// Basiswerte berechnen
const values = {
netzbezug: gridDraw,
netzeinspeisung: gridFeed,
batterieladung: batteryCharge,
eigenversorgungBatterie: batteryDischarge,
direktverbrauch: Math.max(0, production - gridFeed - batteryCharge),
verbrauch: load // Rohwert zum Vergleich
};
// Zwischen-Log der Basiswerte
log('Berechnete Basiswerte:', 'debug');
Object.entries(values).forEach(([key, value]) => {
log(`${key}: ${value.toFixed(3)} kWh`, 'debug');
});
// Abgeleitete Werte berechnen
values.eigenverbrauch = values.direktverbrauch + values.batterieladung;
values.erzeugung = values.netzeinspeisung + values.eigenverbrauch;
values.eigenversorgungPV = values.direktverbrauch;
values.eigenversorgung = values.eigenversorgungPV + values.eigenversorgungBatterie;
values.gesamtverbrauch = values.eigenversorgung + values.netzbezug;
// Final-Log der abgeleiteten Werte
log('Abgeleitete Werte:', 'debug');
Object.entries(values).forEach(([key, value]) => {
log(`${key}: ${value.toFixed(3)} kWh`, 'debug');
});
// Plausibilitätscheck
log('Plausibilitätsprüfung:', 'debug');
log(`Gesamtverbrauch = Eigenversorgung + Netzbezug: ${values.gesamtverbrauch.toFixed(3)} = ${values.eigenversorgung.toFixed(3)} + ${values.netzbezug.toFixed(3)}`, 'debug');
log(`Erzeugung = Netzeinspeisung + Eigenverbrauch: ${values.erzeugung.toFixed(3)} = ${values.netzeinspeisung.toFixed(3)} + ${values.eigenverbrauch.toFixed(3)}`, 'debug');
return values;
} catch (error) {
log(`Fehler bei der Berechnung der aktuellen Werte: ${error}`, 'error');
return null;
}
};
// Hauptregel fĂĽr die Berechnungen
const pvRule = rules.JSRule({
name: "PV-Anlage Berechnung",
description: "Berechnet PV-Anlagen Werte",
triggers: [
triggers.SystemStartlevelTrigger(100),
triggers.GenericCronTrigger("0 */5 * * * ?"), // Alle 5 Minuten
triggers.GenericCronTrigger("0 1 0 * * ?"), // 1 Minute nach Mitternacht
triggers.ItemStateChangeTrigger("PV_Zeitraum")
],
execute: async (event) => {
try {
// Kurze Verzögerung nach Mitternacht
if (event.triggerType === 'GenericCronTrigger' &&
event.triggerConfig === "0 1 0 * * ?") {
await new Promise(resolve => setTimeout(resolve, 5000));
}
const zeitraum = items.getItem('PV_Zeitraum').state;
const [startTime, endTime] = getTimeRange(zeitraum);
const values = await calculateCurrentValues(startTime, endTime);
if (values) {
updateDisplayItems(values);
log(`Werte fĂĽr ${zeitraum} aktualisiert`);
}
} catch (error) {
log(`Fehler in der PV-Regel: ${error}`, 'error');
}
}
});
and here is the widget code:
uid: pv_overview_v2
tags: []
props:
parameters:
- default: heute
description: Zeitraum fĂĽr die Anzeige
label: Zeitraum
name: period
required: false
type: TEXT
- description: Item fĂĽr die Zeitraumsteuerung
label: Zeitraum Item
name: periodItem
required: true
type: TEXT
- label: Eigenverbrauch Prozent Item
name: eigenverbrauchProzentItem
required: true
type: TEXT
- label: Eigenversorgung Prozent Item
name: eigenversorgungProzentItem
required: true
type: TEXT
- label: Erzeugung Item
name: erzeugungItem
required: true
type: TEXT
- label: Eigenverbrauch Item
name: eigenverbrauchItem
required: true
type: TEXT
- label: Direktverbrauch Item
name: direktverbrauchItem
required: true
type: TEXT
- label: Batterieladung Item
name: batterieladungItem
required: true
type: TEXT
- label: Netzeinspeisung Item
name: netzeinpeisungItem
required: true
type: TEXT
- label: Gesamtverbrauch Item
name: gesamtverbrauchItem
required: true
type: TEXT
- label: Eigenversorgung Item
name: eigenversorgungItem
required: true
type: TEXT
- label: Eigenversorgung PV Item
name: eigenversorgungPVItem
required: true
type: TEXT
- label: Eigenversorgung Batterie Item
name: eigenversorgungBatterieItem
required: true
type: TEXT
- label: Netzbezug Item
name: netzbezugItem
required: true
type: TEXT
- default: heute;Heute,gestern;Gestern,woche;Woche,monat;Monat,jahr;Jahr
description: "Format: heute;Heute,gestern;Gestern,woche;Woche,monat;Monat,jahr;Jahr"
label: Button Konfiguration
name: buttons
required: true
type: TEXT
parameterGroups: []
timestamp: Feb 14, 2025, 10:50:46 PM
component: f7-card
config:
style:
--f7-card-margin-horizontal: 10px
--f7-card-margin-vertical: 3px
border-radius: var(--f7-card-expandable-border-radius)
margin: 5px
padding-bottom: 5px
slots:
default:
- component: f7-row
config:
style:
align-items: center
margin-bottom: 5px
margin-left: 10px
slots:
default:
- component: f7-col
config:
style:
white-space: nowrap
width: 75
slots:
default:
- component: Label
config:
style:
font-size: 1.2em
font-weight: bold
text: ="Erzeugung " + items[props.periodItem].state.charAt(0).toUpperCase() +
items[props.periodItem].state.slice(1)
- component: f7-col
config:
style:
display: flex
justify-content: flex-end
margin-top: 10px
padding-right: 5px
width: 25
slots:
default:
- component: oh-gauge
config:
bgColor: transparent
borderBgColor: "#e0e0e0"
borderColor: "#FDD835"
borderWidth: 8
item: =props.eigenverbrauchProzentItem
labelFontSize: 8
labelTextColor: var(--f7-text-color)
size: 52
type: circle
valueFontSize: 10
valueTextColor: var(--f7-text-color)
- component: f7-row
config:
style:
margin-bottom: 15px
margin-left: 60px
margin-top: -5px
text-align: center
slots:
default:
- component: Label
config:
style:
color: "=(themeOptions.dark == 'dark') ? 'white' : 'black'"
font-size: 1.3em
font-weight: 500
text: =items[props.erzeugungItem].state
- component: f7-list
config:
noHairlines: true
noHairlinesBetween: true
slots:
default:
- component: oh-list-item
config:
icon: f7:house_fill
style:
--oh-list-item-title-font-size: 1.1em
title: Eigenverbrauch
slots:
after:
- component: Label
config:
style:
color: "=(themeOptions.dark == 'dark') ? 'white' : 'black'"
text: =items[props.eigenverbrauchItem].state
- component: oh-list-item
config:
icon: material:solar_power
style:
--oh-list-item-media-margin: 10px
--oh-list-item-title-font-size: 1em
margin-left: 35px
title: Direktverbrauch
slots:
after:
- component: Label
config:
style:
color: "=(themeOptions.dark == 'dark') ? 'white' : 'black'"
text: =items[props.direktverbrauchItem].state
- component: oh-list-item
config:
icon: "=(themeOptions.dark == 'dark') ? 'to_battery' : 'to_battery_dark'"
style:
--oh-list-item-media-margin: 10px
--oh-list-item-title-font-size: 1em
margin-left: 35px
title: Batterieladung
slots:
after:
- component: Label
config:
style:
color: "=(themeOptions.dark == 'dark') ? 'white' : 'black'"
text: =items[props.batterieladungItem].state
- component: oh-list-item
config:
icon: "=(themeOptions.dark == 'dark') ? 'to_grid' : 'to_grid_black'"
style:
--oh-list-item-title-font-size: 1.1em
title: Netzeinspeisung
slots:
after:
- component: Label
config:
style:
color: "=(themeOptions.dark == 'dark') ? 'white' : 'black'"
text: =items[props.netzeinpeisungItem].state
- component: f7-row
config:
style:
align-items: center
margin: 20px 0 5px 0
margin-left: 10px
slots:
default:
- component: f7-col
config:
style:
white-space: nowrap
width: 75
slots:
default:
- component: Label
config:
style:
font-size: 1.2em
font-weight: bold
text: ="Verbrauch " + items[props.periodItem].state.charAt(0).toUpperCase() +
items[props.periodItem].state.slice(1)
- component: f7-col
config:
style:
display: flex
justify-content: flex-end
margin-top: 10px
padding-right: 5px
width: 25
slots:
default:
- component: oh-gauge
config:
bgColor: transparent
borderBgColor: "#e0e0e0"
borderColor: "#FDD835"
borderWidth: 8
item: =props.eigenversorgungProzentItem
labelFontSize: 8
labelTextColor: var(--f7-text-color)
size: 52
type: circle
valueFontSize: 10
valueTextColor: var(--f7-text-color)
- component: f7-row
config:
style:
margin-bottom: 15px
margin-left: 60px
margin-top: -5px
text-align: center
slots:
default:
- component: Label
config:
style:
color: "=(themeOptions.dark == 'dark') ? 'white' : 'black'"
font-size: 1.3em
font-weight: 500
text: =items[props.gesamtverbrauchItem].state
- component: f7-list
config:
noHairlines: true
noHairlinesBetween: true
slots:
default:
- component: oh-list-item
config:
icon: f7:house_fill
style:
--oh-list-item-title-font-size: 1.1em
title: Eigenversorgung
slots:
after:
- component: Label
config:
style:
color: "=(themeOptions.dark == 'dark') ? 'white' : 'black'"
text: =items[props.eigenversorgungItem].state
- component: oh-list-item
config:
icon: material:solar_power
style:
--oh-list-item-media-margin: 10px
--oh-list-item-title-font-size: 1em
margin-left: 35px
title: aus PV
slots:
after:
- component: Label
config:
style:
color: "=(themeOptions.dark == 'dark') ? 'white' : 'black'"
text: =items[props.eigenversorgungPVItem].state
- component: oh-list-item
config:
icon: "=(themeOptions.dark == 'dark') ? 'from_battery' : 'from_battery_dark'"
style:
--oh-list-item-media-margin: 10px
--oh-list-item-title-font-size: 1em
margin-left: 35px
title: aus Batterie
slots:
after:
- component: Label
config:
style:
color: "=(themeOptions.dark == 'dark') ? 'white' : 'black'"
text: =items[props.eigenversorgungBatterieItem].state
- component: oh-list-item
config:
icon: "=(themeOptions.dark == 'dark') ? 'from_grid' : 'from_grid_black'"
style:
--oh-list-item-title-font-size: 1.1em
title: Netzbezug
slots:
after:
- component: Label
config:
style:
color: "=(themeOptions.dark == 'dark') ? 'white' : 'black'"
text: =items[props.netzbezugItem].state
- component: f7-row
config:
style:
display: flex
justify-content: center
margin-bottom: 10px
margin-top: 10px
slots:
default:
- component: f7-block
config:
style:
display: flex
flex-wrap: wrap
gap: 1px
justify-content: center
margin-bottom: 0
padding-bottom: 0
slots:
default:
- component: oh-repeater
config:
for: button
fragment: true
in: =props.buttons.split(',')
slots:
default:
- component: oh-button
config:
action: command
actionCommand: =loop.button.split(';')[0]
actionItem: =props.periodItem
fill: =items[props.periodItem].state === loop.button.split(';')[0]
style:
font-size: 0.9em
height: 26px
line-height: 22px
margin: 1px
min-width: 55px
padding: 2px 6px
text: =loop.button.split(';')[1]
What you also need for all the widget icons to work are the following PNG images. These must be stored in the classic icon folder of your OpenHAB installation as usual - it’s important to have both versions of the images, otherwise the switch between light and dark mode won’t work!