From f59058c241a3ce5f3acc7a6a2385771706f25847 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Fri, 13 Mar 2026 19:02:03 +0000 Subject: [PATCH 01/13] fixed solar battery calculation logic, breaking change! now supports battery export to grid --- .../mysolarpvbattery/mysolarpvbattery.php | 267 +++++++++++------- 1 file changed, 159 insertions(+), 108 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 2e9736d..39567c2 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -272,7 +272,10 @@ - + +
+
BATTERY TO GRID
0 kWh
+
@@ -373,12 +376,12 @@ function getTranslations(){ // ---------------------------------------------------------------------- config.app = { // Standard mysolar feeds - "use":{"type":"feed", "autoname":"use", "description":"Building consumption in watts (not including battery charging)"}, - "solar":{"type":"feed", "autoname":"solar", "description":"Solar pv generation in watts"}, + "use":{"type":"feed", "autoname":"use", "description":"House or building use in watts"}, + "solar":{"type":"feed", "autoname":"solar", "description":"Solar generation in watts"}, // Battery feeds - "battery_charge":{"type":"feed", "autoname":"battery_charge", "description":"Battery charge power in watts"}, - "battery_discharge":{"type":"feed", "autoname":"battery_discharge", "description":"Battery discharge power in watts"}, - "battery_soc":{"optional":true, "type":"feed", "autoname":"battery_soc", "description":"Battery state of charge %"}, + "battery_power":{"type":"feed", "autoname":"battery_power", "description":"Battery power in watts (positive for discharge, negative for charge)"}, + "grid":{"type":"feed", "autoname":"grid", "description":"Grid power in watts (positive for import, negative for export)"}, + "battery_soc":{"optional":true, "type":"feed", "autoname":"battery_soc", "description":"Battery state of charge in kWh"}, // History feeds "use_kwh":{"optional":true, "type":"feed", "autoname":"use_kwh", "description":"Building consumption in kWh (not including battery charging)"}, @@ -390,9 +393,7 @@ function getTranslations(){ // Other options "kw":{"type":"checkbox", "default":0, "name": "Show kW", "description": "Display power as kW"}, - "battery_capacity_kwh":{"type":"value", "default":0, "name":"Battery Capacity", "description":"Battery capacity in kWh"}, - - "is_dc_battery":{"type":"checkbox", "name": "DC Battery", "default": 0, "optional":true, "description":"Is the Battery on the DC side?"} + "battery_capacity_kwh":{"type":"value", "default":0, "name":"Battery Capacity", "description":"Battery capacity in kWh"} } config.id = ; @@ -573,8 +574,10 @@ function livefn() if (feeds === null) { return; } var solar_now = parseInt(feeds[config.app.solar.value].value); var use_now = parseInt(feeds[config.app.use.value].value); - var battery_charge_now = parseInt(feeds[config.app.battery_charge.value].value); - var battery_discharge_now = parseInt(feeds[config.app.battery_discharge.value].value); + // battery_power: positive = discharge, negative = charge + var battery_power_now = parseInt(feeds[config.app.battery_power.value].value); + // grid: positive = import, negative = export + var grid_now = parseInt(feeds[config.app.grid.value].value); var battery_soc_now = "---"; if (config.app.battery_soc.value && feeds[config.app.battery_soc.value] != undefined) { @@ -588,10 +591,10 @@ function livefn() timeseries.append("use",updatetime,use_now); timeseries.trim_start("use",view.start*0.001); - timeseries.append("battery_charge",updatetime,battery_charge_now); - timeseries.trim_start("battery_charge",view.start*0.001); - timeseries.append("battery_discharge",updatetime,battery_discharge_now); - timeseries.trim_start("battery_discharge",view.start*0.001); + timeseries.append("battery_power",updatetime,battery_power_now); + timeseries.trim_start("battery_power",view.start*0.001); + timeseries.append("grid",updatetime,grid_now); + timeseries.trim_start("grid",view.start*0.001); if (config.app.battery_soc.value) { timeseries.append("battery_soc",updatetime,battery_soc_now); @@ -602,17 +605,21 @@ function livefn() view.end = now; view.start = now - live_timerange; } - // Lower limit for solar & battery charge/discharge - if (solar_now<10) solar_now = 0; - if (battery_charge_now<10) battery_charge_now = 0; - if (battery_discharge_now<10) battery_discharge_now = 0; - - var battery = 0; - if(config.app.is_dc_battery.value) { - balance = solar_now - use_now; + + // Conservation of energy: use = solar + battery_power + grid + use_now = solar_now + battery_power_now + grid_now; + + var battery_charge_now = 0; + var battery_discharge_now = 0; + if (battery_power_now > 0) { + battery_discharge_now = battery_power_now; } else { - balance = solar_now - use_now - battery_charge_now + battery_discharge_now; + battery_charge_now = -battery_power_now; } + + // balance = grid export (positive) or import (negative), shown from grid perspective + // negative grid_now = export = positive balance + var balance = -grid_now; // convert W to kW if(powerUnit === 'kW') { @@ -702,10 +709,10 @@ function load_powergraph() { if (reload) { reload = false; // getdata params: feedid,start,end,interval,average=0,delta=0,skipmissing=0,limitinterval=0,callback=false,context=false,timeformat='unixms' - timeseries.load("solar",feed.getdata(config.app.solar.value,view.start,view.end,view.interval,0,0,0,0,false,false,'notime')); - timeseries.load("use",feed.getdata(config.app.use.value,view.start,view.end,view.interval,0,0,0,0,false,false,'notime')); - timeseries.load("battery_charge",feed.getdata(config.app.battery_charge.value,view.start,view.end,view.interval,0,0,0,0,false,false,'notime')); - timeseries.load("battery_discharge",feed.getdata(config.app.battery_discharge.value,view.start,view.end,view.interval,0,0,0,0,false,false,'notime')); + timeseries.load("solar",feed.getdata(config.app.solar.value,view.start,view.end,view.interval,1,0,0,0,false,false,'notime')); + timeseries.load("use",feed.getdata(config.app.use.value,view.start,view.end,view.interval,1,0,0,0,false,false,'notime')); + timeseries.load("battery_power",feed.getdata(config.app.battery_power.value,view.start,view.end,view.interval,1,0,0,0,false,false,'notime')); + timeseries.load("grid",feed.getdata(config.app.grid.value,view.start,view.end,view.interval,1,0,0,0,false,false,'notime')); if (config.app.battery_soc.value) { timeseries.load("battery_soc",feed.getdata(config.app.battery_soc.value,view.start,view.end,view.interval,0,0,0,0,false,false,'notime')); @@ -713,43 +720,49 @@ function load_powergraph() { } // ------------------------------------------------------------------------------------------------------- - var use_data = []; - var solar_data = []; - var battery_charge_data = []; - var battery_discharge_data = []; + var solar_to_load_data = []; + var solar_to_grid_data = []; + var solar_to_battery_data = []; + var battery_to_load_data = []; + var battery_to_grid_data = []; + var grid_to_load_data = []; + var grid_to_battery_data = []; var battery_soc_data = []; - var t = 0; var use_now = 0; var solar_now = 0; - var battery_charge_now = 0; - var battery_discharge_now = 0; + var battery_power_now = 0; + var grid_now = 0; var battery_soc_now = 0; var total_solar_kwh = 0; var total_use_kwh = 0; var total_import_kwh = 0; - var total_solar_direct_kwh = 0; - var total_battery_charge_kwh = 0; - var total_battery_discharge_kwh = 0; + var total_export_kwh = 0; + var total_solar_to_load_kwh = 0; + var total_solar_to_grid_kwh = 0; + var total_solar_to_battery_kwh = 0; + var total_battery_to_load_kwh = 0; + var total_battery_to_grid_kwh = 0; + var total_grid_to_load_kwh = 0; + var total_grid_to_battery_kwh = 0; var datastart = timeseries.start_time("solar"); var last_solar = 0; var last_use = 0; - var last_charge = 0; - var last_discharge = 0; + var last_battery_power = 0; + var last_grid = 0; var last_soc = 0; var timeout = 600*1000; var interval = view.interval; + var power_to_kwh = interval / 3600000.0; + for (var z=0; zuse_now) solar_direct = use_now; - - var battery = 0; - if(config.app.is_dc_battery.value) { - balance = solar_now - use_now; + var use = solar_now + battery_power_now + grid_now; + var solar = solar_now; + var battery_power = battery_power_now; + var grid = grid_now; + + var import_power = 0; + var export_power = 0; + if (grid > 0) { + import_power = grid; } else { - balance = solar_now - use_now - battery_charge_now + battery_discharge_now; + export_power = -grid; } - - var excess = 0; - var unmet = 0; - if (balance>0) { - excess = balance - } else { - unmet = -1*balance; + + // SOLAR flows + var solar_to_load = Math.min(solar, use); + var solar_to_battery = 0; + if (battery_power < 0) { + // Battery is charging: solar to battery is the lesser of available solar and battery charge power + solar_to_battery = Math.min(solar - solar_to_load, -battery_power); } - - total_solar_kwh += (solar_now*interval)/(1000*3600); - total_use_kwh += (use_now*interval)/(1000*3600); - total_solar_direct_kwh += (solar_direct*interval)/(1000*3600); - total_import_kwh += (unmet*interval)/(1000*3600); - total_battery_charge_kwh += (battery_charge_now*interval)/(1000*3600); - total_battery_discharge_kwh += (battery_discharge_now*interval)/(1000*3600); - - use_data.push([time,use_now]); - solar_data.push([time,solar_now]); - battery_charge_data.push([time,battery_charge_now]); - battery_discharge_data.push([time,battery_discharge_now]); - battery_soc_data.push([time,battery_soc_now]); + var solar_to_grid = solar - solar_to_load - solar_to_battery; + + // BATTERY flows + var battery_to_load = 0; + var battery_to_grid = 0; + if (battery_power > 0) { + // Battery is discharging + battery_to_load = Math.min(battery_power, use - solar_to_load); + battery_to_grid = battery_power - battery_to_load; + } + + // GRID flows + var grid_to_load = 0; + var grid_to_battery = 0; + if (import_power > 0) { + grid_to_load = Math.min(import_power, use - solar_to_load - battery_to_load); + grid_to_battery = Math.min(import_power - grid_to_load, battery_power < 0 ? -battery_power - solar_to_battery : 0); + } + + // Accumulate kWh totals + total_solar_kwh += solar * power_to_kwh; + total_use_kwh += use * power_to_kwh; + total_import_kwh += import_power * power_to_kwh; + total_export_kwh += export_power * power_to_kwh; + total_solar_to_load_kwh += solar_to_load * power_to_kwh; + total_solar_to_grid_kwh += solar_to_grid * power_to_kwh; + total_solar_to_battery_kwh += solar_to_battery * power_to_kwh; + total_battery_to_load_kwh += battery_to_load * power_to_kwh; + total_battery_to_grid_kwh += battery_to_grid * power_to_kwh; + total_grid_to_load_kwh += grid_to_load * power_to_kwh; + total_grid_to_battery_kwh += grid_to_battery * power_to_kwh; + + solar_to_load_data.push([time, solar_to_load]); + solar_to_grid_data.push([time, solar_to_grid]); + solar_to_battery_data.push([time, solar_to_battery]); + battery_to_load_data.push([time, battery_to_load]); + battery_to_grid_data.push([time, battery_to_grid]); + grid_to_load_data.push([time, grid_to_load]); + grid_to_battery_data.push([time, grid_to_battery]); + battery_soc_data.push([time, battery_soc_now]); } else { - use_data.push([time,null]); - solar_data.push([time,null]); - battery_charge_data.push([time,null]); - battery_discharge_data.push([time,null]); - battery_soc_data.push([time,null]); + solar_to_load_data.push([time, null]); + solar_to_grid_data.push([time, null]); + solar_to_battery_data.push([time, null]); + battery_to_load_data.push([time, null]); + battery_to_grid_data.push([time, null]); + grid_to_load_data.push([time, null]); + grid_to_battery_data.push([time, null]); + battery_soc_data.push([time, null]); } - - t += interval; } - - var total_import_direct_kwh = total_use_kwh - total_battery_discharge_kwh - total_solar_direct_kwh; - var total_import_for_battery_kwh = total_import_kwh - total_import_direct_kwh; - var total_battery_charge_from_solar_kwh = total_battery_charge_kwh - total_import_for_battery_kwh; - var total_solar_export_kwh = total_solar_kwh - total_solar_direct_kwh - total_battery_charge_from_solar_kwh; - var total_grid_balance_kwh = total_import_kwh - total_solar_export_kwh; + // Derived totals for display + var total_solar_direct_kwh = total_solar_to_load_kwh; + var total_solar_export_kwh = total_solar_to_grid_kwh; // solar→grid only + var total_all_export_kwh = total_solar_to_grid_kwh + total_battery_to_grid_kwh; // total export for grid balance + var total_battery_charge_from_solar_kwh = total_solar_to_battery_kwh; + var total_import_direct_kwh = total_grid_to_load_kwh; + var total_import_for_battery_kwh = total_grid_to_battery_kwh; + var total_battery_discharge_kwh = total_battery_to_load_kwh; // battery→load only + var total_grid_balance_kwh = total_import_kwh - total_all_export_kwh; $(".total_solar_kwh").html(total_solar_kwh.toFixed(1)); $(".total_use_kwh").html(total_use_kwh.toFixed(1)); @@ -843,7 +891,8 @@ function load_powergraph() { $(".total_battery_charge_from_solar_kwh").html(total_battery_charge_from_solar_kwh.toFixed(1)); $(".total_import_for_battery_kwh").html(total_import_for_battery_kwh.toFixed(1)); $(".total_battery_discharge_kwh").html(total_battery_discharge_kwh.toFixed(1)); - $(".use_from_battery_prc").html((100*total_battery_discharge_kwh/total_use_kwh).toFixed(0)+"%"); + $(".total_battery_to_grid_kwh").html(total_battery_to_grid_kwh.toFixed(1)); + $(".use_from_battery_prc").html((100*total_battery_to_load_kwh/total_use_kwh).toFixed(0)+"%"); if (total_import_for_battery_kwh>=0.1) { $("#battery_import").show(); @@ -851,6 +900,12 @@ function load_powergraph() { $("#battery_import").hide(); } + if (total_battery_to_grid_kwh>=0.1) { + $("#battery_export").show(); + } else { + $("#battery_export").hide(); + } + var soc_change = 0; if (config.app.battery_soc.value) { @@ -860,21 +915,14 @@ function load_powergraph() { $(".battery_soc_change").html(sign+soc_change.toFixed(1)); powerseries = []; - - powerseries.push({data:solar_data, label: "Solar", color: "#dccc1f", stack:1, lines:{lineWidth:0, fill:1.0}}); - if(config.app.is_dc_battery.value) - { - powerseries.push({data:battery_charge_data, label: "Charge", color: "#fb7b50", stack:2, lines:{lineWidth:0, fill:0.8}}); - powerseries.push({data:battery_discharge_data, label: "Discharge", color: "#fbb450", stack:2, lines:{lineWidth:0, fill:0.8}}); - powerseries.push({data:use_data, label: "House", color: "#82cbfc", stack:3, lines:{lineWidth:0, fill:0.8}}); - } - else - { - powerseries.push({data:use_data, label: "House", color: "#82cbfc", stack:2, lines:{lineWidth:0, fill:0.8}}); - powerseries.push({data:battery_charge_data, label: "Charge", color: "#fb7b50", stack:2, lines:{lineWidth:0, fill:0.8}}); - powerseries.push({data:battery_discharge_data, label: "Discharge", color: "#fbb450", stack:1, lines:{lineWidth:0, fill:0.8}}); - } + powerseries.push({data: solar_to_load_data, label: "Solar to Load", color: "#abddff", stack: 1, lines: {lineWidth: 0, fill: 0.75}}); + powerseries.push({data: solar_to_grid_data, label: "Solar to Grid", color: "#dccc1f", stack: 1, lines: {lineWidth: 0, fill: 1.0}}); + powerseries.push({data: solar_to_battery_data, label: "Solar to Battery", color: "#fba050", stack: 1, lines: {lineWidth: 0, fill: 0.8}}); + powerseries.push({data: battery_to_load_data, label: "Battery to Load", color: "#ffd08e", stack: 1, lines: {lineWidth: 0, fill: 0.8}}); + powerseries.push({data: battery_to_grid_data, label: "Battery to Grid", color: "#fabb68", stack: 1, lines: {lineWidth: 0, fill: 0.8}}); + powerseries.push({data: grid_to_load_data, label: "Grid to Load", color: "#82cbfc", stack: 1, lines: {lineWidth: 0, fill: 0.8}}); + powerseries.push({data: grid_to_battery_data, label: "Grid to Battery", color: "#fb7b50", stack: 1, lines: {lineWidth: 0, fill: 0.8}}); if (show_battery_soc && config.app.battery_soc.value) powerseries.push({data:battery_soc_data, label: "SOC", yaxis:2, color: "#888"}); } @@ -1161,6 +1209,9 @@ function bargraph_events() { $("#battery_import").hide(); } + // battery_to_grid is not available from cumulative kWh feeds, hide it + $("#battery_export").hide(); + $(".battery_soc_change").html("---"); } else { From 0f5825aaede7b380ffd8e30d1f4aa9aaa4270c7e Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sat, 14 Mar 2026 16:42:14 +0000 Subject: [PATCH 02/13] minor comments change --- .../mysolarpvbattery/mysolarpvbattery.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 39567c2..3683bbd 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -375,12 +375,15 @@ function getTranslations(){ // Configuration // ---------------------------------------------------------------------- config.app = { - // Standard mysolar feeds + // == Key power feeds == + // technically we should only need 3 out of these 4 feeds as they are linked by conservation of energy + // perhaps this could be intelligently checked in the future to simplify setup "use":{"type":"feed", "autoname":"use", "description":"House or building use in watts"}, "solar":{"type":"feed", "autoname":"solar", "description":"Solar generation in watts"}, - // Battery feeds "battery_power":{"type":"feed", "autoname":"battery_power", "description":"Battery power in watts (positive for discharge, negative for charge)"}, "grid":{"type":"feed", "autoname":"grid", "description":"Grid power in watts (positive for import, negative for export)"}, + + // Battery state of charge feed (optional) "battery_soc":{"optional":true, "type":"feed", "autoname":"battery_soc", "description":"Battery state of charge in kWh"}, // History feeds From e3977d8e109a985069203343cf7cf7e7a161e6ca Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sat, 14 Mar 2026 16:55:36 +0000 Subject: [PATCH 03/13] fix daily data history --- .../mysolarpvbattery/mysolarpvbattery.php | 226 ++++++++++-------- 1 file changed, 131 insertions(+), 95 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 3683bbd..890416d 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -386,13 +386,14 @@ function getTranslations(){ // Battery state of charge feed (optional) "battery_soc":{"optional":true, "type":"feed", "autoname":"battery_soc", "description":"Battery state of charge in kWh"}, - // History feeds - "use_kwh":{"optional":true, "type":"feed", "autoname":"use_kwh", "description":"Building consumption in kWh (not including battery charging)"}, - "solar_kwh":{"optional":true, "type":"feed", "autoname":"solar_kwh", "description":"Cumulative solar generation in kWh"}, - "solar_direct_kwh":{"optional":true, "type":"feed", "autoname":"solar_direct_kwh", "description":"Cumulative solar generation used directly in kWh"}, - "import_kwh":{"optional":true, "type":"feed", "autoname":"import_kwh", "description":"Cumulative grid import in kWh"}, - "battery_charge_kwh":{"optional":true, "type":"feed", "autoname":"battery_charge_kwh", "description":"Battery charge energy in kWh"}, - "battery_discharge_kwh":{"optional":true, "type":"feed", "autoname":"battery_discharge_kwh", "description":"Battery discharge energy in kWh"}, + // History feeds (energy flow breakdown from solarbatterykwh post-processor) + "solar_to_load_kwh":{"optional":true, "type":"feed", "autoname":"solar_to_load_kwh", "description":"Cumulative solar to load energy in kWh"}, + "solar_to_grid_kwh":{"optional":true, "type":"feed", "autoname":"solar_to_grid_kwh", "description":"Cumulative solar to grid (export) energy in kWh"}, + "solar_to_battery_kwh":{"optional":true, "type":"feed", "autoname":"solar_to_battery_kwh", "description":"Cumulative solar to battery energy in kWh"}, + "battery_to_load_kwh":{"optional":true, "type":"feed", "autoname":"battery_to_load_kwh", "description":"Cumulative battery to load energy in kWh"}, + "battery_to_grid_kwh":{"optional":true, "type":"feed", "autoname":"battery_to_grid_kwh", "description":"Cumulative battery to grid energy in kWh"}, + "grid_to_load_kwh":{"optional":true, "type":"feed", "autoname":"grid_to_load_kwh", "description":"Cumulative grid to load energy in kWh"}, + "grid_to_battery_kwh":{"optional":true, "type":"feed", "autoname":"grid_to_battery_kwh", "description":"Cumulative grid to battery energy in kWh"}, // Other options "kw":{"type":"checkbox", "default":0, "name": "Show kW", "description": "Display power as kW"}, @@ -468,7 +469,9 @@ function init() view.start = view.end - timeWindow; live_timerange = timeWindow; - if (config.app.solar_kwh.value && config.app.use_kwh.value && config.app.import_kwh.value && config.app.battery_charge_kwh.value && config.app.battery_discharge_kwh.value) { + if (config.app.solar_to_load_kwh.value && config.app.solar_to_grid_kwh.value && config.app.solar_to_battery_kwh.value && + config.app.battery_to_load_kwh.value && config.app.battery_to_grid_kwh.value && + config.app.grid_to_load_kwh.value && config.app.grid_to_battery_kwh.value) { init_bargraph(); $(".viewhistory").show(); } else { @@ -524,7 +527,9 @@ function show() { app_log("INFO","solar & battery show"); - if (config.app.solar_kwh.value && config.app.use_kwh.value && config.app.import_kwh.value) { + if (config.app.solar_to_load_kwh.value && config.app.solar_to_grid_kwh.value && config.app.solar_to_battery_kwh.value && + config.app.battery_to_load_kwh.value && config.app.battery_to_grid_kwh.value && + config.app.grid_to_load_kwh.value && config.app.grid_to_battery_kwh.value) { if (!bargraph_initialized) init_bargraph(); } @@ -1018,19 +1023,21 @@ function powergraph_events() { // -------------------------------------------------------------------------------------- function init_bargraph() { bargraph_initialized = true; - // Fetch the start_time covering all kwh feeds - this is used for the 'all time' button + // Fetch the start_time covering all flow kWh feeds - this is used for the 'all time' button latest_start_time = 0; - var solar_meta = feed.getmeta(config.app.solar_kwh.value); - var use_meta = feed.getmeta(config.app.use_kwh.value); - var import_meta = feed.getmeta(config.app.import_kwh.value); - if (solar_meta.start_time > latest_start_time) latest_start_time = solar_meta.start_time; - if (use_meta.start_time > latest_start_time) latest_start_time = use_meta.start_time; - if (import_meta.start_time > latest_start_time) latest_start_time = import_meta.start_time; - latest_start_time = latest_start_time; - - var earliest_start_time = solar_meta.start_time; - earliest_start_time = Math.min(earliest_start_time, use_meta.start_time); - earliest_start_time = Math.min(earliest_start_time, import_meta.start_time); + var flow_feeds = [ + config.app.solar_to_load_kwh.value, + config.app.solar_to_grid_kwh.value, + config.app.solar_to_battery_kwh.value, + config.app.battery_to_load_kwh.value, + config.app.battery_to_grid_kwh.value, + config.app.grid_to_load_kwh.value, + config.app.grid_to_battery_kwh.value + ]; + for (var i = 0; i < flow_feeds.length; i++) { + var m = feed.getmeta(flow_feeds[i]); + if (m.start_time > latest_start_time) latest_start_time = m.start_time; + } view.first_data = latest_start_time * 1000; } @@ -1038,60 +1045,76 @@ function load_bargraph() { var interval = 3600*24; var intervalms = interval * 1000; - end = view.end - start = view.start + end = view.end; + start = view.start; end = Math.ceil(end/intervalms)*intervalms; start = Math.floor(start/intervalms)*intervalms; - // Load kWh data - var solar_kwh_data = feed.getdata(config.app.solar_kwh.value,start,end,"daily",0,1); - var use_kwh_data = feed.getdata(config.app.use_kwh.value,start,end,"daily",0,1); - var import_kwh_data = feed.getdata(config.app.import_kwh.value,start,end,"daily",0,1); - var battery_charge_kwh_data = feed.getdata(config.app.battery_charge_kwh.value,start,end,"daily",0,1); - var battery_discharge_kwh_data = feed.getdata(config.app.battery_discharge_kwh.value,start,end,"daily",0,1); - var solar_direct_kwh_data = feed.getdata(config.app.solar_direct_kwh.value,start,end,"daily",0,1); + // Load energy flow kWh data directly from post-processor feeds + var solar_to_load_kwh_data = feed.getdata(config.app.solar_to_load_kwh.value, start,end,"daily",0,1); + var solar_to_grid_kwh_data = feed.getdata(config.app.solar_to_grid_kwh.value, start,end,"daily",0,1); + var solar_to_battery_kwh_data = feed.getdata(config.app.solar_to_battery_kwh.value,start,end,"daily",0,1); + var battery_to_load_kwh_data = feed.getdata(config.app.battery_to_load_kwh.value, start,end,"daily",0,1); + var battery_to_grid_kwh_data = feed.getdata(config.app.battery_to_grid_kwh.value, start,end,"daily",0,1); + var grid_to_load_kwh_data = feed.getdata(config.app.grid_to_load_kwh.value, start,end,"daily",0,1); + var grid_to_battery_kwh_data = feed.getdata(config.app.grid_to_battery_kwh.value, start,end,"daily",0,1); - solar_kwhd_data = []; - use_kwhd_data = []; - export_kwhd_data = []; - solar_direct_kwhd_data = []; - battery_charge_kwhd_data = []; - battery_discharge_kwhd_data = []; - import_kwhd_data = []; + // Per-day arrays for graph and hover access + solar_to_load_kwhd_data = []; + solar_to_grid_kwhd_data = []; + solar_to_battery_kwhd_data = []; + battery_to_load_kwhd_data = []; + battery_to_grid_kwhd_data = []; + grid_to_load_kwhd_data = []; + grid_to_battery_kwhd_data = []; + // Derived totals retained for hover labels + solar_kwhd_data = []; + use_kwhd_data = []; + import_kwhd_data = []; + export_kwhd_data = []; - if (solar_kwh_data.length) { - for (var day=0; day=0.1) { + if (total_grid_to_battery>=0.1) { $("#battery_import").show(); } else { $("#battery_import").hide(); } - // battery_to_grid is not available from cumulative kWh feeds, hide it - $("#battery_export").hide(); + if (total_battery_to_grid>=0.1) { + $("#battery_export").show(); + } else { + $("#battery_export").hide(); + } $(".battery_soc_change").html("---"); @@ -1231,7 +1267,7 @@ function bargraph_events() { history_start = view.start history_end = view.end - view.start = solar_kwhd_data[z][0]; + view.start = solar_to_load_kwhd_data[z][0]; view.end = view.start + 86400*1000; $(".balanceline").attr('disabled',false); From 66eb0f74aa63dd9c07316c9959140a9e3bf450c4 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sat, 14 Mar 2026 17:08:46 +0000 Subject: [PATCH 04/13] a few more fixes and clean up --- .../mysolarpvbattery/mysolarpvbattery.php | 50 ++++++------------- 1 file changed, 16 insertions(+), 34 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 890416d..09f5f07 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -331,7 +331,7 @@

This app can be used to explore onsite solar generation, self consumption, battery integration, export and building consumption.

Auto configure: This app can auto-configure connecting to emoncms feeds with the names shown on the right, alternatively feeds can be selected by clicking on the edit button.

-

Cumulative kWh feeds can be generated from power feeds with the power_to_kwh input processor.

+

History view: Daily energy flow breakdown feeds can be generated from the power feeds using the Solar battery kWh flows post-processor (solarbatterykwh).

@@ -672,8 +672,8 @@ function livefn() $(".battery_charge_discharge").html(net_battery_charge); $(".discharge_time_left").html("--"); } else if (net_battery_charge<0) { - if (config.app && config.app.kw && config.app.battery_capacity_kwh.value > 0 && battery_soc_now >= 0) { - const total_capacity = config.app.battery_capacity_kwh.value * 1000; + if (config.app.battery_capacity_kwh.value > 0 && battery_soc_now >= 0 && powerUnit === 'kW') { + const total_capacity = config.app.battery_capacity_kwh.value; const energy_remaining = total_capacity * battery_soc_now / 100; const total_time_left_mins = (energy_remaining / -(net_battery_charge)) * 60; @@ -1023,8 +1023,8 @@ function powergraph_events() { // -------------------------------------------------------------------------------------- function init_bargraph() { bargraph_initialized = true; - // Fetch the start_time covering all flow kWh feeds - this is used for the 'all time' button - latest_start_time = 0; + // Fetch the earliest start_time across all flow kWh feeds - this is used for the 'all time' button + var earliest_start_time = Infinity; var flow_feeds = [ config.app.solar_to_load_kwh.value, config.app.solar_to_grid_kwh.value, @@ -1036,8 +1036,9 @@ function init_bargraph() { ]; for (var i = 0; i < flow_feeds.length; i++) { var m = feed.getmeta(flow_feeds[i]); - if (m.start_time > latest_start_time) latest_start_time = m.start_time; + if (m.start_time < earliest_start_time) earliest_start_time = m.start_time; } + latest_start_time = earliest_start_time; view.first_data = latest_start_time * 1000; } @@ -1045,8 +1046,8 @@ function load_bargraph() { var interval = 3600*24; var intervalms = interval * 1000; - end = view.end; - start = view.start; + var end = view.end; + var start = view.start; end = Math.ceil(end/intervalms)*intervalms; start = Math.floor(start/intervalms)*intervalms; @@ -1068,11 +1069,6 @@ function load_bargraph() { battery_to_grid_kwhd_data = []; grid_to_load_kwhd_data = []; grid_to_battery_kwhd_data = []; - // Derived totals retained for hover labels - solar_kwhd_data = []; - use_kwhd_data = []; - import_kwhd_data = []; - export_kwhd_data = []; if (solar_to_load_kwh_data.length) { for (var day=0; dayAbove: Onsite Use & Total Use"); - $('#placeholder').append("
Below: Exported solar
"); + $('#placeholder').append("
Above: Onsite Use & Total Use
"); + $('#placeholder').append("
Below: Total export (solar + battery to grid)
"); } // ------------------------------------------------------------------------------------------ From 471e4527f1f44c4adae1df1d3a9416f46eb5e3c7 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sat, 14 Mar 2026 17:31:28 +0000 Subject: [PATCH 05/13] solarpv battery template app, used for developing and testing key calculations, could be useful in future --- apps/OpenEnergyMonitor/solartemplate/app.json | 5 + .../solartemplate/solartemplate.js | 567 ++++++++++++++++++ .../solartemplate/solartemplate.php | 167 ++++++ apps/template/app.json | 2 +- 4 files changed, 740 insertions(+), 1 deletion(-) create mode 100644 apps/OpenEnergyMonitor/solartemplate/app.json create mode 100644 apps/OpenEnergyMonitor/solartemplate/solartemplate.js create mode 100644 apps/OpenEnergyMonitor/solartemplate/solartemplate.php diff --git a/apps/OpenEnergyMonitor/solartemplate/app.json b/apps/OpenEnergyMonitor/solartemplate/app.json new file mode 100644 index 0000000..b5db377 --- /dev/null +++ b/apps/OpenEnergyMonitor/solartemplate/app.json @@ -0,0 +1,5 @@ +{ + "title" : "Solar PV & Battery Template", + "description" : "A simpler version of the My Solar Battery app, power view only and key calculations", + "order" : 20 +} diff --git a/apps/OpenEnergyMonitor/solartemplate/solartemplate.js b/apps/OpenEnergyMonitor/solartemplate/solartemplate.js new file mode 100644 index 0000000..63ab61d --- /dev/null +++ b/apps/OpenEnergyMonitor/solartemplate/solartemplate.js @@ -0,0 +1,567 @@ +// These are used by the feed api to handle user auth requirements +feed.apikey = apikey; + +// Hide the config button if in public view +if (!sessionwrite) $(".config-open").hide(); + +// Used by the apps module configuration library to build the app configuration form +config.app = { + "use": { + "type": "feed", + "autoname": "use", + "engine": "5", + "description": "House or building use in watts" + }, + "solar": { + "type": "feed", + "autoname": "solar", + "engine": "5", + "description": "Solar generation in watts" + }, + "battery_power": { + "type": "feed", + "autoname": "battery_power", + "engine": "5", + "description": "Battery power in watts (positive for discharge, negative for charge)" + }, + "grid": { + "type": "feed", + "autoname": "grid", + "engine": "5", + "description": "Grid power in watts (positive for import, negative for export)" + }, + "battery_soc": { + "type": "feed", + "autoname": "battery_soc", + "engine": "5", + "description": "Battery state of charge in kWh" + } +}; + +// Fetch user feed list +config.feeds = feed.list(); + +config.initapp = function () { + init() +}; +config.showapp = function () { + show() +}; +config.hideapp = function () { + clear() +}; + +// ---------------------------------------------------------------------- +// APPLICATION +// ---------------------------------------------------------------------- + +// Feeds to load and their graph options +var feeds_to_load = { + "use": { label: "Use", yaxis: 1, color: "#0699fa", lines: { show: true, fill: 0.75, lineWidth: 0 } }, + "solar": { label: "Solar", yaxis: 1, color: "#f4c009", lines: { show: true, fill: 0.75, lineWidth: 0 } }, + "battery_power": { label: "Battery", yaxis: 1, color: "#888", lines: { show: true, fill: 0.75, lineWidth: 0 } }, + "grid": { label: "Grid", yaxis: 1, color: "#f00", lines: { show: true, fill: 0.75, lineWidth: 0 } }, + "battery_soc": { label: "Battery SOC", yaxis: 2, color: "#ccc", lines: { show: true, fill: false, lineWidth: 2 } } +}; + +// Graph variables +var data = {}; +var powergraph_series = {}; + +var options = { + canvas: true, + xaxis: { + mode: "time", + timezone: "browser" + }, + yaxes: [ + { + min: 0 + }, + ], + grid: { + show: true, + color: "#aaa", + borderWidth: 0, + hoverable: true + }, + legend: { + show: false + }, + selection: { + mode: "x", + color: "#555" + } +}; + +// used for tooltip +var previousPoint = null; + +config.init(); + +function init() { + +} + +function show() { + $(".ajax-loader").hide(); + resize(); + updater(); + updaterinst = setInterval(updater, 10000); + + // Starting view + view.end = +new Date; + meta = feed.getmeta(config.app.use.value); + // Limit end time to feed end time + if (view.end * 0.001 > meta.end_time) { + view.end = meta.end_time * 1000; + } + // Set start time to 7 days ago + view.start = view.end - (3600000 * 24.0 * 1); + + load(); +} + +function load() { + view.calc_interval(1500); + + // Compile list of feedids + var feedids = []; + for (var key in feeds_to_load) { + feedids.push(config.app[key].value); + } + + // Options for feed.getdata + var skipmissing = 0; + var limitinterval = 0; + var average = 1; + if (view.interval < 15) { + average = 0; + } + + + // Fetch the data + feed.getdata(feedids, view.start, view.end, view.interval, average, 0, skipmissing, limitinterval, function (all_data) { + + // Transfer from data to all_data by key + var feed_index = 0; + for (var key in feeds_to_load) { + if (all_data[feed_index] != undefined) { + // Data object used for calculations + data[key] = remove_null_values(all_data[feed_index].data, view.interval); + feed_index++; + } + } + + + var power_to_kwh = view.interval / 3600000.0; + + var use_kwh = 0; + var solar_kwh = 0; + var import_kwh = 0; + var export_kwh = 0; + + var solar_to_load_kwh = 0; + var solar_to_grid_kwh = 0; + var solar_to_battery_kwh = 0; + var battery_to_load_kwh = 0; + var battery_to_grid_kwh = 0; + var grid_to_load_kwh = 0; + var grid_to_battery_kwh = 0; + + data["solar_to_load"] = []; + data["solar_to_grid"] = []; + data["solar_to_battery"] = []; + + data["battery_to_load"] = []; + data["battery_to_grid"] = []; + + data["grid_to_load"] = []; + data["grid_to_battery"] = []; + + for (var z in data["use"]) { + let time = data["use"][z][0]; + let use = data["use"][z][1]; + let solar = data["solar"][z][1]; + // positive battery power means discharge, negative means charge + let battery_power = data["battery_power"][z][1]; + // positive grid means import, negative means export + let grid = data["grid"][z][1]; + + // skip if any of the values are null + if (use == null || solar == null || battery_power == null || grid == null) { + continue; + } + + // Verify conservation of energy + // while we will likely have 4x meter points + // we only actually have 3x independent variables + // If we dont have use, we can calculate it as solar + battery + grid + // If we dont have solar, we can calculate it as use - battery - grid + // If we dont have battery, we can calculate it as use - solar - grid + // If we dont have grid, we can calculate it as use - solar - battery + // If these dont balance, then we have an issue with the data + + let use_check = solar + battery_power + grid; + let solar_check = use - battery_power - grid; + let battery_check = use - solar - grid; + let grid_check = use - solar - battery_power; + + // if (Math.abs(use - use_check) > 0.1) { + // console.error("Use does not balance! " + use + " vs " + use_check); + // } + + // Override for conservation of energy + use = use_check; + + let import_power = 0; + let export_power = 0; + + if (grid > 0) { + import_power = grid; + } else { + export_power = -grid; + } + + // ------------------------------------------------------------------------------------------------ + // SOLAR + // ------------------------------------------------------------------------------------------------ + + // Calculate solar to load and solar to grid + let solar_to_load = Math.min(solar, use); + + // Calculate solar to battery + let solar_to_battery = 0; + if (battery_power < 0) { + // Battery is charging, so solar to battery is the lesser of the available solar and the battery charge power + solar_to_battery = Math.min(solar - solar_to_load, -battery_power); + } + + let solar_to_grid = solar - solar_to_load - solar_to_battery; + + // ------------------------------------------------------------------------------------------------ + // BATTERY + // ------------------------------------------------------------------------------------------------ + + // Calculate battery to load and battery to grid + let battery_to_load = 0; + let battery_to_grid = 0; + if (battery_power > 0) { + // Battery is discharging, so battery to load is the lesser of the available battery power and the remaining load after solar + battery_to_load = Math.min(battery_power, use - solar_to_load); + battery_to_grid = battery_power - battery_to_load; + } + + // ------------------------------------------------------------------------------------------------ + // GIRD (TO LOAD, TO BATTERY) + // ------------------------------------------------------------------------------------------------ + let grid_to_load = 0; + let grid_to_battery = 0; + if (import_power > 0) { + // Grid is importing, so grid to load is the lesser of the import power and the remaining load after solar and battery + grid_to_load = Math.min(import_power, use - solar_to_load - battery_to_load); + grid_to_battery = Math.min(import_power - grid_to_load, battery_power < 0 ? -battery_power - solar_to_battery : 0); + } + + + // ------------------------------------------------------------------------------------------------ + + // Calculate kWh for the period + use_kwh += use * power_to_kwh; + solar_kwh += solar * power_to_kwh; + import_kwh += import_power * power_to_kwh; + export_kwh += export_power * power_to_kwh; + + solar_to_load_kwh += solar_to_load * power_to_kwh; + solar_to_grid_kwh += solar_to_grid * power_to_kwh; + solar_to_battery_kwh += solar_to_battery * power_to_kwh; + + battery_to_load_kwh += battery_to_load * power_to_kwh; + battery_to_grid_kwh += battery_to_grid * power_to_kwh; + + grid_to_load_kwh += grid_to_load * power_to_kwh; + grid_to_battery_kwh += grid_to_battery * power_to_kwh; + + // ------------------------------------------------------------------------------------------------ + + data["use"][z][1] = use; + data["solar"][z][1] = solar; + + data["solar_to_load"].push([time, solar_to_load]); + data["solar_to_grid"].push([time, solar_to_grid]); + data["solar_to_battery"].push([time, solar_to_battery]); + + data["battery_to_load"].push([time, battery_to_load]); + data["battery_to_grid"].push([time, battery_to_grid]); + + data["grid_to_load"].push([time, grid_to_load]); + data["grid_to_battery"].push([time, grid_to_battery]); + + // ------------------------------------------------------------------------------------------------ + } + + $("#use_kwh").html(use_kwh.toFixed(1)); + $("#solar_kwh").html(solar_kwh.toFixed(1)); + $("#import_kwh").html(import_kwh.toFixed(1)); + $("#export_kwh").html(export_kwh.toFixed(1)); + + $("#solar_to_load_kwh").html(solar_to_load_kwh.toFixed(1)); + $("#solar_to_grid_kwh").html(solar_to_grid_kwh.toFixed(1)); + $("#solar_to_battery_kwh").html(solar_to_battery_kwh.toFixed(1)); + + $("#battery_to_load_kwh").html(battery_to_load_kwh.toFixed(1)); + $("#battery_to_grid_kwh").html(battery_to_grid_kwh.toFixed(1)); + + $("#grid_to_load_kwh").html(grid_to_load_kwh.toFixed(1)); + $("#grid_to_battery_kwh").html(grid_to_battery_kwh.toFixed(1)); + + + // --------------------------------------------------------------------------------- + // VALIDATION CHECKS + // --------------------------------------------------------------------------------- + + var solar_kwh_check = solar_to_load_kwh + solar_to_grid_kwh + solar_to_battery_kwh; + var use_kwh_check = solar_to_load_kwh + battery_to_load_kwh + grid_to_load_kwh; + var import_kwh_check = grid_to_load_kwh + grid_to_battery_kwh; + var export_kwh_check = solar_to_grid_kwh + battery_to_grid_kwh; + + // Validate calculations + // There are usually minor discrepancies in use_kwh + if (Math.abs(solar_kwh - solar_kwh_check) > 0.1) { + console.error("Solar kWh does not balance! " + solar_kwh + " vs " + solar_kwh_check); + } + if (Math.abs(use_kwh - use_kwh_check) > 0.1) { + console.error("Use kWh does not balance! " + use_kwh + " vs " + use_kwh_check); + } + if (Math.abs(import_kwh - import_kwh_check) > 0.1) { + console.error("Import kWh does not balance! " + import_kwh + " vs " + import_kwh_check); + } + if (Math.abs(export_kwh - export_kwh_check) > 0.1) { + console.error("Export kWh does not balance! " + export_kwh + " vs " + export_kwh_check); + } + + // --------------------------------------------------------------------------------- + + reset_series(); + + add_series("solar_to_load", data["solar_to_load"], { + label: "Solar to Load", + yaxis: 1, + color: "#abddffff", + stack: true, + lines: { show: true, fill: 0.75, lineWidth: 0 }, + }); + + add_series("solar_to_grid", data["solar_to_grid"], { + label: "Solar to Grid", + yaxis: 1, + color: "#dccc1f", + stack: true, + lines: { show: true, fill: 1.0, lineWidth: 0 } + }); + + + add_series("solar_to_battery", data["solar_to_battery"], { + label: "Solar to Battery", + yaxis: 1, + color: "#fba050ff", + stack: true, + lines: { show: true, fill: 0.8, lineWidth: 0 } + }); + + add_series("battery_to_load", data["battery_to_load"], { + label: "Battery to Load", + yaxis: 1, + color: "#ffd08eff", + stack: true, + lines: { show: true, fill: 0.8, lineWidth: 0 } + }); + + add_series("battery_to_grid", data["battery_to_grid"], { + label: "Battery to Grid", + yaxis: 1, + color: "#fabb68ff", + stack: true, + lines: { show: true, fill: 0.8, lineWidth: 0 } + }); + + add_series("grid_to_load", data["grid_to_load"], { + label: "Grid to Load", + yaxis: 1, + color: "#82cbfc", + stack: true, + lines: { show: true, fill: 0.8, lineWidth: 0 } + }); + + add_series("grid_to_battery", data["grid_to_battery"], { + label: "Grid to Battery", + yaxis: 1, + color: "#fb7b50", + stack: true, + lines: { show: true, fill: 0.8, lineWidth: 0 } + }); + + // add soc + add_series("battery_soc", data["battery_soc"], { + label: "Battery SOC", + yaxis: 2, + color: "#ccc", + lines: { show: true, fill: false, lineWidth: 2 } + }); + + + draw(); + }, false, "notime"); + + +} + +function reset_series() { + powergraph_series = {}; +} + +function add_series(key, data, options) { + let series = options; + series.data = data; + powergraph_series[key] = series; +} + +function draw() { + + options.xaxis.min = view.start; + options.xaxis.max = view.end; + + // Remove keys + var powergraph_series_without_key = []; + for (var key in powergraph_series) { + powergraph_series_without_key.push(powergraph_series[key]); + } + $.plot($('#graph'), powergraph_series_without_key, options); + +} + +function updater() { + var use_id = config.app.use.value; + var solar_id = config.app.solar.value; + var battery_id = config.app.battery_power.value; + var grid_id = config.app.grid.value; + var soc_id = config.app.battery_soc.value; + + var feeds = feed.listbyid(); + + var use_w = feeds[use_id] ? feeds[use_id].value * 1 : 0; + var solar_w = feeds[solar_id] ? feeds[solar_id].value * 1 : 0; + var battery_w = feeds[battery_id] ? feeds[battery_id].value * 1 : 0; + var grid_w = feeds[grid_id] ? feeds[grid_id].value * 1 : 0; + var soc = feeds[soc_id] ? feeds[soc_id].value * 1 : null; + + $("#powernow").html(use_w.toFixed(0)); + $("#solarnow").html(solar_w.toFixed(0)); + + // Battery: positive = discharging (orange), negative = charging (purple) + $("#batterynow").html(Math.abs(battery_w).toFixed(0)); + if (battery_w > 10) { + $("#battery-label").text("BAT DISCHG").css("color", "#fba050"); + $("#battery-value").css("color", "#fba050"); + } else if (battery_w < -10) { + $("#battery-label").text("BAT CHRG").css("color", "#c084fc"); + $("#battery-value").css("color", "#c084fc"); + } else { + $("#battery-label").text("BATTERY").css("color", "#aaa"); + $("#battery-value").css("color", "#aaa"); + } + + // Grid: positive = import (light blue), negative = export (yellow-green) + $("#gridnow").html(Math.abs(grid_w).toFixed(0)); + if (grid_w > 10) { + $("#grid-label").text("IMPORT").css("color", "#82cbfc"); + $("#grid-value").css("color", "#82cbfc"); + } else if (grid_w < -10) { + $("#grid-label").text("EXPORT").css("color", "#dccc1f"); + $("#grid-value").css("color", "#dccc1f"); + } else { + $("#grid-label").text("GRID").css("color", "#aaa"); + $("#grid-value").css("color", "#aaa"); + } + + // Battery SOC + if (soc !== null) { + $("#socnow").html(soc.toFixed(0)); + } else { + $("#socnow").html("--"); + } +} + +function resize() { + if ($('#app-block').is(":visible")) { + draw(); + } +} + +function clear() { + clearInterval(updaterinst); +} + +// Graph navigation buttons +$("#zoomout").click(function () { view.zoomout(); load(); }); +$("#zoomin").click(function () { view.zoomin(); load(); }); +$('#right').click(function () { view.panright(); load(); }); +$('#left').click(function () { view.panleft(); load(); }); + +$('.time').click(function () { + view.timewindow($(this).attr("time") / 24.0); + load(); +}); + +// Tooltip code +$('#graph').bind("plothover", function (event, pos, item) { + if (item) { + var i = item.dataIndex; + + if (previousPoint != item.datapoint) { + previousPoint = item.datapoint; + $("#tooltip").remove(); + + var itemTime = item.datapoint[0]; + var itemValue = item.datapoint[1]; + + if (itemValue != null) itemValue = itemValue.toFixed(0); + tooltip(item.pageX, item.pageY, itemValue + "W
" + tooltip_date(itemTime), "#fff", "#000"); + } + } else $("#tooltip").remove(); +}); + +$('#graph').bind("plotselected", function (event, ranges) { + view.start = ranges.xaxis.from; + view.end = ranges.xaxis.to; + load(); +}); + +$(window).resize(function () { + resize(); +}); + +// ---------------------------------------------------------------------- +// App log +// ---------------------------------------------------------------------- +function app_log(level, message) { + if (level == "ERROR") alert(level + ": " + message); + console.log(level + ": " + message); +} + +// Remove null values from feed data +function remove_null_values(data, interval) { + var last_valid_pos = 0; + for (var pos = 0; pos < data.length; pos++) { + if (data[pos][1] != null) { + let null_time = (pos - last_valid_pos) * interval; + if (null_time < 900) { + for (var x = last_valid_pos + 1; x < pos; x++) { + data[x][1] = data[last_valid_pos][1]; + } + } + last_valid_pos = pos; + } + } + return data; +} \ No newline at end of file diff --git a/apps/OpenEnergyMonitor/solartemplate/solartemplate.php b/apps/OpenEnergyMonitor/solartemplate/solartemplate.php new file mode 100644 index 0000000..4ec678a --- /dev/null +++ b/apps/OpenEnergyMonitor/solartemplate/solartemplate.php @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+

+

A simpler version of the My Solar Battery app, power view only and key calculations

+
+
+
+
+
+
+ +
+ + + diff --git a/apps/template/app.json b/apps/template/app.json index 90378e5..5a59c40 100644 --- a/apps/template/app.json +++ b/apps/template/app.json @@ -1,5 +1,5 @@ { "title" : "Example 1", "description" : "A basic app example useful for developing new apps.", - "order" : 15 + "order" : 19 } From 9be42613a2b86425169706f7c04c2d32f0655371 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sat, 14 Mar 2026 17:41:48 +0000 Subject: [PATCH 06/13] clean up --- .../solartemplate/solartemplate.js | 413 +++++++----------- .../solartemplate/solartemplate.php | 4 +- 2 files changed, 172 insertions(+), 245 deletions(-) diff --git a/apps/OpenEnergyMonitor/solartemplate/solartemplate.js b/apps/OpenEnergyMonitor/solartemplate/solartemplate.js index 63ab61d..cd87fbd 100644 --- a/apps/OpenEnergyMonitor/solartemplate/solartemplate.js +++ b/apps/OpenEnergyMonitor/solartemplate/solartemplate.js @@ -4,7 +4,12 @@ feed.apikey = apikey; // Hide the config button if in public view if (!sessionwrite) $(".config-open").hide(); -// Used by the apps module configuration library to build the app configuration form +// Used by the apps module configuration library to build the app configuration form. +// Each key maps to a feed the user selects in the configuration panel. +// - type: "feed" tells the config UI to show a feed picker +// - autoname: the default feed name to pre-select if it exists +// - engine: 5 = PHPFina (fixed-interval time series) +// - description: shown in the configuration UI config.app = { "use": { "type": "feed", @@ -22,52 +27,58 @@ config.app = { "type": "feed", "autoname": "battery_power", "engine": "5", - "description": "Battery power in watts (positive for discharge, negative for charge)" + "description": "Battery power in watts (positive = discharge, negative = charge)" }, "grid": { "type": "feed", "autoname": "grid", "engine": "5", - "description": "Grid power in watts (positive for import, negative for export)" + "description": "Grid power in watts (positive = import, negative = export)" }, "battery_soc": { "type": "feed", "autoname": "battery_soc", "engine": "5", - "description": "Battery state of charge in kWh" + "description": "Battery state of charge in percent (%)" } }; -// Fetch user feed list +// Fetch user feed list (used by the config UI to populate feed pickers) config.feeds = feed.list(); -config.initapp = function () { - init() -}; -config.showapp = function () { - show() -}; -config.hideapp = function () { - clear() -}; +// config.initapp – called once when the app module first loads +// config.showapp – called each time the app becomes visible +// config.hideapp – called when the app is hidden (e.g. switching tabs) +config.initapp = function () { init(); }; +config.showapp = function () { show(); }; +config.hideapp = function () { clear(); }; // ---------------------------------------------------------------------- // APPLICATION // ---------------------------------------------------------------------- -// Feeds to load and their graph options -var feeds_to_load = { - "use": { label: "Use", yaxis: 1, color: "#0699fa", lines: { show: true, fill: 0.75, lineWidth: 0 } }, - "solar": { label: "Solar", yaxis: 1, color: "#f4c009", lines: { show: true, fill: 0.75, lineWidth: 0 } }, - "battery_power": { label: "Battery", yaxis: 1, color: "#888", lines: { show: true, fill: 0.75, lineWidth: 0 } }, - "grid": { label: "Grid", yaxis: 1, color: "#f00", lines: { show: true, fill: 0.75, lineWidth: 0 } }, - "battery_soc": { label: "Battery SOC", yaxis: 2, color: "#ccc", lines: { show: true, fill: false, lineWidth: 2 } } -}; - -// Graph variables +// Ordered list of feed keys fetched from the server for graph data. +// The ORDER here must be preserved — it determines the index used when +// mapping the batch response from feed.getdata() back to named keys. +// Note: graph series colours/options are defined separately in the +// add_series() calls inside load(), keeping fetch config and display +// config cleanly separated. +var feeds_to_load = [ + "use", + "solar", + "battery_power", + "grid", + "battery_soc" +]; + +// Graph data store — populated after each fetch var data = {}; + +// Flot series store — rebuilt on each draw var powergraph_series = {}; +// Flot graph options +// yaxis 1: power (W), yaxis 2: battery SOC (%) var options = { canvas: true, xaxis: { @@ -75,9 +86,8 @@ var options = { timezone: "browser" }, yaxes: [ - { - min: 0 - }, + { min: 0 }, // yaxis 1: power — never show negative + { min: 0, max: 100, position: "right" } // yaxis 2: SOC % ], grid: { show: true, @@ -88,36 +98,42 @@ var options = { legend: { show: false }, - selection: { + selection: { mode: "x", color: "#555" } }; -// used for tooltip +// used for tooltip de-duplication (avoids redrawing on every mouse move) var previousPoint = null; config.init(); +// init() is called once when the app is first loaded. +// Use this for any one-time setup that should happen before show() is called. function init() { - + // Nothing needed for this template — extend here as required. } function show() { $(".ajax-loader").hide(); resize(); - updater(); - updaterinst = setInterval(updater, 10000); - // Starting view - view.end = +new Date; - meta = feed.getmeta(config.app.use.value); - // Limit end time to feed end time + // Start the live updater immediately, then refresh every 10 seconds + updater(); + let updaterinst_local = setInterval(updater, 10000); + // Store on window so clear() can cancel it + window.updaterinst = updaterinst_local; + + // Set the initial view window. + // End: now (clamped to the feed's last data point so we don't load empty future time). + // Start: 24 hours before end. + view.end = +new Date(); + let meta = feed.getmeta(config.app.use.value); if (view.end * 0.001 > meta.end_time) { view.end = meta.end_time * 1000; } - // Set start time to 7 days ago - view.start = view.end - (3600000 * 24.0 * 1); + view.start = view.end - (3600000 * 24.0); load(); } @@ -125,50 +141,44 @@ function show() { function load() { view.calc_interval(1500); - // Compile list of feedids - var feedids = []; - for (var key in feeds_to_load) { - feedids.push(config.app[key].value); - } + // Build the ordered list of feed IDs to request, matching feeds_to_load order + let feedids = feeds_to_load.map(function (key) { + return config.app[key].value; + }); - // Options for feed.getdata - var skipmissing = 0; - var limitinterval = 0; - var average = 1; - if (view.interval < 15) { - average = 0; - } + // Use averaging for intervals >= 15s to keep the graph smooth and reduce data volume + let skipmissing = 0; + let limitinterval = 0; + let average = (view.interval >= 15) ? 1 : 0; - - // Fetch the data + // Fetch all feed data in a single batch request feed.getdata(feedids, view.start, view.end, view.interval, average, 0, skipmissing, limitinterval, function (all_data) { - // Transfer from data to all_data by key - var feed_index = 0; - for (var key in feeds_to_load) { - if (all_data[feed_index] != undefined) { - // Data object used for calculations - data[key] = remove_null_values(all_data[feed_index].data, view.interval); - feed_index++; + // Map the indexed response array back to named keys + feeds_to_load.forEach(function (key, index) { + if (all_data[index] != undefined) { + data[key] = remove_null_values(all_data[index].data, view.interval); } - } + }); - var power_to_kwh = view.interval / 3600000.0; + // Conversion factor: multiply instantaneous watts by this to get kWh for one interval + let power_to_kwh = view.interval / 3600000.0; - var use_kwh = 0; - var solar_kwh = 0; - var import_kwh = 0; - var export_kwh = 0; + let use_kwh = 0; + let solar_kwh = 0; + let import_kwh = 0; + let export_kwh = 0; - var solar_to_load_kwh = 0; - var solar_to_grid_kwh = 0; - var solar_to_battery_kwh = 0; - var battery_to_load_kwh = 0; - var battery_to_grid_kwh = 0; - var grid_to_load_kwh = 0; - var grid_to_battery_kwh = 0; + let solar_to_load_kwh = 0; + let solar_to_grid_kwh = 0; + let solar_to_battery_kwh = 0; + let battery_to_load_kwh = 0; + let battery_to_grid_kwh = 0; + let grid_to_load_kwh = 0; + let grid_to_battery_kwh = 0; + // Initialise derived time-series arrays (built up per data point below) data["solar_to_load"] = []; data["solar_to_grid"] = []; data["solar_to_battery"] = []; @@ -179,111 +189,91 @@ function load() { data["grid_to_load"] = []; data["grid_to_battery"] = []; - for (var z in data["use"]) { + for (let z in data["use"]) { let time = data["use"][z][0]; let use = data["use"][z][1]; let solar = data["solar"][z][1]; - // positive battery power means discharge, negative means charge + // positive battery_power = discharging, negative = charging let battery_power = data["battery_power"][z][1]; - // positive grid means import, negative means export + // positive grid = importing, negative = exporting let grid = data["grid"][z][1]; - // skip if any of the values are null + // Skip data points where any feed has a null value if (use == null || solar == null || battery_power == null || grid == null) { continue; } - // Verify conservation of energy - // while we will likely have 4x meter points - // we only actually have 3x independent variables - // If we dont have use, we can calculate it as solar + battery + grid - // If we dont have solar, we can calculate it as use - battery - grid - // If we dont have battery, we can calculate it as use - solar - grid - // If we dont have grid, we can calculate it as use - solar - battery - // If these dont balance, then we have an issue with the data - - let use_check = solar + battery_power + grid; - let solar_check = use - battery_power - grid; - let battery_check = use - solar - grid; - let grid_check = use - solar - battery_power; - - // if (Math.abs(use - use_check) > 0.1) { - // console.error("Use does not balance! " + use + " vs " + use_check); - // } - - // Override for conservation of energy - use = use_check; - - let import_power = 0; - let export_power = 0; - - if (grid > 0) { - import_power = grid; - } else { - export_power = -grid; - } - // ------------------------------------------------------------------------------------------------ - // SOLAR + // CONSERVATION OF ENERGY + // We have 4 meter points but only 3 independent variables. + // The system must balance: use = solar + battery_power + grid + // We override 'use' with the calculated value from the other three feeds so that + // all downstream flow calculations are internally consistent. // ------------------------------------------------------------------------------------------------ + use = solar + battery_power + grid; - // Calculate solar to load and solar to grid + let import_power = (grid > 0) ? grid : 0; + let export_power = (grid < 0) ? -grid : 0; + + // ------------------------------------------------------------------------------------------------ + // SOLAR FLOWS + // Priority: solar → load first, then surplus → battery charge, then remainder → grid + // ------------------------------------------------------------------------------------------------ let solar_to_load = Math.min(solar, use); - // Calculate solar to battery let solar_to_battery = 0; if (battery_power < 0) { - // Battery is charging, so solar to battery is the lesser of the available solar and the battery charge power + // Battery is charging — solar covers as much of the charge power as available solar_to_battery = Math.min(solar - solar_to_load, -battery_power); } let solar_to_grid = solar - solar_to_load - solar_to_battery; // ------------------------------------------------------------------------------------------------ - // BATTERY + // BATTERY FLOWS + // Only relevant when discharging (battery_power > 0) + // Priority: battery → load first, remainder → grid // ------------------------------------------------------------------------------------------------ - - // Calculate battery to load and battery to grid let battery_to_load = 0; let battery_to_grid = 0; if (battery_power > 0) { - // Battery is discharging, so battery to load is the lesser of the available battery power and the remaining load after solar battery_to_load = Math.min(battery_power, use - solar_to_load); battery_to_grid = battery_power - battery_to_load; } // ------------------------------------------------------------------------------------------------ - // GIRD (TO LOAD, TO BATTERY) + // GRID FLOWS + // Only relevant when importing (import_power > 0) + // Priority: grid → load first (after solar + battery), remainder → battery charge // ------------------------------------------------------------------------------------------------ let grid_to_load = 0; let grid_to_battery = 0; if (import_power > 0) { - // Grid is importing, so grid to load is the lesser of the import power and the remaining load after solar and battery grid_to_load = Math.min(import_power, use - solar_to_load - battery_to_load); grid_to_battery = Math.min(import_power - grid_to_load, battery_power < 0 ? -battery_power - solar_to_battery : 0); } - // ------------------------------------------------------------------------------------------------ - - // Calculate kWh for the period - use_kwh += use * power_to_kwh; - solar_kwh += solar * power_to_kwh; + // ACCUMULATE kWh TOTALS + // ------------------------------------------------------------------------------------------------ + use_kwh += use * power_to_kwh; + solar_kwh += solar * power_to_kwh; import_kwh += import_power * power_to_kwh; export_kwh += export_power * power_to_kwh; - solar_to_load_kwh += solar_to_load * power_to_kwh; - solar_to_grid_kwh += solar_to_grid * power_to_kwh; + solar_to_load_kwh += solar_to_load * power_to_kwh; + solar_to_grid_kwh += solar_to_grid * power_to_kwh; solar_to_battery_kwh += solar_to_battery * power_to_kwh; battery_to_load_kwh += battery_to_load * power_to_kwh; battery_to_grid_kwh += battery_to_grid * power_to_kwh; - grid_to_load_kwh += grid_to_load * power_to_kwh; + grid_to_load_kwh += grid_to_load * power_to_kwh; grid_to_battery_kwh += grid_to_battery * power_to_kwh; // ------------------------------------------------------------------------------------------------ - + // STORE TIME-SERIES DATA POINTS FOR GRAPH + // ------------------------------------------------------------------------------------------------ data["use"][z][1] = use; data["solar"][z][1] = solar; @@ -296,10 +286,9 @@ function load() { data["grid_to_load"].push([time, grid_to_load]); data["grid_to_battery"].push([time, grid_to_battery]); - - // ------------------------------------------------------------------------------------------------ } + // Update the kWh summary totals in the UI $("#use_kwh").html(use_kwh.toFixed(1)); $("#solar_kwh").html(solar_kwh.toFixed(1)); $("#import_kwh").html(import_kwh.toFixed(1)); @@ -315,100 +304,40 @@ function load() { $("#grid_to_load_kwh").html(grid_to_load_kwh.toFixed(1)); $("#grid_to_battery_kwh").html(grid_to_battery_kwh.toFixed(1)); - // --------------------------------------------------------------------------------- - // VALIDATION CHECKS + // VALIDATION — cross-check flow totals against meter totals + // All four identities should hold; any significant error indicates a data issue. + // solar = solar_to_load + solar_to_grid + solar_to_battery + // use = solar_to_load + battery_to_load + grid_to_load + // import = grid_to_load + grid_to_battery + // export = solar_to_grid + battery_to_grid // --------------------------------------------------------------------------------- + let solar_kwh_check = solar_to_load_kwh + solar_to_grid_kwh + solar_to_battery_kwh; + let use_kwh_check = solar_to_load_kwh + battery_to_load_kwh + grid_to_load_kwh; + let import_kwh_check = grid_to_load_kwh + grid_to_battery_kwh; + let export_kwh_check = solar_to_grid_kwh + battery_to_grid_kwh; - var solar_kwh_check = solar_to_load_kwh + solar_to_grid_kwh + solar_to_battery_kwh; - var use_kwh_check = solar_to_load_kwh + battery_to_load_kwh + grid_to_load_kwh; - var import_kwh_check = grid_to_load_kwh + grid_to_battery_kwh; - var export_kwh_check = solar_to_grid_kwh + battery_to_grid_kwh; - - // Validate calculations - // There are usually minor discrepancies in use_kwh - if (Math.abs(solar_kwh - solar_kwh_check) > 0.1) { - console.error("Solar kWh does not balance! " + solar_kwh + " vs " + solar_kwh_check); - } - if (Math.abs(use_kwh - use_kwh_check) > 0.1) { - console.error("Use kWh does not balance! " + use_kwh + " vs " + use_kwh_check); - } - if (Math.abs(import_kwh - import_kwh_check) > 0.1) { - console.error("Import kWh does not balance! " + import_kwh + " vs " + import_kwh_check); - } - if (Math.abs(export_kwh - export_kwh_check) > 0.1) { - console.error("Export kWh does not balance! " + export_kwh + " vs " + export_kwh_check); - } + if (Math.abs(solar_kwh - solar_kwh_check) > 0.1) console.warn("Solar kWh imbalance: " + solar_kwh.toFixed(2) + " vs " + solar_kwh_check.toFixed(2)); + if (Math.abs(use_kwh - use_kwh_check) > 0.1) console.warn("Use kWh imbalance: " + use_kwh.toFixed(2) + " vs " + use_kwh_check.toFixed(2)); + if (Math.abs(import_kwh - import_kwh_check) > 0.1) console.warn("Import kWh imbalance: " + import_kwh.toFixed(2) + " vs " + import_kwh_check.toFixed(2)); + if (Math.abs(export_kwh - export_kwh_check) > 0.1) console.warn("Export kWh imbalance: " + export_kwh.toFixed(2) + " vs " + export_kwh_check.toFixed(2)); // --------------------------------------------------------------------------------- - + // BUILD GRAPH SERIES + // Series are stacked in the order added — solar flows first, then battery, then grid. + // Colours match the kWh summary labels below the graph. + // Note: battery_soc uses yaxis 2 (right axis, 0–100%) and is not stacked. + // --------------------------------------------------------------------------------- reset_series(); - add_series("solar_to_load", data["solar_to_load"], { - label: "Solar to Load", - yaxis: 1, - color: "#abddffff", - stack: true, - lines: { show: true, fill: 0.75, lineWidth: 0 }, - }); - - add_series("solar_to_grid", data["solar_to_grid"], { - label: "Solar to Grid", - yaxis: 1, - color: "#dccc1f", - stack: true, - lines: { show: true, fill: 1.0, lineWidth: 0 } - }); - - - add_series("solar_to_battery", data["solar_to_battery"], { - label: "Solar to Battery", - yaxis: 1, - color: "#fba050ff", - stack: true, - lines: { show: true, fill: 0.8, lineWidth: 0 } - }); - - add_series("battery_to_load", data["battery_to_load"], { - label: "Battery to Load", - yaxis: 1, - color: "#ffd08eff", - stack: true, - lines: { show: true, fill: 0.8, lineWidth: 0 } - }); - - add_series("battery_to_grid", data["battery_to_grid"], { - label: "Battery to Grid", - yaxis: 1, - color: "#fabb68ff", - stack: true, - lines: { show: true, fill: 0.8, lineWidth: 0 } - }); - - add_series("grid_to_load", data["grid_to_load"], { - label: "Grid to Load", - yaxis: 1, - color: "#82cbfc", - stack: true, - lines: { show: true, fill: 0.8, lineWidth: 0 } - }); - - add_series("grid_to_battery", data["grid_to_battery"], { - label: "Grid to Battery", - yaxis: 1, - color: "#fb7b50", - stack: true, - lines: { show: true, fill: 0.8, lineWidth: 0 } - }); - - // add soc - add_series("battery_soc", data["battery_soc"], { - label: "Battery SOC", - yaxis: 2, - color: "#ccc", - lines: { show: true, fill: false, lineWidth: 2 } - }); - + add_series("solar_to_load", data["solar_to_load"], { label: "Solar to Load", yaxis: 1, color: "#abddff", stack: true, lines: { show: true, fill: 0.8, lineWidth: 0 } }); + add_series("solar_to_grid", data["solar_to_grid"], { label: "Solar to Grid", yaxis: 1, color: "#dccc1f", stack: true, lines: { show: true, fill: 0.8, lineWidth: 0 } }); + add_series("solar_to_battery", data["solar_to_battery"], { label: "Solar to Battery", yaxis: 1, color: "#fba050", stack: true, lines: { show: true, fill: 0.8, lineWidth: 0 } }); + add_series("battery_to_load", data["battery_to_load"], { label: "Battery to Load", yaxis: 1, color: "#ffd08e", stack: true, lines: { show: true, fill: 0.8, lineWidth: 0 } }); + add_series("battery_to_grid", data["battery_to_grid"], { label: "Battery to Grid", yaxis: 1, color: "#fabb68", stack: true, lines: { show: true, fill: 0.8, lineWidth: 0 } }); + add_series("grid_to_load", data["grid_to_load"], { label: "Grid to Load", yaxis: 1, color: "#82cbfc", stack: true, lines: { show: true, fill: 0.8, lineWidth: 0 } }); + add_series("grid_to_battery", data["grid_to_battery"], { label: "Grid to Battery", yaxis: 1, color: "#fb7b50", stack: true, lines: { show: true, fill: 0.8, lineWidth: 0 } }); + add_series("battery_soc", data["battery_soc"], { label: "Battery SOC (%)", yaxis: 2, color: "#ccc", lines: { show: true, fill: false, lineWidth: 2 } }); draw(); }, false, "notime"); @@ -420,24 +349,19 @@ function reset_series() { powergraph_series = {}; } -function add_series(key, data, options) { - let series = options; - series.data = data; - powergraph_series[key] = series; +// Add a named series to the graph. A shallow copy of the options object is made +// so that the original options literal is not mutated by attaching .data to it. +function add_series(key, seriesData, seriesOptions) { + powergraph_series[key] = Object.assign({}, seriesOptions, { data: seriesData }); } function draw() { - options.xaxis.min = view.start; options.xaxis.max = view.end; - // Remove keys - var powergraph_series_without_key = []; - for (var key in powergraph_series) { - powergraph_series_without_key.push(powergraph_series[key]); - } - $.plot($('#graph'), powergraph_series_without_key, options); - + // Flot requires a plain array (not a keyed object), so convert here + let series_array = Object.values(powergraph_series); + $.plot($('#graph'), series_array, options); } function updater() { @@ -499,7 +423,7 @@ function resize() { } function clear() { - clearInterval(updaterinst); + clearInterval(window.updaterinst); } // Graph navigation buttons @@ -513,22 +437,24 @@ $('.time').click(function () { load(); }); -// Tooltip code +// Tooltip: show the series label and value when hovering over the graph $('#graph').bind("plothover", function (event, pos, item) { if (item) { - var i = item.dataIndex; - if (previousPoint != item.datapoint) { previousPoint = item.datapoint; $("#tooltip").remove(); - var itemTime = item.datapoint[0]; - var itemValue = item.datapoint[1]; + let itemTime = item.datapoint[0]; + let itemValue = item.datapoint[1]; + let label = item.series.label || ""; - if (itemValue != null) itemValue = itemValue.toFixed(0); - tooltip(item.pageX, item.pageY, itemValue + "W
" + tooltip_date(itemTime), "#fff", "#000"); + let displayValue = (itemValue != null) ? itemValue.toFixed(0) + "W" : "N/A"; + tooltip(item.pageX, item.pageY, label + "
" + displayValue + "
" + tooltip_date(itemTime), "#fff", "#000"); } - } else $("#tooltip").remove(); + } else { + $("#tooltip").remove(); + previousPoint = null; + } }); $('#graph').bind("plotselected", function (event, ranges) { @@ -542,21 +468,22 @@ $(window).resize(function () { }); // ---------------------------------------------------------------------- -// App log +// App log — simple wrapper; extend for production logging if needed // ---------------------------------------------------------------------- function app_log(level, message) { - if (level == "ERROR") alert(level + ": " + message); + if (level === "ERROR") alert(level + ": " + message); console.log(level + ": " + message); } -// Remove null values from feed data +// Remove null gaps shorter than 15 minutes by forward-filling from the last +// known good value. Longer gaps are left as null so the graph shows a break. function remove_null_values(data, interval) { - var last_valid_pos = 0; - for (var pos = 0; pos < data.length; pos++) { + let last_valid_pos = 0; + for (let pos = 0; pos < data.length; pos++) { if (data[pos][1] != null) { - let null_time = (pos - last_valid_pos) * interval; - if (null_time < 900) { - for (var x = last_valid_pos + 1; x < pos; x++) { + let null_duration_ms = (pos - last_valid_pos) * interval; + if (null_duration_ms < 900000) { // 900000 ms = 15 minutes + for (let x = last_valid_pos + 1; x < pos; x++) { data[x][1] = data[last_valid_pos][1]; } } diff --git a/apps/OpenEnergyMonitor/solartemplate/solartemplate.php b/apps/OpenEnergyMonitor/solartemplate/solartemplate.php index 4ec678a..986f264 100644 --- a/apps/OpenEnergyMonitor/solartemplate/solartemplate.php +++ b/apps/OpenEnergyMonitor/solartemplate/solartemplate.php @@ -87,8 +87,8 @@
-
-

---kWh

+
+

---kWh

From 568099e731d1cb3ec115092ff3e9192e420a1f8b Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sat, 14 Mar 2026 17:44:20 +0000 Subject: [PATCH 07/13] fix ms to seconds --- apps/OpenEnergyMonitor/solartemplate/solartemplate.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/OpenEnergyMonitor/solartemplate/solartemplate.js b/apps/OpenEnergyMonitor/solartemplate/solartemplate.js index cd87fbd..ea76c1c 100644 --- a/apps/OpenEnergyMonitor/solartemplate/solartemplate.js +++ b/apps/OpenEnergyMonitor/solartemplate/solartemplate.js @@ -481,8 +481,8 @@ function remove_null_values(data, interval) { let last_valid_pos = 0; for (let pos = 0; pos < data.length; pos++) { if (data[pos][1] != null) { - let null_duration_ms = (pos - last_valid_pos) * interval; - if (null_duration_ms < 900000) { // 900000 ms = 15 minutes + let null_duration_s = (pos - last_valid_pos) * interval; + if (null_duration_s < 900) { // 900000 ms = 15 minutes for (let x = last_valid_pos + 1; x < pos; x++) { data[x][1] = data[last_valid_pos][1]; } From 9579152d41afe5b4b161b9f2f215c2544182775e Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sat, 14 Mar 2026 18:15:38 +0000 Subject: [PATCH 08/13] provisional triggering of post-processor from mysolarpvbattery app controller --- .../mysolarpvbattery/mysolarpvbattery.php | 12 +++ .../mysolarpvbattery_controller.php | 95 +++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 09f5f07..d4d316d 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -540,6 +540,18 @@ function show() livefn(); live = setInterval(livefn,5000); + // Trigger post processor for kWh data + let process_timeout = 1; + + $.ajax({ + url: path + "app/process", + data: { id: config.id, apikey: apikey, timeout: process_timeout }, + async: true, + success: function (result) { + console.log("Post processor triggered successfully"); + console.log(result); + } + }); } function resize() diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php new file mode 100644 index 0000000..4cbcbc3 --- /dev/null +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php @@ -0,0 +1,95 @@ +action == "view" || $route->action == "") { + $route->format = "html"; + $result = "\n"; + $result .= "\n" . ''; + $result .= "\n" . ''; + $result .= "\n\n \n"; + + $dir = $appconfig->get_app_dir($app->app); + $result .= view($dir.$app->app.".php",array("id"=>$app->id, "name"=>$app->name, "public"=>$app->public, "appdir"=>$dir, "config"=>$app->config, "apikey"=>$apikey)); + return $result; + } + + // ---------------------------------------------------- + // Trigger post-processor route + // ---------------------------------------------------- + else if ($route->action == "process" && $session['write']) { + $route->format = "json"; + $userid = $session['userid']; + + require_once "Modules/feed/feed_model.php"; + $feed = new Feed($mysqli,$redis,$settings['feed']); + + include "Modules/postprocess/postprocess_model.php"; + $postprocess = new PostProcess($mysqli, $redis, $feed); + $processes = $postprocess->get_processes("$linked_modules_dir/postprocess"); + $process_classes = $postprocess->get_process_classes(); + + $clear_solarbatterykwh = false; + $solarbatterykwh_config = (object) array( + // Input feeds + "solar" => $feed->get_id($userid, "solar"), + "use" => $feed->get_id($userid, "use"), + "grid" => $feed->get_id($userid, "grid"), + "battery_power" => $feed->get_id($userid, "battery_power"), + + // Output kWh flow feeds + "solar_to_load_kwh" => get_or_create_feed($feed, $userid, "solarbatterykwh", "solar_to_load_kwh", 10, $clear_solarbatterykwh), + "solar_to_grid_kwh" => get_or_create_feed($feed, $userid, "solarbatterykwh", "solar_to_grid_kwh", 10, $clear_solarbatterykwh), + "solar_to_battery_kwh" => get_or_create_feed($feed, $userid, "solarbatterykwh", "solar_to_battery_kwh", 10, $clear_solarbatterykwh), + "battery_to_load_kwh" => get_or_create_feed($feed, $userid, "solarbatterykwh", "battery_to_load_kwh", 10, $clear_solarbatterykwh), + "battery_to_grid_kwh" => get_or_create_feed($feed, $userid, "solarbatterykwh", "battery_to_grid_kwh", 10, $clear_solarbatterykwh), + "grid_to_load_kwh" => get_or_create_feed($feed, $userid, "solarbatterykwh", "grid_to_load_kwh", 10, $clear_solarbatterykwh), + "grid_to_battery_kwh" => get_or_create_feed($feed, $userid, "solarbatterykwh", "grid_to_battery_kwh", 10, $clear_solarbatterykwh), + + // Control params + "process_mode" => "all", + "process_start" => 0, + "process" => "solarbatterykwh" + ); + + return $process_classes[$solarbatterykwh_config->process]->process($solarbatterykwh_config); + } +} + +function get_or_create_feed($feed, $userid, $node, $feedname, $interval, $clear = false) { + + $feedid = $feed->exists_tag_name($userid, $node, $feedname); + if (!$feedid) { + $meta = new stdClass(); + $meta->interval = $interval; + $result = $feed->create($userid, $node, $feedname, 5, $meta); + if ($result['success']) { + $feedid = $result['feedid']; + } + } + + if ($clear) { + $feed->clear($feedid); + } + + return $feedid; +} \ No newline at end of file From 8e776308f025f56d53af6420dfa24562619f85fa Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sat, 14 Mar 2026 22:25:27 +0000 Subject: [PATCH 09/13] initial implementation of autogen section of app config --- Lib/appconf.js | 4 + app_controller.php | 2 +- .../mysolarpvbattery/mysolarpvbattery.php | 255 ++++++++++++++++-- .../mysolarpvbattery_controller.php | 66 +++-- 4 files changed, 273 insertions(+), 54 deletions(-) diff --git a/Lib/appconf.js b/Lib/appconf.js index 0ad5630..af59017 100644 --- a/Lib/appconf.js +++ b/Lib/appconf.js @@ -147,6 +147,10 @@ var config = { out += "
"; for (var z in config.app) { + + // Skip any entries that are listed as autogenerate + if (config.app[z].autogenerate!=undefined && config.app[z].autogenerate) continue; + out += "
"; if (config.app[z].type=="feed") { diff --git a/app_controller.php b/app_controller.php index b6997fe..145a4bc 100644 --- a/app_controller.php +++ b/app_controller.php @@ -16,7 +16,7 @@ function app_controller() { global $mysqli,$redis,$path,$session,$route,$user,$settings,$v; // Force cache reload of css and javascript - $v = 39; + $v = 41; $result = false; diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index d4d316d..7892433 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -332,8 +332,42 @@ This app can be used to explore onsite solar generation, self consumption, battery integration, export and building consumption.

Auto configure: This app can auto-configure connecting to emoncms feeds with the names shown on the right, alternatively feeds can be selected by clicking on the edit button.

History view: Daily energy flow breakdown feeds can be generated from the power feeds using the Solar battery kWh flows post-processor (solarbatterykwh).

-
+ +
+

Auto generate kWh flow feeds

+

+ The following feeds are required for the history view. They are generated from the power feeds + using the Solar battery kWh flows post-processor (solarbatterykwh). + The feed tag/node will be set to match the app name: +

+ + + + + + + + + + + +
Feed nameNodeStatus
+ +
+ + + + +
+
+
@@ -356,8 +390,8 @@ function getTranslations(){ // ---------------------------------------------------------------------- // Globals // ---------------------------------------------------------------------- -var apikey = ""; -var sessionwrite = ; +var apikey = ""; +var sessionwrite = ; feed.apikey = apikey; feed.public_userid = public_userid; feed.public_username = public_username; @@ -387,25 +421,27 @@ function getTranslations(){ "battery_soc":{"optional":true, "type":"feed", "autoname":"battery_soc", "description":"Battery state of charge in kWh"}, // History feeds (energy flow breakdown from solarbatterykwh post-processor) - "solar_to_load_kwh":{"optional":true, "type":"feed", "autoname":"solar_to_load_kwh", "description":"Cumulative solar to load energy in kWh"}, - "solar_to_grid_kwh":{"optional":true, "type":"feed", "autoname":"solar_to_grid_kwh", "description":"Cumulative solar to grid (export) energy in kWh"}, - "solar_to_battery_kwh":{"optional":true, "type":"feed", "autoname":"solar_to_battery_kwh", "description":"Cumulative solar to battery energy in kWh"}, - "battery_to_load_kwh":{"optional":true, "type":"feed", "autoname":"battery_to_load_kwh", "description":"Cumulative battery to load energy in kWh"}, - "battery_to_grid_kwh":{"optional":true, "type":"feed", "autoname":"battery_to_grid_kwh", "description":"Cumulative battery to grid energy in kWh"}, - "grid_to_load_kwh":{"optional":true, "type":"feed", "autoname":"grid_to_load_kwh", "description":"Cumulative grid to load energy in kWh"}, - "grid_to_battery_kwh":{"optional":true, "type":"feed", "autoname":"grid_to_battery_kwh", "description":"Cumulative grid to battery energy in kWh"}, + "solar_to_load_kwh":{"autogenerate":true, "optional":true, "type":"feed", "autoname":"solar_to_load_kwh", "description":"Cumulative solar to load energy in kWh"}, + "solar_to_grid_kwh":{"autogenerate":true, "optional":true, "type":"feed", "autoname":"solar_to_grid_kwh", "description":"Cumulative solar to grid (export) energy in kWh"}, + "solar_to_battery_kwh":{"autogenerate":true, "optional":true, "type":"feed", "autoname":"solar_to_battery_kwh", "description":"Cumulative solar to battery energy in kWh"}, + "battery_to_load_kwh":{"autogenerate":true, "optional":true, "type":"feed", "autoname":"battery_to_load_kwh", "description":"Cumulative battery to load energy in kWh"}, + "battery_to_grid_kwh":{"autogenerate":true, "optional":true, "type":"feed", "autoname":"battery_to_grid_kwh", "description":"Cumulative battery to grid energy in kWh"}, + "grid_to_load_kwh":{"autogenerate":true, "optional":true, "type":"feed", "autoname":"grid_to_load_kwh", "description":"Cumulative grid to load energy in kWh"}, + "grid_to_battery_kwh":{"autogenerate":true, "optional":true, "type":"feed", "autoname":"grid_to_battery_kwh", "description":"Cumulative grid to battery energy in kWh"}, // Other options "kw":{"type":"checkbox", "default":0, "name": "Show kW", "description": "Display power as kW"}, "battery_capacity_kwh":{"type":"value", "default":0, "name":"Battery Capacity", "description":"Battery capacity in kWh"} } -config.id = ; -config.name = ""; -config.public = ; -config.db = ; +config.id = ; +config.name = ""; +config.public = ; +config.db = ; config.feeds = feed.list(); +var feeds_by_tag_name = feed.by_tag_and_name(config.feeds); + config.initapp = function(){init()}; config.showapp = function(){show()}; config.hideapp = function(){hide()}; @@ -447,6 +483,7 @@ function getTranslations(){ function init() { app_log("INFO","solar & battery init"); + render_autogen_feed_list(); view.end = power_end; view.start = power_start; @@ -541,8 +578,8 @@ function show() live = setInterval(livefn,5000); // Trigger post processor for kWh data - let process_timeout = 1; - + let process_timeout = 60; // seconds + /* $.ajax({ url: path + "app/process", data: { id: config.id, apikey: apikey, timeout: process_timeout }, @@ -552,6 +589,7 @@ function show() console.log(result); } }); + */ } function resize() @@ -1344,4 +1382,187 @@ function app_log (level, message) { // if (level=="ERROR") alert(level+": "+message); console.log(level+": "+message); } - + +// ---------------------------------------------------------------------- +// Helper: return array of feeds that should be auto-generated +// ---------------------------------------------------------------------- +function get_autogen_feeds() { + + var autogen_node_name = "app_mysolarpvbattery_"+config.id; + + var autogen_feeds = []; + for (var key in config.app) { + if (config.app.hasOwnProperty(key) && config.app[key].autogenerate) { + let feed_name = config.app[key].autoname || key; + let feedid = false; + + if (feeds_by_tag_name[autogen_node_name]!=undefined) { + if (feeds_by_tag_name[autogen_node_name][feed_name]!=undefined) { + feedid = feeds_by_tag_name[autogen_node_name][feed_name]['id']; + } + } + + autogen_feeds.push({ + key: key, + name: feed_name, + feedid: feedid + }); + } + } + return autogen_feeds; +} + +// ---------------------------------------------------------------------- +// Auto-generate feed list +// ---------------------------------------------------------------------- +function render_autogen_feed_list() { + var autogen_feeds = get_autogen_feeds(); + + var autogen_node_name = "app_mysolarpvbattery_"+config.id; + $(".autogen-appname").text(autogen_node_name); + + var tbody = $("#autogen-feed-rows"); + tbody.empty(); + var missing_count = 0; + + for (var j = 0; j < autogen_feeds.length; j++) { + + var status_html = "✓ exists"; + if (autogen_feeds[j].feedid==false) { + status_html = "✗ missing"; + missing_count++; + } + + tbody.append( + "" + + " " + autogen_feeds[j].name + "" + + " " + autogen_node_name + "" + + " " + status_html + "" + + "" + ); + } + + var all_present = (missing_count === 0); + $("#btn-create-feeds").toggle(!all_present); + $("#btn-run-processor").toggle(all_present); + $("#btn-reset-feeds").toggle(all_present); + $("#autogen-status").text(""); +} + +// ---------------------------------------------------------------------- +// Auto-generate feed actions +// ---------------------------------------------------------------------- +function create_missing_feeds() { + var autogen_node_name = "app_mysolarpvbattery_"+config.id; + + $("#autogen-status").text("Creating feeds...").css("color","#aaa"); + $("#btn-create-feeds").prop("disabled", true); + + var missing = []; + var autogen_feeds = get_autogen_feeds(); + for (var i = 0; i < autogen_feeds.length; i++) { + if (autogen_feeds[i].feedid == false) { + missing.push(autogen_feeds[i].name); + } + } + + var requests = []; + for (var i = 0; i < missing.length; i++) { + requests.push($.ajax({ + url: path + "feed/create.json", + data: { tag: autogen_node_name, name: missing[i], datatype: 1, engine: 5, + options: JSON.stringify({ interval: 1800 }), apikey: apikey }, + dataType: "json" + })); + } + + $.when.apply($, requests).then(function() { + var results = missing.length === 1 ? [arguments] : Array.prototype.slice.call(arguments); + var errors = results.filter(function(r) { return !(r[0] && r[0].feedid); }).length; + var created = results.length - errors; + config.feeds = feed.list(); + render_autogen_feed_list(); + $("#autogen-status") + .text(errors === 0 ? "Created " + created + " feed(s) successfully." + : "Created " + created + " feed(s), " + errors + " error(s).") + .css("color", errors === 0 ? "#5cb85c" : "#f0ad4e"); + }, function() { + $("#autogen-status").text("One or more feeds could not be created.").css("color","#d9534f"); + }).always(function() { + $("#btn-create-feeds").prop("disabled", false); + }); +} + +function run_post_processor() { + $("#autogen-status").text("Starting post-processor...").css("color","#aaa"); + $("#btn-run-processor").prop("disabled", true); + + var autogen_node_name = "app_mysolarpvbattery_"+config.id; + + $.ajax({ + url: path + "app/process", + data: { id: config.id, apikey: apikey, tag: autogen_node_name }, + dataType: "json", + timeout: 120000, + success: function(result) { + console.log("run_post_processor: result", result); + if (result && result.success) { + $("#autogen-status").text("Post-processor completed successfully.").css("color","#5cb85c"); + } else { + var msg = (result && result.message) ? result.message : "Unknown response"; + $("#autogen-status").text("Post-processor: " + msg).css("color","#f0ad4e"); + } + }, + error: function(xhr) { + console.error("run_post_processor: AJAX error", xhr.responseText); + $("#autogen-status").text("Post-processor failed: " + xhr.statusText).css("color","#d9534f"); + }, + complete: function() { $("#btn-run-processor").prop("disabled", false); } + }); +} + +function reset_feeds() { + if (!confirm("Are you sure you want to clear all 7 kWh flow feeds? This cannot be undone.")) return; + $("#autogen-status").text("Clearing feeds...").css("color","#aaa"); + $("#btn-reset-feeds").prop("disabled", true); + + var autogen_feeds = get_autogen_feeds(); + var feed_ids = []; + for (var i = 0; i < autogen_feeds.length; i++) { + if (autogen_feeds[i].feedid) { + feed_ids.push(autogen_feeds[i].feedid); + } + } + + if (feed_ids.length === 0) { + $("#autogen-status").text("No matching feeds found to clear.").css("color","#f0ad4e"); + $("#btn-reset-feeds").prop("disabled", false); + return; + } + + var requests = []; + for (var i = 0; i < feed_ids.length; i++) { + requests.push($.ajax({ + url: path + "feed/clear.json", + data: { id: feed_ids[i], apikey: apikey }, + dataType: "json" + })); + } + + $.when.apply($, requests).then(function() { + var results = feed_ids.length === 1 ? [arguments] : Array.prototype.slice.call(arguments); + var errors = results.filter(function(r) { return !(r[0] && r[0].success); }).length; + var cleared = results.length - errors; + config.feeds = feed.list(); + render_autogen_feed_list(); + $("#autogen-status") + .text(errors === 0 ? "Cleared " + cleared + " feed(s) successfully." + : "Cleared " + cleared + " feed(s), " + errors + " error(s).") + .css("color", errors === 0 ? "#5cb85c" : "#f0ad4e"); + }, function() { + $("#autogen-status").text("One or more feeds could not be cleared.").css("color","#d9534f"); + }).always(function() { + $("#btn-reset-feeds").prop("disabled", false); + }); +} + \ No newline at end of file diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php index 4cbcbc3..0cfdf40 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php @@ -40,56 +40,50 @@ function mysolarpvbattery_app_controller($route,$app,$appconfig,$apikey) $route->format = "json"; $userid = $session['userid']; + $tag = isset($_GET['tag']) ? $_GET['tag'] : (isset($_POST['tag']) ? $_POST['tag'] : ''); + if (empty($tag)) { + return array("success" => false, "message" => "Missing tag parameter"); + } + require_once "Modules/feed/feed_model.php"; $feed = new Feed($mysqli,$redis,$settings['feed']); + $required = ["solar_to_load_kwh","solar_to_grid_kwh","solar_to_battery_kwh", + "battery_to_load_kwh","battery_to_grid_kwh","grid_to_load_kwh","grid_to_battery_kwh"]; + + $resolved = array(); + foreach ($required as $name) { + $fid = $feed->exists_tag_name($userid, $tag, $name); + if (!$fid) { + return array("success" => false, "message" => "Feed not found: $tag/$name"); + } + $resolved[$name] = intval($fid); + } + include "Modules/postprocess/postprocess_model.php"; $postprocess = new PostProcess($mysqli, $redis, $feed); $processes = $postprocess->get_processes("$linked_modules_dir/postprocess"); $process_classes = $postprocess->get_process_classes(); - $clear_solarbatterykwh = false; $solarbatterykwh_config = (object) array( - // Input feeds - "solar" => $feed->get_id($userid, "solar"), - "use" => $feed->get_id($userid, "use"), - "grid" => $feed->get_id($userid, "grid"), - "battery_power" => $feed->get_id($userid, "battery_power"), + "solar" => $feed->get_id($userid, "solar"), + "use" => $feed->get_id($userid, "use"), + "grid" => $feed->get_id($userid, "grid"), + "battery_power" => $feed->get_id($userid, "battery_power"), - // Output kWh flow feeds - "solar_to_load_kwh" => get_or_create_feed($feed, $userid, "solarbatterykwh", "solar_to_load_kwh", 10, $clear_solarbatterykwh), - "solar_to_grid_kwh" => get_or_create_feed($feed, $userid, "solarbatterykwh", "solar_to_grid_kwh", 10, $clear_solarbatterykwh), - "solar_to_battery_kwh" => get_or_create_feed($feed, $userid, "solarbatterykwh", "solar_to_battery_kwh", 10, $clear_solarbatterykwh), - "battery_to_load_kwh" => get_or_create_feed($feed, $userid, "solarbatterykwh", "battery_to_load_kwh", 10, $clear_solarbatterykwh), - "battery_to_grid_kwh" => get_or_create_feed($feed, $userid, "solarbatterykwh", "battery_to_grid_kwh", 10, $clear_solarbatterykwh), - "grid_to_load_kwh" => get_or_create_feed($feed, $userid, "solarbatterykwh", "grid_to_load_kwh", 10, $clear_solarbatterykwh), - "grid_to_battery_kwh" => get_or_create_feed($feed, $userid, "solarbatterykwh", "grid_to_battery_kwh", 10, $clear_solarbatterykwh), + "solar_to_load_kwh" => $resolved["solar_to_load_kwh"], + "solar_to_grid_kwh" => $resolved["solar_to_grid_kwh"], + "solar_to_battery_kwh" => $resolved["solar_to_battery_kwh"], + "battery_to_load_kwh" => $resolved["battery_to_load_kwh"], + "battery_to_grid_kwh" => $resolved["battery_to_grid_kwh"], + "grid_to_load_kwh" => $resolved["grid_to_load_kwh"], + "grid_to_battery_kwh" => $resolved["grid_to_battery_kwh"], - // Control params - "process_mode" => "all", + "process_mode" => "all", "process_start" => 0, - "process" => "solarbatterykwh" + "process" => "solarbatterykwh" ); return $process_classes[$solarbatterykwh_config->process]->process($solarbatterykwh_config); } -} - -function get_or_create_feed($feed, $userid, $node, $feedname, $interval, $clear = false) { - - $feedid = $feed->exists_tag_name($userid, $node, $feedname); - if (!$feedid) { - $meta = new stdClass(); - $meta->interval = $interval; - $result = $feed->create($userid, $node, $feedname, 5, $meta); - if ($result['success']) { - $feedid = $result['feedid']; - } - } - - if ($clear) { - $feed->clear($feedid); - } - - return $feedid; } \ No newline at end of file From 4f4368356a0dcb853a1e3591c05a4a1bacb1a56f Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sat, 14 Mar 2026 22:29:53 +0000 Subject: [PATCH 10/13] minor fix --- apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 7892433..56196df 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -1481,6 +1481,7 @@ function create_missing_feeds() { var errors = results.filter(function(r) { return !(r[0] && r[0].feedid); }).length; var created = results.length - errors; config.feeds = feed.list(); + feeds_by_tag_name = feed.by_tag_and_name(config.feeds); render_autogen_feed_list(); $("#autogen-status") .text(errors === 0 ? "Created " + created + " feed(s) successfully." From 069d473a7335d1305eac60a5179e41c699b9414e Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sat, 14 Mar 2026 22:37:06 +0000 Subject: [PATCH 11/13] cleanup post process trigger --- .../mysolarpvbattery_controller.php | 36 ++++++------------- 1 file changed, 10 insertions(+), 26 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php index 0cfdf40..13b01de 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php @@ -39,51 +39,35 @@ function mysolarpvbattery_app_controller($route,$app,$appconfig,$apikey) else if ($route->action == "process" && $session['write']) { $route->format = "json"; $userid = $session['userid']; - - $tag = isset($_GET['tag']) ? $_GET['tag'] : (isset($_POST['tag']) ? $_POST['tag'] : ''); - if (empty($tag)) { - return array("success" => false, "message" => "Missing tag parameter"); - } + $tag = prop("tag",true); require_once "Modules/feed/feed_model.php"; $feed = new Feed($mysqli,$redis,$settings['feed']); - $required = ["solar_to_load_kwh","solar_to_grid_kwh","solar_to_battery_kwh", - "battery_to_load_kwh","battery_to_grid_kwh","grid_to_load_kwh","grid_to_battery_kwh"]; - - $resolved = array(); - foreach ($required as $name) { - $fid = $feed->exists_tag_name($userid, $tag, $name); - if (!$fid) { - return array("success" => false, "message" => "Feed not found: $tag/$name"); - } - $resolved[$name] = intval($fid); - } - include "Modules/postprocess/postprocess_model.php"; $postprocess = new PostProcess($mysqli, $redis, $feed); $processes = $postprocess->get_processes("$linked_modules_dir/postprocess"); $process_classes = $postprocess->get_process_classes(); - $solarbatterykwh_config = (object) array( + $process_conf = (object) array( "solar" => $feed->get_id($userid, "solar"), "use" => $feed->get_id($userid, "use"), "grid" => $feed->get_id($userid, "grid"), "battery_power" => $feed->get_id($userid, "battery_power"), - "solar_to_load_kwh" => $resolved["solar_to_load_kwh"], - "solar_to_grid_kwh" => $resolved["solar_to_grid_kwh"], - "solar_to_battery_kwh" => $resolved["solar_to_battery_kwh"], - "battery_to_load_kwh" => $resolved["battery_to_load_kwh"], - "battery_to_grid_kwh" => $resolved["battery_to_grid_kwh"], - "grid_to_load_kwh" => $resolved["grid_to_load_kwh"], - "grid_to_battery_kwh" => $resolved["grid_to_battery_kwh"], + "solar_to_load_kwh" => $feed->exists_tag_name($userid, $tag, "solar_to_load_kwh"), + "solar_to_grid_kwh" => $feed->exists_tag_name($userid, $tag, "solar_to_grid_kwh"), + "solar_to_battery_kwh" => $feed->exists_tag_name($userid, $tag, "solar_to_battery_kwh"), + "battery_to_load_kwh" => $feed->exists_tag_name($userid, $tag, "battery_to_load_kwh"), + "battery_to_grid_kwh" => $feed->exists_tag_name($userid, $tag, "battery_to_grid_kwh"), + "grid_to_load_kwh" => $feed->exists_tag_name($userid, $tag, "grid_to_load_kwh"), + "grid_to_battery_kwh" => $feed->exists_tag_name($userid, $tag, "grid_to_battery_kwh"), "process_mode" => "all", "process_start" => 0, "process" => "solarbatterykwh" ); - return $process_classes[$solarbatterykwh_config->process]->process($solarbatterykwh_config); + return $process_classes[$process_conf->process]->process($process_conf); } } \ No newline at end of file From 0f807809d021f1064041671345c3bfc3f5323219 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sat, 14 Mar 2026 22:41:46 +0000 Subject: [PATCH 12/13] silence progress dots --- .../mysolarpvbattery/mysolarpvbattery_controller.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php index 13b01de..63da29e 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php @@ -68,6 +68,10 @@ function mysolarpvbattery_app_controller($route,$app,$appconfig,$apikey) "process" => "solarbatterykwh" ); - return $process_classes[$process_conf->process]->process($process_conf); + // capture and silence any internal prints + ob_start(); + $result = $process_classes[$process_conf->process]->process($process_conf); + ob_end_clean(); + return $result; } } \ No newline at end of file From a0e142a0e75e85f830616cec0fc779318dd00906 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sat, 14 Mar 2026 22:47:51 +0000 Subject: [PATCH 13/13] use app config object directly --- .../mysolarpvbattery/mysolarpvbattery.php | 4 +--- .../mysolarpvbattery/mysolarpvbattery_controller.php | 11 ++++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 56196df..2ff8976 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -1498,11 +1498,9 @@ function run_post_processor() { $("#autogen-status").text("Starting post-processor...").css("color","#aaa"); $("#btn-run-processor").prop("disabled", true); - var autogen_node_name = "app_mysolarpvbattery_"+config.id; - $.ajax({ url: path + "app/process", - data: { id: config.id, apikey: apikey, tag: autogen_node_name }, + data: { id: config.id, apikey: apikey }, dataType: "json", timeout: 120000, success: function(result) { diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php index 63da29e..e565bdd 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php @@ -39,7 +39,6 @@ function mysolarpvbattery_app_controller($route,$app,$appconfig,$apikey) else if ($route->action == "process" && $session['write']) { $route->format = "json"; $userid = $session['userid']; - $tag = prop("tag",true); require_once "Modules/feed/feed_model.php"; $feed = new Feed($mysqli,$redis,$settings['feed']); @@ -49,11 +48,13 @@ function mysolarpvbattery_app_controller($route,$app,$appconfig,$apikey) $processes = $postprocess->get_processes("$linked_modules_dir/postprocess"); $process_classes = $postprocess->get_process_classes(); + $tag = "app_mysolarpvbattery_".$app->id; + $process_conf = (object) array( - "solar" => $feed->get_id($userid, "solar"), - "use" => $feed->get_id($userid, "use"), - "grid" => $feed->get_id($userid, "grid"), - "battery_power" => $feed->get_id($userid, "battery_power"), + "solar" => (int) $app->config->solar, + "use" => (int) $app->config->use, + "grid" => (int) $app->config->grid, + "battery_power" => (int) $app->config->battery_power, "solar_to_load_kwh" => $feed->exists_tag_name($userid, $tag, "solar_to_load_kwh"), "solar_to_grid_kwh" => $feed->exists_tag_name($userid, $tag, "solar_to_grid_kwh"),