Fronius Widget for Generation and Consumption including Rules

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!




5 Likes