From f59058c241a3ce5f3acc7a6a2385771706f25847 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Fri, 13 Mar 2026 19:02:03 +0000 Subject: [PATCH 001/110] 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 2e9736d8..39567c2d 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 002/110] 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 39567c2d..3683bbd0 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 003/110] 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 3683bbd0..890416d9 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 004/110] 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 890416d9..09f5f071 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 005/110] 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 00000000..b5db3771 --- /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 00000000..63ab61d1 --- /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 00000000..4ec678a4 --- /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 90378e5e..5a59c409 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 006/110] 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 63ab61d1..cd87fbd1 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 4ec678a4..986f264e 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 007/110] 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 cd87fbd1..ea76c1cf 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 008/110] 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 09f5f071..d4d316d1 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 00000000..4cbcbc37 --- /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 009/110] 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 0ad56304..af590178 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 b6997fe3..145a4bc2 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 d4d316d1..78924337 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 4cbcbc37..0cfdf408 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 010/110] 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 78924337..56196dfd 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 011/110] 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 0cfdf408..13b01de0 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 012/110] 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 13b01de0..63da29e8 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 013/110] 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 56196dfd..2ff89766 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 63da29e8..e565bdd2 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"), From e2ffad34d3e4b268b540cd490d15942432f9b764 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sun, 15 Mar 2026 17:26:42 +0000 Subject: [PATCH 014/110] refactor auto gen create/reset feeds --- .../mysolarpvbattery/mysolarpvbattery.php | 116 ++++++++---------- 1 file changed, 49 insertions(+), 67 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 2ff89766..65e7e2ef 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -1453,45 +1453,35 @@ function render_autogen_feed_list() { // Auto-generate feed actions // ---------------------------------------------------------------------- function create_missing_feeds() { - var autogen_node_name = "app_mysolarpvbattery_"+config.id; + const autogen_node_name = "app_mysolarpvbattery_" + config.id; + const missing = get_autogen_feeds().filter(f => !f.feedid); + let created = 0, errors = 0; - $("#autogen-status").text("Creating feeds...").css("color","#aaa"); + $("#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({ + missing.forEach(item => { + $.ajax({ url: path + "feed/create.json", - data: { tag: autogen_node_name, name: missing[i], datatype: 1, engine: 5, + data: { tag: autogen_node_name, name: item.name, 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(); - 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." - : "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); + dataType: "json", + async: false, + success: (res) => (res && res.feedid) ? created++ : errors++, + error: () => errors++ + }); }); + + // Update state and UI + config.feeds = feed.list(); + feeds_by_tag_name = feed.by_tag_and_name(config.feeds); + render_autogen_feed_list(); + + const statusText = errors === 0 ? `Created ${created} feed(s) successfully.` + : `Created ${created} feed(s), ${errors} error(s).`; + + $("#autogen-status").text(statusText).css("color", errors === 0 ? "#5cb85c" : "#f0ad4e"); + $("#btn-create-feeds").prop("disabled", false); } function run_post_processor() { @@ -1522,46 +1512,38 @@ function run_post_processor() { 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); - } - } + const feed_ids = get_autogen_feeds().filter(f => f.feedid).map(f => f.feedid); + if (feed_ids.length === 0) { - $("#autogen-status").text("No matching feeds found to clear.").css("color","#f0ad4e"); - $("#btn-reset-feeds").prop("disabled", false); + $("#autogen-status").text("No matching feeds found to clear.").css("color", "#f0ad4e"); 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" - })); - } + $("#autogen-status").text("Clearing feeds...").css("color", "#aaa"); + $("#btn-reset-feeds").prop("disabled", true); - $.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); + let cleared = 0, errors = 0; + + feed_ids.forEach(id => { + $.ajax({ + url: path + "feed/clear.json", + data: { id: id, apikey: apikey }, + dataType: "json", + async: false, + success: (res) => (res && res.success) ? cleared++ : errors++, + error: () => errors++ + }); }); + + // Refresh data and update UI + config.feeds = feed.list(); + render_autogen_feed_list(); + + const statusText = errors === 0 ? `Cleared ${cleared} feed(s) successfully.` + : `Cleared ${cleared} feed(s), ${errors} error(s).`; + + $("#autogen-status").text(statusText).css("color", errors === 0 ? "#5cb85c" : "#f0ad4e"); + $("#btn-reset-feeds").prop("disabled", false); } \ No newline at end of file From 47bf0839e4044090879f19159f7edd0cb6be0712 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sun, 15 Mar 2026 17:34:23 +0000 Subject: [PATCH 015/110] move autogen code to appconf --- Lib/appconf.js | 189 ++++++++++++++++++ app_controller.php | 2 +- .../mysolarpvbattery/mysolarpvbattery.php | 160 ++------------- 3 files changed, 202 insertions(+), 149 deletions(-) diff --git a/Lib/appconf.js b/Lib/appconf.js index af590178..8def0686 100644 --- a/Lib/appconf.js +++ b/Lib/appconf.js @@ -533,5 +533,194 @@ var config = { var aName = a.longname.toLowerCase(); var bName = b.longname.toLowerCase(); return ((aName < bName) ? -1 : ((aName > bName) ? 1 : 0)); + }, + + // ----------------------------------------------------------------------- + // autogen: helpers for apps that declare feeds with autogenerate:true + // + // Apps opt in by setting before calling config.init(): + // + // config.autogen_node_prefix = "app_myappname"; + // The tag used for auto-generated feeds becomes + // config.autogen_node_prefix + "_" + config.id + // Defaults to "app_" + config.id when not set. + // + // config.autogen_feed_defaults = { datatype: 1, engine: 5, options: { interval: 1800 } }; + // Merged into each feed/create.json request. + // Defaults to { datatype: 1, engine: 5, options: { interval: 1800 } } when not set. + // + // config.autogen_feeds_by_tag_name = feed.by_tag_and_name(config.feeds); + // Lookup used to check which feeds already exist. + // Must be kept up to date by the app after refreshing config.feeds. + // ----------------------------------------------------------------------- + autogen: { + + // Return the node tag string used for all auto-generated feeds + node_name: function() { + var prefix = config.autogen_node_prefix || ("app_" + config.id); + return prefix + "_" + config.id; + }, + + // Return array of { key, name, feedid } for every feed marked autogenerate:true + get_feeds: function() { + var node_name = config.autogen.node_name(); + var lookup = config.autogen_feeds_by_tag_name || {}; + var feeds = []; + + for (var key in config.app) { + if (config.app.hasOwnProperty(key) && config.app[key].autogenerate) { + var feed_name = config.app[key].autoname || key; + var feedid = false; + + if (lookup[node_name] != undefined) { + if (lookup[node_name][feed_name] != undefined) { + feedid = lookup[node_name][feed_name]['id']; + } + } + + feeds.push({ key: key, name: feed_name, feedid: feedid }); + } + } + return feeds; + }, + + // Render the feed status table into #autogen-feed-rows and toggle action buttons + render_feed_list: function() { + var autogen_feeds = config.autogen.get_feeds(); + var node_name = config.autogen.node_name(); + + $(".autogen-appname").text(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) { + status_html = "✗ missing"; + missing_count++; + } + + tbody.append( + "" + + " " + autogen_feeds[j].name + "" + + " " + 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(""); + }, + + // Create any feeds that are missing + create_missing_feeds: function() { + var node_name = config.autogen.node_name(); + var missing = config.autogen.get_feeds().filter(function(f) { return !f.feedid; }); + var defaults = config.autogen_feed_defaults || { datatype: 1, engine: 5, options: { interval: 1800 } }; + var created = 0, errors = 0; + + $("#autogen-status").text("Creating feeds...").css("color", "#aaa"); + $("#btn-create-feeds").prop("disabled", true); + + missing.forEach(function(item) { + $.ajax({ + url: path + "feed/create.json", + data: $.extend({}, defaults, { + tag: node_name, + name: item.name, + options: JSON.stringify(defaults.options || { interval: 1800 }), + apikey: apikey + }), + dataType: "json", + async: false, + success: function(res) { (res && res.feedid) ? created++ : errors++; }, + error: function() { errors++; } + }); + }); + + // Refresh feed list + config.feeds = feed.list(); + config.autogen_feeds_by_tag_name = feed.by_tag_and_name(config.feeds); + config.autogen.render_feed_list(); + + var statusText = errors === 0 + ? "Created " + created + " feed(s) successfully." + : "Created " + created + " feed(s), " + errors + " error(s)."; + $("#autogen-status").text(statusText).css("color", errors === 0 ? "#5cb85c" : "#f0ad4e"); + $("#btn-create-feeds").prop("disabled", false); + }, + + // Trigger the app post-processor via app/process + run_post_processor: function() { + $("#autogen-status").text("Starting post-processor...").css("color", "#aaa"); + $("#btn-run-processor").prop("disabled", true); + + $.ajax({ + url: path + "app/process", + data: { id: config.id, apikey: apikey }, + dataType: "json", + timeout: 120000, + success: function(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) { + $("#autogen-status").text("Post-processor failed: " + xhr.statusText).css("color", "#d9534f"); + }, + complete: function() { $("#btn-run-processor").prop("disabled", false); } + }); + }, + + // Clear (reset) all auto-generated feeds after confirmation + reset_feeds: function() { + var autogen_feeds = config.autogen.get_feeds(); + var count = autogen_feeds.filter(function(f) { return f.feedid; }).length; + + if (!confirm("Are you sure you want to clear all " + count + " auto-generated feed(s)? This cannot be undone.")) return; + + var feed_ids = autogen_feeds.filter(function(f) { return f.feedid; }).map(function(f) { return f.feedid; }); + + if (feed_ids.length === 0) { + $("#autogen-status").text("No matching feeds found to clear.").css("color", "#f0ad4e"); + return; + } + + $("#autogen-status").text("Clearing feeds...").css("color", "#aaa"); + $("#btn-reset-feeds").prop("disabled", true); + + var cleared = 0, errors = 0; + + feed_ids.forEach(function(id) { + $.ajax({ + url: path + "feed/clear.json", + data: { id: id, apikey: apikey }, + dataType: "json", + async: false, + success: function(res) { (res && res.success) ? cleared++ : errors++; }, + error: function() { errors++; } + }); + }); + + // Refresh feed list + config.feeds = feed.list(); + config.autogen_feeds_by_tag_name = feed.by_tag_and_name(config.feeds); + config.autogen.render_feed_list(); + + var statusText = errors === 0 + ? "Cleared " + cleared + " feed(s) successfully." + : "Cleared " + cleared + " feed(s), " + errors + " error(s)."; + $("#autogen-status").text(statusText).css("color", errors === 0 ? "#5cb85c" : "#f0ad4e"); + $("#btn-reset-feeds").prop("disabled", false); + } } } diff --git a/app_controller.php b/app_controller.php index 145a4bc2..6c81d57b 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 = 41; + $v = 42; $result = false; diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 65e7e2ef..bb47105b 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -442,6 +442,10 @@ function getTranslations(){ var feeds_by_tag_name = feed.by_tag_and_name(config.feeds); +config.autogen_node_prefix = "app_mysolarpvbattery"; +config.autogen_feed_defaults = { datatype: 1, engine: 5, options: { interval: 1800 } }; +config.autogen_feeds_by_tag_name = feeds_by_tag_name; + config.initapp = function(){init()}; config.showapp = function(){show()}; config.hideapp = function(){hide()}; @@ -1385,165 +1389,25 @@ function app_log (level, message) { // ---------------------------------------------------------------------- // Helper: return array of feeds that should be auto-generated +// (delegates to config.autogen.get_feeds in appconf.js) // ---------------------------------------------------------------------- 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; + return config.autogen.get_feeds(); } // ---------------------------------------------------------------------- // Auto-generate feed list +// (delegates to config.autogen.render_feed_list in appconf.js) // ---------------------------------------------------------------------- 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(""); + config.autogen.render_feed_list(); } // ---------------------------------------------------------------------- // Auto-generate feed actions +// (delegate to config.autogen.* in appconf.js) // ---------------------------------------------------------------------- -function create_missing_feeds() { - const autogen_node_name = "app_mysolarpvbattery_" + config.id; - const missing = get_autogen_feeds().filter(f => !f.feedid); - let created = 0, errors = 0; - - $("#autogen-status").text("Creating feeds...").css("color", "#aaa"); - $("#btn-create-feeds").prop("disabled", true); - - missing.forEach(item => { - $.ajax({ - url: path + "feed/create.json", - data: { tag: autogen_node_name, name: item.name, datatype: 1, engine: 5, - options: JSON.stringify({ interval: 1800 }), apikey: apikey }, - dataType: "json", - async: false, - success: (res) => (res && res.feedid) ? created++ : errors++, - error: () => errors++ - }); - }); - - // Update state and UI - config.feeds = feed.list(); - feeds_by_tag_name = feed.by_tag_and_name(config.feeds); - render_autogen_feed_list(); - - const statusText = errors === 0 ? `Created ${created} feed(s) successfully.` - : `Created ${created} feed(s), ${errors} error(s).`; - - $("#autogen-status").text(statusText).css("color", errors === 0 ? "#5cb85c" : "#f0ad4e"); - $("#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); - - $.ajax({ - url: path + "app/process", - data: { id: config.id, apikey: apikey }, - 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; - - const feed_ids = get_autogen_feeds().filter(f => f.feedid).map(f => f.feedid); - - if (feed_ids.length === 0) { - $("#autogen-status").text("No matching feeds found to clear.").css("color", "#f0ad4e"); - return; - } - - $("#autogen-status").text("Clearing feeds...").css("color", "#aaa"); - $("#btn-reset-feeds").prop("disabled", true); - - let cleared = 0, errors = 0; - - feed_ids.forEach(id => { - $.ajax({ - url: path + "feed/clear.json", - data: { id: id, apikey: apikey }, - dataType: "json", - async: false, - success: (res) => (res && res.success) ? cleared++ : errors++, - error: () => errors++ - }); - }); - - // Refresh data and update UI - config.feeds = feed.list(); - render_autogen_feed_list(); - - const statusText = errors === 0 ? `Cleared ${cleared} feed(s) successfully.` - : `Cleared ${cleared} feed(s), ${errors} error(s).`; - - $("#autogen-status").text(statusText).css("color", errors === 0 ? "#5cb85c" : "#f0ad4e"); - $("#btn-reset-feeds").prop("disabled", false); -} +function create_missing_feeds() { config.autogen.create_missing_feeds(); } +function run_post_processor() { config.autogen.run_post_processor(); } +function reset_feeds() { config.autogen.reset_feeds(); } \ No newline at end of file From 68451c73bdb5a3bff56f7a423634e56f5421f4a5 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sun, 15 Mar 2026 17:39:47 +0000 Subject: [PATCH 016/110] set app config after creating autogen feeds --- Lib/appconf.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Lib/appconf.js b/Lib/appconf.js index 8def0686..04a10db0 100644 --- a/Lib/appconf.js +++ b/Lib/appconf.js @@ -644,9 +644,17 @@ var config = { }); }); - // Refresh feed list + // Refresh feed lookups config.feeds = feed.list(); + config.feedsbyname = {}; + config.feedsbyid = {}; + for (var z in config.feeds) config.feedsbyname[config.feeds[z].name] = config.feeds[z]; + for (var z in config.feeds) config.feedsbyid[config.feeds[z].id] = config.feeds[z]; config.autogen_feeds_by_tag_name = feed.by_tag_and_name(config.feeds); + + // Populate config.app[key].value for the newly created autogen feeds + config.load(); + config.autogen.render_feed_list(); var statusText = errors === 0 From c6a26e1221836f105ed689413231f5367ca03eeb Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sun, 15 Mar 2026 18:22:50 +0000 Subject: [PATCH 017/110] move appconf template, stage 1 --- Lib/appconf.php | 52 ++++++++++++++++++ .../mysolarpvbattery/mysolarpvbattery.php | 55 +------------------ 2 files changed, 53 insertions(+), 54 deletions(-) create mode 100644 Lib/appconf.php diff --git a/Lib/appconf.php b/Lib/appconf.php new file mode 100644 index 00000000..7afe7910 --- /dev/null +++ b/Lib/appconf.php @@ -0,0 +1,52 @@ +
+ +
+
+
+
+

+

+ 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
+ +
+ + + + +
+
+ +
+
+
+
+
\ No newline at end of file diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index bb47105b..f6fa8a0d 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -320,60 +320,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.

-

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
- -
- - - - -
-
- -
-
-
-
-
- +
From 839586d16c34ca2f3e87887b0f70ec12c4955889 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sun, 15 Mar 2026 18:41:09 +0000 Subject: [PATCH 018/110] first stage of tariff explorer refactor to use solar pv battery post processed feeds --- .../octopus/tariff_explorer.js | 345 ++++++++---------- 1 file changed, 155 insertions(+), 190 deletions(-) diff --git a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js index c9efce2e..c99bd272 100644 --- a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js +++ b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js @@ -52,35 +52,47 @@ config.app = { "name": "Title", "description": "Optional title for app" }, - "import": { + "solar_to_load_kwh": { "optional": true, "type": "feed", - "autoname": "import" + "autoname": "solar_to_load_kwh", + "description": "Cumulative solar to load energy in kWh" }, - "import_kwh": { + "solar_to_grid_kwh": { "optional": true, "type": "feed", - "autoname": "import_kwh" + "autoname": "solar_to_grid_kwh", + "description": "Cumulative solar to grid (export) energy in kWh" }, - "use_kwh": { + "solar_to_battery_kwh": { "optional": true, "type": "feed", - "autoname": "use_kwh" + "autoname": "solar_to_battery_kwh", + "description": "Cumulative solar to battery energy in kWh" }, - "solar_kwh": { + "battery_to_load_kwh": { "optional": true, "type": "feed", - "autoname": "solar_kwh" + "autoname": "battery_to_load_kwh", + "description": "Cumulative battery to load energy in kWh" }, - "battery_charge_kwh": { + "battery_to_grid_kwh": { "optional": true, "type": "feed", - "autoname": "battery_charge_kwh" + "autoname": "battery_to_grid_kwh", + "description": "Cumulative battery to grid energy in kWh" }, - "battery_discharge_kwh": { + "grid_to_load_kwh": { "optional": true, "type": "feed", - "autoname": "battery_discharge_kwh" + "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" }, "meter_kwh_hh": { "optional": true, @@ -164,9 +176,7 @@ var updaterinst = false; var this_halfhour_index = -1; // disable x axis limit view.limit_x = false; -var cumulative_import_data = false; -var solarpv_mode = false; -var battery_mode = false; +var flow_mode = false; var smart_meter_data = false; var use_meter_kwh_hh = false; @@ -199,8 +209,7 @@ function show() { if (config.app[key].value) feeds[key] = config.feedsbyid[config.app[key].value]; } - solarpv_mode = false; - battery_mode = false; + flow_mode = false; resize(); @@ -298,14 +307,6 @@ function updater() { for (var key in config.app) { if (config.app[key].value) feeds[key] = result[config.app[key].value]; } - - if (feeds["import"] != undefined) { - if (feeds["import"].value < 10000) { - $("#power_now").html(Math.round(feeds["import"].value) + "W"); - } else { - $("#power_now").html((feeds["import"].value * 0.001).toFixed(1) + "kW"); - } - } }); } @@ -341,71 +342,51 @@ function graph_load() { datetimepicker2.setStartDate(new Date(view.start)); } - // Determine if required solar PV feeds are available - if (feeds["use_kwh"] != undefined && feeds["solar_kwh"] != undefined) { - solarpv_mode = true; - } - - // Determine if required battery feeds are available - if (feeds["battery_charge_kwh"] != undefined && feeds["battery_discharge_kwh"] != undefined && feeds["use_kwh"] != undefined) { - battery_mode = true; - } - - if (feeds["meter_kwh_hh"] != undefined) { - smart_meter_data = true; + // Determine if all 7 energy flow feeds are available (post-processed cumulative kWh feeds) + flow_mode = ( + feeds["solar_to_load_kwh"] != undefined && + feeds["solar_to_grid_kwh"] != undefined && + feeds["solar_to_battery_kwh"] != undefined && + feeds["battery_to_load_kwh"] != undefined && + feeds["battery_to_grid_kwh"] != undefined && + feeds["grid_to_load_kwh"] != undefined && + feeds["grid_to_battery_kwh"] != undefined + ); + + smart_meter_data = feeds["meter_kwh_hh"] != undefined; + if (smart_meter_data) { $("#use_meter_kwh_hh_bound").show(); } - if (feeds["import_kwh"] != undefined) { - cumulative_import_data = true; - } - - var import_kwh = []; - var use_kwh = []; - var solar_kwh = []; - var battery_charge_kwh = []; - var battery_discharge_kwh = []; + // Load energy flow feeds (cumulative kWh, delta=1 returns half-hourly differences directly) + var solar_to_load_kwh_data = []; + var solar_to_grid_kwh_data = []; + var solar_to_battery_kwh_data = []; + var battery_to_load_kwh_data = []; + var battery_to_grid_kwh_data = []; + var grid_to_load_kwh_data = []; + var grid_to_battery_kwh_data = []; var meter_kwh_hh = []; - if (cumulative_import_data) { - import_kwh = feed.getdata(feeds["import_kwh"].id, view.start, view.end, interval); - } - - if (solarpv_mode || battery_mode) { - use_kwh = feed.getdata(feeds["use_kwh"].id, view.start, view.end, interval); - } - - if (solarpv_mode) { - solar_kwh = feed.getdata(feeds["solar_kwh"].id, view.start, view.end, interval); - } - - if (battery_mode) { - battery_charge_kwh = feed.getdata(feeds["battery_charge_kwh"].id, view.start, view.end, interval); - battery_discharge_kwh = feed.getdata(feeds["battery_discharge_kwh"].id, view.start, view.end, interval); + if (flow_mode) { + solar_to_load_kwh_data = feed.getdata(feeds["solar_to_load_kwh"].id, view.start, view.end, interval, 0, 1); + solar_to_grid_kwh_data = feed.getdata(feeds["solar_to_grid_kwh"].id, view.start, view.end, interval, 0, 1); + solar_to_battery_kwh_data = feed.getdata(feeds["solar_to_battery_kwh"].id, view.start, view.end, interval, 0, 1); + battery_to_load_kwh_data = feed.getdata(feeds["battery_to_load_kwh"].id, view.start, view.end, interval, 0, 1); + battery_to_grid_kwh_data = feed.getdata(feeds["battery_to_grid_kwh"].id, view.start, view.end, interval, 0, 1); + grid_to_load_kwh_data = feed.getdata(feeds["grid_to_load_kwh"].id, view.start, view.end, interval, 0, 1); + grid_to_battery_kwh_data = feed.getdata(feeds["grid_to_battery_kwh"].id, view.start, view.end, interval, 0, 1); } if (smart_meter_data) meter_kwh_hh = feed.getdata(feeds["meter_kwh_hh"].id, view.start, view.end, interval); - // Add last half hour of current day to cumulative data if missing + // Detect current half-hour index for live stats (use grid_to_load feed or meter as reference) this_halfhour_index = -1; - var this_halfhour = Math.floor((new Date()).getTime() / 1800000) * 1800000 - for (var z = 1; z < import_kwh.length; z++) { - if (import_kwh[z][0] == this_halfhour) { - import_kwh[z + 1] = [this_halfhour + 1800000, feeds["import_kwh"].value] - this_halfhour_index = z - - if (solarpv_mode || battery_mode) { - use_kwh[z + 1] = [this_halfhour + 1800000, feeds["use_kwh"].value] - } - - if (solarpv_mode) { - solar_kwh[z + 1] = [this_halfhour + 1800000, feeds["solar_kwh"].value] - } - - if (battery_mode) { - battery_charge_kwh[z + 1] = [this_halfhour + 1800000, feeds["battery_charge_kwh"].value] - battery_discharge_kwh[z + 1] = [this_halfhour + 1800000, feeds["battery_discharge_kwh"].value] - } + var ref_data = flow_mode ? grid_to_load_kwh_data : (smart_meter_data ? meter_kwh_hh : []); + var this_halfhour = Math.floor((new Date()).getTime() / 1800000) * 1800000; + for (var z = 0; z < ref_data.length; z++) { + if (ref_data[z][0] == this_halfhour) { + this_halfhour_index = z; break; } } @@ -426,10 +407,10 @@ function graph_load() { data["tariff_B"] = getdataremote(octopus_feed_list[config.app.tariff_B.value][config.app.region.value], view.start, view.end, interval); } - // Outgoing - if (config.app.region != undefined && (solarpv_mode || battery_mode)) { + // Outgoing (export tariff) - only needed in flow mode + if (config.app.region != undefined && flow_mode) { data["outgoing"] = getdataremote(regions_outgoing[config.app.region.value], view.start, view.end, interval); - // Invert export tariff + // Invert export tariff so it reads as a positive earning rate for (var z in data["outgoing"]) data["outgoing"][z][1] *= -1; } @@ -444,9 +425,10 @@ function graph_load() { data["import_cost_tariff_B"] = []; data["export"] = []; data["export_cost"] = []; - data["solar_direct"] = []; + data["solar_to_load"] = []; data["solar_to_battery"] = []; - data["solar_used"] = [] + data["battery_to_load"] = []; + data["solar_used"] = []; data["solar_used_cost"] = []; data["meter_kwh_hh"] = meter_kwh_hh; data["meter_kwh_hh_cost"] = []; @@ -479,23 +461,14 @@ function graph_load() { var monthly_data = {}; - // Convert cumulative import data to half hourly kwh - // Core import data conversion - var import_kwh_hh = convert_cumulative_kwh_to_kwh_hh(import_kwh, true); - - // Solar PV mode data conversions - var use_kwh_hh = convert_cumulative_kwh_to_kwh_hh(use_kwh, true); - var solar_kwh_hh = convert_cumulative_kwh_to_kwh_hh(solar_kwh, true); - var battery_charge_kwh_hh = convert_cumulative_kwh_to_kwh_hh(battery_charge_kwh, true); - var battery_discharge_kwh_hh = convert_cumulative_kwh_to_kwh_hh(battery_discharge_kwh, true); - + // Determine data length and primary time reference var data_length = 0; - if (cumulative_import_data) data_length = import_kwh_hh.length; + if (flow_mode) data_length = grid_to_load_kwh_data.length; else if (smart_meter_data) data_length = meter_kwh_hh.length; for (var z = 0; z < data_length; z++) { let time = 0; - if (cumulative_import_data) time = import_kwh[z][0]; + if (flow_mode) time = grid_to_load_kwh_data[z][0]; else if (smart_meter_data) time = meter_kwh_hh[z][0]; d.setTime(time) @@ -503,58 +476,44 @@ function graph_load() { // get start of month timestamp to calculate monthly data let startOfMonth = new Date(d.getFullYear(), d.getMonth(), 1).getTime(); - + let kwh_import = 0; let kwh_export = 0; let kwh_use = 0; - let kwh_solar = 0; - let kwh_battery_charge = 0; - let kwh_battery_discharge = 0; - let balance = 0; + let kwh_solar_to_load = 0; + let kwh_solar_to_grid = 0; + let kwh_solar_to_battery = 0; + let kwh_battery_to_load = 0; + let kwh_battery_to_grid = 0; + let kwh_grid_to_load = 0; + let kwh_grid_to_battery = 0; + + if (flow_mode) { + // Read half-hourly energy flow values directly from post-processed feeds (delta=1) + // Clamp negatives to zero for safety + kwh_solar_to_load = Math.max(0, solar_to_load_kwh_data[z][1] != null ? solar_to_load_kwh_data[z][1] : 0); + kwh_solar_to_grid = Math.max(0, solar_to_grid_kwh_data[z][1] != null ? solar_to_grid_kwh_data[z][1] : 0); + kwh_solar_to_battery = Math.max(0, solar_to_battery_kwh_data[z][1] != null ? solar_to_battery_kwh_data[z][1] : 0); + kwh_battery_to_load = Math.max(0, battery_to_load_kwh_data[z][1] != null ? battery_to_load_kwh_data[z][1] : 0); + kwh_battery_to_grid = Math.max(0, battery_to_grid_kwh_data[z][1] != null ? battery_to_grid_kwh_data[z][1] : 0); + kwh_grid_to_load = Math.max(0, grid_to_load_kwh_data[z][1] != null ? grid_to_load_kwh_data[z][1] : 0); + kwh_grid_to_battery = Math.max(0, grid_to_battery_kwh_data[z][1] != null ? grid_to_battery_kwh_data[z][1] : 0); + + // Derive aggregate values from flows + kwh_import = kwh_grid_to_load + kwh_grid_to_battery; + kwh_export = kwh_solar_to_grid + kwh_battery_to_grid; + kwh_use = kwh_solar_to_load + kwh_battery_to_load + kwh_grid_to_load; - // Use cumulative import data if available by default - if (cumulative_import_data) { - kwh_import = import_kwh_hh[z][1]; - - // If we dont have cumulative import data, but we have smart meter data, use that instead } else if (smart_meter_data) { - kwh_import = meter_kwh_hh[z][1]; - } - - // If solar or battery mode fetch kwh_use - if (solarpv_mode || battery_mode) { - kwh_use = use_kwh_hh[z][1]; - balance += kwh_use; - } - - // If solar mode, fetch kwh_solar - if (solarpv_mode) { - kwh_solar = solar_kwh_hh[z][1]; - balance -= kwh_solar; - } - - // if battery mode, fetch battery charge and discharge - if (battery_mode) { - kwh_battery_charge = battery_charge_kwh_hh[z][1]; - kwh_battery_discharge = battery_discharge_kwh_hh[z][1]; - balance += kwh_battery_charge; - balance -= kwh_battery_discharge; + // Fallback: use smart meter half-hourly data directly as import + kwh_import = meter_kwh_hh[z][1] != null ? meter_kwh_hh[z][1] : 0; + kwh_grid_to_load = kwh_import; } - // If solar mode and no battery, calculate import from use and solar - if (solarpv_mode || battery_mode) { - if (balance >= 0) { - kwh_import = balance; - kwh_export = 0; - } else { - kwh_import = 0; - kwh_export = balance * -1; - } - } - - // Alternatively use meter data in place of cumulative import data if user selected + // Alternatively use meter data in place of flow import data if user selected if (smart_meter_data && use_meter_kwh_hh) { - kwh_import = meter_kwh_hh[z][1]; + kwh_import = meter_kwh_hh[z][1] != null ? meter_kwh_hh[z][1] : 0; + kwh_grid_to_load = kwh_import; } data["import"].push([time, kwh_import]); @@ -562,8 +521,8 @@ function graph_load() { // Unit and import cost on tariff A let unitcost_tariff_A = null; - let hh_cost_tariff_A = null; - if (data.tariff_A[z][1] != null) { + let hh_cost_tariff_A = null; + if (data.tariff_A[z] != undefined && data.tariff_A[z][1] != null) { unitcost_tariff_A = data.tariff_A[z][1] * 0.01; hh_cost_tariff_A = kwh_import * unitcost_tariff_A; @@ -572,13 +531,13 @@ function graph_load() { // Generate profile profile_kwh[hh][1] += kwh_import - profile_cost[hh][1] += hh_cost_tariff_A + profile_cost[hh][1] += hh_cost_tariff_A } // Unit and import cost on tariff B let unitcost_tariff_B = null; let hh_cost_tariff_B = null; - if (data.tariff_B[z][1] != null) { + if (data.tariff_B[z] != undefined && data.tariff_B[z][1] != null) { unitcost_tariff_B = data.tariff_B[z][1] * 0.01; hh_cost_tariff_B = kwh_import * unitcost_tariff_B; @@ -604,7 +563,7 @@ function graph_load() { if (hh_cost_tariff_A != null) { monthly_data[startOfMonth]["import_tariff_A"] += kwh_import - monthly_data[startOfMonth]["cost_import_tariff_A"] += hh_cost_tariff_A + monthly_data[startOfMonth]["cost_import_tariff_A"] += hh_cost_tariff_A } if (hh_cost_tariff_B != null) { @@ -614,43 +573,46 @@ function graph_load() { // Carbon Intensity if (show_carbonintensity) { - let co2intensity = data.carbonintensity[z][1]; - let co2_hh = kwh_import * (co2intensity * 0.001) - total.co2 += co2_hh + let co2intensity = data.carbonintensity[z] != undefined ? data.carbonintensity[z][1] : null; + if (co2intensity != null) { + let co2_hh = kwh_import * (co2intensity * 0.001) + total.co2 += co2_hh + } } - // We may explort in battery mode - if (solarpv_mode || battery_mode) { + // Export and solar flows (flow mode only) + if (flow_mode) { data["use"].push([time, kwh_use]); data["export"].push([time, kwh_export * -1]); - total.export_tariff.kwh += kwh_export - let cost_export = data.outgoing[z][1] * 0.01 * -1; - data["export_cost"].push([time, kwh_export * cost_export * -1]); - total.export_tariff.cost += kwh_export * cost_export - } - - // Solar used calculation - if (solarpv_mode) { + total.export_tariff.kwh += kwh_export; + if (data.outgoing[z] != undefined && data.outgoing[z][1] != null) { + let cost_export = data.outgoing[z][1] * 0.01 * -1; + data["export_cost"].push([time, kwh_export * cost_export * -1]); + total.export_tariff.cost += kwh_export * cost_export; + } else { + data["export_cost"].push([time, null]); + } - let solar_direct = Math.min(kwh_solar, kwh_use); - data["solar_direct"].push([time, solar_direct]); - // Any additional solar used to charge battery - let solar_to_battery = Math.min(Math.max(0, kwh_solar - solar_direct), kwh_battery_charge); - data["solar_to_battery"].push([time, solar_to_battery]); + // Solar flows for graph + data["solar_to_load"].push([time, kwh_solar_to_load]); + data["solar_to_battery"].push([time, kwh_solar_to_battery]); + data["battery_to_load"].push([time, kwh_battery_to_load]); - let kwh_solar_used = kwh_solar - kwh_export; + // Solar self-consumption = solar_to_load + solar_to_battery (all solar that stayed onsite) + let kwh_solar_used = kwh_solar_to_load + kwh_solar_to_battery; data["solar_used"].push([time, kwh_solar_used]); - total.solar_used.kwh += kwh_solar_used - data["solar_used_cost"].push([time, kwh_solar_used * unitcost_tariff_A]); - total.solar_used.cost += kwh_solar_used * unitcost_tariff_A + total.solar_used.kwh += kwh_solar_used; + if (unitcost_tariff_A != null) { + data["solar_used_cost"].push([time, kwh_solar_used * unitcost_tariff_A]); + total.solar_used.cost += kwh_solar_used * unitcost_tariff_A; + } else { + data["solar_used_cost"].push([time, null]); + } } } - - // -------------------------------------------------------------------------------------- - - if (smart_meter_data) { - calibration_line_of_best_fit(import_kwh_hh, meter_kwh_hh); + if (smart_meter_data && !flow_mode) { + calibration_line_of_best_fit(data["import"], meter_kwh_hh); } draw_tables(total, monthly_data); @@ -689,7 +651,7 @@ function draw_tables(total, monthly_data) { $("#carbonintensity_result").html("Total CO2: " + (total.co2).toFixed(1) + "kgCO2, Consumption intensity: " + window_co2_intensity.toFixed(0) + " gCO2/kWh") } - if (solarpv_mode || battery_mode) { + if (flow_mode) { var unit_cost_export = (total.export_tariff.cost / total.export_tariff.kwh); out += ""; out += "Export"; @@ -697,9 +659,7 @@ function draw_tables(total, monthly_data) { out += "£" + total.export_tariff.cost.toFixed(2) + ""; out += "" + (unit_cost_export * 100 * 1.05).toFixed(1) + "p/kWh (inc VAT)"; out += ""; - } - if (solarpv_mode) { var unit_cost_solar_used = (total.solar_used.cost / total.solar_used.kwh); out += ""; out += "Solar self consumption"; @@ -839,19 +799,16 @@ function graph_draw() { graph_series = []; - // Solar used data - if (solarpv_mode) { + // Solar and battery flow data (flow_mode only) + if (flow_mode) { graph_series.push({ - label: "Solar direct", - data: data["solar_direct"], + label: "Solar to Load", + data: data["solar_to_load"], yaxis: 1, color: "#bec745", stack: true, bars: bars }); - } - - if (solarpv_mode && battery_mode) { graph_series.push({ label: "Solar to Battery", data: data["solar_to_battery"], @@ -860,9 +817,17 @@ function graph_draw() { stack: true, bars: bars }); + graph_series.push({ + label: "Battery to Load", + data: data["battery_to_load"], + yaxis: 1, + color: "#fbb450", + stack: true, + bars: bars + }); } - // Import data + // Import (grid to load + grid to battery) data graph_series.push({ label: "Import", data: data["import"], @@ -872,8 +837,8 @@ function graph_draw() { bars: bars }); - // Export data - if (solarpv_mode || battery_mode) { + // Export data (flow_mode only) + if (flow_mode) { graph_series.push({ label: "Export", data: data["export"], @@ -885,7 +850,7 @@ function graph_draw() { } // Smart meter data - if (smart_meter_data && !solarpv_mode) { + if (smart_meter_data && !flow_mode) { graph_series.push({ label: "Import Actual", data: data["meter_kwh_hh"], @@ -910,7 +875,7 @@ function graph_draw() { } }); - if (solarpv_mode) { + if (flow_mode) { graph_series.push({ label: "Outgoing", data: data["outgoing"], @@ -1416,8 +1381,8 @@ $("#download-csv").click(function() { var csv = []; - if (solarpv_mode) { - keys = ["tariff_A", "tariff_B", "outgoing", "use", "import", "import_cost_tariff_A", "import_cost_tariff_B", "export", "export_cost", "solar_used", "solar_used_cost", "meter_kwh_hh", "meter_kwh_hh_cost"] + if (flow_mode) { + keys = ["tariff_A", "tariff_B", "outgoing", "use", "import", "import_cost_tariff_A", "import_cost_tariff_B", "export", "export_cost", "solar_to_load", "solar_to_battery", "battery_to_load", "solar_used", "solar_used_cost", "meter_kwh_hh", "meter_kwh_hh_cost"] } else { keys = ["tariff_A", "tariff_B", "import", "import_cost_tariff_A", "import_cost_tariff_B", "meter_kwh_hh", "meter_kwh_hh_cost"] } From 05195c4e04b33e4411dfd12dec68e3ec22585835 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sun, 15 Mar 2026 18:58:48 +0000 Subject: [PATCH 019/110] refactor part 2 --- .../octopus/tariff_explorer.js | 175 ++++++++++-------- 1 file changed, 94 insertions(+), 81 deletions(-) diff --git a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js index c99bd272..7bad5aff 100644 --- a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js +++ b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js @@ -426,8 +426,12 @@ function graph_load() { data["export"] = []; data["export_cost"] = []; 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"] = []; data["solar_used"] = []; data["solar_used_cost"] = []; data["meter_kwh_hh"] = meter_kwh_hh; @@ -456,6 +460,24 @@ function graph_load() { export_tariff: { kwh: 0, cost: 0 }, solar_used: { kwh: 0, cost: 0 }, + // Per-flow kWh totals + solar_to_load_kwh: 0, + solar_to_grid_kwh: 0, + solar_to_battery_kwh: 0, + battery_to_load_kwh: 0, + battery_to_grid_kwh: 0, + grid_to_load_kwh: 0, + grid_to_battery_kwh: 0, + + // Per-flow value at tariff A (avoided cost / earned) + solar_to_load_value: 0, + solar_to_grid_value: 0, + solar_to_battery_value: 0, + battery_to_load_value: 0, + battery_to_grid_value: 0, + grid_to_load_cost: 0, + grid_to_battery_cost: 0, + co2: 0 } @@ -585,20 +607,50 @@ function graph_load() { data["use"].push([time, kwh_use]); data["export"].push([time, kwh_export * -1]); total.export_tariff.kwh += kwh_export; - if (data.outgoing[z] != undefined && data.outgoing[z][1] != null) { - let cost_export = data.outgoing[z][1] * 0.01 * -1; - data["export_cost"].push([time, kwh_export * cost_export * -1]); - total.export_tariff.cost += kwh_export * cost_export; - } else { - data["export_cost"].push([time, null]); - } - // Solar flows for graph + // All 7 disaggregated flow data arrays data["solar_to_load"].push([time, kwh_solar_to_load]); + data["solar_to_grid"].push([time, kwh_solar_to_grid]); data["solar_to_battery"].push([time, kwh_solar_to_battery]); data["battery_to_load"].push([time, kwh_battery_to_load]); + data["battery_to_grid"].push([time, kwh_battery_to_grid]); + data["grid_to_load"].push([time, kwh_grid_to_load]); + data["grid_to_battery"].push([time, kwh_grid_to_battery]); + + // Accumulate per-flow kWh totals + total.solar_to_load_kwh += kwh_solar_to_load; + total.solar_to_grid_kwh += kwh_solar_to_grid; + total.solar_to_battery_kwh += kwh_solar_to_battery; + total.battery_to_load_kwh += kwh_battery_to_load; + total.battery_to_grid_kwh += kwh_battery_to_grid; + total.grid_to_load_kwh += kwh_grid_to_load; + total.grid_to_battery_kwh += kwh_grid_to_battery; + + let outgoing_unit = (data.outgoing[z] != undefined && data.outgoing[z][1] != null) + ? data.outgoing[z][1] * 0.01 * -1 // already inverted, so this is positive p/kWh + : null; + + if (outgoing_unit != null) { + let cost_export = kwh_export * outgoing_unit; + data["export_cost"].push([time, cost_export]); + total.export_tariff.cost += cost_export; + // Per-flow export values + total.solar_to_grid_value += kwh_solar_to_grid * outgoing_unit; + total.battery_to_grid_value += kwh_battery_to_grid * outgoing_unit; + } else { + data["export_cost"].push([time, null]); + } + + if (unitcost_tariff_A != null) { + // Per-flow avoided/saved cost at tariff A rate + total.solar_to_load_value += kwh_solar_to_load * unitcost_tariff_A; + total.solar_to_battery_value += kwh_solar_to_battery * unitcost_tariff_A; + total.battery_to_load_value += kwh_battery_to_load * unitcost_tariff_A; + total.grid_to_load_cost += kwh_grid_to_load * unitcost_tariff_A; + total.grid_to_battery_cost += kwh_grid_to_battery * unitcost_tariff_A; + } - // Solar self-consumption = solar_to_load + solar_to_battery (all solar that stayed onsite) + // Solar self-consumption = solar_to_load + solar_to_battery let kwh_solar_used = kwh_solar_to_load + kwh_solar_to_battery; data["solar_used"].push([time, kwh_solar_used]); total.solar_used.kwh += kwh_solar_used; @@ -652,29 +704,26 @@ function draw_tables(total, monthly_data) { } if (flow_mode) { - var unit_cost_export = (total.export_tariff.cost / total.export_tariff.kwh); - out += ""; - out += "Export"; - out += "" + total.export_tariff.kwh.toFixed(1) + " kWh"; - out += "£" + total.export_tariff.cost.toFixed(2) + ""; - out += "" + (unit_cost_export * 100 * 1.05).toFixed(1) + "p/kWh (inc VAT)"; - out += ""; - - var unit_cost_solar_used = (total.solar_used.cost / total.solar_used.kwh); - out += ""; - out += "Solar self consumption"; - out += "" + total.solar_used.kwh.toFixed(1) + " kWh"; - out += "£" + total.solar_used.cost.toFixed(2) + ""; - out += "" + (unit_cost_solar_used * 100 * 1.05).toFixed(1) + "p/kWh (inc VAT)"; - out += ""; - - var unit_cost_solar_combined = ((total.solar_used.cost + total.export_tariff.cost) / (total.solar_used.kwh + total.export_tariff.kwh)); - out += ""; - out += "Solar + Export"; - out += "" + (total.solar_used.kwh + total.export_tariff.kwh).toFixed(1) + " kWh"; - out += "£" + (total.solar_used.cost + total.export_tariff.cost).toFixed(2) + ""; - out += "" + (unit_cost_solar_combined * 100 * 1.05).toFixed(1) + "p/kWh (inc VAT)"; - out += ""; + // Helper: one table row per energy flow + function flow_row(label, kwh, value_gbp, value_label, color) { + var r = ""; + r += "" + label + ""; + r += "" + kwh.toFixed(2) + " kWh"; + r += "" + (value_gbp !== null ? (value_gbp >= 0 ? "\u00a3" : "-\u00a3") + Math.abs(value_gbp).toFixed(2) : "—") + ""; + r += "" + value_label + ""; + r += ""; + return r; + } + + out += "Energy flow breakdown"; + + out += flow_row("☀ Solar → Load", total.solar_to_load_kwh, total.solar_to_load_value * 1.05, "avoided import cost (tariff A)", "#bec745"); + out += flow_row("☀ Solar → Battery", total.solar_to_battery_kwh, total.solar_to_battery_value * 1.05, "avoided import cost (tariff A)", "#a3d977"); + out += flow_row("☀ Solar → Grid (export)",total.solar_to_grid_kwh, total.solar_to_grid_value * 1.05, "earned at export tariff", "#dccc1f"); + out += flow_row("🔋 Battery → Load", total.battery_to_load_kwh, total.battery_to_load_value * 1.05, "avoided import cost (tariff A)", "#fbb450"); + out += flow_row("🔋 Battery → Grid (export)", total.battery_to_grid_kwh, total.battery_to_grid_value * 1.05, "earned at export tariff", "#f0913a"); + out += flow_row("💡 Grid → Load", total.grid_to_load_kwh, -(total.grid_to_load_cost * 1.05), "cost at tariff A", "#44b3e2"); + out += flow_row("💡 Grid → Battery", total.grid_to_battery_kwh, -(total.grid_to_battery_cost * 1.05),"cost at tariff A", "#82cbfc"); } $("#show_profile").show(); @@ -799,57 +848,21 @@ function graph_draw() { graph_series = []; - // Solar and battery flow data (flow_mode only) if (flow_mode) { - graph_series.push({ - label: "Solar to Load", - data: data["solar_to_load"], - yaxis: 1, - color: "#bec745", - stack: true, - bars: bars - }); - graph_series.push({ - label: "Solar to Battery", - data: data["solar_to_battery"], - yaxis: 1, - color: "#a3d977", - stack: true, - bars: bars - }); - graph_series.push({ - label: "Battery to Load", - data: data["battery_to_load"], - yaxis: 1, - color: "#fbb450", - stack: true, - bars: bars - }); - } - - // Import (grid to load + grid to battery) data - graph_series.push({ - label: "Import", - data: data["import"], - yaxis: 1, - color: "#44b3e2", - stack: true, - bars: bars - }); - - // Export data (flow_mode only) - if (flow_mode) { - graph_series.push({ - label: "Export", - data: data["export"], - yaxis: 1, - color: "#dccc1f", - stack: false, - bars: bars - }); + // All 7 disaggregated flows stacked as positive bars + graph_series.push({ label: "Solar to Load", data: data["solar_to_load"], yaxis: 1, color: "#bec745", stack: true, bars: bars }); + graph_series.push({ label: "Solar to Battery", data: data["solar_to_battery"], yaxis: 1, color: "#a3d977", stack: true, bars: bars }); + graph_series.push({ label: "Solar to Grid", data: data["solar_to_grid"], yaxis: 1, color: "#dccc1f", stack: true, bars: bars }); + graph_series.push({ label: "Battery to Load", data: data["battery_to_load"], yaxis: 1, color: "#fbb450", stack: true, bars: bars }); + graph_series.push({ label: "Battery to Grid", data: data["battery_to_grid"], yaxis: 1, color: "#f0913a", stack: true, bars: bars }); + graph_series.push({ label: "Grid to Load", data: data["grid_to_load"], yaxis: 1, color: "#44b3e2", stack: true, bars: bars }); + graph_series.push({ label: "Grid to Battery", data: data["grid_to_battery"], yaxis: 1, color: "#82cbfc", stack: true, bars: bars }); + } else { + // Non-flow mode: show aggregated import bar + graph_series.push({ label: "Import", data: data["import"], yaxis: 1, color: "#44b3e2", stack: true, bars: bars }); } - // Smart meter data + // Smart meter data (non-flow fallback only) if (smart_meter_data && !flow_mode) { graph_series.push({ label: "Import Actual", @@ -1382,7 +1395,7 @@ $("#download-csv").click(function() { var csv = []; if (flow_mode) { - keys = ["tariff_A", "tariff_B", "outgoing", "use", "import", "import_cost_tariff_A", "import_cost_tariff_B", "export", "export_cost", "solar_to_load", "solar_to_battery", "battery_to_load", "solar_used", "solar_used_cost", "meter_kwh_hh", "meter_kwh_hh_cost"] + keys = ["tariff_A", "tariff_B", "outgoing", "solar_to_load", "solar_to_battery", "solar_to_grid", "battery_to_load", "battery_to_grid", "grid_to_load", "grid_to_battery", "import", "import_cost_tariff_A", "import_cost_tariff_B", "export", "export_cost", "solar_used", "solar_used_cost", "meter_kwh_hh"] } else { keys = ["tariff_A", "tariff_B", "import", "import_cost_tariff_A", "import_cost_tariff_B", "meter_kwh_hh", "meter_kwh_hh_cost"] } From 715a260b35ce081785f7763b406698415291038c Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sun, 15 Mar 2026 19:01:49 +0000 Subject: [PATCH 020/110] updated tooltip --- .../octopus/tariff_explorer.js | 80 +++++++++++++++---- 1 file changed, 63 insertions(+), 17 deletions(-) diff --git a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js index 7bad5aff..49d32eb7 100644 --- a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js +++ b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js @@ -1221,27 +1221,73 @@ $('#placeholder').bind("plothover", function(event, pos, item) { let outgoing = get_data_value_at_index("outgoing", z); let carbonintensity = get_data_value_at_index("carbonintensity", z); - if (import_kwh != null) { - text += "Import: " + import_kwh.toFixed(3) + " kWh"; - if (tariff_A != null) { - let cost = import_kwh * tariff_A; - text += " (" + cost.toFixed(2) + "p cost)"; + let solar_to_load_kwh = get_data_value_at_index("solar_to_load", z); + let solar_to_grid_kwh = get_data_value_at_index("solar_to_grid", z); + let solar_to_battery_kwh = get_data_value_at_index("solar_to_battery", z); + let battery_to_load_kwh = get_data_value_at_index("battery_to_load", z); + let battery_to_grid_kwh = get_data_value_at_index("battery_to_grid", z); + let grid_to_load_kwh = get_data_value_at_index("grid_to_load", z); + let grid_to_battery_kwh = get_data_value_at_index("grid_to_battery", z); + + if (flow_mode) { + if (solar_to_load_kwh != null) { + text += "☀ Solar → Load: " + solar_to_load_kwh.toFixed(3) + " kWh"; + if (tariff_A != null) text += " (" + (solar_to_load_kwh * tariff_A).toFixed(2) + "p saved)
"; + else text += "
"; + } + if (solar_to_battery_kwh != null) { + text += "☀ Solar → Battery: " + solar_to_battery_kwh.toFixed(3) + " kWh"; + if (tariff_A != null) text += " (" + (solar_to_battery_kwh * tariff_A).toFixed(2) + "p saved)
"; + else text += "
"; + } + if (solar_to_grid_kwh != null) { + text += "☀ Solar → Grid: " + solar_to_grid_kwh.toFixed(3) + " kWh"; + if (outgoing != null) text += " (" + (solar_to_grid_kwh * outgoing).toFixed(2) + "p gained)
"; + else text += "
"; + } + if (battery_to_load_kwh != null) { + text += "🔋 Battery → Load: " + battery_to_load_kwh.toFixed(3) + " kWh"; + if (tariff_A != null) text += " (" + (battery_to_load_kwh * tariff_A).toFixed(2) + "p saved)
"; + else text += "
"; + } + if (battery_to_grid_kwh != null) { + text += "🔋 Battery → Grid: " + battery_to_grid_kwh.toFixed(3) + " kWh"; + if (outgoing != null) text += " (" + (battery_to_grid_kwh * outgoing).toFixed(2) + "p gained)
"; + else text += "
"; + } + if (grid_to_load_kwh != null) { + text += "💡 Grid → Load: " + grid_to_load_kwh.toFixed(3) + " kWh"; + if (tariff_A != null) text += " (" + (grid_to_load_kwh * tariff_A).toFixed(2) + "p cost)
"; + else text += "
"; + } + if (grid_to_battery_kwh != null) { + text += "💡 Grid → Battery: " + grid_to_battery_kwh.toFixed(3) + " kWh"; + if (tariff_A != null) text += " (" + (grid_to_battery_kwh * tariff_A).toFixed(2) + "p cost)
"; + else text += "
"; + } + } else { + if (import_kwh != null) { + text += "Import: " + import_kwh.toFixed(3) + " kWh"; + if (tariff_A != null) { + let cost = import_kwh * tariff_A; + text += " (" + cost.toFixed(2) + "p cost)"; + } } - } - if (solar_used_kwh != null) { - text += "
Used Solar: " + solar_used_kwh.toFixed(3) + " kWh"; - if (tariff_A != null) { - let cost = solar_used_kwh * tariff_A; - text += " (" + cost.toFixed(2) + "p saved)"; + if (solar_used_kwh != null) { + text += "
Used Solar: " + solar_used_kwh.toFixed(3) + " kWh"; + if (tariff_A != null) { + let cost = solar_used_kwh * tariff_A; + text += " (" + cost.toFixed(2) + "p saved)"; + } } - } - if (export_kwh != null) { - text += "
Export: " + (export_kwh * -1).toFixed(3) + " kWh"; - if (outgoing != null) { - let cost = export_kwh * outgoing; - text += " (" + cost.toFixed(2) + "p gained)"; + if (export_kwh != null) { + text += "
Export: " + (export_kwh * -1).toFixed(3) + " kWh"; + if (outgoing != null) { + let cost = export_kwh * outgoing; + text += " (" + cost.toFixed(2) + "p gained)"; + } } } From f1f681ee93e0edc33651b17166ae617579601f9c Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sun, 15 Mar 2026 19:36:56 +0000 Subject: [PATCH 021/110] clean up --- .../octopus/tariff_explorer.js | 461 ++++++------------ 1 file changed, 143 insertions(+), 318 deletions(-) diff --git a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js index 49d32eb7..cc29f4bc 100644 --- a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js +++ b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js @@ -176,7 +176,6 @@ var updaterinst = false; var this_halfhour_index = -1; // disable x axis limit view.limit_x = false; -var flow_mode = false; var smart_meter_data = false; var use_meter_kwh_hh = false; @@ -209,8 +208,6 @@ function show() { if (config.app[key].value) feeds[key] = config.feedsbyid[config.app[key].value]; } - flow_mode = false; - resize(); $.ajax({ @@ -342,17 +339,6 @@ function graph_load() { datetimepicker2.setStartDate(new Date(view.start)); } - // Determine if all 7 energy flow feeds are available (post-processed cumulative kWh feeds) - flow_mode = ( - feeds["solar_to_load_kwh"] != undefined && - feeds["solar_to_grid_kwh"] != undefined && - feeds["solar_to_battery_kwh"] != undefined && - feeds["battery_to_load_kwh"] != undefined && - feeds["battery_to_grid_kwh"] != undefined && - feeds["grid_to_load_kwh"] != undefined && - feeds["grid_to_battery_kwh"] != undefined - ); - smart_meter_data = feeds["meter_kwh_hh"] != undefined; if (smart_meter_data) { $("#use_meter_kwh_hh_bound").show(); @@ -368,21 +354,19 @@ function graph_load() { var grid_to_battery_kwh_data = []; var meter_kwh_hh = []; - if (flow_mode) { - solar_to_load_kwh_data = feed.getdata(feeds["solar_to_load_kwh"].id, view.start, view.end, interval, 0, 1); - solar_to_grid_kwh_data = feed.getdata(feeds["solar_to_grid_kwh"].id, view.start, view.end, interval, 0, 1); - solar_to_battery_kwh_data = feed.getdata(feeds["solar_to_battery_kwh"].id, view.start, view.end, interval, 0, 1); - battery_to_load_kwh_data = feed.getdata(feeds["battery_to_load_kwh"].id, view.start, view.end, interval, 0, 1); - battery_to_grid_kwh_data = feed.getdata(feeds["battery_to_grid_kwh"].id, view.start, view.end, interval, 0, 1); - grid_to_load_kwh_data = feed.getdata(feeds["grid_to_load_kwh"].id, view.start, view.end, interval, 0, 1); - grid_to_battery_kwh_data = feed.getdata(feeds["grid_to_battery_kwh"].id, view.start, view.end, interval, 0, 1); - } + solar_to_load_kwh_data = feed.getdata(feeds["solar_to_load_kwh"].id, view.start, view.end, interval, 0, 1); + solar_to_grid_kwh_data = feed.getdata(feeds["solar_to_grid_kwh"].id, view.start, view.end, interval, 0, 1); + solar_to_battery_kwh_data = feed.getdata(feeds["solar_to_battery_kwh"].id, view.start, view.end, interval, 0, 1); + battery_to_load_kwh_data = feed.getdata(feeds["battery_to_load_kwh"].id, view.start, view.end, interval, 0, 1); + battery_to_grid_kwh_data = feed.getdata(feeds["battery_to_grid_kwh"].id, view.start, view.end, interval, 0, 1); + grid_to_load_kwh_data = feed.getdata(feeds["grid_to_load_kwh"].id, view.start, view.end, interval, 0, 1); + grid_to_battery_kwh_data = feed.getdata(feeds["grid_to_battery_kwh"].id, view.start, view.end, interval, 0, 1); if (smart_meter_data) meter_kwh_hh = feed.getdata(feeds["meter_kwh_hh"].id, view.start, view.end, interval); // Detect current half-hour index for live stats (use grid_to_load feed or meter as reference) this_halfhour_index = -1; - var ref_data = flow_mode ? grid_to_load_kwh_data : (smart_meter_data ? meter_kwh_hh : []); + var ref_data = grid_to_load_kwh_data; var this_halfhour = Math.floor((new Date()).getTime() / 1800000) * 1800000; for (var z = 0; z < ref_data.length; z++) { if (ref_data[z][0] == this_halfhour) { @@ -408,7 +392,7 @@ function graph_load() { } // Outgoing (export tariff) - only needed in flow mode - if (config.app.region != undefined && flow_mode) { + if (config.app.region != undefined) { data["outgoing"] = getdataremote(regions_outgoing[config.app.region.value], view.start, view.end, interval); // Invert export tariff so it reads as a positive earning rate for (var z in data["outgoing"]) data["outgoing"][z][1] *= -1; @@ -419,12 +403,6 @@ function graph_load() { data["carbonintensity"] = getdataremote(428391, view.start, view.end, interval); } - data["use"] = []; - data["import"] = []; - data["import_cost_tariff_A"] = []; - data["import_cost_tariff_B"] = []; - data["export"] = []; - data["export_cost"] = []; data["solar_to_load"] = []; data["solar_to_grid"] = []; data["solar_to_battery"] = []; @@ -432,8 +410,7 @@ function graph_load() { data["battery_to_grid"] = []; data["grid_to_load"] = []; data["grid_to_battery"] = []; - data["solar_used"] = []; - data["solar_used_cost"] = []; + data["meter_kwh_hh"] = meter_kwh_hh; data["meter_kwh_hh_cost"] = []; @@ -452,13 +429,8 @@ function graph_load() { } var total = { - import_kwh: 0, - export_kwh: 0, - import_tariff_A: { kwh: 0, cost: 0 }, import_tariff_B: { kwh: 0, cost: 0 }, - export_tariff: { kwh: 0, cost: 0 }, - solar_used: { kwh: 0, cost: 0 }, // Per-flow kWh totals solar_to_load_kwh: 0, @@ -484,14 +456,10 @@ function graph_load() { var monthly_data = {}; // Determine data length and primary time reference - var data_length = 0; - if (flow_mode) data_length = grid_to_load_kwh_data.length; - else if (smart_meter_data) data_length = meter_kwh_hh.length; + var data_length = grid_to_load_kwh_data.length; for (var z = 0; z < data_length; z++) { - let time = 0; - if (flow_mode) time = grid_to_load_kwh_data[z][0]; - else if (smart_meter_data) time = meter_kwh_hh[z][0]; + let time = grid_to_load_kwh_data[z][0]; d.setTime(time) let hh = d.getHours() * 2 + d.getMinutes() / 30 @@ -502,6 +470,7 @@ function graph_load() { let kwh_import = 0; let kwh_export = 0; let kwh_use = 0; + let kwh_solar_to_load = 0; let kwh_solar_to_grid = 0; let kwh_solar_to_battery = 0; @@ -510,27 +479,20 @@ function graph_load() { let kwh_grid_to_load = 0; let kwh_grid_to_battery = 0; - if (flow_mode) { - // Read half-hourly energy flow values directly from post-processed feeds (delta=1) - // Clamp negatives to zero for safety - kwh_solar_to_load = Math.max(0, solar_to_load_kwh_data[z][1] != null ? solar_to_load_kwh_data[z][1] : 0); - kwh_solar_to_grid = Math.max(0, solar_to_grid_kwh_data[z][1] != null ? solar_to_grid_kwh_data[z][1] : 0); - kwh_solar_to_battery = Math.max(0, solar_to_battery_kwh_data[z][1] != null ? solar_to_battery_kwh_data[z][1] : 0); - kwh_battery_to_load = Math.max(0, battery_to_load_kwh_data[z][1] != null ? battery_to_load_kwh_data[z][1] : 0); - kwh_battery_to_grid = Math.max(0, battery_to_grid_kwh_data[z][1] != null ? battery_to_grid_kwh_data[z][1] : 0); - kwh_grid_to_load = Math.max(0, grid_to_load_kwh_data[z][1] != null ? grid_to_load_kwh_data[z][1] : 0); - kwh_grid_to_battery = Math.max(0, grid_to_battery_kwh_data[z][1] != null ? grid_to_battery_kwh_data[z][1] : 0); - - // Derive aggregate values from flows - kwh_import = kwh_grid_to_load + kwh_grid_to_battery; - kwh_export = kwh_solar_to_grid + kwh_battery_to_grid; - kwh_use = kwh_solar_to_load + kwh_battery_to_load + kwh_grid_to_load; - - } else if (smart_meter_data) { - // Fallback: use smart meter half-hourly data directly as import - kwh_import = meter_kwh_hh[z][1] != null ? meter_kwh_hh[z][1] : 0; - kwh_grid_to_load = kwh_import; - } + // Read half-hourly energy flow values directly from post-processed feeds (delta=1) + // Clamp negatives to zero for safety + kwh_solar_to_load = Math.max(0, solar_to_load_kwh_data[z][1] != null ? solar_to_load_kwh_data[z][1] : 0); + kwh_solar_to_grid = Math.max(0, solar_to_grid_kwh_data[z][1] != null ? solar_to_grid_kwh_data[z][1] : 0); + kwh_solar_to_battery = Math.max(0, solar_to_battery_kwh_data[z][1] != null ? solar_to_battery_kwh_data[z][1] : 0); + kwh_battery_to_load = Math.max(0, battery_to_load_kwh_data[z][1] != null ? battery_to_load_kwh_data[z][1] : 0); + kwh_battery_to_grid = Math.max(0, battery_to_grid_kwh_data[z][1] != null ? battery_to_grid_kwh_data[z][1] : 0); + kwh_grid_to_load = Math.max(0, grid_to_load_kwh_data[z][1] != null ? grid_to_load_kwh_data[z][1] : 0); + kwh_grid_to_battery = Math.max(0, grid_to_battery_kwh_data[z][1] != null ? grid_to_battery_kwh_data[z][1] : 0); + + // Derive aggregate values from flows + kwh_import = kwh_grid_to_load + kwh_grid_to_battery; + kwh_export = kwh_solar_to_grid + kwh_battery_to_grid; + kwh_use = kwh_solar_to_load + kwh_battery_to_load + kwh_grid_to_load; // Alternatively use meter data in place of flow import data if user selected if (smart_meter_data && use_meter_kwh_hh) { @@ -538,59 +500,14 @@ function graph_load() { kwh_grid_to_load = kwh_import; } - data["import"].push([time, kwh_import]); - total.import_kwh += kwh_import; - // Unit and import cost on tariff A let unitcost_tariff_A = null; - let hh_cost_tariff_A = null; if (data.tariff_A[z] != undefined && data.tariff_A[z][1] != null) { unitcost_tariff_A = data.tariff_A[z][1] * 0.01; - hh_cost_tariff_A = kwh_import * unitcost_tariff_A; - - total.import_tariff_A.kwh += kwh_import - total.import_tariff_A.cost += hh_cost_tariff_A // Generate profile profile_kwh[hh][1] += kwh_import - profile_cost[hh][1] += hh_cost_tariff_A - } - - // Unit and import cost on tariff B - let unitcost_tariff_B = null; - let hh_cost_tariff_B = null; - if (data.tariff_B[z] != undefined && data.tariff_B[z][1] != null) { - unitcost_tariff_B = data.tariff_B[z][1] * 0.01; - hh_cost_tariff_B = kwh_import * unitcost_tariff_B; - - total.import_tariff_B.kwh += kwh_import - total.import_tariff_B.cost += hh_cost_tariff_B - } - - data["import_cost_tariff_A"].push([time, hh_cost_tariff_A]); - data["import_cost_tariff_B"].push([time, hh_cost_tariff_B]); - - // Calculate monthly data - if (monthly_data[startOfMonth] == undefined) { - monthly_data[startOfMonth] = { - "import": 0, - "import_tariff_A": 0, - "import_tariff_B": 0, - "cost_import_tariff_A": 0, - "cost_import_tariff_B": 0 - } - } - - monthly_data[startOfMonth]["import"] += kwh_import - - if (hh_cost_tariff_A != null) { - monthly_data[startOfMonth]["import_tariff_A"] += kwh_import - monthly_data[startOfMonth]["cost_import_tariff_A"] += hh_cost_tariff_A - } - - if (hh_cost_tariff_B != null) { - monthly_data[startOfMonth]["import_tariff_B"] += kwh_import - monthly_data[startOfMonth]["cost_import_tariff_B"] += hh_cost_tariff_B + profile_cost[hh][1] += kwh_import * unitcost_tariff_A; } // Carbon Intensity @@ -602,70 +519,47 @@ function graph_load() { } } - // Export and solar flows (flow mode only) - if (flow_mode) { - data["use"].push([time, kwh_use]); - data["export"].push([time, kwh_export * -1]); - total.export_tariff.kwh += kwh_export; - - // All 7 disaggregated flow data arrays - data["solar_to_load"].push([time, kwh_solar_to_load]); - data["solar_to_grid"].push([time, kwh_solar_to_grid]); - data["solar_to_battery"].push([time, kwh_solar_to_battery]); - data["battery_to_load"].push([time, kwh_battery_to_load]); - data["battery_to_grid"].push([time, kwh_battery_to_grid]); - data["grid_to_load"].push([time, kwh_grid_to_load]); - data["grid_to_battery"].push([time, kwh_grid_to_battery]); - - // Accumulate per-flow kWh totals - total.solar_to_load_kwh += kwh_solar_to_load; - total.solar_to_grid_kwh += kwh_solar_to_grid; - total.solar_to_battery_kwh += kwh_solar_to_battery; - total.battery_to_load_kwh += kwh_battery_to_load; - total.battery_to_grid_kwh += kwh_battery_to_grid; - total.grid_to_load_kwh += kwh_grid_to_load; - total.grid_to_battery_kwh += kwh_grid_to_battery; - - let outgoing_unit = (data.outgoing[z] != undefined && data.outgoing[z][1] != null) - ? data.outgoing[z][1] * 0.01 * -1 // already inverted, so this is positive p/kWh - : null; - - if (outgoing_unit != null) { - let cost_export = kwh_export * outgoing_unit; - data["export_cost"].push([time, cost_export]); - total.export_tariff.cost += cost_export; - // Per-flow export values - total.solar_to_grid_value += kwh_solar_to_grid * outgoing_unit; - total.battery_to_grid_value += kwh_battery_to_grid * outgoing_unit; - } else { - data["export_cost"].push([time, null]); - } - - if (unitcost_tariff_A != null) { - // Per-flow avoided/saved cost at tariff A rate - total.solar_to_load_value += kwh_solar_to_load * unitcost_tariff_A; - total.solar_to_battery_value += kwh_solar_to_battery * unitcost_tariff_A; - total.battery_to_load_value += kwh_battery_to_load * unitcost_tariff_A; - total.grid_to_load_cost += kwh_grid_to_load * unitcost_tariff_A; - total.grid_to_battery_cost += kwh_grid_to_battery * unitcost_tariff_A; - } + // All 7 disaggregated flow data arrays + data["solar_to_load"].push([time, kwh_solar_to_load]); + data["solar_to_grid"].push([time, kwh_solar_to_grid]); + data["solar_to_battery"].push([time, kwh_solar_to_battery]); + data["battery_to_load"].push([time, kwh_battery_to_load]); + data["battery_to_grid"].push([time, kwh_battery_to_grid]); + data["grid_to_load"].push([time, kwh_grid_to_load]); + data["grid_to_battery"].push([time, kwh_grid_to_battery]); + + // Accumulate per-flow kWh totals + total.solar_to_load_kwh += kwh_solar_to_load; + total.solar_to_grid_kwh += kwh_solar_to_grid; + total.solar_to_battery_kwh += kwh_solar_to_battery; + total.battery_to_load_kwh += kwh_battery_to_load; + total.battery_to_grid_kwh += kwh_battery_to_grid; + total.grid_to_load_kwh += kwh_grid_to_load; + total.grid_to_battery_kwh += kwh_grid_to_battery; + + let outgoing_unit = (data.outgoing[z] != undefined && data.outgoing[z][1] != null) + ? data.outgoing[z][1] * 0.01 * -1 // already inverted, so this is positive p/kWh + : null; + + if (outgoing_unit != null) { + // Per-flow export values + total.solar_to_grid_value += kwh_solar_to_grid * outgoing_unit; + total.battery_to_grid_value += kwh_battery_to_grid * outgoing_unit; + } - // Solar self-consumption = solar_to_load + solar_to_battery - let kwh_solar_used = kwh_solar_to_load + kwh_solar_to_battery; - data["solar_used"].push([time, kwh_solar_used]); - total.solar_used.kwh += kwh_solar_used; - if (unitcost_tariff_A != null) { - data["solar_used_cost"].push([time, kwh_solar_used * unitcost_tariff_A]); - total.solar_used.cost += kwh_solar_used * unitcost_tariff_A; - } else { - data["solar_used_cost"].push([time, null]); - } + if (unitcost_tariff_A != null) { + // Per-flow avoided/saved cost at tariff A rate + total.solar_to_load_value += kwh_solar_to_load * unitcost_tariff_A; + total.solar_to_battery_value += kwh_solar_to_battery * unitcost_tariff_A; + total.battery_to_load_value += kwh_battery_to_load * unitcost_tariff_A; + total.grid_to_load_cost += kwh_grid_to_load * unitcost_tariff_A; + total.grid_to_battery_cost += kwh_grid_to_battery * unitcost_tariff_A; } } - if (smart_meter_data && !flow_mode) { - calibration_line_of_best_fit(data["import"], meter_kwh_hh); - } + // if (smart_meter_data && !flow_mode) { + // calibration_line_of_best_fit(data["import"], meter_kwh_hh); + // } draw_tables(total, monthly_data); } @@ -703,28 +597,27 @@ function draw_tables(total, monthly_data) { $("#carbonintensity_result").html("Total CO2: " + (total.co2).toFixed(1) + "kgCO2, Consumption intensity: " + window_co2_intensity.toFixed(0) + " gCO2/kWh") } - if (flow_mode) { - // Helper: one table row per energy flow - function flow_row(label, kwh, value_gbp, value_label, color) { - var r = ""; - r += "" + label + ""; - r += "" + kwh.toFixed(2) + " kWh"; - r += "" + (value_gbp !== null ? (value_gbp >= 0 ? "\u00a3" : "-\u00a3") + Math.abs(value_gbp).toFixed(2) : "—") + ""; - r += "" + value_label + ""; - r += ""; - return r; - } + // Helper: one table row per energy flow + function flow_row(label, kwh, value_gbp, value_label, color) { + var r = ""; + r += "" + label + ""; + r += "" + kwh.toFixed(2) + " kWh"; + r += "" + (value_gbp !== null ? (value_gbp >= 0 ? "\u00a3" : "-\u00a3") + Math.abs(value_gbp).toFixed(2) : "—") + ""; + r += "" + value_label + ""; + r += ""; + return r; + } - out += "Energy flow breakdown"; + out += "Energy flow breakdown"; + + out += flow_row("☀ Solar → Load", total.solar_to_load_kwh, total.solar_to_load_value * 1.05, "avoided import cost (tariff A)", "#bec745"); + out += flow_row("☀ Solar → Battery", total.solar_to_battery_kwh, total.solar_to_battery_value * 1.05, "avoided import cost (tariff A)", "#a3d977"); + out += flow_row("☀ Solar → Grid (export)",total.solar_to_grid_kwh, total.solar_to_grid_value * 1.05, "earned at export tariff", "#dccc1f"); + out += flow_row("🔋 Battery → Load", total.battery_to_load_kwh, total.battery_to_load_value * 1.05, "avoided import cost (tariff A)", "#fbb450"); + out += flow_row("🔋 Battery → Grid (export)", total.battery_to_grid_kwh, total.battery_to_grid_value * 1.05, "earned at export tariff", "#f0913a"); + out += flow_row("💡 Grid → Load", total.grid_to_load_kwh, -(total.grid_to_load_cost * 1.05), "cost at tariff A", "#44b3e2"); + out += flow_row("💡 Grid → Battery", total.grid_to_battery_kwh, -(total.grid_to_battery_cost * 1.05),"cost at tariff A", "#82cbfc"); - out += flow_row("☀ Solar → Load", total.solar_to_load_kwh, total.solar_to_load_value * 1.05, "avoided import cost (tariff A)", "#bec745"); - out += flow_row("☀ Solar → Battery", total.solar_to_battery_kwh, total.solar_to_battery_value * 1.05, "avoided import cost (tariff A)", "#a3d977"); - out += flow_row("☀ Solar → Grid (export)",total.solar_to_grid_kwh, total.solar_to_grid_value * 1.05, "earned at export tariff", "#dccc1f"); - out += flow_row("🔋 Battery → Load", total.battery_to_load_kwh, total.battery_to_load_value * 1.05, "avoided import cost (tariff A)", "#fbb450"); - out += flow_row("🔋 Battery → Grid (export)", total.battery_to_grid_kwh, total.battery_to_grid_value * 1.05, "earned at export tariff", "#f0913a"); - out += flow_row("💡 Grid → Load", total.grid_to_load_kwh, -(total.grid_to_load_cost * 1.05), "cost at tariff A", "#44b3e2"); - out += flow_row("💡 Grid → Battery", total.grid_to_battery_kwh, -(total.grid_to_battery_cost * 1.05),"cost at tariff A", "#82cbfc"); - } $("#show_profile").show(); $("#octopus_totals").html(out); @@ -732,6 +625,7 @@ function draw_tables(total, monthly_data) { $("#tariff_A").val(config.app.tariff_A.value); $("#tariff_B").val(config.app.tariff_B.value); + /* var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; // Populate monthly data if more than one month of data @@ -811,12 +705,14 @@ function draw_tables(total, monthly_data) { $("#monthly-data-body").html(monthly_out); $("#monthly-data").show(); } + */ } function graph_draw() { profile_mode = false; $("#history-title").html("HISTORY"); + /* if (this_halfhour_index != -1) { let kwh_last_halfhour = data["import"][this_halfhour_index][1]; @@ -836,7 +732,7 @@ function graph_draw() { $(".last_halfhour_stats").show(); } else { $(".last_halfhour_stats").hide(); - } + }*/ var bars = { show: true, @@ -848,31 +744,14 @@ function graph_draw() { graph_series = []; - if (flow_mode) { - // All 7 disaggregated flows stacked as positive bars - graph_series.push({ label: "Solar to Load", data: data["solar_to_load"], yaxis: 1, color: "#bec745", stack: true, bars: bars }); - graph_series.push({ label: "Solar to Battery", data: data["solar_to_battery"], yaxis: 1, color: "#a3d977", stack: true, bars: bars }); - graph_series.push({ label: "Solar to Grid", data: data["solar_to_grid"], yaxis: 1, color: "#dccc1f", stack: true, bars: bars }); - graph_series.push({ label: "Battery to Load", data: data["battery_to_load"], yaxis: 1, color: "#fbb450", stack: true, bars: bars }); - graph_series.push({ label: "Battery to Grid", data: data["battery_to_grid"], yaxis: 1, color: "#f0913a", stack: true, bars: bars }); - graph_series.push({ label: "Grid to Load", data: data["grid_to_load"], yaxis: 1, color: "#44b3e2", stack: true, bars: bars }); - graph_series.push({ label: "Grid to Battery", data: data["grid_to_battery"], yaxis: 1, color: "#82cbfc", stack: true, bars: bars }); - } else { - // Non-flow mode: show aggregated import bar - graph_series.push({ label: "Import", data: data["import"], yaxis: 1, color: "#44b3e2", stack: true, bars: bars }); - } - - // Smart meter data (non-flow fallback only) - if (smart_meter_data && !flow_mode) { - graph_series.push({ - label: "Import Actual", - data: data["meter_kwh_hh"], - yaxis: 1, - color: "#1d8dbc", - stack: false, - bars: bars - }); - } + // All 7 disaggregated flows stacked as positive bars + graph_series.push({ label: "Solar to Load", data: data["solar_to_load"], yaxis: 1, color: "#bec745", stack: true, bars: bars }); + graph_series.push({ label: "Solar to Battery", data: data["solar_to_battery"], yaxis: 1, color: "#a3d977", stack: true, bars: bars }); + graph_series.push({ label: "Solar to Grid", data: data["solar_to_grid"], yaxis: 1, color: "#dccc1f", stack: true, bars: bars }); + graph_series.push({ label: "Battery to Load", data: data["battery_to_load"], yaxis: 1, color: "#fbb450", stack: true, bars: bars }); + graph_series.push({ label: "Battery to Grid", data: data["battery_to_grid"], yaxis: 1, color: "#f0913a", stack: true, bars: bars }); + graph_series.push({ label: "Grid to Load", data: data["grid_to_load"], yaxis: 1, color: "#44b3e2", stack: true, bars: bars }); + graph_series.push({ label: "Grid to Battery", data: data["grid_to_battery"], yaxis: 1, color: "#82cbfc", stack: true, bars: bars }); // price signals graph_series.push({ @@ -888,20 +767,18 @@ function graph_draw() { } }); - if (flow_mode) { - graph_series.push({ - label: "Outgoing", - data: data["outgoing"], - yaxis: 2, - color: "#941afb", - lines: { - show: true, - steps: true, - align: "center", - lineWidth: 1 - } - }); - } + graph_series.push({ + label: "Outgoing", + data: data["outgoing"], + yaxis: 2, + color: "#941afb", + lines: { + show: true, + steps: true, + align: "center", + lineWidth: 1 + } + }); if (show_carbonintensity) { graph_series.push({ @@ -1107,23 +984,6 @@ function getdataremote(id, start, end, interval) { return data; } - -function convert_cumulative_kwh_to_kwh_hh(cumulative_kwh_data, limit_positive=false) { - var kwh_hh_data = []; - - for (var z = 1; z < cumulative_kwh_data.length; z++) { - let time = cumulative_kwh_data[z - 1][0]; - let kwh_hh = 0; - if (cumulative_kwh_data[z][1] != null && cumulative_kwh_data[z - 1][1] != null) { - kwh_hh = cumulative_kwh_data[z][1] - cumulative_kwh_data[z - 1][1]; - } - if (limit_positive && kwh_hh < 0.0) kwh_hh = 0.0; - kwh_hh_data.push([time, kwh_hh]); - } - - return kwh_hh_data; -} - function calibration_line_of_best_fit(import_kwh, meter_kwh_hh) { if (import_kwh.length != meter_kwh_hh.length) { @@ -1182,13 +1042,11 @@ $('#placeholder').bind("plothover", function(event, pos, item) { z = Math.floor(z / 2); } - if (previousPoint != item.datapoint) { previousPoint = item.datapoint; $("#tooltip").remove(); var itemTime = item.datapoint[0]; - var itemValue = item.datapoint[1]; var d = new Date(itemTime); var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; @@ -1213,9 +1071,6 @@ $('#placeholder').bind("plothover", function(event, pos, item) { } } - let import_kwh = get_data_value_at_index("import", z); - let solar_used_kwh = get_data_value_at_index("solar_used", z); - let export_kwh = get_data_value_at_index("export", z); let tariff_A = get_data_value_at_index("tariff_A", z); let tariff_B = get_data_value_at_index("tariff_B", z); let outgoing = get_data_value_at_index("outgoing", z); @@ -1229,66 +1084,40 @@ $('#placeholder').bind("plothover", function(event, pos, item) { let grid_to_load_kwh = get_data_value_at_index("grid_to_load", z); let grid_to_battery_kwh = get_data_value_at_index("grid_to_battery", z); - if (flow_mode) { - if (solar_to_load_kwh != null) { - text += "☀ Solar → Load: " + solar_to_load_kwh.toFixed(3) + " kWh"; - if (tariff_A != null) text += " (" + (solar_to_load_kwh * tariff_A).toFixed(2) + "p saved)
"; - else text += "
"; - } - if (solar_to_battery_kwh != null) { - text += "☀ Solar → Battery: " + solar_to_battery_kwh.toFixed(3) + " kWh"; - if (tariff_A != null) text += " (" + (solar_to_battery_kwh * tariff_A).toFixed(2) + "p saved)
"; - else text += "
"; - } - if (solar_to_grid_kwh != null) { - text += "☀ Solar → Grid: " + solar_to_grid_kwh.toFixed(3) + " kWh"; - if (outgoing != null) text += " (" + (solar_to_grid_kwh * outgoing).toFixed(2) + "p gained)
"; - else text += "
"; - } - if (battery_to_load_kwh != null) { - text += "🔋 Battery → Load: " + battery_to_load_kwh.toFixed(3) + " kWh"; - if (tariff_A != null) text += " (" + (battery_to_load_kwh * tariff_A).toFixed(2) + "p saved)
"; - else text += "
"; - } - if (battery_to_grid_kwh != null) { - text += "🔋 Battery → Grid: " + battery_to_grid_kwh.toFixed(3) + " kWh"; - if (outgoing != null) text += " (" + (battery_to_grid_kwh * outgoing).toFixed(2) + "p gained)
"; - else text += "
"; - } - if (grid_to_load_kwh != null) { - text += "💡 Grid → Load: " + grid_to_load_kwh.toFixed(3) + " kWh"; - if (tariff_A != null) text += " (" + (grid_to_load_kwh * tariff_A).toFixed(2) + "p cost)
"; - else text += "
"; - } - if (grid_to_battery_kwh != null) { - text += "💡 Grid → Battery: " + grid_to_battery_kwh.toFixed(3) + " kWh"; - if (tariff_A != null) text += " (" + (grid_to_battery_kwh * tariff_A).toFixed(2) + "p cost)
"; - else text += "
"; - } - } else { - if (import_kwh != null) { - text += "Import: " + import_kwh.toFixed(3) + " kWh"; - if (tariff_A != null) { - let cost = import_kwh * tariff_A; - text += " (" + cost.toFixed(2) + "p cost)"; - } - } - - if (solar_used_kwh != null) { - text += "
Used Solar: " + solar_used_kwh.toFixed(3) + " kWh"; - if (tariff_A != null) { - let cost = solar_used_kwh * tariff_A; - text += " (" + cost.toFixed(2) + "p saved)"; - } - } - - if (export_kwh != null) { - text += "
Export: " + (export_kwh * -1).toFixed(3) + " kWh"; - if (outgoing != null) { - let cost = export_kwh * outgoing; - text += " (" + cost.toFixed(2) + "p gained)"; - } - } + if (solar_to_load_kwh != null) { + text += "☀ Solar → Load: " + solar_to_load_kwh.toFixed(3) + " kWh"; + if (tariff_A != null) text += " (" + (solar_to_load_kwh * tariff_A).toFixed(2) + "p saved)
"; + else text += "
"; + } + if (solar_to_battery_kwh != null) { + text += "☀ Solar → Battery: " + solar_to_battery_kwh.toFixed(3) + " kWh"; + if (tariff_A != null) text += " (" + (solar_to_battery_kwh * tariff_A).toFixed(2) + "p saved)
"; + else text += "
"; + } + if (solar_to_grid_kwh != null) { + text += "☀ Solar → Grid: " + solar_to_grid_kwh.toFixed(3) + " kWh"; + if (outgoing != null) text += " (" + (solar_to_grid_kwh * outgoing).toFixed(2) + "p gained)
"; + else text += "
"; + } + if (battery_to_load_kwh != null) { + text += "🔋 Battery → Load: " + battery_to_load_kwh.toFixed(3) + " kWh"; + if (tariff_A != null) text += " (" + (battery_to_load_kwh * tariff_A).toFixed(2) + "p saved)
"; + else text += "
"; + } + if (battery_to_grid_kwh != null) { + text += "🔋 Battery → Grid: " + battery_to_grid_kwh.toFixed(3) + " kWh"; + if (outgoing != null) text += " (" + (battery_to_grid_kwh * outgoing).toFixed(2) + "p gained)
"; + else text += "
"; + } + if (grid_to_load_kwh != null) { + text += "💡 Grid → Load: " + grid_to_load_kwh.toFixed(3) + " kWh"; + if (tariff_A != null) text += " (" + (grid_to_load_kwh * tariff_A).toFixed(2) + "p cost)
"; + else text += "
"; + } + if (grid_to_battery_kwh != null) { + text += "💡 Grid → Battery: " + grid_to_battery_kwh.toFixed(3) + " kWh"; + if (tariff_A != null) text += " (" + (grid_to_battery_kwh * tariff_A).toFixed(2) + "p cost)
"; + else text += "
"; } text += "
"; @@ -1440,11 +1269,7 @@ $("#download-csv").click(function() { var csv = []; - if (flow_mode) { - keys = ["tariff_A", "tariff_B", "outgoing", "solar_to_load", "solar_to_battery", "solar_to_grid", "battery_to_load", "battery_to_grid", "grid_to_load", "grid_to_battery", "import", "import_cost_tariff_A", "import_cost_tariff_B", "export", "export_cost", "solar_used", "solar_used_cost", "meter_kwh_hh"] - } else { - keys = ["tariff_A", "tariff_B", "import", "import_cost_tariff_A", "import_cost_tariff_B", "meter_kwh_hh", "meter_kwh_hh_cost"] - } + keys = ["tariff_A", "tariff_B", "outgoing", "solar_to_load", "solar_to_battery", "solar_to_grid", "battery_to_load", "battery_to_grid", "grid_to_load", "grid_to_battery", "import", "import_cost_tariff_A", "import_cost_tariff_B", "export", "export_cost", "solar_used", "solar_used_cost", "meter_kwh_hh"] csv.push("time," + keys.join(",")) From b6203edd322664bcbd07045b836bdd14e87bb43d Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sun, 15 Mar 2026 20:08:51 +0000 Subject: [PATCH 022/110] calculate total costs for both tariff A and B --- .../octopus/tariff_explorer.js | 84 ++++++++++++++----- 1 file changed, 61 insertions(+), 23 deletions(-) diff --git a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js index cc29f4bc..71214191 100644 --- a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js +++ b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js @@ -442,13 +442,26 @@ function graph_load() { grid_to_battery_kwh: 0, // Per-flow value at tariff A (avoided cost / earned) - solar_to_load_value: 0, - solar_to_grid_value: 0, - solar_to_battery_value: 0, - battery_to_load_value: 0, - battery_to_grid_value: 0, - grid_to_load_cost: 0, - grid_to_battery_cost: 0, + tariff_A: { + solar_to_load_value: 0, + solar_to_grid_value: 0, + solar_to_battery_value: 0, + battery_to_load_value: 0, + battery_to_grid_value: 0, + grid_to_load_cost: 0, + grid_to_battery_cost: 0, + }, + + // Per-flow value at tariff B (avoided cost / earned) + tariff_B: { + solar_to_load_value: 0, + solar_to_grid_value: 0, + solar_to_battery_value: 0, + battery_to_load_value: 0, + battery_to_grid_value: 0, + grid_to_load_cost: 0, + grid_to_battery_cost: 0, + }, co2: 0 } @@ -468,8 +481,6 @@ function graph_load() { let startOfMonth = new Date(d.getFullYear(), d.getMonth(), 1).getTime(); let kwh_import = 0; - let kwh_export = 0; - let kwh_use = 0; let kwh_solar_to_load = 0; let kwh_solar_to_grid = 0; @@ -510,6 +521,12 @@ function graph_load() { profile_cost[hh][1] += kwh_import * unitcost_tariff_A; } + // Unit and import cost on tariff B + let unitcost_tariff_B = null; + if (data.tariff_B[z] != undefined && data.tariff_B[z][1] != null) { + unitcost_tariff_B = data.tariff_B[z][1] * 0.01; + } + // Carbon Intensity if (show_carbonintensity) { let co2intensity = data.carbonintensity[z] != undefined ? data.carbonintensity[z][1] : null; @@ -543,17 +560,29 @@ function graph_load() { if (outgoing_unit != null) { // Per-flow export values - total.solar_to_grid_value += kwh_solar_to_grid * outgoing_unit; - total.battery_to_grid_value += kwh_battery_to_grid * outgoing_unit; + total.tariff_A.solar_to_grid_value += kwh_solar_to_grid * outgoing_unit; + total.tariff_A.battery_to_grid_value += kwh_battery_to_grid * outgoing_unit; + + total.tariff_B.solar_to_grid_value += kwh_solar_to_grid * outgoing_unit; + total.tariff_B.battery_to_grid_value += kwh_battery_to_grid * outgoing_unit; } if (unitcost_tariff_A != null) { // Per-flow avoided/saved cost at tariff A rate - total.solar_to_load_value += kwh_solar_to_load * unitcost_tariff_A; - total.solar_to_battery_value += kwh_solar_to_battery * unitcost_tariff_A; - total.battery_to_load_value += kwh_battery_to_load * unitcost_tariff_A; - total.grid_to_load_cost += kwh_grid_to_load * unitcost_tariff_A; - total.grid_to_battery_cost += kwh_grid_to_battery * unitcost_tariff_A; + total.tariff_A.solar_to_load_value += kwh_solar_to_load * unitcost_tariff_A; + total.tariff_A.solar_to_battery_value += kwh_solar_to_battery * unitcost_tariff_A; + total.tariff_A.battery_to_load_value += kwh_battery_to_load * unitcost_tariff_A; + total.tariff_A.grid_to_load_cost += kwh_grid_to_load * unitcost_tariff_A; + total.tariff_A.grid_to_battery_cost += kwh_grid_to_battery * unitcost_tariff_A; + } + + if (unitcost_tariff_B != null) { + // Per-flow avoided/saved cost at tariff B rate + total.tariff_B.solar_to_load_value += kwh_solar_to_load * unitcost_tariff_B; + total.tariff_B.solar_to_battery_value += kwh_solar_to_battery * unitcost_tariff_B; + total.tariff_B.battery_to_load_value += kwh_battery_to_load * unitcost_tariff_B; + total.tariff_B.grid_to_load_cost += kwh_grid_to_load * unitcost_tariff_B; + total.tariff_B.grid_to_battery_cost += kwh_grid_to_battery * unitcost_tariff_B; } } @@ -603,6 +632,15 @@ function draw_tables(total, monthly_data) { r += "" + label + ""; r += "" + kwh.toFixed(2) + " kWh"; r += "" + (value_gbp !== null ? (value_gbp >= 0 ? "\u00a3" : "-\u00a3") + Math.abs(value_gbp).toFixed(2) : "—") + ""; + + // unit price value_gbp / kwh, only if kwh > 0 and value_gbp is not null + if (value_gbp !== null && kwh > 0) { + let unit_price = value_gbp / kwh; + r += "" + (unit_price * 100).toFixed(1) + " p/kWh"; + } else { + r += "—"; + } + r += "" + value_label + ""; r += ""; return r; @@ -610,13 +648,13 @@ function draw_tables(total, monthly_data) { out += "Energy flow breakdown"; - out += flow_row("☀ Solar → Load", total.solar_to_load_kwh, total.solar_to_load_value * 1.05, "avoided import cost (tariff A)", "#bec745"); - out += flow_row("☀ Solar → Battery", total.solar_to_battery_kwh, total.solar_to_battery_value * 1.05, "avoided import cost (tariff A)", "#a3d977"); - out += flow_row("☀ Solar → Grid (export)",total.solar_to_grid_kwh, total.solar_to_grid_value * 1.05, "earned at export tariff", "#dccc1f"); - out += flow_row("🔋 Battery → Load", total.battery_to_load_kwh, total.battery_to_load_value * 1.05, "avoided import cost (tariff A)", "#fbb450"); - out += flow_row("🔋 Battery → Grid (export)", total.battery_to_grid_kwh, total.battery_to_grid_value * 1.05, "earned at export tariff", "#f0913a"); - out += flow_row("💡 Grid → Load", total.grid_to_load_kwh, -(total.grid_to_load_cost * 1.05), "cost at tariff A", "#44b3e2"); - out += flow_row("💡 Grid → Battery", total.grid_to_battery_kwh, -(total.grid_to_battery_cost * 1.05),"cost at tariff A", "#82cbfc"); + out += flow_row("☀ Solar → Load", total.solar_to_load_kwh, total.tariff_A.solar_to_load_value * 1.05, "avoided import cost (tariff A)", "#bec745"); + out += flow_row("☀ Solar → Battery", total.solar_to_battery_kwh, total.tariff_A.solar_to_battery_value * 1.05, "avoided import cost (tariff A)", "#a3d977"); + out += flow_row("☀ Solar → Grid (export)",total.solar_to_grid_kwh, total.tariff_A.solar_to_grid_value * 1.05, "earned at export tariff", "#dccc1f"); + out += flow_row("🔋 Battery → Load", total.battery_to_load_kwh, total.tariff_A.battery_to_load_value * 1.05, "avoided import cost (tariff A)", "#fbb450"); + out += flow_row("🔋 Battery → Grid (export)", total.battery_to_grid_kwh, total.tariff_A.battery_to_grid_value * 1.05, "earned at export tariff", "#f0913a"); + out += flow_row("💡 Grid → Load", total.grid_to_load_kwh, (total.tariff_A.grid_to_load_cost * 1.05), "cost at tariff A", "#44b3e2"); + out += flow_row("💡 Grid → Battery", total.grid_to_battery_kwh, (total.tariff_A.grid_to_battery_cost * 1.05),"cost at tariff A", "#82cbfc"); $("#show_profile").show(); From 484c2d93b3c52824e6bc242c2a3b6711bd969a85 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sun, 15 Mar 2026 20:45:12 +0000 Subject: [PATCH 023/110] overall cost --- apps/OpenEnergyMonitor/octopus/octopus.php | 17 ++++- .../octopus/tariff_explorer.js | 70 ++++++++++--------- 2 files changed, 51 insertions(+), 36 deletions(-) diff --git a/apps/OpenEnergyMonitor/octopus/octopus.php b/apps/OpenEnergyMonitor/octopus/octopus.php index ea1f1aa2..8deed7a6 100644 --- a/apps/OpenEnergyMonitor/octopus/octopus.php +++ b/apps/OpenEnergyMonitor/octopus/octopus.php @@ -94,12 +94,25 @@ "); + $('#placeholder').append("
Below: Total export (solar + battery to grid)
"); +} + +// ------------------------------------------------------------------------------------------ +// BAR GRAPH EVENTS +// - show bar values on hover +// - click through to power graph +// ------------------------------------------------------------------------------------------ +function bargraph_events() { + $(".visnav[time=1]").hide(); + $(".visnav[time=3]").hide(); + $(".visnav[time=6]").hide(); + $(".visnav[time=24]").hide(); + + $('#placeholder').unbind("plotclick"); + $('#placeholder').unbind("plothover"); + $('#placeholder').unbind("plotselected"); + $('.bargraph-viewall').unbind("click"); + + // Show day's figures on the bottom of the page + + $('#placeholder').bind("plothover", function (event, pos, item) + { + if (item) { + var z = item.dataIndex; + + // Read directly from the fine-grained flow feed data arrays + var total_solar_to_load = solar_to_load_kwhd_data[z][1]; + var total_solar_to_grid = solar_to_grid_kwhd_data[z][1]; + var total_solar_to_battery = solar_to_battery_kwhd_data[z][1]; + var total_battery_to_load = battery_to_load_kwhd_data[z][1]; + var total_battery_to_grid = battery_to_grid_kwhd_data[z][1]; + var total_grid_to_load = grid_to_load_kwhd_data[z][1]; + var total_grid_to_battery = grid_to_battery_kwhd_data[z][1]; + + // Reconstruct aggregate totals + var total_solar_kwh = total_solar_to_load + total_solar_to_grid + total_solar_to_battery; + var total_use_kwh = total_solar_to_load + total_battery_to_load + total_grid_to_load; + var total_import_kwh = total_grid_to_load + total_grid_to_battery; + var total_export_kwh = total_solar_to_grid + total_battery_to_grid; + var total_grid_balance_kwh = total_import_kwh - total_export_kwh; + + $(".total_solar_kwh").html(total_solar_kwh.toFixed(1)); + $(".total_use_kwh").html(total_use_kwh.toFixed(1)); + $(".total_import_direct_kwh").html(total_grid_to_load.toFixed(1)); + $(".total_grid_balance_kwh").html(total_grid_balance_kwh.toFixed(1)); + if (total_solar_kwh) { + $(".total_solar_direct_kwh").html(total_solar_to_load.toFixed(1)); + $(".total_solar_export_kwh").html(total_solar_to_grid.toFixed(1)); + $(".solar_export_prc").html((100*total_solar_to_grid/total_solar_kwh).toFixed(0)+"%"); + $(".solar_direct_prc").html((100*total_solar_to_load/total_solar_kwh).toFixed(0)+"%"); + $(".solar_to_battery_prc").html((100*total_solar_to_battery/total_solar_kwh).toFixed(0)+"%"); + + $(".use_from_solar_prc").html((100*total_solar_to_load/total_use_kwh).toFixed(0)+"%"); + } + $(".use_from_import_prc").html((100*total_grid_to_load/total_use_kwh).toFixed(0)+"%"); + $(".total_battery_charge_from_solar_kwh").html(total_solar_to_battery.toFixed(1)); + $(".total_import_for_battery_kwh").html(total_grid_to_battery.toFixed(1)); + $(".total_battery_discharge_kwh").html(total_battery_to_load.toFixed(1)); + $(".total_battery_to_grid_kwh").html(total_battery_to_grid.toFixed(1)); + $(".use_from_battery_prc").html((100*total_battery_to_load/total_use_kwh).toFixed(0)+"%"); + + if (total_grid_to_battery>=0.1) { + $("#battery_import").show(); + } else { + $("#battery_import").hide(); + } + + if (total_battery_to_grid>=0.1) { + $("#battery_export").show(); + } else { + $("#battery_export").hide(); + } + + $(".battery_soc_change").html("---"); + + } else { + // Hide tooltip + hide_tooltip(); + } + }); + + // Auto click through to power graph + $('#placeholder').bind("plotclick", function (event, pos, item) + { + if (item && !panning) { + var z = item.dataIndex; + + history_start = view.start + history_end = view.end + view.start = solar_to_load_kwhd_data[z][0]; + view.end = view.start + 86400*1000; + + $(".balanceline").attr('disabled',false); + $(".viewhistory").toggleClass('active'); + + reload = true; + autoupdate = false; + viewmode = "powergraph"; + + draw(true); + powergraph_events(); + } + }); + + + $('#placeholder').bind("plotselected", function (event, ranges) { + view.start = ranges.xaxis.from; + view.end = ranges.xaxis.to; + draw(true); + panning = true; setTimeout(function() {panning = false; }, 100); + }); + + $('.bargraph-viewall').click(function () { + view.start = latest_start_time * 1000; + view.end = +new Date; + draw(true); + }); +} + +// ------------------------------------------------------------------------------------------ +// TOOLTIP HANDLING +// Show & hide the tooltip +// ------------------------------------------------------------------------------------------ +function show_tooltip(x, y, values) { + var tooltip = $('#tooltip'); + if (!tooltip[0]) { + tooltip = $('
') + .css({ + position: "absolute", + display: "none", + border: "1px solid #545454", + padding: "8px", + "background-color": "#333", + }) + .appendTo("body"); + } + + tooltip.html(''); + var table = $('').appendTo(tooltip); + + for (i = 0; i < values.length; i++) { + var value = values[i]; + var row = $('').appendTo(table); + $('').appendTo(row); + $('').appendTo(row); + } + + tooltip + .css({ + left: x, + top: y + }) + .show(); +} + +function hide_tooltip() { + $('#tooltip').hide(); +} + +$(function() { + $(document).on('window.resized hidden.sidebar.collapse shown.sidebar.collapse', function(){ + resize() + }) +}) + +// ---------------------------------------------------------------------- +// App log +// ---------------------------------------------------------------------- +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 +// (delegates to config.autogen.get_feeds in appconf.js) +// ---------------------------------------------------------------------- +function get_autogen_feeds() { + return config.autogen.get_feeds(); +} + +// ---------------------------------------------------------------------- +// Auto-generate feed list +// (delegates to config.autogen.render_feed_list in appconf.js) +// ---------------------------------------------------------------------- +function render_autogen_feed_list() { + config.autogen.render_feed_list(); +} + +// ---------------------------------------------------------------------- +// Auto-generate feed actions +// (delegate to config.autogen.* in appconf.js) +// ---------------------------------------------------------------------- +function create_missing_feeds() { config.autogen.create_missing_feeds(); } +function run_post_processor() { config.autogen.run_post_processor(); } +function reset_feeds() { config.autogen.reset_feeds(); } \ No newline at end of file diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index f6fa8a0d..74ef86b4 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -17,154 +17,8 @@ - - - - - + +
'+value[0]+''+value[1]+' '+value[2]+'
- - - - - - - - " + - " " + - " " + - " " + - "" - ); - } - - 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(""); + vue_config.renderAutogenFeedList(); }, // Create any feeds that are missing @@ -655,8 +385,8 @@ var config = { var defaults = config.autogen_feed_defaults || { datatype: 1, engine: 5, options: { interval: 1800 } }; var created = 0, errors = 0; - $("#autogen-status").text("Creating feeds...").css("color", "#aaa"); - $("#btn-create-feeds").prop("disabled", true); + vue_config.autogen_status = "Creating feeds..."; + vue_config.autogen_status_color = "#aaa"; missing.forEach(function(item) { $.ajax({ @@ -690,14 +420,14 @@ var config = { var statusText = errors === 0 ? "Created " + created + " feed(s) successfully." : "Created " + created + " feed(s), " + errors + " error(s)."; - $("#autogen-status").text(statusText).css("color", errors === 0 ? "#5cb85c" : "#f0ad4e"); - $("#btn-create-feeds").prop("disabled", false); + vue_config.autogen_status = statusText; + vue_config.autogen_status_color = errors === 0 ? "#5cb85c" : "#f0ad4e"; }, // Trigger the app post-processor via app/process run_post_processor: function() { - $("#autogen-status").text("Starting post-processor...").css("color", "#aaa"); - $("#btn-run-processor").prop("disabled", true); + vue_config.autogen_status = "Starting post-processor..."; + vue_config.autogen_status_color = "#aaa"; $.ajax({ url: path + "app/process", @@ -706,16 +436,19 @@ var config = { timeout: 120000, success: function(result) { if (result && result.success) { - $("#autogen-status").text("Post-processor completed successfully.").css("color", "#5cb85c"); + vue_config.autogen_status = "Post-processor completed successfully."; + vue_config.autogen_status_color = "#5cb85c"; } else { var msg = (result && result.message) ? result.message : "Unknown response"; - $("#autogen-status").text("Post-processor: " + msg).css("color", "#f0ad4e"); + vue_config.autogen_status = "Post-processor: " + msg; + vue_config.autogen_status_color = "#f0ad4e"; } }, error: function(xhr) { - $("#autogen-status").text("Post-processor failed: " + xhr.statusText).css("color", "#d9534f"); + vue_config.autogen_status = "Post-processor failed: " + xhr.statusText; + vue_config.autogen_status_color = "#d9534f"; }, - complete: function() { $("#btn-run-processor").prop("disabled", false); } + complete: function() { } }); }, @@ -729,12 +462,13 @@ var config = { var feed_ids = autogen_feeds.filter(function(f) { return f.feedid; }).map(function(f) { return f.feedid; }); if (feed_ids.length === 0) { - $("#autogen-status").text("No matching feeds found to clear.").css("color", "#f0ad4e"); + vue_config.autogen_status = "No matching feeds found to clear."; + vue_config.autogen_status_color = "#f0ad4e"; return; } - $("#autogen-status").text("Clearing feeds...").css("color", "#aaa"); - $("#btn-reset-feeds").prop("disabled", true); + vue_config.autogen_status = "Clearing feeds..."; + vue_config.autogen_status_color = "#aaa"; var cleared = 0, errors = 0; @@ -757,8 +491,211 @@ var config = { var statusText = errors === 0 ? "Cleared " + cleared + " feed(s) successfully." : "Cleared " + cleared + " feed(s), " + errors + " error(s)."; - $("#autogen-status").text(statusText).css("color", errors === 0 ? "#5cb85c" : "#f0ad4e"); - $("#btn-reset-feeds").prop("disabled", false); + vue_config.autogen_status = statusText; + vue_config.autogen_status_color = errors === 0 ? "#5cb85c" : "#f0ad4e"; } } } + + +var vue_config = new Vue({ + el: '#vue-config', + data: { + app_name: "App Name", + app_description: "This app can be used to explore onsite solar generation, self consumption, battery integration, export and building consumption.", + config_name: "", + config_public: false, + config_items: [], + config_valid: false, + autogen_node: "", + autogen_feeds: [], + autogen_all_present: false, + autogen_status: "", + autogen_status_color: "#aaa" + }, + methods: { + + // Build config_items from config.app and refresh all derived state. + // Called by config.UI() every time the panel needs to (re-)render. + renderUI: function() { + if (typeof config.ui_before_render === 'function') config.ui_before_render(); + + if (config.db.constructor === Array) config.db = {}; + for (var z in config.db) { if (config.db[z] == undefined) delete config.db[z]; } + + this.config_name = config.name; + this.config_public = !!config.public; + + var items = []; + for (var z in config.app) { + if (config.app[z].autogenerate != undefined && config.app[z].autogenerate) continue; + if (config.app[z].hidden === true) continue; + + var item = { + key: z, + type: config.app[z].type, + label: config.app[z].name || config.app[z].autoname || z, + description: config.app[z].description || "", + // feed-specific + displayName: config.app[z].autoname || z, + selectionMode: "AUTO", + isValid: false, + showSelector: false, + feedGroups: [], + selectedFeedId: 0, + // value / checkbox / select + inputValue: config.app[z].default !== undefined ? config.app[z].default : "", + selectOptions: config.app[z].options || [] + }; + + if (item.type === "feed") { + // Build grouped feed options + var byGroup = {}; + for (var f in config.feedsbyid) { + if (config.engine_check(config.feedsbyid[f], config.app[z])) { + var grp = (config.feedsbyid[f].tag === null ? "NoGroup" : config.feedsbyid[f].tag); + if (grp !== "Deleted") { + if (!byGroup[grp]) byGroup[grp] = []; + byGroup[grp].push(config.feedsbyid[f]); + } + } + } + var feedGroups = []; + for (var g in byGroup) feedGroups.push({ name: g, feeds: byGroup[g] }); + item.feedGroups = feedGroups; + + // Resolve current selection + var feedvalid = false; + if (config.db[z] != undefined) { + if (config.db[z] === "disable") { + item.selectionMode = "DISABLED"; + item.selectedFeedId = "disable"; + } else { + var feedid = config.db[z]; + if (config.feedsbyid[feedid] != undefined && config.engine_check(config.feedsbyid[feedid], config.app[z])) { + var keyappend = (z != config.feedsbyid[feedid].name) ? z + ": " : ""; + item.displayName = keyappend + config.feedsbyid[feedid].name; + item.selectionMode = ""; + item.selectedFeedId = feedid * 1; + feedvalid = true; + } else { + delete config.db[z]; + } + } + } + // Auto-match by name if nothing is configured + if (config.db[z] == undefined) { + for (var n in config.feedsbyid) { + if (config.feedsbyid[n].name == config.app[z].autoname && config.engine_check(config.feedsbyid[n], config.app[z])) { + item.selectedFeedId = "auto"; + item.selectionMode = "AUTO"; + feedvalid = true; + } + } + } + item.isValid = feedvalid; + + } else if (item.type === "value" || item.type === "checkbox" || item.type === "select") { + if (config.db[z] != undefined) item.inputValue = config.db[z]; + } + + items.push(item); + } + + this.config_items = items; + this.config_valid = config.check(); + }, + + editFeed: function(key) { + var item = this.config_items.find(function(i) { return i.key === key; }); + item.showSelector = true; + }, + + selectFeed: function(key, feedid) { + if (feedid == 0) return; + var item = this.config_items.find(function(i) { return i.key === key; }); + + if (feedid !== "auto" && feedid !== "disable") { + config.db[key] = feedid; + var keyappend = (key != config.feedsbyid[feedid].name) ? key + ": " : ""; + item.displayName = keyappend + config.feedsbyid[feedid].name; + item.selectionMode = ""; + item.isValid = true; + } + if (feedid === "auto") { + delete config.db[key]; + item.displayName = config.app[key].autoname; + item.selectionMode = "AUTO"; + item.isValid = true; + } + if (feedid === "disable") { + config.db[key] = "disable"; + item.displayName = config.app[key].autoname; + item.selectionMode = "DISABLED"; + item.isValid = false; + } + + item.showSelector = false; + config.set(); + this.config_valid = config.check(); + }, + + changeValue: function(key, value) { + var item = this.config_items.find(function(i) { return i.key === key; }); + item.inputValue = value; + config.db[key] = value; + config.set(); + if (typeof config.ui_after_value_change === 'function') config.ui_after_value_change(key); + }, + + changeName: function(event) { + var value = event.target.value; + this.config_name = value; + config.name = value; + config.set_name(); + }, + + changePublic: function(event) { + var value = event.target.checked; + this.config_public = value; + config.public = value ? 1 : 0; + config.set_public(); + }, + + setNode: function() { + config.autogen.set_node(); + }, + + // Rebuild the autogen feed list rows and button visibility from config.autogen state. + // Replaces config.autogen.render_feed_list(). + renderAutogenFeedList: function() { + var autogen_feeds = config.autogen.get_feeds(); + var node_name = config.autogen.node_name(); + + this.autogen_node = node_name; + + var missing_count = 0; + var rows = []; + for (var j = 0; j < autogen_feeds.length; j++) { + if (!autogen_feeds[j].feedid) missing_count++; + rows.push({ name: autogen_feeds[j].name, node: node_name, feedid: autogen_feeds[j].feedid }); + } + this.autogen_feeds = rows; + this.autogen_all_present = (missing_count === 0); + this.autogen_status = ""; + this.autogen_status_color = "#aaa"; + }, + + createMissingFeeds: function() { + config.autogen.create_missing_feeds(); + }, + + runPostProcessor: function() { + config.autogen.run_post_processor(); + }, + + resetFeeds: function() { + config.autogen.reset_feeds(); + } + } +}); diff --git a/Lib/appconf/appconf.php b/Lib/appconf/appconf.php index a6554015..031727e2 100644 --- a/Lib/appconf/appconf.php +++ b/Lib/appconf/appconf.php @@ -2,21 +2,24 @@ + + + - -
+
+ +
-

-

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

+

{{ app_name }}

+

{{ app_description }}

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.

@@ -28,16 +31,16 @@
- +
- -
-
+
SOLAR
@@ -93,7 +93,7 @@
+
0 kWh
@@ -108,25 +108,25 @@
+
0 kWh
+
GRID CHARGE
0 kWh
+
0 kWh
+
BATTERY TO GRID
0 kWh
@@ -140,14 +140,14 @@
+
BATTERY
0 %
+
0 kWh
@@ -200,4 +200,4 @@ function getTranslations(){ ] - \ No newline at end of file + \ No newline at end of file diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php index e565bdd2..2cfe4dde 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php @@ -16,7 +16,7 @@ function mysolarpvbattery_app_controller($route,$app,$appconfig,$apikey) { global $path, $session, $settings, $mysqli, $redis, $user, $linked_modules_dir; - $v = 2; + $v = 3; // ---------------------------------------------------- // Main app view route From 1126a95ceec526ed8a12518623e91392c80af3a0 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Mon, 16 Mar 2026 20:04:28 +0000 Subject: [PATCH 031/110] clean up --- .../mysolarpvbattery/mysolarpvbattery.js | 279 ++++++++---------- .../mysolarpvbattery/mysolarpvbattery.php | 2 +- .../mysolarpvbattery_controller.php | 4 +- 3 files changed, 124 insertions(+), 161 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index df81166b..70837c4f 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -90,13 +90,13 @@ config.check = function() { if (has_solar && has_battery) { // Need at least 3 of the 4 feeds - return ((use_ok?1:0) + (solar_ok?1:0) + (bat_ok?1:0) + (grid_ok?1:0)) >= 3; + return [use_ok, solar_ok, bat_ok, grid_ok].filter(Boolean).length >= 3; } else if (has_solar && !has_battery) { // Need at least 2 of (use, solar, grid) - return ((use_ok?1:0) + (solar_ok?1:0) + (grid_ok?1:0)) >= 2; + return [use_ok, solar_ok, grid_ok].filter(Boolean).length >= 2; } else if (!has_solar && has_battery) { // Need at least 2 of (use, battery_power, grid) - return ((use_ok?1:0) + (bat_ok?1:0) + (grid_ok?1:0)) >= 2; + return [use_ok, bat_ok, grid_ok].filter(Boolean).length >= 2; } else { // Consumption only: need at least 1 of (use, grid) return use_ok || grid_ok; @@ -187,29 +187,7 @@ function check_history_feeds(mode) { return true; } -// ---------------------------------------------------------------------- -// update_mode_ui: show/hide app-block sections based on the current mode -// ---------------------------------------------------------------------- -function update_mode_ui(mode) { - if (mode.has_solar) { - $(".solar-section").show(); - } else { - $(".solar-section").hide(); - } - - if (mode.has_battery) { - $(".battery-section").show(); - } else { - $(".battery-section").hide(); - } - // The stats table rows for solar export and battery flows - if (mode.has_solar) { - $("#statsbox-generation").show(); - } else { - $("#statsbox-generation").hide(); - } -} var timeWindow = (3600000*24.0*30); var history_end = +new Date; @@ -236,9 +214,6 @@ function init() // Apply hidden flags (also used by autogen feed list and config UI) config.ui_before_render(); - // Show/hide UI sections based on mode - update_mode_ui(mode); - render_autogen_feed_list(); view.end = power_end; @@ -264,12 +239,8 @@ function init() // Show history bargraph button only when all required kWh flow feeds are available for the current mode var has_history = check_history_feeds(mode); - if (has_history) { - init_bargraph(); - $(".viewhistory").show(); - } else { - $(".viewhistory").hide(); - } + if (has_history) init_bargraph(); + $(".viewhistory").toggle(has_history); // The buttons for these powergraph events are hidden when in historic mode // The events are loaded at the start here and dont need to be unbinded and binded again. @@ -435,24 +406,18 @@ function livefn() if (autoupdate) { var updatetime = feeds[ref_feed_id] ? feeds[ref_feed_id].time : now * 0.001; - if (mode.has_solar && config.app.solar.value) { - timeseries.append("solar", updatetime, solar_now); - timeseries.trim_start("solar", view.start*0.001); - } - timeseries.append("use", updatetime, use_now); - timeseries.trim_start("use", view.start*0.001); - - if (mode.has_battery && config.app.battery_power.value) { - 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 (mode.has_battery && config.app.battery_soc.value) { - timeseries.append("battery_soc", updatetime, battery_soc_now); - timeseries.trim_start("battery_soc", view.start*0.001); - } + var ts_updates = [ + { key: "solar", value: solar_now, guard: mode.has_solar && config.app.solar.value }, + { key: "use", value: use_now, guard: true }, + { key: "battery_power", value: battery_power_now, guard: mode.has_battery && config.app.battery_power.value }, + { key: "grid", value: grid_now, guard: true }, + { key: "battery_soc", value: battery_soc_now, guard: mode.has_battery && config.app.battery_soc.value } + ]; + ts_updates.forEach(function(ts) { + if (!ts.guard) return; + timeseries.append(ts.key, updatetime, ts.value); + timeseries.trim_start(ts.key, view.start * 0.001); + }); // Advance view view.end = now; @@ -492,20 +457,16 @@ function livefn() $('#app-block').removeClass('in_kw'); } - if (balance==0) { - $(".balance-label").html("PERFECT BALANCE"); - $(".balance").html("--"); - } - - if (balance>0) { - $(".balance-label").html("EXPORTING"); - $(".balance").html(""+Math.round(Math.abs(balance))+powerUnit+""); - } - - if (balance<0) { - $(".balance-label").html("IMPORTING"); - $(".balance").html(""+Math.round(Math.abs(balance))+powerUnit+""); - } + var balanceInfo = balance === 0 + ? { label: "PERFECT BALANCE", value: "--", color: null } + : balance > 0 + ? { label: "EXPORTING", value: Math.round(Math.abs(balance)) + powerUnit, color: "#2ed52e" } + : { label: "IMPORTING", value: Math.round(Math.abs(balance)) + powerUnit, color: "#d52e2e" }; + $(".balance-label").html(balanceInfo.label); + $(".balance").html(balanceInfo.color + ? "" + balanceInfo.value + "" + : balanceInfo.value + ); $(".generationnow").html(gen_now); $(".usenow").html(use_now); @@ -766,37 +727,18 @@ function load_powergraph() { 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)); - $(".total_import_direct_kwh").html(total_import_direct_kwh.toFixed(1)); - $(".total_grid_balance_kwh").html(total_grid_balance_kwh.toFixed(1)); - if (total_solar_kwh) { - $(".total_solar_direct_kwh").html(total_solar_direct_kwh.toFixed(1)); - $(".total_solar_export_kwh").html(total_solar_export_kwh.toFixed(1)); - $(".solar_export_prc").html((100*total_solar_export_kwh/total_solar_kwh).toFixed(0)+"%"); - $(".solar_direct_prc").html((100*total_solar_direct_kwh/total_solar_kwh).toFixed(0)+"%"); - $(".solar_to_battery_prc").html((100*total_battery_charge_from_solar_kwh/total_solar_kwh).toFixed(0)+"%"); - - $(".use_from_solar_prc").html((100*total_solar_direct_kwh/total_use_kwh).toFixed(0)+"%"); - } - $(".use_from_import_prc").html((100*total_import_direct_kwh/total_use_kwh).toFixed(0)+"%"); - $(".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)); - $(".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(); - } else { - $("#battery_import").hide(); - } - - if (total_battery_to_grid_kwh>=0.1) { - $("#battery_export").show(); - } else { - $("#battery_export").hide(); - } + updateStats({ + solar_kwh: total_solar_kwh, + use_kwh: total_use_kwh, + grid_balance_kwh: total_grid_balance_kwh, + solar_to_load: total_solar_direct_kwh, + solar_to_grid: total_solar_export_kwh, + solar_to_battery: total_battery_charge_from_solar_kwh, + battery_to_load: total_battery_discharge_kwh, + battery_to_grid: total_battery_to_grid_kwh, + grid_to_load: total_import_direct_kwh, + grid_to_battery: total_import_for_battery_kwh + }, mode); var soc_change = 0; @@ -832,6 +774,54 @@ function load_powergraph() { if (show_battery_soc && mode.has_battery && config.app.battery_soc.value) powerseries.push({data:battery_soc_data, label: "SOC", yaxis:2, color: "#888"}); } +// ---------------------------------------------------------------------- +// updateStats: write all stats-box DOM values from a flat flow data object. +// Keys match the flow naming convention used throughout the app. +// mode: { has_solar, has_battery } +// ---------------------------------------------------------------------- +function updateStats(d, mode) { + $(".total_solar_kwh").html(d.solar_kwh.toFixed(1)); + $(".total_use_kwh").html(d.use_kwh.toFixed(1)); + $(".total_import_direct_kwh").html(d.grid_to_load.toFixed(1)); + $(".total_grid_balance_kwh").html(d.grid_balance_kwh.toFixed(1)); + $(".use_from_import_prc").html((100 * d.grid_to_load / d.use_kwh).toFixed(0) + "%"); + + if (mode.has_solar && d.solar_kwh) { + $(".total_solar_direct_kwh").html(d.solar_to_load.toFixed(1)); + $(".total_solar_export_kwh").html(d.solar_to_grid.toFixed(1)); + $(".solar_export_prc").html((100 * d.solar_to_grid / d.solar_kwh).toFixed(0) + "%"); + $(".solar_direct_prc").html((100 * d.solar_to_load / d.solar_kwh).toFixed(0) + "%"); + $(".solar_to_battery_prc").html((100 * d.solar_to_battery / d.solar_kwh).toFixed(0) + "%"); + $(".use_from_solar_prc").html((100 * d.solar_to_load / d.use_kwh).toFixed(0) + "%"); + } + + if (mode.has_battery) { + $(".total_battery_charge_from_solar_kwh").html(d.solar_to_battery.toFixed(1)); + $(".total_import_for_battery_kwh").html(d.grid_to_battery.toFixed(1)); + $(".total_battery_discharge_kwh").html(d.battery_to_load.toFixed(1)); + $(".total_battery_to_grid_kwh").html(d.battery_to_grid.toFixed(1)); + $(".use_from_battery_prc").html((100 * d.battery_to_load / d.use_kwh).toFixed(0) + "%"); + toggleBatteryFlowVisibility(d.grid_to_battery, d.battery_to_grid); + } +} + +// ---------------------------------------------------------------------- +// toggleBatteryFlowVisibility: show/hide the battery import/export rows +// based on whether the flow values are significant (>= 0.1 kWh). +// ---------------------------------------------------------------------- +function toggleBatteryFlowVisibility(grid_to_battery, battery_to_grid) { + $("#battery_import").toggle(grid_to_battery >= 0.1); + $("#battery_export").toggle(battery_to_grid >= 0.1); +} + +// ---------------------------------------------------------------------- +// kwhd_val: safely read a daily kWh value from a feed data array. +// Returns 0 when the entry or its value is null/undefined. +// ---------------------------------------------------------------------- +function kwhd_val(arr, idx) { + return (arr[idx] && arr[idx][1] !== null) ? arr[idx][1] : 0; +} + function draw_powergraph() { var options = { @@ -853,10 +843,7 @@ function draw_powergraph() { // POWER GRAPH EVENTS // ------------------------------------------------------------------------------------------ function powergraph_events() { - $(".visnav[time=1]").show(); - $(".visnav[time=3]").show(); - $(".visnav[time=6]").show(); - $(".visnav[time=24]").show(); + $(".visnav[time=1], .visnav[time=3], .visnav[time=6], .visnav[time=24]").show(); $('#placeholder').unbind("plotclick"); $('#placeholder').unbind("plothover"); @@ -980,13 +967,13 @@ function load_bargraph() { { var time = ref_data[day][0]; - var solar_to_load = (solar_to_load_kwh_data[day] && solar_to_load_kwh_data[day][1] !== null) ? solar_to_load_kwh_data[day][1] : 0; - var solar_to_grid = (solar_to_grid_kwh_data[day] && solar_to_grid_kwh_data[day][1] !== null) ? solar_to_grid_kwh_data[day][1] : 0; - var solar_to_battery = (solar_to_battery_kwh_data[day] && solar_to_battery_kwh_data[day][1] !== null) ? solar_to_battery_kwh_data[day][1] : 0; - var battery_to_load = (battery_to_load_kwh_data[day] && battery_to_load_kwh_data[day][1] !== null) ? battery_to_load_kwh_data[day][1] : 0; - var battery_to_grid = (battery_to_grid_kwh_data[day] && battery_to_grid_kwh_data[day][1] !== null) ? battery_to_grid_kwh_data[day][1] : 0; - var grid_to_load = (grid_to_load_kwh_data[day] && grid_to_load_kwh_data[day][1] !== null) ? grid_to_load_kwh_data[day][1] : 0; - var grid_to_battery = (grid_to_battery_kwh_data[day] && grid_to_battery_kwh_data[day][1] !== null) ? grid_to_battery_kwh_data[day][1] : 0; + var solar_to_load = kwhd_val(solar_to_load_kwh_data, day); + var solar_to_grid = kwhd_val(solar_to_grid_kwh_data, day); + var solar_to_battery = kwhd_val(solar_to_battery_kwh_data, day); + var battery_to_load = kwhd_val(battery_to_load_kwh_data, day); + var battery_to_grid = kwhd_val(battery_to_grid_kwh_data, day); + var grid_to_load = kwhd_val(grid_to_load_kwh_data, day); + var grid_to_battery = kwhd_val(grid_to_battery_kwh_data, day); // Only skip if the required feeds for this mode are null var required_ok = (grid_to_load_kwh_data[day] && grid_to_load_kwh_data[day][1] !== null) || @@ -1083,10 +1070,7 @@ function draw_bargraph() // - click through to power graph // ------------------------------------------------------------------------------------------ function bargraph_events() { - $(".visnav[time=1]").hide(); - $(".visnav[time=3]").hide(); - $(".visnav[time=6]").hide(); - $(".visnav[time=24]").hide(); + $(".visnav[time=1], .visnav[time=3], .visnav[time=6], .visnav[time=24]").hide(); $('#placeholder').unbind("plotclick"); $('#placeholder').unbind("plothover"); @@ -1102,54 +1086,33 @@ function bargraph_events() { var mode = get_mode(); // Read directly from the fine-grained flow feed data arrays (0 when not applicable in mode) - var total_solar_to_load = (solar_to_load_kwhd_data[z] && solar_to_load_kwhd_data[z][1] !== null) ? solar_to_load_kwhd_data[z][1] : 0; - var total_solar_to_grid = (solar_to_grid_kwhd_data[z] && solar_to_grid_kwhd_data[z][1] !== null) ? solar_to_grid_kwhd_data[z][1] : 0; - var total_solar_to_battery = (solar_to_battery_kwhd_data[z] && solar_to_battery_kwhd_data[z][1] !== null) ? solar_to_battery_kwhd_data[z][1] : 0; - var total_battery_to_load = (battery_to_load_kwhd_data[z] && battery_to_load_kwhd_data[z][1] !== null) ? battery_to_load_kwhd_data[z][1] : 0; - var total_battery_to_grid = (battery_to_grid_kwhd_data[z] && battery_to_grid_kwhd_data[z][1] !== null) ? battery_to_grid_kwhd_data[z][1] : 0; - var total_grid_to_load = (grid_to_load_kwhd_data[z] && grid_to_load_kwhd_data[z][1] !== null) ? grid_to_load_kwhd_data[z][1] : 0; - var total_grid_to_battery = (grid_to_battery_kwhd_data[z] && grid_to_battery_kwhd_data[z][1] !== null) ? grid_to_battery_kwhd_data[z][1] : 0; + var solar_to_load = kwhd_val(solar_to_load_kwhd_data, z); + var solar_to_grid = kwhd_val(solar_to_grid_kwhd_data, z); + var solar_to_battery = kwhd_val(solar_to_battery_kwhd_data, z); + var battery_to_load = kwhd_val(battery_to_load_kwhd_data, z); + var battery_to_grid = kwhd_val(battery_to_grid_kwhd_data, z); + var grid_to_load = kwhd_val(grid_to_load_kwhd_data, z); + var grid_to_battery = kwhd_val(grid_to_battery_kwhd_data, z); // Reconstruct aggregate totals - var total_solar_kwh = total_solar_to_load + total_solar_to_grid + total_solar_to_battery; - var total_use_kwh = total_solar_to_load + total_battery_to_load + total_grid_to_load; - var total_import_kwh = total_grid_to_load + total_grid_to_battery; - var total_export_kwh = total_solar_to_grid + total_battery_to_grid; - var total_grid_balance_kwh = total_import_kwh - total_export_kwh; - - $(".total_solar_kwh").html(total_solar_kwh.toFixed(1)); - $(".total_use_kwh").html(total_use_kwh.toFixed(1)); - $(".total_import_direct_kwh").html(total_grid_to_load.toFixed(1)); - $(".total_grid_balance_kwh").html(total_grid_balance_kwh.toFixed(1)); - if (mode.has_solar && total_solar_kwh) { - $(".total_solar_direct_kwh").html(total_solar_to_load.toFixed(1)); - $(".total_solar_export_kwh").html(total_solar_to_grid.toFixed(1)); - $(".solar_export_prc").html((100*total_solar_to_grid/total_solar_kwh).toFixed(0)+"%"); - $(".solar_direct_prc").html((100*total_solar_to_load/total_solar_kwh).toFixed(0)+"%"); - $(".solar_to_battery_prc").html((100*total_solar_to_battery/total_solar_kwh).toFixed(0)+"%"); - $(".use_from_solar_prc").html((100*total_solar_to_load/total_use_kwh).toFixed(0)+"%"); - } - $(".use_from_import_prc").html((100*total_grid_to_load/total_use_kwh).toFixed(0)+"%"); - if (mode.has_battery) { - $(".total_battery_charge_from_solar_kwh").html(total_solar_to_battery.toFixed(1)); - $(".total_import_for_battery_kwh").html(total_grid_to_battery.toFixed(1)); - $(".total_battery_discharge_kwh").html(total_battery_to_load.toFixed(1)); - $(".total_battery_to_grid_kwh").html(total_battery_to_grid.toFixed(1)); - $(".use_from_battery_prc").html((100*total_battery_to_load/total_use_kwh).toFixed(0)+"%"); - - if (total_grid_to_battery>=0.1) { - $("#battery_import").show(); - } else { - $("#battery_import").hide(); - } - - if (total_battery_to_grid>=0.1) { - $("#battery_export").show(); - } else { - $("#battery_export").hide(); - } - } - + var solar_kwh = solar_to_load + solar_to_grid + solar_to_battery; + var use_kwh = solar_to_load + battery_to_load + grid_to_load; + var import_kwh = grid_to_load + grid_to_battery; + var export_kwh = solar_to_grid + battery_to_grid; + var grid_balance = import_kwh - export_kwh; + + updateStats({ + solar_kwh: solar_kwh, + use_kwh: use_kwh, + grid_balance_kwh: grid_balance, + solar_to_load: solar_to_load, + solar_to_grid: solar_to_grid, + solar_to_battery: solar_to_battery, + battery_to_load: battery_to_load, + battery_to_grid: battery_to_grid, + grid_to_load: grid_to_load, + grid_to_battery: grid_to_battery + }, mode); $(".battery_soc_change").html("---"); } else { diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index d5866cc0..0b642ced 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -200,4 +200,4 @@ function getTranslations(){ ] - \ No newline at end of file + \ No newline at end of file diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php index 2cfe4dde..165db760 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php @@ -53,8 +53,8 @@ function mysolarpvbattery_app_controller($route,$app,$appconfig,$apikey) $process_conf = (object) array( "solar" => (int) $app->config->solar, "use" => (int) $app->config->use, - "grid" => (int) $app->config->grid, - "battery_power" => (int) $app->config->battery_power, + "grid" => (int) isset($app->config->grid) ? $app->config->grid : 0, + "battery_power" => (int) isset($app->config->battery_power) ? $app->config->battery_power : 0, "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"), From 9a3b32d77e781f5c95fe9a4b87bd6dcb9159c5e6 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Mon, 16 Mar 2026 20:21:25 +0000 Subject: [PATCH 032/110] split out daily export series --- .../mysolarpvbattery/mysolarpvbattery.js | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index 70837c4f..af488ee7 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -1019,27 +1019,37 @@ function load_bargraph() { stack: 1 }); - // Stack 0: exports (shown as negative bars below zero) - var export_kwhd_data = []; - for (var i = 0; i < solar_to_grid_kwhd_data.length; i++) { - var export_val = 0; - if (mode.has_solar) export_val += solar_to_grid_kwhd_data[i][1]; - if (mode.has_battery) export_val += battery_to_grid_kwhd_data[i][1]; - export_kwhd_data.push([solar_to_grid_kwhd_data[i][0], -1 * export_val]); - } - if (export_kwhd_data.length > 0) { + // Stack 0: exports (shown as negative bars below zero, split by source) + if (mode.has_solar && solar_to_grid_kwhd_data.length > 0) { series.push({ - data: export_kwhd_data, - label: "Total Export", + data: invert_kwhd_data(solar_to_grid_kwhd_data), + label: "Solar to Grid", color: "#dccc1f", bars: { show: true, align: "center", barWidth: 0.8*3600*24*1000, fill: 0.9, lineWidth: 0 }, stack: 0 }); } + if (mode.has_battery && battery_to_grid_kwhd_data.length > 0) { + series.push({ + data: invert_kwhd_data(battery_to_grid_kwhd_data), + label: "Battery to Grid", + color: "#fbb450", + bars: { show: true, align: "center", barWidth: 0.8*3600*24*1000, fill: 0.9, lineWidth: 0 }, + stack: 0 + }); + } historyseries = series; } +function invert_kwhd_data(data) { + var neg_data = []; + for (var i = 0; i < data.length; i++) { + neg_data.push([data[i][0], -1 * data[i][1]]); + } + return neg_data; +} + // ------------------------------------------------------------------------------------------ // DRAW BAR GRAPH // Because the data for the bargraph only needs to be loaded once at the start we seperate out From a30e0edf86724c5ff348790129fbd1b6cb5e7a75 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Mon, 16 Mar 2026 20:35:11 +0000 Subject: [PATCH 033/110] clean up --- .../mysolarpvbattery/mysolarpvbattery.js | 170 +++++++----------- 1 file changed, 68 insertions(+), 102 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index af488ee7..47c239a5 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -167,6 +167,7 @@ var latest_start_time = 0; var panning = false; var bargraph_initialized = false; var bargraph_loaded = false; +var kwhd_data = {}; // ---------------------------------------------------------------------- // check_history_feeds: return true if the right kWh flow feeds are available @@ -940,106 +941,71 @@ function load_bargraph() { end = Math.ceil(end/intervalms)*intervalms; start = Math.floor(start/intervalms)*intervalms; - - // Load energy flow kWh data directly from post-processor feeds (only those relevant to the mode) - var grid_to_load_kwh_data = config.app.grid_to_load_kwh.value ? feed.getdata(config.app.grid_to_load_kwh.value, start,end,"daily",0,1) : []; - var solar_to_load_kwh_data = (mode.has_solar && config.app.solar_to_load_kwh.value) ? feed.getdata(config.app.solar_to_load_kwh.value, start,end,"daily",0,1) : []; - var solar_to_grid_kwh_data = (mode.has_solar && config.app.solar_to_grid_kwh.value) ? feed.getdata(config.app.solar_to_grid_kwh.value, start,end,"daily",0,1) : []; - var solar_to_battery_kwh_data = (mode.has_solar && mode.has_battery && config.app.solar_to_battery_kwh.value) ? feed.getdata(config.app.solar_to_battery_kwh.value,start,end,"daily",0,1) : []; - var battery_to_load_kwh_data = (mode.has_battery && config.app.battery_to_load_kwh.value) ? feed.getdata(config.app.battery_to_load_kwh.value, start,end,"daily",0,1) : []; - var battery_to_grid_kwh_data = (mode.has_battery && config.app.battery_to_grid_kwh.value) ? feed.getdata(config.app.battery_to_grid_kwh.value, start,end,"daily",0,1) : []; - var grid_to_battery_kwh_data = (mode.has_battery && config.app.grid_to_battery_kwh.value) ? feed.getdata(config.app.grid_to_battery_kwh.value, start,end,"daily",0,1) : []; - - // 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 = []; - - // Use grid_to_load as the reference dataset length - var ref_data = grid_to_load_kwh_data.length ? grid_to_load_kwh_data : solar_to_load_kwh_data; - - if (ref_data.length) { - for (var day=0; day { guard, feedkey } + // guard: condition under which this flow feed is applicable to the current mode + var flow_defs = [ + { key: 'grid_to_load', guard: true, }, + { key: 'solar_to_load', guard: mode.has_solar, }, + { key: 'solar_to_grid', guard: mode.has_solar, }, + { key: 'solar_to_battery', guard: mode.has_solar && mode.has_battery }, + { key: 'battery_to_load', guard: mode.has_battery, }, + { key: 'battery_to_grid', guard: mode.has_battery, }, + { key: 'grid_to_battery', guard: mode.has_battery, } + ]; + + // Load raw daily delta data for each applicable flow + var raw = {}; + flow_defs.forEach(function(d) { + raw[d.key] = (d.guard && config.app[d.key+"_kwh"].value) + ? feed.getdata(config.app[d.key+"_kwh"].value, start, end, "daily", 0, 1) + : []; }); - // Stack 0: exports (shown as negative bars below zero, split by source) - if (mode.has_solar && solar_to_grid_kwhd_data.length > 0) { - series.push({ - data: invert_kwhd_data(solar_to_grid_kwhd_data), - label: "Solar to Grid", - color: "#dccc1f", - bars: { show: true, align: "center", barWidth: 0.8*3600*24*1000, fill: 0.9, lineWidth: 0 }, - stack: 0 - }); - } - if (mode.has_battery && battery_to_grid_kwhd_data.length > 0) { - series.push({ - data: invert_kwhd_data(battery_to_grid_kwhd_data), - label: "Battery to Grid", - color: "#fbb450", - bars: { show: true, align: "center", barWidth: 0.8*3600*24*1000, fill: 0.9, lineWidth: 0 }, - stack: 0 + // Per-day arrays for graph and hover access (global so bargraph_events can read them) + kwhd_data = {}; + flow_defs.forEach(function(d) { kwhd_data[d.key] = []; }); + + // Use grid_to_load as the reference dataset, falling back to solar_to_load + var ref_data = raw['grid_to_load'].length ? raw['grid_to_load'] : raw['solar_to_load']; + + for (var day = 0; day < ref_data.length; day++) { + var time = ref_data[day][0]; + + // Only skip days where both reference feeds are null + // var required_ok = (raw['grid_to_load'][day] && raw['grid_to_load'][day][1] !== null) || + // (raw['solar_to_load'][day] && raw['solar_to_load'][day][1] !== null); + // if (!required_ok) continue; + + flow_defs.forEach(function(d) { + kwhd_data[d.key].push([time, kwhd_val(raw[d.key], day)]); }); } + + // Series definitions: label, color, stack (1=positive/load, 0=negative/export) + var series_defs = [ + // Stack 1: onsite use breakdown (positive bars above zero) + { key: 'solar_to_load', label: "Solar to Load", color: "#dccc1f", stack: 1, guard: mode.has_solar, invert: false }, + { key: 'battery_to_load', label: "Battery to Load", color: "#fbb450", stack: 1, guard: mode.has_battery, invert: false }, + { key: 'grid_to_load', label: "Grid to Load", color: "#82cbfc", stack: 1, guard: true, invert: false }, + // Stack 0: exports (negative bars below zero) + { key: 'solar_to_grid', label: "Solar to Grid", color: "#dccc1f", stack: 0, guard: mode.has_solar, invert: true }, + { key: 'battery_to_grid', label: "Battery to Grid", color: "#fbb450", stack: 0, guard: mode.has_battery, invert: true } + ]; + + historyseries = []; - historyseries = series; + series_defs.forEach(function(def) { + var data = kwhd_data[def.key]; + if (!def.guard || !data.length) return; + historyseries.push({ + data: def.invert ? invert_kwhd_data(data) : data, + label: def.label, + color: def.color, + bars: { show: true, align: "center", barWidth: 0.8 * 3600 * 24 * 1000, fill: 0.9, lineWidth: 0 }, + stack: def.stack + }); + }); } function invert_kwhd_data(data) { @@ -1096,13 +1062,13 @@ function bargraph_events() { var mode = get_mode(); // Read directly from the fine-grained flow feed data arrays (0 when not applicable in mode) - var solar_to_load = kwhd_val(solar_to_load_kwhd_data, z); - var solar_to_grid = kwhd_val(solar_to_grid_kwhd_data, z); - var solar_to_battery = kwhd_val(solar_to_battery_kwhd_data, z); - var battery_to_load = kwhd_val(battery_to_load_kwhd_data, z); - var battery_to_grid = kwhd_val(battery_to_grid_kwhd_data, z); - var grid_to_load = kwhd_val(grid_to_load_kwhd_data, z); - var grid_to_battery = kwhd_val(grid_to_battery_kwhd_data, z); + var solar_to_load = kwhd_val(kwhd_data['solar_to_load'], z); + var solar_to_grid = kwhd_val(kwhd_data['solar_to_grid'], z); + var solar_to_battery = kwhd_val(kwhd_data['solar_to_battery'], z); + var battery_to_load = kwhd_val(kwhd_data['battery_to_load'], z); + var battery_to_grid = kwhd_val(kwhd_data['battery_to_grid'], z); + var grid_to_load = kwhd_val(kwhd_data['grid_to_load'], z); + var grid_to_battery = kwhd_val(kwhd_data['grid_to_battery'], z); // Reconstruct aggregate totals var solar_kwh = solar_to_load + solar_to_grid + solar_to_battery; @@ -1140,7 +1106,7 @@ function bargraph_events() { history_start = view.start; history_end = view.end; // Use whichever per-day data array has data - var ref_day_data = grid_to_load_kwhd_data.length ? grid_to_load_kwhd_data : solar_to_load_kwhd_data; + var ref_day_data = kwhd_data['grid_to_load'].length ? kwhd_data['grid_to_load'] : kwhd_data['solar_to_load']; view.start = ref_day_data[z][0]; view.end = view.start + 86400*1000; From 4dd421dc45bf72c1483c2e82175d59e1f04f859b Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Mon, 16 Mar 2026 21:03:59 +0000 Subject: [PATCH 034/110] use async call for bargraph --- .../mysolarpvbattery/mysolarpvbattery.js | 108 +++++++++++------- 1 file changed, 64 insertions(+), 44 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index 47c239a5..6f3184b9 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -510,8 +510,13 @@ function draw(load) { draw_powergraph(); } if (viewmode=="bargraph") { - if (load) load_bargraph(); - draw_bargraph(); + if (load) { + // draw called from load + load_bargraph(); + } else { + draw_bargraph(); + } + } } @@ -954,58 +959,73 @@ function load_bargraph() { { key: 'grid_to_battery', guard: mode.has_battery, } ]; - // Load raw daily delta data for each applicable flow - var raw = {}; + var keys_to_load = []; + var feedids = []; flow_defs.forEach(function(d) { - raw[d.key] = (d.guard && config.app[d.key+"_kwh"].value) - ? feed.getdata(config.app[d.key+"_kwh"].value, start, end, "daily", 0, 1) - : []; + if (d.guard && config.app[d.key + "_kwh"] && config.app[d.key + "_kwh"].value) { + keys_to_load.push(d.key); + feedids.push(config.app[d.key + "_kwh"].value); + } }); + + // Load raw daily delta data for each applicable flow + feed.getdata(feedids, start, end, "daily", 0, 1, 0, 0, function (all_data) { - // Per-day arrays for graph and hover access (global so bargraph_events can read them) - kwhd_data = {}; - flow_defs.forEach(function(d) { kwhd_data[d.key] = []; }); + var raw = {}; + var idx = 0; + keys_to_load.forEach(function(key) { + raw[key] = all_data[idx].data; + idx++; + }); - // Use grid_to_load as the reference dataset, falling back to solar_to_load - var ref_data = raw['grid_to_load'].length ? raw['grid_to_load'] : raw['solar_to_load']; + // Per-day arrays for graph and hover access (global so bargraph_events can read them) + kwhd_data = {}; + flow_defs.forEach(function(d) { kwhd_data[d.key] = []; }); - for (var day = 0; day < ref_data.length; day++) { - var time = ref_data[day][0]; + // Use grid_to_load as the reference dataset, falling back to solar_to_load + var ref_data = raw['grid_to_load'].length ? raw['grid_to_load'] : raw['solar_to_load']; - // Only skip days where both reference feeds are null - // var required_ok = (raw['grid_to_load'][day] && raw['grid_to_load'][day][1] !== null) || - // (raw['solar_to_load'][day] && raw['solar_to_load'][day][1] !== null); - // if (!required_ok) continue; + for (var day = 0; day < ref_data.length; day++) { + var time = ref_data[day][0]; - flow_defs.forEach(function(d) { - kwhd_data[d.key].push([time, kwhd_val(raw[d.key], day)]); - }); - } + // Only skip days where both reference feeds are null + // var required_ok = (raw['grid_to_load'][day] && raw['grid_to_load'][day][1] !== null) || + // (raw['solar_to_load'][day] && raw['solar_to_load'][day][1] !== null); + // if (!required_ok) continue; - // Series definitions: label, color, stack (1=positive/load, 0=negative/export) - var series_defs = [ - // Stack 1: onsite use breakdown (positive bars above zero) - { key: 'solar_to_load', label: "Solar to Load", color: "#dccc1f", stack: 1, guard: mode.has_solar, invert: false }, - { key: 'battery_to_load', label: "Battery to Load", color: "#fbb450", stack: 1, guard: mode.has_battery, invert: false }, - { key: 'grid_to_load', label: "Grid to Load", color: "#82cbfc", stack: 1, guard: true, invert: false }, - // Stack 0: exports (negative bars below zero) - { key: 'solar_to_grid', label: "Solar to Grid", color: "#dccc1f", stack: 0, guard: mode.has_solar, invert: true }, - { key: 'battery_to_grid', label: "Battery to Grid", color: "#fbb450", stack: 0, guard: mode.has_battery, invert: true } - ]; + flow_defs.forEach(function(d) { + kwhd_data[d.key].push([time, kwhd_val(raw[d.key], day)]); + }); + } - historyseries = []; - - series_defs.forEach(function(def) { - var data = kwhd_data[def.key]; - if (!def.guard || !data.length) return; - historyseries.push({ - data: def.invert ? invert_kwhd_data(data) : data, - label: def.label, - color: def.color, - bars: { show: true, align: "center", barWidth: 0.8 * 3600 * 24 * 1000, fill: 0.9, lineWidth: 0 }, - stack: def.stack + // Series definitions: label, color, stack (1=positive/load, 0=negative/export) + var series_defs = [ + // Stack 1: onsite use breakdown (positive bars above zero) + { key: 'solar_to_load', label: "Solar to Load", color: "#dccc1f", stack: 1, guard: mode.has_solar, invert: false }, + { key: 'battery_to_load', label: "Battery to Load", color: "#fbb450", stack: 1, guard: mode.has_battery, invert: false }, + { key: 'grid_to_load', label: "Grid to Load", color: "#82cbfc", stack: 1, guard: true, invert: false }, + // Stack 0: exports (negative bars below zero) + { key: 'solar_to_grid', label: "Solar to Grid", color: "#dccc1f", stack: 0, guard: mode.has_solar, invert: true }, + { key: 'battery_to_grid', label: "Battery to Grid", color: "#fbb450", stack: 0, guard: mode.has_battery, invert: true } + ]; + + historyseries = []; + + series_defs.forEach(function(def) { + var data = kwhd_data[def.key]; + if (!def.guard || !data.length) return; + historyseries.push({ + data: def.invert ? invert_kwhd_data(data) : data, + label: def.label, + color: def.color, + bars: { show: true, align: "center", barWidth: 0.8 * 3600 * 24 * 1000, fill: 0.9, lineWidth: 0 }, + stack: def.stack + }); }); - }); + + draw_bargraph(); + + }, false); } function invert_kwhd_data(data) { From 625fb907a861487d3246d79af675a2c12847e1a5 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Mon, 16 Mar 2026 21:27:56 +0000 Subject: [PATCH 035/110] clean up --- .../mysolarpvbattery/mysolarpvbattery.js | 63 +++++++------------ 1 file changed, 24 insertions(+), 39 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index 6f3184b9..42966b3e 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -723,30 +723,17 @@ function load_powergraph() { } } - // 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; - + // Update stats boxes with totals and ratios derived from the flow decomposition updateStats({ - solar_kwh: total_solar_kwh, - use_kwh: total_use_kwh, - grid_balance_kwh: total_grid_balance_kwh, - solar_to_load: total_solar_direct_kwh, - solar_to_grid: total_solar_export_kwh, - solar_to_battery: total_battery_charge_from_solar_kwh, - battery_to_load: total_battery_discharge_kwh, + solar_to_load: total_solar_to_load_kwh, + solar_to_grid: total_solar_to_grid_kwh, + solar_to_battery: total_solar_to_battery_kwh, + battery_to_load: total_battery_to_load_kwh, battery_to_grid: total_battery_to_grid_kwh, - grid_to_load: total_import_direct_kwh, - grid_to_battery: total_import_for_battery_kwh + grid_to_load: total_grid_to_load_kwh, + grid_to_battery: total_grid_to_battery_kwh }, mode); - var soc_change = 0; if (mode.has_battery && config.app.battery_soc.value) { soc_change = battery_soc_now-timeseries.value("battery_soc",0); @@ -786,10 +773,18 @@ function load_powergraph() { // mode: { has_solar, has_battery } // ---------------------------------------------------------------------- function updateStats(d, mode) { - $(".total_solar_kwh").html(d.solar_kwh.toFixed(1)); - $(".total_use_kwh").html(d.use_kwh.toFixed(1)); + + // Reconstruct aggregate totals + var solar_kwh = d.solar_to_load + d.solar_to_grid + d.solar_to_battery; + var use_kwh = d.solar_to_load + d.battery_to_load + d.grid_to_load; + var import_kwh = d.grid_to_load + d.grid_to_battery; + var export_kwh = d.solar_to_grid + d.battery_to_grid; + var grid_balance_kwh = import_kwh - export_kwh; + + $(".total_solar_kwh").html(solar_kwh.toFixed(1)); + $(".total_use_kwh").html(use_kwh.toFixed(1)); $(".total_import_direct_kwh").html(d.grid_to_load.toFixed(1)); - $(".total_grid_balance_kwh").html(d.grid_balance_kwh.toFixed(1)); + $(".total_grid_balance_kwh").html(grid_balance_kwh.toFixed(1)); $(".use_from_import_prc").html((100 * d.grid_to_load / d.use_kwh).toFixed(0) + "%"); if (mode.has_solar && d.solar_kwh) { @@ -1001,19 +996,19 @@ function load_bargraph() { // Series definitions: label, color, stack (1=positive/load, 0=negative/export) var series_defs = [ // Stack 1: onsite use breakdown (positive bars above zero) - { key: 'solar_to_load', label: "Solar to Load", color: "#dccc1f", stack: 1, guard: mode.has_solar, invert: false }, - { key: 'battery_to_load', label: "Battery to Load", color: "#fbb450", stack: 1, guard: mode.has_battery, invert: false }, - { key: 'grid_to_load', label: "Grid to Load", color: "#82cbfc", stack: 1, guard: true, invert: false }, + { key: 'solar_to_load', label: "Solar to Load", color: "#dccc1f", stack: 1, invert: false }, + { key: 'battery_to_load', label: "Battery to Load", color: "#fbb450", stack: 1, invert: false }, + { key: 'grid_to_load', label: "Grid to Load", color: "#82cbfc", stack: 1, invert: false }, // Stack 0: exports (negative bars below zero) - { key: 'solar_to_grid', label: "Solar to Grid", color: "#dccc1f", stack: 0, guard: mode.has_solar, invert: true }, - { key: 'battery_to_grid', label: "Battery to Grid", color: "#fbb450", stack: 0, guard: mode.has_battery, invert: true } + { key: 'solar_to_grid', label: "Solar to Grid", color: "#dccc1f", stack: 0, invert: true }, + { key: 'battery_to_grid', label: "Battery to Grid", color: "#fbb450", stack: 0, invert: true } ]; historyseries = []; series_defs.forEach(function(def) { var data = kwhd_data[def.key]; - if (!def.guard || !data.length) return; + if (!data.length) return; historyseries.push({ data: def.invert ? invert_kwhd_data(data) : data, label: def.label, @@ -1090,17 +1085,7 @@ function bargraph_events() { var grid_to_load = kwhd_val(kwhd_data['grid_to_load'], z); var grid_to_battery = kwhd_val(kwhd_data['grid_to_battery'], z); - // Reconstruct aggregate totals - var solar_kwh = solar_to_load + solar_to_grid + solar_to_battery; - var use_kwh = solar_to_load + battery_to_load + grid_to_load; - var import_kwh = grid_to_load + grid_to_battery; - var export_kwh = solar_to_grid + battery_to_grid; - var grid_balance = import_kwh - export_kwh; - updateStats({ - solar_kwh: solar_kwh, - use_kwh: use_kwh, - grid_balance_kwh: grid_balance, solar_to_load: solar_to_load, solar_to_grid: solar_to_grid, solar_to_battery: solar_to_battery, From a9dae366d7719c6fce9c58bdb2369c63aed0f9ba Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Mon, 16 Mar 2026 21:42:59 +0000 Subject: [PATCH 036/110] clean up --- .../mysolarpvbattery/mysolarpvbattery.js | 93 +++++++------------ 1 file changed, 35 insertions(+), 58 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index 42966b3e..fe3b81f1 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -781,29 +781,33 @@ function updateStats(d, mode) { var export_kwh = d.solar_to_grid + d.battery_to_grid; var grid_balance_kwh = import_kwh - export_kwh; + var use_from_import_prc = d.use_kwh > 0 ? (100 * d.grid_to_load / d.use_kwh).toFixed(0) + "%" : ""; + var solar_export_prc = solar_kwh > 0 ? (100 * d.solar_to_grid / solar_kwh).toFixed(0) + "%" : ""; + var solar_to_load_prc = solar_kwh > 0 ? (100 * d.solar_to_load / solar_kwh).toFixed(0) + "%" : ""; + var solar_to_battery_prc = solar_kwh > 0 ? (100 * d.solar_to_battery / solar_kwh).toFixed(0) + "%" : ""; + var use_from_solar_prc = use_kwh > 0 ? (100 * d.solar_to_load / use_kwh).toFixed(0) + "%" : ""; + var use_from_battery_prc = use_kwh > 0 ? (100 * d.battery_to_load / use_kwh).toFixed(0) + "%" : ""; + $(".total_solar_kwh").html(solar_kwh.toFixed(1)); $(".total_use_kwh").html(use_kwh.toFixed(1)); $(".total_import_direct_kwh").html(d.grid_to_load.toFixed(1)); $(".total_grid_balance_kwh").html(grid_balance_kwh.toFixed(1)); - $(".use_from_import_prc").html((100 * d.grid_to_load / d.use_kwh).toFixed(0) + "%"); - - if (mode.has_solar && d.solar_kwh) { - $(".total_solar_direct_kwh").html(d.solar_to_load.toFixed(1)); - $(".total_solar_export_kwh").html(d.solar_to_grid.toFixed(1)); - $(".solar_export_prc").html((100 * d.solar_to_grid / d.solar_kwh).toFixed(0) + "%"); - $(".solar_direct_prc").html((100 * d.solar_to_load / d.solar_kwh).toFixed(0) + "%"); - $(".solar_to_battery_prc").html((100 * d.solar_to_battery / d.solar_kwh).toFixed(0) + "%"); - $(".use_from_solar_prc").html((100 * d.solar_to_load / d.use_kwh).toFixed(0) + "%"); - } - - if (mode.has_battery) { - $(".total_battery_charge_from_solar_kwh").html(d.solar_to_battery.toFixed(1)); - $(".total_import_for_battery_kwh").html(d.grid_to_battery.toFixed(1)); - $(".total_battery_discharge_kwh").html(d.battery_to_load.toFixed(1)); - $(".total_battery_to_grid_kwh").html(d.battery_to_grid.toFixed(1)); - $(".use_from_battery_prc").html((100 * d.battery_to_load / d.use_kwh).toFixed(0) + "%"); - toggleBatteryFlowVisibility(d.grid_to_battery, d.battery_to_grid); - } + $(".use_from_import_prc").html(use_from_import_prc); + + $(".total_solar_direct_kwh").html(d.solar_to_load.toFixed(1)); + $(".total_solar_export_kwh").html(d.solar_to_grid.toFixed(1)); + $(".solar_export_prc").html(solar_export_prc); + $(".solar_direct_prc").html(solar_to_load_prc); + $(".solar_to_battery_prc").html(solar_to_battery_prc); + $(".use_from_solar_prc").html(use_from_solar_prc); + + $(".total_battery_charge_from_solar_kwh").html(d.solar_to_battery.toFixed(1)); + $(".total_import_for_battery_kwh").html(d.grid_to_battery.toFixed(1)); + $(".total_battery_discharge_kwh").html(d.battery_to_load.toFixed(1)); + $(".total_battery_to_grid_kwh").html(d.battery_to_grid.toFixed(1)); + $(".use_from_battery_prc").html(use_from_battery_prc); + + toggleBatteryFlowVisibility(d.grid_to_battery, d.battery_to_grid); } // ---------------------------------------------------------------------- @@ -908,25 +912,9 @@ function powergraph_events() { // -------------------------------------------------------------------------------------- function init_bargraph() { bargraph_initialized = true; - var mode = get_mode(); - // Fetch the earliest start_time across all available flow kWh feeds - var earliest_start_time = Infinity; - var flow_feeds = [config.app.grid_to_load_kwh.value]; - if (mode.has_solar) { - flow_feeds.push(config.app.solar_to_load_kwh.value); - flow_feeds.push(config.app.solar_to_grid_kwh.value); - } - if (mode.has_battery) { - if (mode.has_solar) flow_feeds.push(config.app.solar_to_battery_kwh.value); - flow_feeds.push(config.app.battery_to_load_kwh.value); - flow_feeds.push(config.app.battery_to_grid_kwh.value); - flow_feeds.push(config.app.grid_to_battery_kwh.value); - } - for (var i = 0; i < flow_feeds.length; i++) { - if (!flow_feeds[i]) continue; - var m = feed.getmeta(flow_feeds[i]); - if (m.start_time < earliest_start_time) earliest_start_time = m.start_time; - } + // Fetch the earliest start_time from grid_to_load + var m = feed.getmeta(config.app.grid_to_load_kwh.value); + var earliest_start_time = m.start_time; latest_start_time = earliest_start_time; view.first_data = latest_start_time * 1000; } @@ -977,11 +965,8 @@ function load_bargraph() { kwhd_data = {}; flow_defs.forEach(function(d) { kwhd_data[d.key] = []; }); - // Use grid_to_load as the reference dataset, falling back to solar_to_load - var ref_data = raw['grid_to_load'].length ? raw['grid_to_load'] : raw['solar_to_load']; - - for (var day = 0; day < ref_data.length; day++) { - var time = ref_data[day][0]; + for (var day = 0; day < raw['grid_to_load'].length; day++) { + var time = raw['grid_to_load'][day][0]; // Only skip days where both reference feeds are null // var required_ok = (raw['grid_to_load'][day] && raw['grid_to_load'][day][1] !== null) || @@ -1077,22 +1062,14 @@ function bargraph_events() { var mode = get_mode(); // Read directly from the fine-grained flow feed data arrays (0 when not applicable in mode) - var solar_to_load = kwhd_val(kwhd_data['solar_to_load'], z); - var solar_to_grid = kwhd_val(kwhd_data['solar_to_grid'], z); - var solar_to_battery = kwhd_val(kwhd_data['solar_to_battery'], z); - var battery_to_load = kwhd_val(kwhd_data['battery_to_load'], z); - var battery_to_grid = kwhd_val(kwhd_data['battery_to_grid'], z); - var grid_to_load = kwhd_val(kwhd_data['grid_to_load'], z); - var grid_to_battery = kwhd_val(kwhd_data['grid_to_battery'], z); - updateStats({ - solar_to_load: solar_to_load, - solar_to_grid: solar_to_grid, - solar_to_battery: solar_to_battery, - battery_to_load: battery_to_load, - battery_to_grid: battery_to_grid, - grid_to_load: grid_to_load, - grid_to_battery: grid_to_battery + solar_to_load: kwhd_val(kwhd_data['solar_to_load'], z), + solar_to_grid: kwhd_val(kwhd_data['solar_to_grid'], z), + solar_to_battery: kwhd_val(kwhd_data['solar_to_battery'], z), + battery_to_load: kwhd_val(kwhd_data['battery_to_load'], z), + battery_to_grid: kwhd_val(kwhd_data['battery_to_grid'], z), + grid_to_load: kwhd_val(kwhd_data['grid_to_load'], z), + grid_to_battery: kwhd_val(kwhd_data['grid_to_battery'], z) }, mode); $(".battery_soc_change").html("---"); From 52b161e84f0855ef658fdb86f8325500c7c0019f Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Mon, 16 Mar 2026 21:51:52 +0000 Subject: [PATCH 037/110] remove unused variables --- .../mysolarpvbattery/mysolarpvbattery.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index fe3b81f1..1d2e7bd1 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -568,10 +568,6 @@ function load_powergraph() { 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_export_kwh = 0; var total_solar_to_load_kwh = 0; var total_solar_to_grid_kwh = 0; var total_solar_to_battery_kwh = 0; @@ -691,10 +687,6 @@ function load_powergraph() { } // 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; From 7729966fea77e464a2b85356e38cd4752c24eb20 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Tue, 17 Mar 2026 10:49:55 +0000 Subject: [PATCH 038/110] start on new flow calculation derivation control --- Lib/timeseries.js | 4 +- .../mysolarpvbattery/mysolarpvbattery.js | 389 ++++++++++-------- 2 files changed, 224 insertions(+), 169 deletions(-) diff --git a/Lib/timeseries.js b/Lib/timeseries.js index faeb5d83..118008de 100644 --- a/Lib/timeseries.js +++ b/Lib/timeseries.js @@ -84,12 +84,12 @@ var timeseries = { value: function (name, index) { if (datastore[name]==undefined) { - app_log("ERROR","timeseries.value datastore["+name+"] is undefined"); + // app_log("ERROR","timeseries.value datastore["+name+"] is undefined"); return false; } if (datastore[name].data[index]==undefined) { - app_log("ERROR","timeseries.value datastore["+name+"].data["+index+"] is undefined, data length: "+datastore[name].data.length); + // app_log("ERROR","timeseries.value datastore["+name+"].data["+index+"] is undefined, data length: "+datastore[name].data.length); return null; } else { return datastore[name].data[index][1]; diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index 1d2e7bd1..ec095b5c 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -169,6 +169,24 @@ var bargraph_initialized = false; var bargraph_loaded = false; var kwhd_data = {}; + +// == Flow decomposition control variables == + +// Which feeds are actually available +var use_available = false; +var solar_available = false; +var battery_power_available = false; +var grid_available = false; + +// which feed to derive (if any) based on the config; false = no derivation needed +var derive = false; + +// which feeds to assume zero +var assume_zero_solar = false; +var assume_zero_battery = false; + +// == End of flow decomposition control variables == + // ---------------------------------------------------------------------- // check_history_feeds: return true if the right kWh flow feeds are available // for the current mode (needed to enable the History bargraph view). @@ -293,6 +311,10 @@ function show() app_log("INFO","solar & battery show"); var mode = get_mode(); + + + flow_available(); + if (check_history_feeds(mode)) { if (!bargraph_initialized) init_bargraph(); } @@ -319,6 +341,132 @@ function show() */ } +// ------------------------------------------------------------------------------------------------------- +// Flow decomposition +// Conservation of energy: use = solar + battery_power + grid +// battery_power: positive = discharge, negative = charge +// grid: positive = import, negative = export +// ------------------------------------------------------------------------------------------------------- + +function flow_available() { + + // 4 feeds: solar, use, battery_power, grid + // 3 feeds + // 2 feeds (need at least use or grid, second can be solar or battery_power) + // 1 feed (use or grid) + + // Availability + if (config.app.solar.value) solar_available = true; + if (config.app.use.value) use_available = true; + if (config.app.battery_power.value) battery_power_available = true; + if (config.app.grid.value) grid_available = true; + + var number_of_feeds = 0; + if (solar_available) number_of_feeds++; + if (use_available) number_of_feeds++; + if (battery_power_available) number_of_feeds++; + if (grid_available) number_of_feeds++; + + // 3 Feeds: Just find the one that is missing + if (number_of_feeds === 3) { + if (!config.app.grid.value) derive = "grid"; + else if (!config.app.use.value) derive = "use"; + else if (!config.app.solar.value) derive = "solar"; + else if (!config.app.battery_power.value) derive = "battery_power"; + } + + // 2 Feeds: Specific logic based on your priority rules + else if (number_of_feeds === 2) { + + if (config.app.solar.value) { + if (config.app.use.value) derive = "grid"; + else if (config.app.grid.value) derive = "use"; + assume_zero_battery = true; // if battery feed is missing, assume no battery power (solar-only mode) + } + + if (config.app.battery_power.value) { + if (config.app.use.value) derive = "grid"; + else if (config.app.grid.value) derive = "use"; + assume_zero_solar = true; // if solar feed is missing, assume no solar generation (battery-only mode) + } + } + + console.log("Number of feeds configured: " + number_of_feeds); + console.log("Deriving feed: " + derive); + console.log("Assume zero solar: " + assume_zero_solar); + console.log("Assume zero battery: " + assume_zero_battery); +} + +function flow_derive_missing(input) { + var solar = input.solar; + var use = input.use; + var battery_power = input.battery_power; + var grid = input.grid; + + if (assume_zero_solar) solar = 0; + if (assume_zero_battery) battery_power = 0; + + if (derive === "grid") { + grid = use - solar - battery_power; + } else if (derive === "use") { + use = solar + battery_power + grid; + } else if (derive === "solar") { + solar = use - battery_power - grid; + } else if (derive === "battery_power") { + battery_power = use - solar - grid; + } + + return input; +} + +function flow_calculation(input) { + + var solar = input.solar; + var use = input.use; + var battery_power = input.battery_power; + var grid = input.grid; + + // Import/export split: positive grid = import, negative grid = export + var import_power = grid > 0 ? grid : 0; + + // 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); + } + 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); + } + + return { + solar_to_load: solar_to_load, + solar_to_battery: solar_to_battery, + solar_to_grid: solar_to_grid, + battery_to_load: battery_to_load, + battery_to_grid: battery_to_grid, + grid_to_load: grid_to_load, + grid_to_battery: grid_to_battery + } +} + + function resize() { app_log("INFO","solar & battery resize"); @@ -363,37 +511,20 @@ function livefn() var feeds = feed.listbyid(); if (feeds === null) { return; } - // Read whichever feeds are configured; treat missing/mode-disabled ones as 0 - var solar_now = (mode.has_solar && config.app.solar.value && feeds[config.app.solar.value]) ? parseInt(feeds[config.app.solar.value].value) : 0; - var use_now = (config.app.use.value && feeds[config.app.use.value]) ? parseInt(feeds[config.app.use.value].value) : null; - var battery_power_now= (mode.has_battery && config.app.battery_power.value && feeds[config.app.battery_power.value]) ? parseInt(feeds[config.app.battery_power.value].value) : 0; - var grid_now = (config.app.grid.value && feeds[config.app.grid.value]) ? parseInt(feeds[config.app.grid.value].value) : null; - - // Derive the missing feed from conservation of energy: - // use = solar + battery_power + grid → any one can be derived from the other three - if (use_now === null && grid_now !== null) { - use_now = grid_now + battery_power_now + solar_now; - } else if (grid_now === null && use_now !== null) { - grid_now = use_now - battery_power_now - solar_now; - } else if (use_now === null && grid_now === null) { - // Both missing – nothing we can do; bail out silently - return; - } - // In battery-only mode, battery_power may be the derived quantity - if (!mode.has_battery) { - battery_power_now = 0; // no battery on this system - } else if (mode.has_battery && !config.app.battery_power.value) { - // battery is present but no feed configured → derive it - battery_power_now = use_now - grid_now - solar_now; + var input = { + solar: solar_available && feeds[config.app.solar.value]!=undefined ? parseFloat(feeds[config.app.solar.value].value) : null, + use: use_available && feeds[config.app.use.value]!=undefined ? parseFloat(feeds[config.app.use.value].value) : null, + battery_power: battery_power_available && feeds[config.app.battery_power.value]!=undefined ? parseFloat(feeds[config.app.battery_power.value].value) : null, + grid: grid_available && feeds[config.app.grid.value]!=undefined ? parseFloat(feeds[config.app.grid.value].value) : null } - // In solar-only mode, solar may be the derived quantity - if (!mode.has_solar) { - solar_now = 0; - } else if (mode.has_solar && !config.app.solar.value) { - solar_now = use_now - grid_now - battery_power_now; - } + input = flow_derive_missing(input); + + var solar_now = input.solar; + var use_now = input.use; + var battery_power_now = input.battery_power; + var grid_now = input.grid; var battery_soc_now = "---"; if (mode.has_battery && config.app.battery_soc.value && feeds[config.app.battery_soc.value] != undefined) { @@ -525,27 +656,30 @@ function load_powergraph() { var mode = get_mode(); + // if we have all 4 feeds then we can just load them directly with no derivation needed + // ------------------------------------------------------------------------------------------------------- // LOAD DATA ON INIT OR RELOAD // Only load feeds that are actually configured; missing ones will be derived. // ------------------------------------------------------------------------------------------------------- if (reload) { reload = false; + // getdata params: feedid,start,end,interval,average=0,delta=0,skipmissing=0,limitinterval=0,callback=false,context=false,timeformat='unixms' - if (mode.has_solar && config.app.solar.value) { - timeseries.load("solar", feed.getdata(config.app.solar.value, view.start, view.end, view.interval, 1, 0, 0, 0, false, false, 'notime')); + if (solar_available) { + timeseries.load("solar", remove_null_values(feed.getdata(config.app.solar.value, view.start, view.end, view.interval, 1, 0, 0, 0, false, false, 'notime'), view.interval)); } - if (config.app.use.value) { - timeseries.load("use", feed.getdata(config.app.use.value, view.start, view.end, view.interval, 1, 0, 0, 0, false, false, 'notime')); + if (use_available) { + timeseries.load("use", remove_null_values(feed.getdata(config.app.use.value, view.start, view.end, view.interval, 1, 0, 0, 0, false, false, 'notime'), view.interval)); } - if (mode.has_battery && config.app.battery_power.value) { - timeseries.load("battery_power", feed.getdata(config.app.battery_power.value, view.start, view.end, view.interval, 1, 0, 0, 0, false, false, 'notime')); + if (battery_power_available) { + timeseries.load("battery_power", remove_null_values(feed.getdata(config.app.battery_power.value, view.start, view.end, view.interval, 1, 0, 0, 0, false, false, 'notime'), view.interval)); } - if (config.app.grid.value) { - timeseries.load("grid", feed.getdata(config.app.grid.value, view.start, view.end, view.interval, 1, 0, 0, 0, false, false, 'notime')); + if (grid_available) { + timeseries.load("grid", remove_null_values(feed.getdata(config.app.grid.value, view.start, view.end, view.interval, 1, 0, 0, 0, false, false, 'notime'), view.interval)); } - if (mode.has_battery && 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')); + if (config.app.battery_soc.value) { + timeseries.load("battery_soc", remove_null_values(feed.getdata(config.app.battery_soc.value, view.start, view.end, view.interval, 0, 0, 0, 0, false, false, 'notime'), view.interval)); } } // ------------------------------------------------------------------------------------------------------- @@ -562,12 +696,6 @@ function load_powergraph() { var grid_to_battery_data = []; var battery_soc_data = []; - var use_now = 0; - var solar_now = 0; - var battery_power_now = 0; - var grid_now = 0; - var battery_soc_now = 0; - var total_solar_to_load_kwh = 0; var total_solar_to_grid_kwh = 0; var total_solar_to_battery_kwh = 0; @@ -576,145 +704,54 @@ function load_powergraph() { var total_grid_to_load_kwh = 0; var total_grid_to_battery_kwh = 0; - // Track which feeds were actually loaded (null = derived) - var has_solar_ts = mode.has_solar && !!config.app.solar.value; - var has_use_ts = !!config.app.use.value; - var has_battery_ts = mode.has_battery && !!config.app.battery_power.value; - var has_grid_ts = !!config.app.grid.value; + let battery_soc_now = null; var datastart = timeseries.start_time(ts_ref); - - var last_solar = 0; - var last_use = 0; - var last_battery_power = 0; - var last_grid = 0; - var last_soc = 0; - - // When only 3 feeds are present, the timeout check only applies to those 3 - // Build the list of feeds to include in the timeout guard - var timeout_keys = []; - if (has_solar_ts) timeout_keys.push("solar"); - if (has_use_ts) timeout_keys.push("use"); - if (has_battery_ts) timeout_keys.push("battery_power"); - if (has_grid_ts) timeout_keys.push("grid"); - - var timeout = 600*1000; - var interval = view.interval; var power_to_kwh = interval / 3600000.0; for (var z=0; z= timeout) data_ok = false; - if (has_use_ts && (time - last_use) >= timeout) data_ok = false; - if (has_battery_ts && (time - last_battery_power) >= timeout) data_ok = false; - if (has_grid_ts && (time - last_grid) >= timeout) data_ok = false; + input = flow_derive_missing(input); - if (data_ok) { - - // ------------------------------------------------------------------------------------------------------- - // Derive the missing feed from conservation of energy: - // use = solar + battery_power + grid - // In no-solar mode solar is always 0; in no-battery mode battery_power is always 0. - // ------------------------------------------------------------------------------------------------------- - var solar = has_solar_ts ? solar_now : (!mode.has_solar ? 0 : (use_now - grid_now - battery_power_now)); - var battery_power= has_battery_ts ? battery_power_now: (!mode.has_battery ? 0 : (use_now - grid_now - solar_now)); - var grid = has_grid_ts ? grid_now : (use_now - solar - battery_power); - var use = has_use_ts ? use_now : (solar + battery_power + grid); - - // ------------------------------------------------------------------------------------------------------- - // Flow decomposition - // Conservation of energy: use = solar + battery_power + grid - // battery_power: positive = discharge, negative = charge - // grid: positive = import, negative = export - // ------------------------------------------------------------------------------------------------------- - var import_power = 0; - var export_power = 0; - if (grid > 0) { - import_power = grid; - } else { - export_power = -grid; - } + if (input.solar !== null || input.use !== null || input.battery_power !== null || input.grid !== null) { - // 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); - } - 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); - } + var flow = flow_calculation(input); // Accumulate kWh totals - 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 { - 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]); + total_solar_to_load_kwh += flow.solar_to_load * power_to_kwh; + total_solar_to_grid_kwh += flow.solar_to_grid * power_to_kwh; + total_solar_to_battery_kwh += flow.solar_to_battery * power_to_kwh; + total_battery_to_load_kwh += flow.battery_to_load * power_to_kwh; + total_battery_to_grid_kwh += flow.battery_to_grid * power_to_kwh; + total_grid_to_load_kwh += flow.grid_to_load * power_to_kwh; + total_grid_to_battery_kwh += flow.grid_to_battery * power_to_kwh; + + solar_to_load_data.push([time, flow.solar_to_load]); + solar_to_grid_data.push([time, flow.solar_to_grid]); + solar_to_battery_data.push([time, flow.solar_to_battery]); + battery_to_load_data.push([time, flow.battery_to_load]); + battery_to_grid_data.push([time, flow.battery_to_grid]); + grid_to_load_data.push([time, flow.grid_to_load]); + grid_to_battery_data.push([time, flow.grid_to_battery]); + + } + + // SOC + if (mode.has_battery && config.app.battery_soc.value) { + battery_soc_now = timeseries.value("battery_soc",z); } + battery_soc_data.push([time, battery_soc_now]); } - + // Update stats boxes with totals and ratios derived from the flow decomposition updateStats({ solar_to_load: total_solar_to_load_kwh, @@ -1157,6 +1194,24 @@ $(function() { }) }) +// 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) { + let last_valid_pos = 0; + for (let pos = 0; pos < data.length; pos++) { + if (data[pos][1] != null) { + 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]; + } + } + last_valid_pos = pos; + } + } + return data; +} + // ---------------------------------------------------------------------- // App log // ---------------------------------------------------------------------- From 49b38217f0bbf785d33caaee9319a5f47ac67c76 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Tue, 17 Mar 2026 10:58:11 +0000 Subject: [PATCH 039/110] derive flow control --- .../mysolarpvbattery/mysolarpvbattery.js | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index ec095b5c..cb5cdf7e 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -356,9 +356,9 @@ function flow_available() { // 1 feed (use or grid) // Availability - if (config.app.solar.value) solar_available = true; + if (config.app.has_solar.value && config.app.solar.value) solar_available = true; if (config.app.use.value) use_available = true; - if (config.app.battery_power.value) battery_power_available = true; + if (config.app.has_battery.value && config.app.battery_power.value) battery_power_available = true; if (config.app.grid.value) grid_available = true; var number_of_feeds = 0; @@ -369,24 +369,28 @@ function flow_available() { // 3 Feeds: Just find the one that is missing if (number_of_feeds === 3) { - if (!config.app.grid.value) derive = "grid"; - else if (!config.app.use.value) derive = "use"; - else if (!config.app.solar.value) derive = "solar"; - else if (!config.app.battery_power.value) derive = "battery_power"; + if (!grid_available) derive = "grid"; + else if (!use_available) derive = "use"; + else if (!solar_available) derive = "solar"; + else if (!battery_power_available) derive = "battery_power"; } // 2 Feeds: Specific logic based on your priority rules else if (number_of_feeds === 2) { + + if (solar_available && battery_power_available) { + // We cant derive in this scenario + } - if (config.app.solar.value) { - if (config.app.use.value) derive = "grid"; - else if (config.app.grid.value) derive = "use"; + if (solar_available) { + if (use_available) derive = "grid"; + else if (grid_available) derive = "use"; assume_zero_battery = true; // if battery feed is missing, assume no battery power (solar-only mode) } - if (config.app.battery_power.value) { - if (config.app.use.value) derive = "grid"; - else if (config.app.grid.value) derive = "use"; + if (battery_power_available) { + if (use_available) derive = "grid"; + else if (grid_available) derive = "use"; assume_zero_solar = true; // if solar feed is missing, assume no solar generation (battery-only mode) } } From f2cc344c853d392fd0c1d9a81da492824c842cc5 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Tue, 17 Mar 2026 15:50:12 +0000 Subject: [PATCH 040/110] further refactoring --- .../mysolarpvbattery/mysolarpvbattery.css | 19 +++ .../mysolarpvbattery/mysolarpvbattery.js | 142 +++++++----------- .../mysolarpvbattery/mysolarpvbattery.php | 20 ++- 3 files changed, 84 insertions(+), 97 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css index 8d157b6d..2ea677bf 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css @@ -138,4 +138,23 @@ border-width: 10px; margin-top: -10px; } +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: auto auto; + gap: 0.5rem 1rem; + padding: 0.5rem 0; +} + +.stats-grid > div { + text-align: center; + min-width: 0; +} + +@media (max-width: 576px) { + .stats-grid { + grid-template-columns: repeat(2, 1fr); + } } \ No newline at end of file diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index cb5cdf7e..a670f83c 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -1,4 +1,3 @@ - // ---------------------------------------------------------------------- // Globals // ---------------------------------------------------------------------- @@ -326,19 +325,8 @@ function show() livefn(); live = setInterval(livefn,5000); - // Trigger post processor for kWh data - let process_timeout = 60; // seconds - /* - $.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); - } - }); - */ + // Trigger process here + run_post_processor(); } // ------------------------------------------------------------------------------------------------------- @@ -515,7 +503,6 @@ function livefn() var feeds = feed.listbyid(); if (feeds === null) { return; } - var input = { solar: solar_available && feeds[config.app.solar.value]!=undefined ? parseFloat(feeds[config.app.solar.value].value) : null, use: use_available && feeds[config.app.use.value]!=undefined ? parseFloat(feeds[config.app.use.value].value) : null, @@ -525,11 +512,6 @@ function livefn() input = flow_derive_missing(input); - var solar_now = input.solar; - var use_now = input.use; - var battery_power_now = input.battery_power; - var grid_now = input.grid; - var battery_soc_now = "---"; if (mode.has_battery && config.app.battery_soc.value && feeds[config.app.battery_soc.value] != undefined) { battery_soc_now = parseInt(feeds[config.app.battery_soc.value].value); @@ -543,10 +525,10 @@ function livefn() var updatetime = feeds[ref_feed_id] ? feeds[ref_feed_id].time : now * 0.001; var ts_updates = [ - { key: "solar", value: solar_now, guard: mode.has_solar && config.app.solar.value }, - { key: "use", value: use_now, guard: true }, - { key: "battery_power", value: battery_power_now, guard: mode.has_battery && config.app.battery_power.value }, - { key: "grid", value: grid_now, guard: true }, + { key: "solar", value: input.solar, guard: mode.has_solar && config.app.solar.value }, + { key: "use", value: input.use, guard: true }, + { key: "battery_power", value: input.battery_power, guard: mode.has_battery && config.app.battery_power.value }, + { key: "grid", value: input.grid, guard: true }, { key: "battery_soc", value: battery_soc_now, guard: mode.has_battery && config.app.battery_soc.value } ]; ts_updates.forEach(function(ts) { @@ -560,85 +542,74 @@ function livefn() view.start = now - live_timerange; } - // balance = grid export (positive) or import (negative), shown from grid perspective - // negative grid_now = export = positive balance - var balance = -grid_now; - - var battery_charge_now = 0; - var battery_discharge_now = 0; - if (mode.has_battery) { - if (battery_power_now > 0) { - battery_discharge_now = battery_power_now; - } else { - battery_charge_now = -battery_power_now; - } - } + // Calculate time left + var time_left = battery_time_left({ + capacity: config.app.battery_capacity_kwh.value, + soc: battery_soc_now, + battery_power: input.battery_power + }); + $(".discharge_time_left").html(time_left); // convert W to kW - var gen_now; if(powerUnit === 'kW') { - gen_now = as_kw(solar_now) - solar_now = as_kw(solar_now) - use_now = as_kw(use_now) - balance = as_kw(balance) - battery_charge_now = as_kw(battery_charge_now) - battery_discharge_now = as_kw(battery_discharge_now) + input.solar = as_kw(input.solar) + input.use = as_kw(input.use) + input.battery_power = as_kw(input.battery_power) + input.grid = as_kw(input.grid) + $('.power-unit').text('kW') $('#app-block').addClass('in_kw'); } else { - solar_now = Math.round(solar_now) - gen_now = solar_now - balance = Math.round(balance) $('.power-unit').text('W') $('#app-block').removeClass('in_kw'); } - var balanceInfo = balance === 0 - ? { label: "PERFECT BALANCE", value: "--", color: null } - : balance > 0 - ? { label: "EXPORTING", value: Math.round(Math.abs(balance)) + powerUnit, color: "#2ed52e" } - : { label: "IMPORTING", value: Math.round(Math.abs(balance)) + powerUnit, color: "#d52e2e" }; - $(".balance-label").html(balanceInfo.label); - $(".balance").html(balanceInfo.color - ? "" + balanceInfo.value + "" - : balanceInfo.value - ); - - $(".generationnow").html(gen_now); - $(".usenow").html(use_now); + $(".solarnow").html(input.solar); + $(".usenow").html(input.use); $(".battery_soc").html(battery_soc_now); + + if (input.grid > 0) { + $(".balance-label").html("IMPORTING"); + $(".balance").html("" + Math.round(input.grid) + " " + powerUnit + ""); + } else if (input.grid < 0) { + $(".balance-label").html("EXPORTING"); + $(".balance").html("" + Math.round(-input.grid) + " " + powerUnit + ""); + } else { + $(".balance-label").html("BALANCED"); + $(".balance").html("--"); + } - const net_battery_charge = battery_charge_now - battery_discharge_now; - if (net_battery_charge>0) { + + // Battery charge/discharge status + if (input.battery_power<0) { $(".battery_charge_discharge_title").html("BATTERY CHARGING"); - $(".battery_charge_discharge").html(net_battery_charge); - $(".discharge_time_left").html("--"); - } else if (net_battery_charge<0) { - 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; - - const hours_left = Math.floor(total_time_left_mins / 60); - const mins_left = Math.floor(total_time_left_mins % 60); - const battery_time_left_text = `${hours_left}h ${mins_left}m` - $(".discharge_time_left").html(battery_time_left_text); - } else { - $(".discharge_time_left").html("--"); - } - + } else if (input.battery_power>0) { $(".battery_charge_discharge_title").html("BATTERY DISCHARGING"); - $(".battery_charge_discharge").html(-net_battery_charge); } else { $(".battery_charge_discharge_title").html("BATTERY POWER"); - $(".battery_charge_discharge").html(0); - $(".discharge_time_left").html("--"); } - + $(".battery_charge_discharge").html(Math.abs(input.battery_power)); + // Only redraw the graph if its the power graph and auto update is turned on if (viewmode=="powergraph" && autoupdate) draw(true); } +// Capacity in kWh, power in W, returns time left as string "Xh Ym" +function battery_time_left({ capacity, soc, battery_power }) { + if (capacity <= 0 || soc < 0 || battery_power === 0) return "--"; + + // if discharging, soc_part is soc; if charging, soc_part is 100-soc (time to full charge) + let soc_part = battery_power>0 ? soc : (100 - soc); + let energy_remaining_kwh = (capacity * soc_part) / 100; + + let battery_power_kw = Math.abs(battery_power * 0.001); // convert W to kW + let time_left_hours = energy_remaining_kwh / battery_power_kw; + + const hours_left = Math.floor(time_left_hours); + const mins_left = Math.floor((time_left_hours*60) % 60); + return `${hours_left}h ${mins_left}m`; +} + function draw(load) { if (viewmode=="powergraph") { if (load) load_powergraph(); @@ -765,7 +736,7 @@ function load_powergraph() { battery_to_grid: total_battery_to_grid_kwh, grid_to_load: total_grid_to_load_kwh, grid_to_battery: total_grid_to_battery_kwh - }, mode); + }); var soc_change = 0; if (mode.has_battery && config.app.battery_soc.value) { @@ -803,9 +774,8 @@ function load_powergraph() { // ---------------------------------------------------------------------- // updateStats: write all stats-box DOM values from a flat flow data object. // Keys match the flow naming convention used throughout the app. -// mode: { has_solar, has_battery } // ---------------------------------------------------------------------- -function updateStats(d, mode) { +function updateStats(d) { // Reconstruct aggregate totals var solar_kwh = d.solar_to_load + d.solar_to_grid + d.solar_to_battery; @@ -1103,7 +1073,7 @@ function bargraph_events() { battery_to_grid: kwhd_val(kwhd_data['battery_to_grid'], z), grid_to_load: kwhd_val(kwhd_data['grid_to_load'], z), grid_to_battery: kwhd_val(kwhd_data['grid_to_battery'], z) - }, mode); + }); $(".battery_soc_change").html("---"); } else { diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 0b642ced..41a32a45 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -18,7 +18,7 @@ - +
" + autogen_feeds[j].name + "" + node_name + "" + status_html + "
+
@@ -45,27 +48,100 @@ - - + + + + + +
Feed nameStatus
{{ feed.name }}{{ feed.node }} + ✓ exists + ✗ missing +
- - - - + {{ autogen_status }}
-
+
+ + +
+ App name (menu) +
+ +
+ +
+ Public +
Make app public + +
+ +
+ +
+ + + + + + + + +
+ +
+
+ + +
+ +
- \ No newline at end of file + + + + From 716f46e601cb76abd759ed17c88832e90b765cce Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Tue, 7 Apr 2026 14:55:40 +0100 Subject: [PATCH 060/110] migrate remaining to vue template --- Lib/appconf/appconf.js | 61 ++++++++++++++++++++--------------------- Lib/appconf/appconf.php | 4 +-- 2 files changed, 32 insertions(+), 33 deletions(-) diff --git a/Lib/appconf/appconf.js b/Lib/appconf/appconf.js index 9de98dbc..edeb9c71 100644 --- a/Lib/appconf/appconf.js +++ b/Lib/appconf/appconf.js @@ -56,37 +56,6 @@ var config = { $("body").on("click",".config-close", function(event) { config.closeConfig(); }); - - // save and show app - $("body").on("click",".app-launch",function() { - $(".ajax-loader").show(); - config.closeConfig(); - config.load(); - if (!config.initialized) { - config.initapp(); - config.initialized = true; - } - config.showapp(); - }); - - $("body").on("click",".app-delete",function(){ - console.log("delete: "+config.id); - $.ajax({ - url: path+"app/remove", - data: "id="+config.id, - dataType: 'text', - async: false, - success: function(result){ - try { - result = JSON.parse(result); - if (result.success != undefined && !result.success) appLog("ERROR", result.message); - window.location = path+"app/view"; - } catch (e) { - app.log("ERROR","Could not parse /setconfig reply, error: "+e); - } - } - }); - }); }, /** @@ -662,6 +631,36 @@ var vue_config = new Vue({ config.set_public(); }, + launchApp: function() { + $(".ajax-loader").show(); + config.closeConfig(); + config.load(); + if (!config.initialized) { + config.initapp(); + config.initialized = true; + } + config.showapp(); + }, + + deleteApp: function() { + console.log("delete: " + config.id); + $.ajax({ + url: path + "app/remove", + data: "id=" + config.id, + dataType: 'text', + async: false, + success: function(result) { + try { + result = JSON.parse(result); + if (result.success != undefined && !result.success) appLog("ERROR", result.message); + window.location = path + "app/view"; + } catch (e) { + console.log("Could not parse /remove reply, error: " + e); + } + } + }); + }, + setNode: function() { config.autogen.set_node(); }, diff --git a/Lib/appconf/appconf.php b/Lib/appconf/appconf.php index 031727e2..60a4d5d5 100644 --- a/Lib/appconf/appconf.php +++ b/Lib/appconf/appconf.php @@ -134,8 +134,8 @@
- - + +
From 55a7c5aa23c5f7638ab4851f7dd7f6c00fe9c7dd Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Tue, 7 Apr 2026 15:16:27 +0100 Subject: [PATCH 061/110] list of main apps for easy access --- Views/app_view.php | 45 ++++++++++++++----- apps/OpenEnergyMonitor/myheatpump/app.json | 3 +- .../mysolarpvbattery/app.json | 3 +- apps/OpenEnergyMonitor/octopus/app.json | 3 +- 4 files changed, 39 insertions(+), 15 deletions(-) diff --git a/Views/app_view.php b/Views/app_view.php index 42ab00d6..8c76cf42 100644 --- a/Views/app_view.php +++ b/Views/app_view.php @@ -1,11 +1,33 @@ +

Available Apps

Create a new instance of an app by clicking on one of the apps below.

-
+
+ +

Core Emoncms apps:

+ +
+
+
{{ app.title }}
+
{{ app.description || "no description..." }}
+
+
+
+ +

Other Emoncms apps:

+ +
+
+
{{ app.title }}
+
{{ app.description || "no description..." }}
+
+
+ +
@@ -36,18 +58,17 @@ var selected_app = ""; var app_new_enable = true; -var out = ""; -for (var z in available_apps) { - if (available_apps[z].description=="") - available_apps[z].description = "no description..."; - out += '
'; - out += '
'+available_apps[z].title+'
'; - out += '
'+available_apps[z].description+'
'; - out += '
'; -} + +var app_list = new Vue({ + el: '#available-apps', + data: { + apps: available_apps + } +}); + + $(function() { - $("#available-apps").html(out); - $(".app-item").first().css("border-top","1px solid #ccc"); + $(".app-group").each(function() { $(this).find(".app-item").first().css("border-top","1px solid #ccc"); }); $(".app-item").click(function(){ if (app_new_enable) { diff --git a/apps/OpenEnergyMonitor/myheatpump/app.json b/apps/OpenEnergyMonitor/myheatpump/app.json index 59aae96a..72b6254e 100644 --- a/apps/OpenEnergyMonitor/myheatpump/app.json +++ b/apps/OpenEnergyMonitor/myheatpump/app.json @@ -1,5 +1,6 @@ { "title" : "My Heatpump", "description" : "Explore heatpump performance: daily electricity consumption, heat output and COP. Zoom in for detailed temperature, power, heat graphs.", - "order" : 7 + "order" : 2, + "primary" : true } diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/app.json b/apps/OpenEnergyMonitor/mysolarpvbattery/app.json index dc465234..daed7811 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/app.json +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/app.json @@ -1,5 +1,6 @@ { "title" : "My Solar Battery", "description" : "Explore solar generation, household consumption and a home battery system", - "order" : 4 + "order" : 1, + "primary" : true } diff --git a/apps/OpenEnergyMonitor/octopus/app.json b/apps/OpenEnergyMonitor/octopus/app.json index a7216b5f..543feb6a 100644 --- a/apps/OpenEnergyMonitor/octopus/app.json +++ b/apps/OpenEnergyMonitor/octopus/app.json @@ -1,5 +1,6 @@ { "title" : "Octopus Agile", "description" : "Explore Octopus Agile tariff energy costs", - "order" : 8 + "order" : 3, + "primary" : true } From 9eae0eda1f08d7ba775c366cf57b50b3405182ef Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Wed, 8 Apr 2026 07:58:22 +0100 Subject: [PATCH 062/110] custom app name and description for vue implementation --- Lib/appconf/appconf.js | 16 ++++++-- Lib/appconf/appconf.php | 6 +-- .../myheatpump/myheatpump.php | 38 ++++++------------- .../mysolarpvbattery/mysolarpvbattery.php | 26 ++++++++----- apps/OpenEnergyMonitor/octopus/octopus.php | 6 +++ 5 files changed, 50 insertions(+), 42 deletions(-) diff --git a/Lib/appconf/appconf.js b/Lib/appconf/appconf.js index edeb9c71..645b2dd4 100644 --- a/Lib/appconf/appconf.js +++ b/Lib/appconf/appconf.js @@ -12,9 +12,18 @@ var config = { feeds: {}, feedsbyid: {}, feedsbyname: {}, - + app_name: "", + // Common options: + // Blue: #44b3e2, Green: #5cb85c, Yellow: #f0ad4e, Red: #d9534f, Grey: #aaa + app_name_color: "#44b3e2", + init: function() { // console.log("CONFIG: Init"); + + vue_config.app_name = config.app_name; + vue_config.app_name_color = config.app_name_color || "#44b3e2"; + let html = $("#appconf-description").html(); + vue_config.app_description = html ? html : ""; for (var z in config.feeds) config.feedsbyname[config.feeds[z].name] = config.feeds[z]; for (var z in config.feeds) config.feedsbyid[config.feeds[z].id] = config.feeds[z]; @@ -471,7 +480,8 @@ var vue_config = new Vue({ el: '#vue-config', data: { app_name: "App Name", - app_description: "This app can be used to explore onsite solar generation, self consumption, battery integration, export and building consumption.", + app_name_color: "#44b3e2", + app_description: "", config_name: "", config_public: false, config_items: [], @@ -697,4 +707,4 @@ var vue_config = new Vue({ config.autogen.reset_feeds(); } } -}); +}); \ No newline at end of file diff --git a/Lib/appconf/appconf.php b/Lib/appconf/appconf.php index 60a4d5d5..6d1c218d 100644 --- a/Lib/appconf/appconf.php +++ b/Lib/appconf/appconf.php @@ -18,12 +18,12 @@
-

{{ app_name }}

-

{{ app_description }}

+

{{ app_name }}

+

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.

-
+

Auto generate kWh flow feeds

The following feeds are required for historic half-hourly and daily analysis. They are generated from the power feeds using feed post-processing. diff --git a/apps/OpenEnergyMonitor/myheatpump/myheatpump.php b/apps/OpenEnergyMonitor/myheatpump/myheatpump.php index 6d2b23b6..68bd5d06 100644 --- a/apps/OpenEnergyMonitor/myheatpump/myheatpump.php +++ b/apps/OpenEnergyMonitor/myheatpump/myheatpump.php @@ -6,9 +6,6 @@ - - - @@ -375,36 +372,23 @@

-
- -
-
-
-
-

-

The My Heatpump app can be used to explore the performance of a heatpump including, electricity consumption, heat output, COP and system temperatures.

-

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 created from power feeds using the power_to_kwh input processor, which converts power data (measured in watts) into energy consumption data (measured in kWh).

-

Share publicly: Check the "public" check box if you want to share your dashboard publicly, and ensure that the associated feeds are also made public by adjusting their settings on the feeds page.

-

Start date: To modify the start date for cumulative total electricity consumption, heat output and SCOP, input a unix timestamp corresponding to your desired starting date and time.

- - - - - -
-
-
-
-
-
- + +
@@ -165,21 +162,10 @@
- -
-
-
-
-
-

-

Calculate room air change rates form CO2 decay curves.

- -
-
-
-
-
-
+ +
@@ -188,6 +174,7 @@ var apikey = ""; var sessionwrite = ; + config.app_name = "CO2 Monitor"; config.id = ; config.name = ""; config.public = ; diff --git a/apps/OpenEnergyMonitor/costcomparison/costcomparison.php b/apps/OpenEnergyMonitor/costcomparison/costcomparison.php index 400d114c..4cf0bfdb 100644 --- a/apps/OpenEnergyMonitor/costcomparison/costcomparison.php +++ b/apps/OpenEnergyMonitor/costcomparison/costcomparison.php @@ -4,8 +4,6 @@ ?> - - @@ -102,22 +100,11 @@
-
- -
-
-
-

-

The Cost Comparison app allows you to compare your energy usage against energy suppliers tariffs including new time of use tariffs.

-

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.

-

Note: If you have solar or renewable energy generation then use the import_kwh feed to view the actual cost based on energy brought from the grid

-

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

- -
-
-
-
-
+ + @@ -46,20 +44,11 @@
-
- -
-
-
-

-

The feed-in tariff app is a simple home energy monitoring app to explore onsite energy generation, feed-in and self-consumption, as well as the buildings overall consumption and cost.

-

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.

-
-
-
-
-
+ + +
@@ -138,6 +127,7 @@ } }; +config.app_name = "Feed-in"; config.id = ; config.name = ""; config.public = ; diff --git a/apps/OpenEnergyMonitor/myboiler/myboiler.php b/apps/OpenEnergyMonitor/myboiler/myboiler.php index 6f9cdd15..7ad145db 100644 --- a/apps/OpenEnergyMonitor/myboiler/myboiler.php +++ b/apps/OpenEnergyMonitor/myboiler/myboiler.php @@ -5,8 +5,6 @@ - - @@ -258,26 +256,12 @@
-
- -
-
-
-
-

-

The My Boiler app can be used to explore the performance of a boiler including, fuel input, electricity consumption, heat output, efficiency and system temperatures.

-

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 created from power feeds using the power_to_kwh input processor, which converts power data (measured in watts) into energy consumption data (measured in kWh).

-

Share publicly: Check the "public" check box if you want to share your dashboard publicly, and ensure that the associated feeds are also made public by adjusting their settings on the feeds page.

-

Start date: To modify the start date for cumulative total fuel and electricity consumption, heat output and efficiency, input a unix timestamp corresponding to your desired starting date and time.

- -
-
-
-
-
-
- + +
@@ -285,6 +269,8 @@ var apikey = ""; var session_write = ; + config.app_name = "My Boiler"; + config.app_name_color = "#fb5e50"; config.id = ; config.name = ""; config.public = ; diff --git a/apps/OpenEnergyMonitor/myelectric/myelectric.php b/apps/OpenEnergyMonitor/myelectric/myelectric.php index a00c4926..084eefed 100644 --- a/apps/OpenEnergyMonitor/myelectric/myelectric.php +++ b/apps/OpenEnergyMonitor/myelectric/myelectric.php @@ -3,9 +3,6 @@ global $path, $session, $v; ?> - - - @@ -110,24 +107,10 @@ - -
- -
-
-
-
-

-

The My Electric app is a simple home energy monitoring app for exploring home or building electricity consumption over time. It includes a real-time view and a historic kWh per day bar graph.

-

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.

- -
-
-
-
-
-
+ +
@@ -175,6 +158,7 @@ function getTranslations(){ "kw":{"type":"checkbox", "default":0, "name": "Show kW", "description":tr("Display power as kW")} }; +config.app_name = "My Electric"; config.id = ; config.name = ""; config.public = ; diff --git a/apps/OpenEnergyMonitor/myelectric2/myelectric2.php b/apps/OpenEnergyMonitor/myelectric2/myelectric2.php index 64883d68..5b9a280c 100644 --- a/apps/OpenEnergyMonitor/myelectric2/myelectric2.php +++ b/apps/OpenEnergyMonitor/myelectric2/myelectric2.php @@ -4,9 +4,7 @@ ?> - - - + @@ -155,22 +153,12 @@ -
- -
-
-
-
-

-

The My Electric app is a simple home energy monitoring app for exploring home or building electricity consumption over time.

-

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.

-
-
-
-
-
-
+ + + +
@@ -206,6 +194,7 @@ "currency":{"type":"value", "default":"£", "name": "Currency", "description":"Currency symbol (£,$..)"}, "showcomparison":{"type":"checkbox", "default":false, "name": "Show comparison", "description":"Energy stack comparison"} }; +config.app_name = "My Electric v2"; config.id = ; config.name = ""; config.public = ; diff --git a/apps/OpenEnergyMonitor/myenergy/myenergy.php b/apps/OpenEnergyMonitor/myenergy/myenergy.php index 5f532594..8abfb7db 100644 --- a/apps/OpenEnergyMonitor/myenergy/myenergy.php +++ b/apps/OpenEnergyMonitor/myenergy/myenergy.php @@ -4,8 +4,6 @@ ?> - - @@ -93,21 +91,11 @@ -
- -
-
-
-

&

-

This app extends the My Solar app by adding in a 'share of UK wind' estimate.

-

The share of wind estimate is calculated by using real-time electricity data from wind power in the uk and then scaling it so that the annual wind generation matches a percentage of annual household consumption. The default estimate assumes 60% or near 2000 kWh annually. This is close to the fuel mix quoted by two of the UK's leading green electricity suppliers.

-

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.

-
-
-
-
-
+
+

This app extends the My Solar app by adding in a 'share of UK wind' estimate.

+

The share of wind estimate is calculated by using real-time electricity data from wind power in the uk and then scaling it so that the annual wind generation matches a percentage of annual household consumption. The default estimate assumes 60% or near 2000 kWh annually. This is close to the fuel mix quoted by two of the UK's leading green electricity suppliers.

+
+
@@ -152,6 +140,9 @@ function getTranslations(){ "kw":{"type":"checkbox", "default":0, "name": "Show kW", "description":tr("Display power as kW")} }; +config.app_name = "My Energy"; +config.app_name_color = "#5cb85c"; + config.id = ; config.name = ""; config.public = ; @@ -254,6 +245,7 @@ function resize() if (height>width) height = width; if (height<180) height = 180; + if (width<200) width = 200; placeholder.width(width); placeholder_bound.height(height); diff --git a/apps/OpenEnergyMonitor/myheatpump/myheatpump.js b/apps/OpenEnergyMonitor/myheatpump/myheatpump.js index a99bdd27..74863c73 100644 --- a/apps/OpenEnergyMonitor/myheatpump/myheatpump.js +++ b/apps/OpenEnergyMonitor/myheatpump/myheatpump.js @@ -129,12 +129,10 @@ function show() { if (!config.app.enable_process_daily.value) { $(".bargraph_mode").hide(); - $("#clear-daily-data").hide(); bargraph_mode = "combined"; } else { $(".bargraph_mode").show(); - $("#clear-daily-data").show(); } $("body").css('background-color', 'WhiteSmoke'); @@ -580,24 +578,6 @@ $('#placeholder').bind("plotselected", function (event, ranges) { setTimeout(function () { panning = false; }, 100); }); -$("#clear-daily-data").click(function () { - $.ajax({ - url: path + "app/cleardaily", - data: { id: config.id, apikey: apikey }, - async: true, - dataType: "json", - success: function (result) { - if (result.success) { - alert("Daily data cleared, please refresh the page to reload data"); - app_log("INFO", "Daily data cleared"); - } else { - alert("Failed to clear daily data"); - app_log("ERROR", "Failed to clear daily data"); - } - } - }); -}); - $("#show_dhw_temp").click(function () { if ($("#show_dhw_temp")[0].checked) { show_dhw_temp = true; diff --git a/apps/OpenEnergyMonitor/myheatpump/myheatpump.php b/apps/OpenEnergyMonitor/myheatpump/myheatpump.php index 68bd5d06..db720d00 100644 --- a/apps/OpenEnergyMonitor/myheatpump/myheatpump.php +++ b/apps/OpenEnergyMonitor/myheatpump/myheatpump.php @@ -376,7 +376,6 @@

The My Heatpump app can be used to explore the performance of a heatpump including, electricity consumption, heat output, COP and system temperatures.

Share publicly: Check the "public" check box if you want to share your dashboard publicly, and ensure that the associated feeds are also made public by adjusting their settings on the feeds page.

Start date: To modify the start date for cumulative total electricity consumption, heat output and SCOP, input a unix timestamp corresponding to your desired starting date and time.

- diff --git a/apps/OpenEnergyMonitor/mysolarpv/mysolarpv.php b/apps/OpenEnergyMonitor/mysolarpv/mysolarpv.php index 0b3a4569..06a2cf50 100644 --- a/apps/OpenEnergyMonitor/mysolarpv/mysolarpv.php +++ b/apps/OpenEnergyMonitor/mysolarpv/mysolarpv.php @@ -46,8 +46,6 @@ min-height:180px; } - - @@ -57,30 +55,32 @@ - + - -
- -
-
-
-
-

-

- The My Solar with Divert app can be used to explore onsite solar (and optionally wind) generation, self consumption, export and building consumption.

-

It is designed for users who divert some or all of their excess generated power to something. For example an immersion heater or electric car. It shows all of this both in realtime with a moving power graph view and historically with a daily and monthly bargraph. -

-

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.

-
-
-
-
-
-
- + +
@@ -366,6 +350,9 @@ function getTranslations(){ "public":{"type":"checkbox", "name": "Public", "default": 0, "optional":true, "description":"Make app public"} } +config.app_name = "My Solar PV Divert"; +config.app_name_color = "#dccc1f"; + config.id = ; config.name = ""; config.public = ; diff --git a/apps/OpenEnergyMonitor/profile/profile.php b/apps/OpenEnergyMonitor/profile/profile.php index 896b16ee..746963cc 100644 --- a/apps/OpenEnergyMonitor/profile/profile.php +++ b/apps/OpenEnergyMonitor/profile/profile.php @@ -3,9 +3,6 @@ global $path, $session, $v; ?> - - - @@ -55,22 +52,10 @@ -
- -
-
-
-
-

-

- Explore average daily profiles for different months of the year. -

-
-
-
-
-
-
+ +
@@ -114,6 +99,7 @@ "feed_type":{"type":"select", "options": ['Cumulative kWh','Power (W)','Other'], "name": "Feed type", "default": 'Cumulative kWh'}, "public":{"type":"checkbox", "name": "Public", "default": 0, "optional":true, "description":"Make app public"} }; +config.app_name = "Profile Explorer"; config.id = ; config.name = ""; config.public = ; diff --git a/apps/OpenEnergyMonitor/solarbatterysim/solarbatterysim.php b/apps/OpenEnergyMonitor/solarbatterysim/solarbatterysim.php index 96ad8ba7..d1006aec 100644 --- a/apps/OpenEnergyMonitor/solarbatterysim/solarbatterysim.php +++ b/apps/OpenEnergyMonitor/solarbatterysim/solarbatterysim.php @@ -4,8 +4,6 @@ ?> - - @@ -238,15 +236,10 @@ - + +
@@ -143,6 +147,8 @@ // Transfer php variables to javascript var apikey = ""; var sessionwrite = ; + + config.app_name = "Solar Template App"; config.id = ; config.name = ""; config.public = ; diff --git a/apps/OpenEnergyMonitor/storagesim/storagesim.js b/apps/OpenEnergyMonitor/storagesim/storagesim.js index 111ea1c4..77572d7c 100644 --- a/apps/OpenEnergyMonitor/storagesim/storagesim.js +++ b/apps/OpenEnergyMonitor/storagesim/storagesim.js @@ -17,13 +17,6 @@ config.app = { "type": "feed", "autoname": "solar", "optional": true - }, - "public": { - "type": "checkbox", - "name": "Public", - "default": 0, - "optional": true, - "description": "Make app public" } }; diff --git a/apps/OpenEnergyMonitor/storagesim/storagesim.php b/apps/OpenEnergyMonitor/storagesim/storagesim.php index abcca072..0e386da6 100644 --- a/apps/OpenEnergyMonitor/storagesim/storagesim.php +++ b/apps/OpenEnergyMonitor/storagesim/storagesim.php @@ -4,9 +4,6 @@ ?> - - - @@ -291,15 +288,10 @@ - - -
- -
-
-
-

-

The My Electric app is a simple home energy monitoring app for exploring home or building electricity consumption over time.

-

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.

-
-
-
-
-
- - + +
@@ -227,6 +212,7 @@ "public":{"type":"checkbox", "name": "Public", "default": 0, "optional":true, "description":"Make app public"} }; +config.app_name = "Time of Use"; config.id = ; config.name = ""; config.public = ; diff --git a/apps/OpenEnergyMonitor/timeofuse2/timeofuse2.php b/apps/OpenEnergyMonitor/timeofuse2/timeofuse2.php index c64fdf34..49dd6748 100644 --- a/apps/OpenEnergyMonitor/timeofuse2/timeofuse2.php +++ b/apps/OpenEnergyMonitor/timeofuse2/timeofuse2.php @@ -5,8 +5,7 @@ - - + @@ -160,67 +159,59 @@ + + -
- -
-
-
-

-

The "Time of Use - flexible" app is a simple home energy monitoring app for exploring home or building electricity consumption and cost over time. It allows you to track multiple electricity tariffs as used in Australia.

-

Cumulative kWh

-

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

-

-

As the number of configuration options for this are quite large, a shorthand has been used to specify - the tiers, days and times they apply and the respective costs.

- -

Assumptions

-
    -
  • Any number of tariffs can be defined, but they must be consistent across weekdays or weekends.
  • -
  • One cost must be defined per tariff tier.
  • -
  • Each weekday (Monday to Friday) has the same tiers and times for each tier.
  • -
  • Each weekend day (Saturday and Sunday) has the same tiers and times for each tier.
  • -
  • Public Holidays are treated the same as a weekend day.
  • -
- -

Shorthand

-

Tier names and tariffs are specified as a comma separated, colon separated list. If there are three - tariffs, Off Peak, Shoulder and Peak, costing 16.5c/kWh, 25.3c/kWh and 59.4c/kWh respectively, they - are specified as:

-

OffPeak:0.165,Shoulder:0.253,Peak:0.594

-

Tier start times are split into two definitions, weekday and weekend. They both use the same format, - <start hour>:<tier>,<start hour>:<tier>,... - <tier> - is the tier number defined above, numbered from 0

- -
-

Example:

-

A weekday with the following tariff times:

-
- OffPeak: 00:00 - 06:59, - Shoulder: 07:00 - 13:59, - Peak: 14:00 - 19:59, - Shoulder: 20:00 - 21:59, - OffPeak: 22:00 - 23:59 -
-

would be defined as: - 0:0,7:1,14:2,20:1,22:0

- -

To specify the public holidays that should be treated the same as weekends, specify a comma separated - list of days of the year (from 1-365/366) per year. - -


-

Example:

-

for public holiays 2017: Jan 2, Apr 14, Apr 17, Apr 25, Jun 12, Oct 2, Dec 25, Dec 26; and 2018: Jan 1 you would specify:

- - 2017:2,104,107,115,163,275,359,360;2018:1 - -

https://www.epochconverter.com/days provides an easy reference.

-
-
-
-
-
@@ -268,6 +259,7 @@ "public":{"type":"checkbox", "name": "Public", "default": 0, "optional":true, "description":"Make app public"} }; +config.app_name = "Time of Use - flexible"; config.id = ; config.name = ""; config.public = ; diff --git a/apps/OpenEnergyMonitor/timeofusecl/timeofusecl.php b/apps/OpenEnergyMonitor/timeofusecl/timeofusecl.php index 7e5c70f0..de948dfe 100644 --- a/apps/OpenEnergyMonitor/timeofusecl/timeofusecl.php +++ b/apps/OpenEnergyMonitor/timeofusecl/timeofusecl.php @@ -5,8 +5,6 @@ - - @@ -159,48 +157,38 @@ - - -
- -
-
-
-

-

The "Time of Use - flexible + CL" app is a simple home energy monitoring app for exploring home or building electricity consumption and cost over time.

-

It allows you to track multiple electricity tariffs as used in Australia. This version adds a daily supply charge and a separately monitored controlled load (such as off-peak hot water).

-

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

-

-

As the number of configuration options for this are quite large, a shorthand has been used to specify - the tiers, days and times they apply and the respective costs.

-

Assumptions: -

    -
  • Any number of tariffs can be defined, but they must be consistent across weekdays or weekends.
  • -
  • One cost must be defined per tariff tier.
  • -
  • Each weekday (Monday to Friday) has the same tiers and times for each tier.
  • -
  • Each weekend day (Saturday and Sunday) has the same tiers and times for each tier.
  • -
  • Public Holidays are treated the same as a weekend day.
  • -
-

Shorthand

-

Tier names and tariffs are specified as a comma separated, colon separated list. If there are three - tariffs, Off Peak, Shoulder and Peak, costing 16.5c/kWh, 25.3c/kWh and 59.4c/kWh respectively, they - are specified as OffPeak:0.165,Shoulder:0.253,Peak:0.594

-

Tier start times are split into two definitions, weekday and weekend. They both use the same format, - <start hour>:<tier>,<start hour>:<tier>,...
- <tier> is the tier number defined above, numbered from 0
- Example: A weekday with the following tariff times: OffPeak: 00:00 - 06:59, Shoulder: 07:00 - - 13:59, Peak: 14:00 - 19:59, Shoulder: 20:00 - 21:59, OffPeak: 22:00 - 23:59 would be defined as - 0:0,7:1,14:2,20:1,22:0

-

To specify the public holidays that should be treated the same as weekends, specify a comma separated - list of days of the year (from 1-365/366) per year. Example: for public holiays 2017: Jan 2, Apr 14, - Apr 17, Apr 25, Jun 12, Oct 2, Dec 25, Dec 26; and 2018: Jan 1 you would specify - 2017:2,104,107,115,163,275,359,360;2018:1
- https://www.epochconverter.com/days provides an easy reference.

-
-
-
-
-
+ +
@@ -254,6 +242,8 @@ "public":{"type":"checkbox", "name": "Public", "default": 0, "optional":true, "description":"Make app public"} }; +config.app_name = "Time of Use - flexible + CL"; + config.id = ; config.name = ""; config.public = ; diff --git a/apps/OpenEnergyMonitor/ukgrid/ukgrid.php b/apps/OpenEnergyMonitor/ukgrid/ukgrid.php index 8efd94f7..020b73a6 100644 --- a/apps/OpenEnergyMonitor/ukgrid/ukgrid.php +++ b/apps/OpenEnergyMonitor/ukgrid/ukgrid.php @@ -6,9 +6,6 @@ ?> - - - @@ -100,20 +97,10 @@

- -
-
-
-
-
-

-

Explore the UK grid fuel mix and wind and solar forecast.

-
-
-
-
-
-
+ +
@@ -132,15 +119,8 @@ // ---------------------------------------------------------------------- // Configuration // ---------------------------------------------------------------------- - config.app = { - "public": { - "type": "checkbox", - "name": "Public", - "default": 0, - "optional": true, - "description": "Make app public" - } - }; + config.app = {}; + config.app_name = "UK Grid Visualisation"; config.id = ; config.name = ""; config.public = ; diff --git a/apps/template/template.php b/apps/template/template.php index 2cbeb87f..db4f318d 100644 --- a/apps/template/template.php +++ b/apps/template/template.php @@ -3,9 +3,6 @@ global $path, $session, $v; ?> - - - @@ -71,22 +68,10 @@
- - -
-
-
-
-
-

-

A basic app example useful for developing new apps.

- -
-
-
-
-
-
+ +
@@ -94,6 +79,7 @@ // Transfer php variables to javascript var apikey = ""; var sessionwrite = ; + config.app_name = "Template App"; config.id = ; config.name = ""; config.public = ; From 71cfd8838ad97885bd0c48966ad7bae657c13fa7 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Wed, 8 Apr 2026 10:21:13 +0100 Subject: [PATCH 064/110] remove duplicate public entries --- .../OpenEnergyMonitor/mysolarpvdivert/mysolarpvdivert.php | 3 +-- apps/OpenEnergyMonitor/octopus/tariff_explorer.js | 8 -------- .../OpenEnergyMonitor/solarbatterysim/solarbatterysim.php | 3 +-- apps/OpenEnergyMonitor/timeofuse/timeofuse.php | 4 +--- apps/OpenEnergyMonitor/timeofuse2/timeofuse2.php | 4 +--- apps/OpenEnergyMonitor/timeofusecl/timeofusecl.php | 4 +--- 6 files changed, 5 insertions(+), 21 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvdivert/mysolarpvdivert.php b/apps/OpenEnergyMonitor/mysolarpvdivert/mysolarpvdivert.php index 87bc40bf..2f68f577 100644 --- a/apps/OpenEnergyMonitor/mysolarpvdivert/mysolarpvdivert.php +++ b/apps/OpenEnergyMonitor/mysolarpvdivert/mysolarpvdivert.php @@ -345,9 +345,8 @@ function getTranslations(){ "wind_kwh":{"optional":true, "type":"feed", "autoname":"wind_kwh", "description":"Cumulative wind generation in kWh"}, "divert_kwh":{"optional":true, "type":"feed", "autoname":"divert_kwh", "description":"Cumulative divert energy in kWh"}, "import_kwh":{"optional":true, "type":"feed", "autoname":"import_kwh", "description":"Cumulative grid import in kWh"}, - "kw":{"type":"checkbox", "default":0, "name": "Show kW", "description":tr("Display power as kW")}, + "kw":{"type":"checkbox", "default":0, "name": "Show kW", "description":tr("Display power as kW")} //"import_unitcost":{"type":"value", "default":0.1508, "name": "Import unit cost", "description":"Unit cost of imported grid electricity"} - "public":{"type":"checkbox", "name": "Public", "default": 0, "optional":true, "description":"Make app public"} } config.app_name = "My Solar PV Divert"; diff --git a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js index ba614620..e4ff7fed 100644 --- a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js +++ b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js @@ -163,14 +163,6 @@ config.app = { "name": "Select tariff B:", "default": "INTELLI-VAR-22-10-14", "options": tariff_options - }, - - "public": { - "type": "checkbox", - "name": "Public", - "default": 0, - "optional": true, - "description": "Make app public" } }; diff --git a/apps/OpenEnergyMonitor/solarbatterysim/solarbatterysim.php b/apps/OpenEnergyMonitor/solarbatterysim/solarbatterysim.php index d1006aec..21341351 100644 --- a/apps/OpenEnergyMonitor/solarbatterysim/solarbatterysim.php +++ b/apps/OpenEnergyMonitor/solarbatterysim/solarbatterysim.php @@ -277,8 +277,7 @@ "solar":{"type":"feed", "autoname":"solar_kwh","optional":true}, "solar_capacity":{"type":"value", "name": "Solar capacity (W)", "default": 1250, "optional":true, "description":"Enter solar capacity"}, "public_solar_feed":{"type":"value", "name": "Public solar feed id", "default": 462987, "optional":true, "description":"Hosted on emoncms.org"}, - "public_solar_capacity":{"type":"value", "name": "Public solar feed id", "default": 1250, "optional":true, "description":"Solar capacity of public dataset"}, - "public":{"type":"checkbox", "name": "Public", "default": 0, "optional":true, "description":"Make app public"} + "public_solar_capacity":{"type":"value", "name": "Public solar feed id", "default": 1250, "optional":true, "description":"Solar capacity of public dataset"} }; config.app_name = "Solar & Battery Simulator"; config.id = ; diff --git a/apps/OpenEnergyMonitor/timeofuse/timeofuse.php b/apps/OpenEnergyMonitor/timeofuse/timeofuse.php index 784745d5..db79f561 100644 --- a/apps/OpenEnergyMonitor/timeofuse/timeofuse.php +++ b/apps/OpenEnergyMonitor/timeofuse/timeofuse.php @@ -207,9 +207,7 @@ "unitcost_day":{"type":"value", "default":0.15, "name": "Day time unit cost", "description":"Day time unit cost of electricity £/kWh"}, "unitcost_night":{"type":"value", "default":0.07, "name": "Night time unit cost", "description":"Night time unit cost of electricity £/kWh"}, - "currency":{"type":"value", "default":"£", "name": "Currency", "description":"Currency symbol (£,$..)"}, - - "public":{"type":"checkbox", "name": "Public", "default": 0, "optional":true, "description":"Make app public"} + "currency":{"type":"value", "default":"£", "name": "Currency", "description":"Currency symbol (£,$..)"} }; config.app_name = "Time of Use"; diff --git a/apps/OpenEnergyMonitor/timeofuse2/timeofuse2.php b/apps/OpenEnergyMonitor/timeofuse2/timeofuse2.php index 49dd6748..29b7f707 100644 --- a/apps/OpenEnergyMonitor/timeofuse2/timeofuse2.php +++ b/apps/OpenEnergyMonitor/timeofuse2/timeofuse2.php @@ -254,9 +254,7 @@ "description":"List of weekend tier start times. See description on the left for details"}, "ph_days":{"type":"value", "default":"2017:2,104,107,115,163,275,359,360;2018:1", "name":"Public Holiday days", - "description":"List of public holidays. See description on the left for details"}, - - "public":{"type":"checkbox", "name": "Public", "default": 0, "optional":true, "description":"Make app public"} + "description":"List of public holidays. See description on the left for details"} }; config.app_name = "Time of Use - flexible"; diff --git a/apps/OpenEnergyMonitor/timeofusecl/timeofusecl.php b/apps/OpenEnergyMonitor/timeofusecl/timeofusecl.php index de948dfe..d2846de4 100644 --- a/apps/OpenEnergyMonitor/timeofusecl/timeofusecl.php +++ b/apps/OpenEnergyMonitor/timeofusecl/timeofusecl.php @@ -237,9 +237,7 @@ "cl_use":{"type":"feed", "autoname":"cl_use", "engine":5, "description":"Controlled Load power feed (W)"}, "cl_kwh":{"type":"feed", "autoname":"cl_kwh", "engine":5, "description":"Controlled Load accumulated kWh"}, "cl_cost":{"type":"value", "default":"0.17", "name":"Controlled Load Cost", - "description":"Cost of the controlled Load accumulated kWh, currency/kWh."}, - - "public":{"type":"checkbox", "name": "Public", "default": 0, "optional":true, "description":"Make app public"} + "description":"Cost of the controlled Load accumulated kWh, currency/kWh."} }; config.app_name = "Time of Use - flexible + CL"; From 085b85cf9c221c616f510f15b2d2fd943346ad61 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Wed, 8 Apr 2026 10:53:24 +0100 Subject: [PATCH 065/110] fix current half hour import/export and tariff price --- apps/OpenEnergyMonitor/octopus/octopus.php | 2 +- .../octopus/tariff_explorer.js | 42 +++++++++++++------ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/apps/OpenEnergyMonitor/octopus/octopus.php b/apps/OpenEnergyMonitor/octopus/octopus.php index d99b5429..b34bc25e 100644 --- a/apps/OpenEnergyMonitor/octopus/octopus.php +++ b/apps/OpenEnergyMonitor/octopus/octopus.php @@ -35,7 +35,7 @@ diff --git a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js index e4ff7fed..3ac61b4b 100644 --- a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js +++ b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js @@ -429,6 +429,22 @@ function updater() { for (var key in config.app) { if (config.app[key].value) feeds[key] = result[config.app[key].value]; } + + // Update live stats value for current half-hour if feeds are present + // use grid feed for import reference. + if (config.app.grid.value != undefined) { + var grid_feed_id = config.app.grid.value; + var grid_value = result[grid_feed_id] != undefined ? result[grid_feed_id].value : null; + if (grid_value != null) { + if (grid_value < 0) { + $("#import_export").html("EXPORT NOW"); + } else { + $("#import_export").html("IMPORT NOW"); + } + $("#power_now").html(Math.abs(grid_value) + " W"); + } + } + }); } @@ -930,27 +946,29 @@ function graph_draw() { profile_mode = false; $("#history-title").html("HISTORY"); - /* + if (this_halfhour_index != -1) { - let kwh_last_halfhour = data["import"][this_halfhour_index][1]; + let kwh_grid_to_load = get_data_value_at_index("grid_to_load", this_halfhour_index); + let kwh_grid_to_battery = get_data_value_at_index("grid_to_battery", this_halfhour_index); + let kwh_last_halfhour = (kwh_grid_to_load != null ? kwh_grid_to_load : 0) + + (kwh_grid_to_battery != null ? kwh_grid_to_battery : 0); - if (kwh_last_halfhour != null) { - $("#kwh_halfhour").html(kwh_last_halfhour.toFixed(2) + "kWh"); - } else { - $("#kwh_halfhour").html("N/A"); - } + $("#kwh_halfhour").html(kwh_last_halfhour.toFixed(2) + "kWh"); - let cost_last_halfhour = data["import_cost_tariff_A"][this_halfhour_index][1] * 100; - $("#cost_halfhour").html("(" + cost_last_halfhour.toFixed(2) + "p)"); + let tariff_A_unit = get_data_value_at_index("tariff_A", this_halfhour_index); + if (tariff_A_unit != null) { + let cost_last_halfhour = kwh_last_halfhour * tariff_A_unit; + $("#cost_halfhour").html("(" + cost_last_halfhour.toFixed(2) + "p)"); - let unit_price = data["tariff_A"][this_halfhour_index][1] * 1.05; - $("#unit_price").html(unit_price.toFixed(2) + "p"); + let unit_price = tariff_A_unit * 1.05; + $("#unit_price").html(unit_price.toFixed(2) + "p"); + } $(".last_halfhour_stats").show(); } else { $(".last_halfhour_stats").hide(); - }*/ + } var bars = { show: true, From ed464a8ca5bf9cc4fe0cc079b30071580db18d25 Mon Sep 17 00:00:00 2001 From: TrystanLea Date: Wed, 8 Apr 2026 19:58:28 +0100 Subject: [PATCH 066/110] fix tariff app --- .../myheatpump/myheatpump.php | 2 +- .../mysolarpvbattery/mysolarpvbattery.php | 2 +- .../mysolarpvbattery_controller.php | 4 ++-- .../octopus/tariff_explorer.js | 21 ++++++++++++------- 4 files changed, 18 insertions(+), 11 deletions(-) diff --git a/apps/OpenEnergyMonitor/myheatpump/myheatpump.php b/apps/OpenEnergyMonitor/myheatpump/myheatpump.php index db720d00..3d0a699e 100644 --- a/apps/OpenEnergyMonitor/myheatpump/myheatpump.php +++ b/apps/OpenEnergyMonitor/myheatpump/myheatpump.php @@ -394,7 +394,7 @@ config.db = ; - + diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 33c188d2..2cfdb83b 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -202,4 +202,4 @@ function getTranslations(){ ] - \ No newline at end of file + \ No newline at end of file diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php index dd4e952a..4022be96 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php @@ -57,7 +57,7 @@ function mysolarpvbattery_app_controller($route,$app,$appconfig,$apikey) "solar" => (int) isset($app->config->solar) ? $app->config->solar : 0, "use" => (int) isset($app->config->use) ? $app->config->use : 0, "grid" => (int) isset($app->config->grid) ? $app->config->grid : 0, - "battery_power" => (int) isset($app->config->battery_power) ? $app->config->battery_power : 0, + "battery" => (int) isset($app->config->battery) ? $app->config->battery : 0, "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"), @@ -78,4 +78,4 @@ function mysolarpvbattery_app_controller($route,$app,$appconfig,$apikey) ob_end_clean(); return $result; } -} \ No newline at end of file +} diff --git a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js index 3ac61b4b..99732f33 100644 --- a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js +++ b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js @@ -646,13 +646,13 @@ function graph_load() { // Read half-hourly energy flow values directly from post-processed feeds (delta=1) // Clamp negatives to zero for safety - kwh_solar_to_load = Math.max(0, solar_to_load_kwh_data[z][1] != null ? solar_to_load_kwh_data[z][1] : 0); - kwh_solar_to_grid = Math.max(0, solar_to_grid_kwh_data[z][1] != null ? solar_to_grid_kwh_data[z][1] : 0); - kwh_solar_to_battery = Math.max(0, solar_to_battery_kwh_data[z][1] != null ? solar_to_battery_kwh_data[z][1] : 0); - kwh_battery_to_load = Math.max(0, battery_to_load_kwh_data[z][1] != null ? battery_to_load_kwh_data[z][1] : 0); - kwh_battery_to_grid = Math.max(0, battery_to_grid_kwh_data[z][1] != null ? battery_to_grid_kwh_data[z][1] : 0); - kwh_grid_to_load = Math.max(0, grid_to_load_kwh_data[z][1] != null ? grid_to_load_kwh_data[z][1] : 0); - kwh_grid_to_battery = Math.max(0, grid_to_battery_kwh_data[z][1] != null ? grid_to_battery_kwh_data[z][1] : 0); + kwh_solar_to_load = Math.max(0, get_value_at_index(solar_to_load_kwh_data, z, 0)); + kwh_solar_to_grid = Math.max(0, get_value_at_index(solar_to_grid_kwh_data, z, 0)); + kwh_solar_to_battery = Math.max(0, get_value_at_index(solar_to_battery_kwh_data, z, 0)); + kwh_battery_to_load = Math.max(0, get_value_at_index(battery_to_load_kwh_data, z, 0)); + kwh_battery_to_grid = Math.max(0, get_value_at_index(battery_to_grid_kwh_data, z, 0)); + kwh_grid_to_load = Math.max(0, get_value_at_index(grid_to_load_kwh_data, z, 0)); + kwh_grid_to_battery = Math.max(0, get_value_at_index(grid_to_battery_kwh_data, z, 0)); // Derive aggregate values from flows kwh_import = kwh_grid_to_load + kwh_grid_to_battery; @@ -730,6 +730,13 @@ function graph_load() { draw_tables(total, monthly_data); } +function get_value_at_index(data_array, index, default_value = null) { + if (data_array[index] != undefined && data_array[index][1] != null) { + return data_array[index][1]; + } + return default_value; +} + function accumulate_flows(bucket, flows, outgoing_unit, unitcost_tariff_A, unitcost_tariff_B) { bucket.solar_to_load_kwh += flows.solar_to_load; bucket.solar_to_grid_kwh += flows.solar_to_grid; From fb951c182f0b27823e2b424a44ae7733e594e416 Mon Sep 17 00:00:00 2001 From: TrystanLea Date: Thu, 9 Apr 2026 07:47:59 +0100 Subject: [PATCH 067/110] process in 10s chunks, fix autogen feed allocation --- Lib/appconf/appconf.js | 71 +++++++++++++++++-- .../mysolarpvbattery/mysolarpvbattery.js | 6 +- .../octopus/octopus_controller.php | 2 +- .../octopus/tariff_explorer.js | 7 +- 4 files changed, 77 insertions(+), 9 deletions(-) diff --git a/Lib/appconf/appconf.js b/Lib/appconf/appconf.js index 555496be..ce26d973 100644 --- a/Lib/appconf/appconf.js +++ b/Lib/appconf/appconf.js @@ -307,6 +307,9 @@ var config = { // ----------------------------------------------------------------------- autogen: { + post_process_already_running: false, + post_process_run_count: 0, + // Return the node tag string used for all auto-generated feeds node_name: function() { return config.app.autogenerate_nodename.value; @@ -353,6 +356,7 @@ var config = { // Render the autogen feed list — delegates to vue_config render_feed_list: function() { + config.autogen.refresh_autogen_feed_references(); vue_config.renderAutogenFeedList(); }, @@ -391,6 +395,7 @@ var config = { config.autogen_feeds_by_tag_name = feed.by_tag_and_name(config.feeds); // Populate config.app[key].value for the newly created autogen feeds + config.autogen.refresh_autogen_feed_references(); config.load(); config.autogen.render_feed_list(); @@ -402,10 +407,30 @@ var config = { vue_config.autogen_status_color = errors === 0 ? "#5cb85c" : "#f0ad4e"; }, + start_post_processor: function() { + if (config.autogen.post_process_already_running) { + vue_config.autogen_status = "Post-processor is already running, please wait..."; + vue_config.autogen_status_color = "#f0ad4e"; + return false; + } else { + config.autogen.post_process_already_running = true; + } + config.autogen.post_process_run_count = 0; + config.autogen.run_post_processor(); + }, + // Trigger the app post-processor via app/process run_post_processor: function() { - vue_config.autogen_status = "Starting post-processor..."; - vue_config.autogen_status_color = "#aaa"; + + if (config.autogen.post_process_run_count == 0) { + vue_config.autogen_status = "Starting post-processor..."; + vue_config.autogen_status_color = "#aaa"; + } else { + vue_config.autogen_status = "Post-processor is still running, please wait..."; + vue_config.autogen_status_color = "#f0ad4e"; + } + + config.autogen.post_process_run_count++; $.ajax({ url: path + "app/process", @@ -414,17 +439,38 @@ var config = { timeout: 120000, success: function(result) { if (result && result.success) { - vue_config.autogen_status = "Post-processor completed successfully."; - vue_config.autogen_status_color = "#5cb85c"; + + if (result.more_to_process) { + vue_config.autogen_status = "Post-processor is still running, please wait..."; + vue_config.autogen_status_color = "#f0ad4e"; + // run post processor again but only a maximum of 30 times (5 mins) to prevent infinite loops in case of an issue + if (config.autogen.post_process_run_count < 30) { + setTimeout(function() { + config.autogen.run_post_processor(); + }, 1000); + } else { + vue_config.autogen_status = "Post-processor is taking longer than expected, please check the app logs."; + vue_config.autogen_status_color = "#f0ad4e"; + config.autogen.post_process_already_running = false; + } + } else { + vue_config.autogen_status = "Post-processor completed successfully."; + vue_config.autogen_status_color = "#5cb85c"; + config.autogen.post_process_already_running = false; + } + + } else { var msg = (result && result.message) ? result.message : "Unknown response"; vue_config.autogen_status = "Post-processor: " + msg; vue_config.autogen_status_color = "#f0ad4e"; + config.autogen.post_process_already_running = false; } }, error: function(xhr) { vue_config.autogen_status = "Post-processor failed: " + xhr.statusText; vue_config.autogen_status_color = "#d9534f"; + config.autogen.post_process_already_running = false; }, complete: function() { } }); @@ -471,6 +517,21 @@ var config = { : "Cleared " + cleared + " feed(s), " + errors + " error(s)."; vue_config.autogen_status = statusText; vue_config.autogen_status_color = errors === 0 ? "#5cb85c" : "#f0ad4e"; + }, + + // Update config.app[key].value for each autogen feed based on current feedid lookups so that the UI shows correct feed selections + refresh_autogen_feed_references: function() { + var feeds = config.autogen.get_feeds(); + for (var i in feeds) { + var f = feeds[i]; + if (f.feedid) { + config.app[f.key].value = f.feedid; + config.db[f.key] = f.feedid; + } else { + delete config.db[f.key]; + config.app[f.key].value = false; + } + } } }, @@ -729,7 +790,7 @@ var vue_config = new Vue({ }, runPostProcessor: function() { - config.autogen.run_post_processor(); + config.autogen.start_post_processor(); }, resetFeeds: function() { diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index 329ed801..e3004030 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -340,7 +340,9 @@ function show() live = setInterval(livefn,5000); // Trigger process here - run_post_processor(); + setTimeout(function() { + start_post_processor(); + }, 1000); } // ------------------------------------------------------------------------------------------------------- @@ -1251,5 +1253,5 @@ function render_autogen_feed_list() { // (delegate to config.autogen.* in appconf.js) // ---------------------------------------------------------------------- function create_missing_feeds() { config.autogen.create_missing_feeds(); } -function run_post_processor() { config.autogen.run_post_processor(); } +function start_post_processor() { config.autogen.start_post_processor(); } function reset_feeds() { config.autogen.reset_feeds(); } \ No newline at end of file diff --git a/apps/OpenEnergyMonitor/octopus/octopus_controller.php b/apps/OpenEnergyMonitor/octopus/octopus_controller.php index d8a61acb..d97dda2d 100644 --- a/apps/OpenEnergyMonitor/octopus/octopus_controller.php +++ b/apps/OpenEnergyMonitor/octopus/octopus_controller.php @@ -57,7 +57,7 @@ function octopus_app_controller($route,$app,$appconfig,$apikey) "solar" => (int) isset($app->config->solar) ? $app->config->solar : 0, "use" => (int) isset($app->config->use) ? $app->config->use : 0, "grid" => (int) isset($app->config->grid) ? $app->config->grid : 0, - "battery_power" => (int) isset($app->config->battery_power) ? $app->config->battery_power : 0, + "battery" => (int) isset($app->config->battery) ? $app->config->battery : 0, "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"), diff --git a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js index 99732f33..04329392 100644 --- a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js +++ b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js @@ -355,6 +355,11 @@ function show() { updater(); updaterinst = setInterval(updater, 5000); $(".ajax-loader").hide(); + + // Trigger process here + setTimeout(function() { + start_post_processor(); + }, 1000); } function setPeriod(period) { @@ -1598,5 +1603,5 @@ function render_autogen_feed_list() { // (delegate to config.autogen.* in appconf.js) // ---------------------------------------------------------------------- function create_missing_feeds() { config.autogen.create_missing_feeds(); } -function run_post_processor() { config.autogen.run_post_processor(); } +function start_post_processor() { config.autogen.start_post_processor(); } function reset_feeds() { config.autogen.reset_feeds(); } \ No newline at end of file From b068cb608c8852b6d5cef3ad445d973c6fe4b53f Mon Sep 17 00:00:00 2001 From: TrystanLea Date: Thu, 9 Apr 2026 09:10:17 +0100 Subject: [PATCH 068/110] change feedin required feeds --- apps/OpenEnergyMonitor/feedin/feedin.php | 39 ++++++------------- .../mysolarpvbattery/mysolarpvbattery.js | 15 +++++++ .../mysolarpvbattery/mysolarpvbattery.php | 8 ++-- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/apps/OpenEnergyMonitor/feedin/feedin.php b/apps/OpenEnergyMonitor/feedin/feedin.php index 8cee0da4..f3ee0c95 100644 --- a/apps/OpenEnergyMonitor/feedin/feedin.php +++ b/apps/OpenEnergyMonitor/feedin/feedin.php @@ -77,34 +77,19 @@ "name": "Title", "description": "Optional title for app" }, - "solar_power": { + "solar": { "type": "feed", - "autoname": "solar_power", + "autoname": "solar", "optional": true }, - "solar_energy": { + "import": { "type": "feed", - "autoname": "solar_energy", - "optional": true - }, - "import_power": { - "type": "feed", - "autoname": "import_power", + "autoname": "import", "optional": false }, - "import_energy": { - "type": "feed", - "autoname": "import_energy", - "optional": false - }, - "export_power": { - "type": "feed", - "autoname": "export_power", - "optional": true - }, - "export_energy": { + "export": { "type": "feed", - "autoname": "export_energy", + "autoname": "export", "optional": true }, "import_cost": { @@ -187,7 +172,7 @@ function setup() { data.setup(Graph.POWER, [Graph.SOLAR, Graph.IMPORT, Graph.EXPORT], config); data.setup(Graph.ENERGY, [Graph.SOLAR, Graph.IMPORT, Graph.EXPORT], config); data.register([Graph.SOLAR, Graph.IMPORT, Graph.EXPORT, - "solar_power", "import_power", "export_power"], config); + "solar", "import", "export"], config); return graph.setup(data, config).then(function(result) { update(); @@ -245,17 +230,17 @@ function drawPowerValues(values) { } return parseFloat(values[key][1]); }; - var imp = getPowerValue("import_power", values); + var imp = getPowerValue("import", values); if (imp == null) { return; } var cons = imp; - var solar = getPowerValue("solar_power", values); + var solar = getPowerValue("solar", values); if (solar == null) { solar = 0; } else { - var exp = getPowerValue("export_power", values); + var exp = getPowerValue("export", values); if (exp != null) { var selfCons = Math.max(0, solar - exp); if (selfCons != null) { @@ -281,7 +266,7 @@ function drawPowerValues(values) { $("#cons-power").html(cons.toFixed(fixed)+""+unit+""); $(".consumption.power").removeClass('cost').show(); - if (values["solar_power"] != undefined && solar != null) { + if (values["solar"] != undefined && solar != null) { $("#gen-power").html(solar.toFixed(fixed)+""+unit+""); $(".generation.power").removeClass('cost').show(); } @@ -304,7 +289,7 @@ function drawPowerValues(values) { $("#cons-power").html(config.app.currency.value+costNow.toFixed(fixed)+"/hr"); $(".consumption.power").addClass('cost').show(); - if (values["solar_power"] != undefined && solar != null && + if (values["solar"] != undefined && solar != null && config.app.export_cost.value > 0) { var fitNow = solar*config.app.export_cost.value*0.001; diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index e3004030..ab4e3e8a 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -327,6 +327,7 @@ function show() flow_available(); + solar_battery_visibility(); if (check_history_feeds(mode)) { if (!bargraph_initialized) init_bargraph(); @@ -630,6 +631,20 @@ function livefn() // Only redraw the graph if its the power graph and auto update is turned on if (viewmode=="powergraph" && autoupdate) draw(true); + // If + +} + +function solar_battery_visibility() { + if (available.solar) { + $("#live-solar-title").addClass("text-light"); + $("#live-solar-value").addClass("text-warning"); + $("#solar-box").css("background-color", "#dccc1f"); + } else { + $("#live-solar-title").removeClass("text-light"); + $("#live-solar-value").removeClass("text-warning"); + $("#solar-box").css("background-color", "#262626"); + } } // Capacity in kWh, power in W, returns time left as string "Xh Ym" diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 2cfdb83b..391bafd3 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -43,8 +43,8 @@

-
-

+
+

@@ -68,7 +68,7 @@
-
IMPORT NOW
+
IMPORT NOW
0
- @@ -125,7 +125,7 @@ @@ -199,7 +199,7 @@ function getTranslations(){ config.public = ; config.db = ; -] + From 5a84b0b6dbed3de56c0b7fb70e727b2e518164b8 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Thu, 9 Apr 2026 10:59:37 +0100 Subject: [PATCH 071/110] only show soc if less than a month --- apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js | 5 ++++- apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index 298ef7a0..2b73260a 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -815,7 +815,10 @@ function load_powergraph() { powerseries.push({data: grid_to_battery_data, label: "Grid to Battery", color: "#fb7b50", stack: 1, lines: {lineWidth: 0, fill: 0.8}}); if (battery_soc_available) { - powerseries.push({data:battery_soc_data, label: "SOC", yaxis:2, color: "#888"}); + // only add if time period is less or equall to 1 month + if ((view.end - view.start) <= 3600000*24*32) { + powerseries.push({data:battery_soc_data, label: "SOC", yaxis:2, color: "#888"}); + } } } diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 72c8f7db..53dd1534 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -202,4 +202,4 @@ function getTranslations(){ - + From e8ebd7ac7f64f2c09d6535fc07b06cbc9631e8f5 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Thu, 9 Apr 2026 14:55:15 +0100 Subject: [PATCH 072/110] toggle visibility of statsbox items --- .../mysolarpvbattery/mysolarpvbattery.css | 13 ++++++---- .../mysolarpvbattery/mysolarpvbattery.js | 20 ++++++++++++++++ .../mysolarpvbattery/mysolarpvbattery.php | 24 +++++++++---------- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css index cbf14d42..90d712e9 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css @@ -9,10 +9,11 @@ text-align: center; vertical-align: middle; background: #262626; + --statsbox-color: #333; } .statsbox-inner-unit { - color: #333; + color: var(--statsbox-color); } .statsbox-padded { @@ -20,7 +21,7 @@ } .statsbox-inner-arrow { - color: #999; + color: var(--statsbox-color); } .statsbox-title { @@ -36,11 +37,13 @@ .statsbox-value { font-weight: bold; font-size: 36px; + color: var(--statsbox-color); } .statsbox-units { font-weight: bold; font-size: 16px; + color: var(--statsbox-color); } .statsbox-prc { @@ -62,7 +65,7 @@ height: 0; position: absolute; pointer-events: none; - border-top-color: #999; + border-top-color: var(--statsbox-color); border-width: 16px; margin-left: -16px; } @@ -81,7 +84,7 @@ height: 0; position: absolute; pointer-events: none; - border-left-color: #999; + border-left-color: var(--statsbox-color); border-width: 16px; margin-top: -16px; } @@ -100,7 +103,7 @@ height: 0; position: absolute; pointer-events: none; - border-right-color: #999; + border-right-color: var(--statsbox-color); border-width: 16px; margin-top: -16px; } diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index 2b73260a..ff02fc29 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -647,11 +647,31 @@ function solar_battery_visibility() { $("#live-solar-title").addClass("text-light"); $("#live-solar-value").addClass("text-warning"); $("#solar-box").css("background-color", "#dccc1f"); + $("#solar-to-grid-box").css("--statsbox-color", "#999"); + $("#solar-to-load-box").css("--statsbox-color", "#999"); + if (available.battery) { + $("#solar-to-battery-box").css("--statsbox-color", "#999"); + } else { + $("#solar-to-battery-box").css("--statsbox-color", "#333"); + } } else { $("#live-solar-title").removeClass("text-light"); $("#live-solar-value").removeClass("text-warning"); $("#solar-box").css("background-color", "#262626"); + $("#solar-to-grid-box").css("--statsbox-color", "#333"); + $("#solar-to-load-box").css("--statsbox-color", "#333"); + $("#solar-to-battery-box").css("--statsbox-color", "#333"); } + + if (available.battery) { + $("#battery-box").css("background-color", "#fb7b50"); + $("#discharge-box").css("--statsbox-color", "#666"); + } else { + $("#battery-box").css("background-color", "#262626"); + $("#discharge-box").css("--statsbox-color", "#333"); + } + + $("#grid-to-load-box").css("--statsbox-color", "#999"); } // Capacity in kWh, power in W, returns time left as string "Xh Ym" diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 53dd1534..279da245 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -16,7 +16,7 @@ - + @@ -90,13 +90,13 @@ - - - - - - - - - - - - - - - " + "" + - "" + - "" + - "" + - "" + + "" + + "" + "" + "" ); @@ -867,8 +823,7 @@ function draw_tables(total, monthly_data) { var monthly_out = ""; var sum_consumption_kwh = 0; - var sum_net_cost_tariff_A = 0; - var sum_net_cost_tariff_B = 0; + var sum_net_cost_tariff = 0; for (var month in monthly_data) { var md = monthly_data[month]; @@ -876,73 +831,56 @@ function draw_tables(total, monthly_data) { var vat = 1.05; // Net cost = grid import costs - export earnings, with VAT - var net_cost_A = ( - (md.tariff_A.grid_to_load_cost + md.tariff_A.grid_to_battery_cost) - - (md.tariff_A.solar_to_grid_value + md.tariff_A.battery_to_grid_value) - ) * vat; - - var net_cost_B = ( - (md.tariff_B.grid_to_load_cost + md.tariff_B.grid_to_battery_cost) - - (md.tariff_B.solar_to_grid_value + md.tariff_B.battery_to_grid_value) + var net_cost = ( + (md.tariff.grid_to_load_cost + md.tariff.grid_to_battery_cost) - + (md.tariff.solar_to_grid_value + md.tariff.battery_to_grid_value) ) * vat; // Effective unit rate against total consumption var consumption = md.solar_to_load_kwh + md.battery_to_load_kwh + md.grid_to_load_kwh; - var unit_rate_A = consumption > 0 ? (net_cost_A / consumption) * 100 : NaN; - var unit_rate_B = consumption > 0 ? (net_cost_B / consumption) * 100 : NaN; + var unit_rate = consumption > 0 ? (net_cost / consumption) * 100 : NaN; monthly_out += ""; monthly_out += ""; monthly_out += ""; // Tariff A - monthly_out += ""; - monthly_out += !isNaN(unit_rate_A) - ? "" - : ""; - - // Tariff B - monthly_out += ""; - monthly_out += !isNaN(unit_rate_B) - ? "" + monthly_out += ""; + monthly_out += !isNaN(unit_rate) + ? "" : ""; // Which tariff is cheaper this month - if (!isNaN(unit_rate_A) && !isNaN(unit_rate_B)) { - if (unit_rate_A < unit_rate_B) { - monthly_out += ""; - } else if (unit_rate_B < unit_rate_A) { - monthly_out += ""; - } else { - monthly_out += ""; - } - } else { - monthly_out += ""; - } + // if (!isNaN(unit_rate) && !isNaN(unit_rate_B)) { + // if (unit_rate < unit_rate_B) { + // monthly_out += ""; + // } else if (unit_rate_B < unit_rate) { + // monthly_out += ""; + // } else { + // monthly_out += ""; + // } + //} else { + // monthly_out += ""; + //} + monthly_out += ""; // Link icon to zoom to this month monthly_out += ""; monthly_out += ""; sum_consumption_kwh += consumption; - sum_net_cost_tariff_A += net_cost_A; - sum_net_cost_tariff_B += net_cost_B; + sum_net_cost_tariff += net_cost; } // Totals row - var total_unit_rate_A = sum_consumption_kwh > 0 ? (sum_net_cost_tariff_A / sum_consumption_kwh) * 100 : NaN; - var total_unit_rate_B = sum_consumption_kwh > 0 ? (sum_net_cost_tariff_B / sum_consumption_kwh) * 100 : NaN; + var total_unit_rate = sum_consumption_kwh > 0 ? (sum_net_cost_tariff / sum_consumption_kwh) * 100 : NaN; monthly_out += ""; monthly_out += ""; monthly_out += ""; - monthly_out += ""; - monthly_out += !isNaN(total_unit_rate_A) - ? "" - : ""; - monthly_out += ""; - monthly_out += !isNaN(total_unit_rate_B) - ? "" + monthly_out += ""; + monthly_out += !isNaN(total_unit_rate) + ? "" : ""; monthly_out += ""; monthly_out += ""; @@ -968,12 +906,12 @@ function graph_draw() { $("#kwh_halfhour").html(kwh_last_halfhour.toFixed(2) + "kWh"); - let tariff_A_unit = get_data_value_at_index("tariff_A", this_halfhour_index); - if (tariff_A_unit != null) { - let cost_last_halfhour = kwh_last_halfhour * tariff_A_unit; + let tariff_unit = get_data_value_at_index("tariff", this_halfhour_index); + if (tariff_unit != null) { + let cost_last_halfhour = kwh_last_halfhour * tariff_unit; $("#cost_halfhour").html("(" + cost_last_halfhour.toFixed(2) + "p)"); - let unit_price = tariff_A_unit * 1.05; + let unit_price = tariff_unit * 1.05; $("#unit_price").html(unit_price.toFixed(2) + "p"); } @@ -1003,8 +941,8 @@ function graph_draw() { // price signals graph_series.push({ - label: config.app.tariff_A.value, - data: data["tariff_A"], + label: config.app.tariff.value, + data: data["tariff"], yaxis: 2, color: "#fb1a80", lines: { @@ -1043,19 +981,6 @@ function graph_draw() { }); } - graph_series.push({ - label: config.app.tariff_B.value, - data: data["tariff_B"], - yaxis: 2, - color: "#7c1a80", - lines: { - show: true, - steps: true, - align: "left", - lineWidth: 1 - } - }); - var options = { xaxis: { mode: "time", @@ -1323,8 +1248,7 @@ $('#placeholder').bind("plothover", function(event, pos, item) { } } - let tariff_A = get_data_value_at_index("tariff_A", z); - let tariff_B = get_data_value_at_index("tariff_B", z); + let tariff = get_data_value_at_index("tariff", z); let outgoing = get_data_value_at_index("outgoing", z); let carbonintensity = get_data_value_at_index("carbonintensity", z); @@ -1338,12 +1262,12 @@ $('#placeholder').bind("plothover", function(event, pos, item) { if (solar_to_load_kwh != null) { text += "☀ Solar → Load: " + solar_to_load_kwh.toFixed(3) + " kWh"; - if (tariff_A != null) text += " (" + (solar_to_load_kwh * tariff_A).toFixed(2) + "p saved)
"; + if (tariff != null) text += " (" + (solar_to_load_kwh * tariff).toFixed(2) + "p saved)
"; else text += "
"; } if (solar_to_battery_kwh != null) { text += "☀ Solar → Battery: " + solar_to_battery_kwh.toFixed(3) + " kWh"; - if (tariff_A != null) text += " (" + (solar_to_battery_kwh * tariff_A).toFixed(2) + "p saved)
"; + if (tariff != null) text += " (" + (solar_to_battery_kwh * tariff).toFixed(2) + "p saved)
"; else text += "
"; } if (solar_to_grid_kwh != null) { @@ -1353,7 +1277,7 @@ $('#placeholder').bind("plothover", function(event, pos, item) { } if (battery_to_load_kwh != null) { text += "🔋 Battery → Load: " + battery_to_load_kwh.toFixed(3) + " kWh"; - if (tariff_A != null) text += " (" + (battery_to_load_kwh * tariff_A).toFixed(2) + "p saved)
"; + if (tariff != null) text += " (" + (battery_to_load_kwh * tariff).toFixed(2) + "p saved)
"; else text += "
"; } if (battery_to_grid_kwh != null) { @@ -1363,12 +1287,12 @@ $('#placeholder').bind("plothover", function(event, pos, item) { } if (grid_to_load_kwh != null) { text += "💡 Grid → Load: " + grid_to_load_kwh.toFixed(3) + " kWh"; - if (tariff_A != null) text += " (" + (grid_to_load_kwh * tariff_A).toFixed(2) + "p cost)
"; + if (tariff != null) text += " (" + (grid_to_load_kwh * tariff).toFixed(2) + "p cost)
"; else text += "
"; } if (grid_to_battery_kwh != null) { text += "💡 Grid → Battery: " + grid_to_battery_kwh.toFixed(3) + " kWh"; - if (tariff_A != null) text += " (" + (grid_to_battery_kwh * tariff_A).toFixed(2) + "p cost)
"; + if (tariff != null) text += " (" + (grid_to_battery_kwh * tariff).toFixed(2) + "p cost)
"; else text += "
"; } @@ -1382,12 +1306,8 @@ $('#placeholder').bind("plothover", function(event, pos, item) { text += "Carbon Intensity: " + carbonintensity.toFixed(0) + " gCO2/kWh
"; } - if (tariff_A != null) { - text += config.app.tariff_A.value+": " + tariff_A.toFixed(2) + " p/kWh (inc VAT)
"; - } - - if (tariff_B != null) { - text += config.app.tariff_B.value+": " + tariff_B.toFixed(2) + " p/kWh (inc VAT)
"; + if (tariff != null) { + text += config.app.tariff.value+": " + tariff.toFixed(2) + " p/kWh (inc VAT)
"; } tooltip(item.pageX, item.pageY, text, "#fff", "#000"); @@ -1491,12 +1411,9 @@ $("#monthly-data").on("click", ".zoom-to-month", function() { return false; }); -$("#tariff_A, #tariff_B").on("change", function() { - config.app.tariff_A.value = $("#tariff_A").val(); - config.app.tariff_B.value = $("#tariff_B").val(); - - config.db.tariff_A = config.app.tariff_A.value; - config.db.tariff_B = config.app.tariff_B.value; +$("#tariff").on("change", function() { + config.app.tariff.value = $("#tariff").val(); + config.db.tariff = config.app.tariff.value; config.set(); @@ -1521,7 +1438,7 @@ $("#download-csv").click(function() { var csv = []; - keys = ["tariff_A", "tariff_B", "outgoing", "solar_to_load", "solar_to_battery", "solar_to_grid", "battery_to_load", "battery_to_grid", "grid_to_load", "grid_to_battery", "import", "import_cost_tariff_A", "import_cost_tariff_B", "export", "export_cost", "solar_used", "solar_used_cost", "meter_kwh_hh"] + keys = ["tariff", "outgoing", "solar_to_load", "solar_to_battery", "solar_to_grid", "battery_to_load", "battery_to_grid", "grid_to_load", "grid_to_battery", "import", "import_cost_tariff", "export", "export_cost", "solar_used", "solar_used_cost", "meter_kwh_hh"] csv.push("time," + keys.join(",")) diff --git a/apps/OpenEnergyMonitor/octopus/tariff_explorer.php b/apps/OpenEnergyMonitor/octopus/tariff_explorer.php index b34bc25e..734ab8d2 100644 --- a/apps/OpenEnergyMonitor/octopus/tariff_explorer.php +++ b/apps/OpenEnergyMonitor/octopus/tariff_explorer.php @@ -95,12 +95,8 @@
- - -
-
- - + +
@@ -143,18 +139,7 @@
+
SOLAR
@@ -202,4 +202,4 @@ function getTranslations(){ ] - \ No newline at end of file + \ No newline at end of file From 91c7e4872c40d86f808810ffc0f26a9cb0fe3eae Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Thu, 9 Apr 2026 09:11:27 +0100 Subject: [PATCH 069/110] derive config --- .../mysolarpvbattery/mysolarpvbattery.js | 9 ++++++++- .../mysolarpvbattery/mysolarpvbattery.php | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index 329ed801..c860db8a 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -374,7 +374,14 @@ function flow_available() { if (!available.grid) derive = "grid"; else if (!available.use) derive = "use"; else if (!available.solar) derive = "solar"; - else if (!available.battery) derive = "battery"; + else if (!available.battery) { + if (config.app.has_battery.value) { + derive = "battery"; + } else { + // If all feeds are preset but battery is disabled by config, assume battery=0 and recalculate grid from use and solar + derive = "grid"; + } + } } // 2 Feeds: Specific logic based on your priority rules diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 33c188d2..2cfdb83b 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -202,4 +202,4 @@ function getTranslations(){ ] - \ No newline at end of file + \ No newline at end of file From 49f26052a8c2e60df1e356d5c4e8926f8b60a71b Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Thu, 9 Apr 2026 10:53:09 +0100 Subject: [PATCH 070/110] styling and other suggestions from Tim Bones --- Lib/appconf/appconf.js | 3 +- apps/OpenEnergyMonitor/feedin/feedin.php | 39 +++++++++++++------ .../mysolarpvbattery/mysolarpvbattery.css | 28 +++++++++---- .../mysolarpvbattery/mysolarpvbattery.js | 8 ++-- .../mysolarpvbattery/mysolarpvbattery.php | 8 ++-- 5 files changed, 57 insertions(+), 29 deletions(-) diff --git a/Lib/appconf/appconf.js b/Lib/appconf/appconf.js index ce26d973..63a9a01b 100644 --- a/Lib/appconf/appconf.js +++ b/Lib/appconf/appconf.js @@ -377,7 +377,8 @@ var config = { tag: node_name, name: item.name, options: JSON.stringify(defaults.options || { interval: 1800 }), - apikey: apikey + apikey: apikey, + unit: "kWh" }), dataType: "json", async: false, diff --git a/apps/OpenEnergyMonitor/feedin/feedin.php b/apps/OpenEnergyMonitor/feedin/feedin.php index f3ee0c95..8cee0da4 100644 --- a/apps/OpenEnergyMonitor/feedin/feedin.php +++ b/apps/OpenEnergyMonitor/feedin/feedin.php @@ -77,19 +77,34 @@ "name": "Title", "description": "Optional title for app" }, - "solar": { + "solar_power": { "type": "feed", - "autoname": "solar", + "autoname": "solar_power", "optional": true }, - "import": { + "solar_energy": { "type": "feed", - "autoname": "import", + "autoname": "solar_energy", + "optional": true + }, + "import_power": { + "type": "feed", + "autoname": "import_power", "optional": false }, - "export": { + "import_energy": { + "type": "feed", + "autoname": "import_energy", + "optional": false + }, + "export_power": { + "type": "feed", + "autoname": "export_power", + "optional": true + }, + "export_energy": { "type": "feed", - "autoname": "export", + "autoname": "export_energy", "optional": true }, "import_cost": { @@ -172,7 +187,7 @@ function setup() { data.setup(Graph.POWER, [Graph.SOLAR, Graph.IMPORT, Graph.EXPORT], config); data.setup(Graph.ENERGY, [Graph.SOLAR, Graph.IMPORT, Graph.EXPORT], config); data.register([Graph.SOLAR, Graph.IMPORT, Graph.EXPORT, - "solar", "import", "export"], config); + "solar_power", "import_power", "export_power"], config); return graph.setup(data, config).then(function(result) { update(); @@ -230,17 +245,17 @@ function drawPowerValues(values) { } return parseFloat(values[key][1]); }; - var imp = getPowerValue("import", values); + var imp = getPowerValue("import_power", values); if (imp == null) { return; } var cons = imp; - var solar = getPowerValue("solar", values); + var solar = getPowerValue("solar_power", values); if (solar == null) { solar = 0; } else { - var exp = getPowerValue("export", values); + var exp = getPowerValue("export_power", values); if (exp != null) { var selfCons = Math.max(0, solar - exp); if (selfCons != null) { @@ -266,7 +281,7 @@ function drawPowerValues(values) { $("#cons-power").html(cons.toFixed(fixed)+""+unit+""); $(".consumption.power").removeClass('cost').show(); - if (values["solar"] != undefined && solar != null) { + if (values["solar_power"] != undefined && solar != null) { $("#gen-power").html(solar.toFixed(fixed)+""+unit+""); $(".generation.power").removeClass('cost').show(); } @@ -289,7 +304,7 @@ function drawPowerValues(values) { $("#cons-power").html(config.app.currency.value+costNow.toFixed(fixed)+"/hr"); $(".consumption.power").addClass('cost').show(); - if (values["solar"] != undefined && solar != null && + if (values["solar_power"] != undefined && solar != null && config.app.export_cost.value > 0) { var fitNow = solar*config.app.export_cost.value*0.001; diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css index 2ea677bf..cbf14d42 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css @@ -29,6 +29,10 @@ padding-bottom: 15px; } +.statsbox-flow-title { + font-size: 16px; +} + .statsbox-value { font-weight: bold; font-size: 36px; @@ -124,19 +128,27 @@ /*Small devices (landscape phones, 576px and up)*/ @media (max-width: 576px) { + #app-block { padding: 0; } #statsbox-generation { padding-bottom:18px; } + .statstable { border-spacing: 4px; } .statsbox-padded { padding: 4px; } - .statsbox-title { font-size: 14px; padding-bottom: 5px; } /* 20px */ - .statsbox-value { font-size:28px; } /* 36px */ - .statsbox-units { font-size:14px; } /* 16px */ - .statsbox-prc { font-size: 14px; } /* 16px */ + .statsbox-title { font-size: 12px; padding-bottom: 4px; } /* 20px */ + .statsbox-flow-title { font-size: 12px; padding-bottom: 2px; line-height:12px; } /* 20px */ + + .statsbox-value { font-size: 20px; } /* 36px */ + .statsbox-units { display: none; } /* hide units */ + .statsbox-prc { font-size: 12px; } /* 16px */ .statsbox-arrow-down:after { - border-width: 10px; - margin-left: -10px; + border-width: 8px; + margin-left: -8px; } .statsbox-arrow-right:after { - border-width: 10px; - margin-top: -10px; + border-width: 8px; + margin-top: -8px; + } + .statsbox-arrow-left:before { + border-width: 8px; + margin-top: -8px; } } diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index eb553f7b..298ef7a0 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -35,7 +35,7 @@ config.app = { // correct minimum set depending on the has_solar / has_battery mode. // Any single missing feed will be derived from the other three (or two in solar/battery-only modes). "use":{"optional":true, "type":"feed", "autoname":"use", "description":"House or building use in watts"}, - "solar":{"optional":true, "type":"feed", "autoname":"solar", "description":"Solar generation in watts (only shown when has_solar is enabled)"}, + "solar":{"optional":true, "type":"feed", "autoname":"solar", "description":"Solar generation in watts"}, "battery":{"optional":true, "type":"feed", "autoname":"battery_power", "description":"Battery power in watts, positive for discharge, negative for charge (only shown when has_battery is enabled)"}, "grid":{"optional":true, "type":"feed", "autoname":"grid", "description":"Grid power in watts (positive for import, negative for export)"}, @@ -617,10 +617,10 @@ function livefn() // Grid import/export status if (input.grid > 0) { $(".balance-label").html("IMPORTING"); - $(".balance").html("" + Math.round(input.grid) + " " + powerUnit + ""); + $(".balance").html(`${Math.round(input.grid)}${powerUnit}`); } else if (input.grid < 0) { $(".balance-label").html("EXPORTING"); - $(".balance").html("" + Math.round(-input.grid) + " " + powerUnit + ""); + $(".balance").html(`${Math.round(-input.grid)}${powerUnit}`); } else { $(".balance-label").html("BALANCED"); $(".balance").html("--"); @@ -807,8 +807,8 @@ function load_powergraph() { powerseries = []; 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: solar_to_grid_data, label: "Solar to Grid", color: "#dccc1f", stack: 1, lines: {lineWidth: 0, fill: 1.0}}); 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}}); diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 72c199ee..72c8f7db 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -16,7 +16,7 @@ - + @@ -113,7 +113,7 @@
-
GRID CHARGE
0 kWh
+
GRID CHARGE
0 kWh
-
BATTERY TO GRID
0 kWh
+
BATTERY TO GRID
0 kWh
+
0 kWh
+
GRID
0 kWh
@@ -105,31 +105,31 @@
+
0 kWh
+
GRID CHARGE
0 kWh
+
0 kWh
+
BATTERY TO GRID
0 kWh
+
0 kWh
@@ -137,20 +137,20 @@
+
BATTERY
0 %
+
0 kWh
+
HOUSE
@@ -202,4 +202,4 @@ function getTranslations(){ - + From ccf2e359c5167eab37fcbab1d7d20be96f928b4a Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Thu, 9 Apr 2026 14:59:29 +0100 Subject: [PATCH 073/110] clean-up conditional styling --- .../mysolarpvbattery/mysolarpvbattery.js | 49 +++++++++---------- .../mysolarpvbattery/mysolarpvbattery.php | 10 ++-- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index ff02fc29..fd986225 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -643,35 +643,30 @@ function livefn() } function solar_battery_visibility() { - if (available.solar) { - $("#live-solar-title").addClass("text-light"); - $("#live-solar-value").addClass("text-warning"); - $("#solar-box").css("background-color", "#dccc1f"); - $("#solar-to-grid-box").css("--statsbox-color", "#999"); - $("#solar-to-load-box").css("--statsbox-color", "#999"); - if (available.battery) { - $("#solar-to-battery-box").css("--statsbox-color", "#999"); - } else { - $("#solar-to-battery-box").css("--statsbox-color", "#333"); - } - } else { - $("#live-solar-title").removeClass("text-light"); - $("#live-solar-value").removeClass("text-warning"); - $("#solar-box").css("background-color", "#262626"); - $("#solar-to-grid-box").css("--statsbox-color", "#333"); - $("#solar-to-load-box").css("--statsbox-color", "#333"); - $("#solar-to-battery-box").css("--statsbox-color", "#333"); - } + var s = available.solar; + var b = available.battery; - if (available.battery) { - $("#battery-box").css("background-color", "#fb7b50"); - $("#discharge-box").css("--statsbox-color", "#666"); - } else { - $("#battery-box").css("background-color", "#262626"); - $("#discharge-box").css("--statsbox-color", "#333"); - } + $("#live-solar-title").toggleClass("text-light", s); + $("#live-solar-value").toggleClass("text-warning", s); + + var boxColors = { + "#solar-box": s ? "#dccc1f" : "#262626", + "#battery-box": b ? "#fb7b50" : "#262626" + }; + for (var id in boxColors) $(id).css("background-color", boxColors[id]); + + var arrowColors = { + "#solar-to-grid-box": s ? "#999" : "#333", + "#solar-to-load-box": s ? "#999" : "#333", + "#solar-to-battery-box": s && b ? "#999" : "#333", + "#discharge-box": b ? "#666" : "#333", + "#grid-to-load-box": "#999" + }; + for (var id in arrowColors) $(id).css("--statsbox-color", arrowColors[id]); - $("#grid-to-load-box").css("--statsbox-color", "#999"); + $(".prc-solar").toggle(s); + $(".prc-battery").toggle(b); + $(".prc-solar-battery").toggle(s && b); } // Capacity in kWh, power in W, returns time left as string "Xh Ym" diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 279da245..831866c4 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -74,15 +74,15 @@
SOLAR
0 kWh
- 0 + 0
- 0 + 0
- 0 + 0
@@ -156,10 +156,10 @@
HOUSE
0 kWh
-
0
+
0
-
0
+
0
0
From b56e864061126f49d86f4f02cbefe70a9d3eb1f4 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Thu, 9 Apr 2026 15:08:57 +0100 Subject: [PATCH 074/110] hide battery section if not enabled --- .../mysolarpvbattery/mysolarpvbattery.css | 2 +- .../mysolarpvbattery/mysolarpvbattery.js | 2 ++ .../mysolarpvbattery/mysolarpvbattery.php | 22 +++++++++---------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css index 90d712e9..f8cb74f4 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css @@ -170,6 +170,6 @@ @media (max-width: 576px) { .stats-grid { - grid-template-columns: repeat(2, 1fr); + grid-template-columns: repeat(3, 1fr); } } \ No newline at end of file diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index fd986225..b6f84ca1 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -667,6 +667,8 @@ function solar_battery_visibility() { $(".prc-solar").toggle(s); $(".prc-battery").toggle(b); $(".prc-solar-battery").toggle(s && b); + + $(".battery-section").toggle(b); } // Capacity in kWh, power in W, returns time left as string "Xh Ym" diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 831866c4..a23270fb 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -34,27 +34,27 @@
-
+

-
+
-

-
+

-
+

-

-
+

-

-
+

-%

@@ -105,13 +105,13 @@
+
0 kWh
+
GRID CHARGE
0 kWh
@@ -123,7 +123,7 @@
+
BATTERY TO GRID
0 kWh
@@ -137,14 +137,14 @@
+
BATTERY
0 %
+
0 kWh
From 33bdd9f45789ac8badf097576a36c7c21017e731 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Thu, 9 Apr 2026 15:15:43 +0100 Subject: [PATCH 075/110] improve resize --- .../mysolarpvbattery/mysolarpvbattery.js | 23 ++++++++++++------- .../mysolarpvbattery/mysolarpvbattery.php | 4 ++-- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index b6f84ca1..13f90dd4 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -334,7 +334,6 @@ function show() } load_powergraph(); - resize(); powergraph_events(); livefn(); @@ -344,6 +343,9 @@ function show() setTimeout(function() { start_post_processor(); }, 1000); + + // resize after a delay to ensure the DOM is fully rendered and dimensions are correct + setTimeout(resize, 100); } // ------------------------------------------------------------------------------------------------------- @@ -505,23 +507,28 @@ function resize() { app_log("INFO","solar & battery resize"); - var top_offset = 0; var placeholder_bound = $('#placeholder_bound'); var placeholder = $('#placeholder'); - var is_landscape = $(window).height() < $(window).width(); var width = placeholder_bound.width(); - var height = $(window).height()*(is_landscape ? 0.3: 0.3); - if (height>width) height = width; + // Calculate height from the top of the chart to the bottom of the viewport, + // leaving enough room for the stats table below to remain visible. + var bottom_margin = $('.statstable').outerHeight(true) + 64; + var offset_top = placeholder_bound.offset().top - $(window).scrollTop(); + var height = $(window).height() - offset_top - bottom_margin; + + // In landscape cap at 60% of window width to avoid an overly tall chart + var is_landscape = $(window).height() < $(window).width(); + if (is_landscape) height = Math.min(height, width * 0.6); // min size to avoid flot errors - if (height<180) height = 180; - if (width<200) width = 200; + if (height < 180) height = 180; + if (width < 200) width = 200; placeholder.width(width); placeholder_bound.height(height); - placeholder.height(height-top_offset); + placeholder.height(height); draw(false) } diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index a23270fb..c041a801 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -16,7 +16,7 @@ - + @@ -202,4 +202,4 @@ function getTranslations(){ - + From 44622bfe8db2c92cebaa1f2ac433afa01b3fe96d Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Thu, 9 Apr 2026 15:26:26 +0100 Subject: [PATCH 076/110] rename from octopus to tariff explorer --- apps/OpenEnergyMonitor/octopus/octopus_controller.php | 2 +- .../octopus/{octopus.php => tariff_explorer.php} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename apps/OpenEnergyMonitor/octopus/{octopus.php => tariff_explorer.php} (100%) diff --git a/apps/OpenEnergyMonitor/octopus/octopus_controller.php b/apps/OpenEnergyMonitor/octopus/octopus_controller.php index d97dda2d..dfef198b 100644 --- a/apps/OpenEnergyMonitor/octopus/octopus_controller.php +++ b/apps/OpenEnergyMonitor/octopus/octopus_controller.php @@ -29,7 +29,7 @@ function octopus_app_controller($route,$app,$appconfig,$apikey) $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)); + $result .= view($dir."tariff_explorer.php",array("id"=>$app->id, "name"=>$app->name, "public"=>$app->public, "appdir"=>$dir, "config"=>$app->config, "apikey"=>$apikey)); return $result; } diff --git a/apps/OpenEnergyMonitor/octopus/octopus.php b/apps/OpenEnergyMonitor/octopus/tariff_explorer.php similarity index 100% rename from apps/OpenEnergyMonitor/octopus/octopus.php rename to apps/OpenEnergyMonitor/octopus/tariff_explorer.php From 6a1760ba13cbfb203f59cdf449a43013ace289e7 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Thu, 9 Apr 2026 15:28:35 +0100 Subject: [PATCH 077/110] rename app in new apps list --- apps/OpenEnergyMonitor/octopus/app.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/OpenEnergyMonitor/octopus/app.json b/apps/OpenEnergyMonitor/octopus/app.json index 543feb6a..c0b64ce2 100644 --- a/apps/OpenEnergyMonitor/octopus/app.json +++ b/apps/OpenEnergyMonitor/octopus/app.json @@ -1,6 +1,6 @@ { - "title" : "Octopus Agile", - "description" : "Explore Octopus Agile tariff energy costs", + "title" : "Tariff Explorer", + "description" : "Explore time of use tariff energy costs", "order" : 3, "primary" : true } From 693eea56dae6b772a57bfc4cea8b1e1b01a9abeb Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Thu, 9 Apr 2026 15:43:01 +0100 Subject: [PATCH 078/110] remove tariff_B logic preparation for simple baseline approach --- .../octopus/tariff_explorer.js | 241 ++++++------------ .../octopus/tariff_explorer.php | 21 +- 2 files changed, 82 insertions(+), 180 deletions(-) diff --git a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js index 04329392..1310750a 100644 --- a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js +++ b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js @@ -151,20 +151,12 @@ config.app = { "options": ["A_Eastern_England", "B_East_Midlands", "C_London", "E_West_Midlands", "D_Merseyside_and_Northern_Wales", "F_North_Eastern_England", "G_North_Western_England", "H_Southern_England", "J_South_Eastern_England", "K_Southern_Wales", "L_South_Western_England", "M_Yorkshire", "N_Southern_Scotland", "P_Northern_Scotland"] }, - "tariff_A": { + "tariff": { "type": "select", - "name": "Select tariff A:", + "name": "Select tariff:", "default": "AGILE-23-12-06", "options": tariff_options - }, - - "tariff_B": { - "type": "select", - "name": "Select tariff B:", - "default": "INTELLI-VAR-22-10-14", - "options": tariff_options } - }; @@ -536,19 +528,13 @@ function graph_load() { } data = {}; - data["tariff_A"] = [] - data["tariff_B"] = [] + data["tariff"] = [] data["outgoing"] = [] data["carbonintensity"] = [] // Tariff A - if (config.app.region != undefined && octopus_feed_list[config.app.tariff_A.value] != undefined && octopus_feed_list[config.app.tariff_A.value][config.app.region.value] != undefined) { - data["tariff_A"] = getdataremote(octopus_feed_list[config.app.tariff_A.value][config.app.region.value], view.start, view.end, interval); - } - - // Tariff B - if (config.app.region != undefined && octopus_feed_list[config.app.tariff_B.value] != undefined && octopus_feed_list[config.app.tariff_B.value][config.app.region.value] != undefined) { - data["tariff_B"] = getdataremote(octopus_feed_list[config.app.tariff_B.value][config.app.region.value], view.start, view.end, interval); + if (config.app.region != undefined && octopus_feed_list[config.app.tariff.value] != undefined && octopus_feed_list[config.app.tariff.value][config.app.region.value] != undefined) { + data["tariff"] = getdataremote(octopus_feed_list[config.app.tariff.value][config.app.region.value], view.start, view.end, interval); } // Outgoing (export tariff) - only needed in flow mode @@ -600,18 +586,7 @@ function graph_load() { grid_to_battery_kwh: 0, // Per-flow value at tariff A (avoided cost / earned) - tariff_A: { - solar_to_load_value: 0, - solar_to_grid_value: 0, - solar_to_battery_value: 0, - battery_to_load_value: 0, - battery_to_grid_value: 0, - grid_to_load_cost: 0, - grid_to_battery_cost: 0, - }, - - // Per-flow value at tariff B (avoided cost / earned) - tariff_B: { + tariff: { solar_to_load_value: 0, solar_to_grid_value: 0, solar_to_battery_value: 0, @@ -619,7 +594,7 @@ function graph_load() { battery_to_grid_value: 0, grid_to_load_cost: 0, grid_to_battery_cost: 0, - }, + }, co2: 0 } @@ -671,19 +646,13 @@ function graph_load() { } // Unit and import cost on tariff A - let unitcost_tariff_A = null; - if (data.tariff_A[z] != undefined && data.tariff_A[z][1] != null) { - unitcost_tariff_A = data.tariff_A[z][1] * 0.01; + let unitcost_tariff = null; + if (data.tariff[z] != undefined && data.tariff[z][1] != null) { + unitcost_tariff = data.tariff[z][1] * 0.01; // Generate profile profile_kwh[hh][1] += kwh_import - profile_cost[hh][1] += kwh_import * unitcost_tariff_A; - } - - // Unit and import cost on tariff B - let unitcost_tariff_B = null; - if (data.tariff_B[z] != undefined && data.tariff_B[z][1] != null) { - unitcost_tariff_B = data.tariff_B[z][1] * 0.01; + profile_cost[hh][1] += kwh_import * unitcost_tariff; } // Carbon Intensity @@ -718,14 +687,14 @@ function graph_load() { grid_to_battery: kwh_grid_to_battery }; - accumulate_flows(total, flows, outgoing_unit, unitcost_tariff_A, unitcost_tariff_B); + accumulate_flows(total, flows, outgoing_unit, unitcost_tariff); // Accumulate monthly data if (monthly_data[startOfMonth] == undefined) { monthly_data[startOfMonth] = JSON.parse(JSON.stringify(total_template)); // deep copy } var m = monthly_data[startOfMonth]; - accumulate_flows(m, flows, outgoing_unit, unitcost_tariff_A, unitcost_tariff_B); + accumulate_flows(m, flows, outgoing_unit, unitcost_tariff); } // if (smart_meter_data && !flow_mode) { @@ -742,7 +711,7 @@ function get_value_at_index(data_array, index, default_value = null) { return default_value; } -function accumulate_flows(bucket, flows, outgoing_unit, unitcost_tariff_A, unitcost_tariff_B) { +function accumulate_flows(bucket, flows, outgoing_unit, unitcost_tariff) { bucket.solar_to_load_kwh += flows.solar_to_load; bucket.solar_to_grid_kwh += flows.solar_to_grid; bucket.solar_to_battery_kwh += flows.solar_to_battery; @@ -752,31 +721,22 @@ function accumulate_flows(bucket, flows, outgoing_unit, unitcost_tariff_A, unitc bucket.grid_to_battery_kwh += flows.grid_to_battery; if (outgoing_unit != null) { - bucket.tariff_A.solar_to_grid_value += flows.solar_to_grid * outgoing_unit; - bucket.tariff_A.battery_to_grid_value += flows.battery_to_grid * outgoing_unit; - bucket.tariff_B.solar_to_grid_value += flows.solar_to_grid * outgoing_unit; - bucket.tariff_B.battery_to_grid_value += flows.battery_to_grid * outgoing_unit; + bucket.tariff.solar_to_grid_value += flows.solar_to_grid * outgoing_unit; + bucket.tariff.battery_to_grid_value += flows.battery_to_grid * outgoing_unit; } - if (unitcost_tariff_A != null) { - bucket.tariff_A.solar_to_load_value += flows.solar_to_load * unitcost_tariff_A; - bucket.tariff_A.solar_to_battery_value += flows.solar_to_battery * unitcost_tariff_A; - bucket.tariff_A.battery_to_load_value += flows.battery_to_load * unitcost_tariff_A; - bucket.tariff_A.grid_to_load_cost += flows.grid_to_load * unitcost_tariff_A; - bucket.tariff_A.grid_to_battery_cost += flows.grid_to_battery * unitcost_tariff_A; - } - if (unitcost_tariff_B != null) { - bucket.tariff_B.solar_to_load_value += flows.solar_to_load * unitcost_tariff_B; - bucket.tariff_B.solar_to_battery_value += flows.solar_to_battery * unitcost_tariff_B; - bucket.tariff_B.battery_to_load_value += flows.battery_to_load * unitcost_tariff_B; - bucket.tariff_B.grid_to_load_cost += flows.grid_to_load * unitcost_tariff_B; - bucket.tariff_B.grid_to_battery_cost += flows.grid_to_battery * unitcost_tariff_B; + if (unitcost_tariff != null) { + bucket.tariff.solar_to_load_value += flows.solar_to_load * unitcost_tariff; + bucket.tariff.solar_to_battery_value += flows.solar_to_battery * unitcost_tariff; + bucket.tariff.battery_to_load_value += flows.battery_to_load * unitcost_tariff; + bucket.tariff.grid_to_load_cost += flows.grid_to_load * unitcost_tariff; + bucket.tariff.grid_to_battery_cost += flows.grid_to_battery * unitcost_tariff; } } function draw_tables(total, monthly_data) { // Populate standalone tariff selectors (built once, then just set value) - ["tariff_A", "tariff_B"].forEach(function(id) { + ["tariff"].forEach(function(id) { var sel = $("#" + id); if (sel.find("option").length === 0) { for (var key in tariff_options) { @@ -784,8 +744,7 @@ function draw_tables(total, monthly_data) { } } }); - $("#tariff_A").val(config.app.tariff_A.value); - $("#tariff_B").val(config.app.tariff_B.value); + $("#tariff").val(config.app.tariff.value); var out = ""; @@ -823,18 +782,18 @@ function draw_tables(total, monthly_data) { return r; } - out += flow_row("☀ Solar → Load", total.solar_to_load_kwh, total.tariff_A.solar_to_load_value * 1.05, "avoided import cost (tariff A)", "#bec745"); - out += flow_row("☀ Solar → Battery", total.solar_to_battery_kwh, total.tariff_A.solar_to_battery_value * 1.05, "avoided import cost (tariff A)", "#a3d977"); - out += flow_row("☀ Solar → Grid (export)",total.solar_to_grid_kwh, total.tariff_A.solar_to_grid_value * 1.05, "earned at export tariff", "#dccc1f"); - out += flow_row("🔋 Battery → Load", total.battery_to_load_kwh, total.tariff_A.battery_to_load_value * 1.05, "avoided import cost (tariff A)", "#fbb450"); - out += flow_row("🔋 Battery → Grid (export)", total.battery_to_grid_kwh, total.tariff_A.battery_to_grid_value * 1.05, "earned at export tariff", "#f0913a"); - out += flow_row("💡 Grid → Load", total.grid_to_load_kwh, (total.tariff_A.grid_to_load_cost * 1.05), "cost at tariff A", "#44b3e2"); - out += flow_row("💡 Grid → Battery", total.grid_to_battery_kwh, (total.tariff_A.grid_to_battery_cost * 1.05),"cost at tariff A", "#82cbfc"); + out += flow_row("☀ Solar → Load", total.solar_to_load_kwh, total.tariff.solar_to_load_value * 1.05, "avoided import cost (tariff A)", "#bec745"); + out += flow_row("☀ Solar → Battery", total.solar_to_battery_kwh, total.tariff.solar_to_battery_value * 1.05, "avoided import cost (tariff A)", "#a3d977"); + out += flow_row("☀ Solar → Grid (export)",total.solar_to_grid_kwh, total.tariff.solar_to_grid_value * 1.05, "earned at export tariff", "#dccc1f"); + out += flow_row("🔋 Battery → Load", total.battery_to_load_kwh, total.tariff.battery_to_load_value * 1.05, "avoided import cost (tariff A)", "#fbb450"); + out += flow_row("🔋 Battery → Grid (export)", total.battery_to_grid_kwh, total.tariff.battery_to_grid_value * 1.05, "earned at export tariff", "#f0913a"); + out += flow_row("💡 Grid → Load", total.grid_to_load_kwh, (total.tariff.grid_to_load_cost * 1.05), "cost at tariff A", "#44b3e2"); + out += flow_row("💡 Grid → Battery", total.grid_to_battery_kwh, (total.tariff.grid_to_battery_cost * 1.05),"cost at tariff A", "#82cbfc"); // Summary row: net cost = grid costs - earnings, unit cost = net cost / total consumption var net_cost_gbp = ( - (total.tariff_A.grid_to_load_cost + total.tariff_A.grid_to_battery_cost) - - (total.tariff_A.solar_to_grid_value + total.tariff_A.battery_to_grid_value) + (total.tariff.grid_to_load_cost + total.tariff.grid_to_battery_cost) - + (total.tariff.solar_to_grid_value + total.tariff.battery_to_grid_value) ) * 1.05; var total_consumption_kwh = total.solar_to_load_kwh + total.battery_to_load_kwh + total.grid_to_load_kwh; @@ -851,15 +810,12 @@ function draw_tables(total, monthly_data) { if (Object.keys(monthly_data).length > 1) { // Update table headers with selected tariff names - var tariff_A_name = config.app.tariff_A.value; - var tariff_B_name = config.app.tariff_B.value; + var tariff_name = config.app.tariff.value; $("#monthly-data thead tr").html( "
MonthConsumption (kWh)Tariff A costTariff A rateTariff B costTariff B rateTariff costTariff rateCheaper tariff
" + d.getFullYear() + " " + months[d.getMonth()] + "" + consumption.toFixed(1) + " kWh" + (net_cost_A >= 0 ? "\u00a3" : "-\u00a3") + Math.abs(net_cost_A).toFixed(2) + "" + unit_rate_A.toFixed(1) + " p/kWh" + (net_cost_B >= 0 ? "\u00a3" : "-\u00a3") + Math.abs(net_cost_B).toFixed(2) + "" + unit_rate_B.toFixed(1) + " p/kWh" + (net_cost >= 0 ? "\u00a3" : "-\u00a3") + Math.abs(net_cost).toFixed(2) + "" + unit_rate.toFixed(1) + " p/kWhAB=AB=
Total" + sum_consumption_kwh.toFixed(1) + " kWh" + (sum_net_cost_tariff_A >= 0 ? "\u00a3" : "-\u00a3") + Math.abs(sum_net_cost_tariff_A).toFixed(2) + "" + total_unit_rate_A.toFixed(1) + " p/kWh" + (sum_net_cost_tariff_B >= 0 ? "\u00a3" : "-\u00a3") + Math.abs(sum_net_cost_tariff_B).toFixed(2) + "" + total_unit_rate_B.toFixed(1) + " p/kWh" + (sum_net_cost_tariff >= 0 ? "\u00a3" : "-\u00a3") + Math.abs(sum_net_cost_tariff).toFixed(2) + "" + total_unit_rate.toFixed(1) + " p/kWh
- - - - - - - - - - - - +
MonthEnergy (kWh)Tariff A Cost (£)Tariff B Cost (£)
From 2efcdd28fd28d1f67d45e6629c5f119a2064e879 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Thu, 9 Apr 2026 15:49:07 +0100 Subject: [PATCH 079/110] clean up --- apps/OpenEnergyMonitor/octopus/profile.js | 2 +- apps/OpenEnergyMonitor/octopus/tariff_explorer.js | 7 +++---- apps/OpenEnergyMonitor/octopus/tariff_explorer.php | 8 +++----- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/apps/OpenEnergyMonitor/octopus/profile.js b/apps/OpenEnergyMonitor/octopus/profile.js index ac347903..3630acb7 100644 --- a/apps/OpenEnergyMonitor/octopus/profile.js +++ b/apps/OpenEnergyMonitor/octopus/profile.js @@ -35,7 +35,7 @@ function profile_draw() { }); graph_series.push({ - label: config.app.tariff_A.value, + label: config.app.tariff.value, data: profile_unitprice, yaxis: 2, color: "#fb1a80", diff --git a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js index 1310750a..565c0dc1 100644 --- a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js +++ b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js @@ -815,8 +815,7 @@ function draw_tables(total, monthly_data) { "Month" + "Consumption (kWh)" + "Tariff cost" + - "Tariff rate" + - "Cheaper tariff" + + "Tariff rate" + "" ); @@ -862,7 +861,7 @@ function draw_tables(total, monthly_data) { //} else { // monthly_out += "—"; //} - monthly_out += "—"; + //monthly_out += "—"; // Link icon to zoom to this month monthly_out += ""; @@ -882,7 +881,7 @@ function draw_tables(total, monthly_data) { monthly_out += !isNaN(total_unit_rate) ? "" + total_unit_rate.toFixed(1) + " p/kWh" : "—"; - monthly_out += ""; + monthly_out += ""; monthly_out += ""; $("#monthly-data-body").html(monthly_out); diff --git a/apps/OpenEnergyMonitor/octopus/tariff_explorer.php b/apps/OpenEnergyMonitor/octopus/tariff_explorer.php index 734ab8d2..25386c7a 100644 --- a/apps/OpenEnergyMonitor/octopus/tariff_explorer.php +++ b/apps/OpenEnergyMonitor/octopus/tariff_explorer.php @@ -93,11 +93,9 @@ - +
0 kWh
@@ -202,4 +202,4 @@ function getTranslations(){ - + From 347c53c34eaf35b717164f929f1526f5e71e844d Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Thu, 9 Apr 2026 17:33:18 +0100 Subject: [PATCH 084/110] fix tooltip data index and remove zero values --- .../octopus/tariff_explorer.js | 35 +++++++++++-------- .../octopus/tariff_explorer.php | 2 ++ 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js index d434f5a0..7ed16e3c 100644 --- a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js +++ b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js @@ -277,6 +277,7 @@ var regions_outgoing = { // ---------------------------------------------------------------------- var feeds = {}; var data = {}; +var time_to_index_map = {}; var graph_series = []; var previousPoint = false; var panning = false; @@ -345,6 +346,7 @@ function show() { }); setPeriod('168'); + $(".time-select").val('168'); graph_load(); graph_draw(); @@ -711,6 +713,13 @@ function graph_load(time_window_changed = true) { // calibration_line_of_best_fit(data["import"], meter_kwh_hh); // } + // Create time to index map using grid_to_load feed as reference (should be present in all modes) + time_to_index_map = {}; + for (var z = 0; z < grid_to_load_kwh_data.length; z++) { + time_to_index_map[grid_to_load_kwh_data[z][0]] = z; + } + + // Clear baseline summary if time window changed (as this may affect the selected baseline period) if (time_window_changed) { baseline_monthly_summary = {}; } @@ -1090,8 +1099,9 @@ function graph_draw() { mode: "x" }, legend: { + show: true, position: "NW", - noColumns: 6 + noColumns: 1 } } $.plot($('#placeholder'), graph_series, options); @@ -1274,13 +1284,6 @@ function calibration_line_of_best_fit(import_kwh, meter_kwh_hh) // ------------------------------------------------------------------------------- $('#placeholder').bind("plothover", function(event, pos, item) { if (item) { - var z = item.dataIndex; - - var isStepped = item.series.lines && item.series.lines.steps; - - if (isStepped) { - z = Math.floor(z / 2); - } if (previousPoint != item.datapoint) { previousPoint = item.datapoint; @@ -1288,6 +1291,8 @@ $('#placeholder').bind("plothover", function(event, pos, item) { $("#tooltip").remove(); var itemTime = item.datapoint[0]; + var z = time_to_index_map[itemTime]; + var d = new Date(itemTime); var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; @@ -1323,37 +1328,37 @@ $('#placeholder').bind("plothover", function(event, pos, item) { let grid_to_load_kwh = get_data_value_at_index("grid_to_load", z); let grid_to_battery_kwh = get_data_value_at_index("grid_to_battery", z); - if (solar_to_load_kwh != null) { + if (solar_to_load_kwh != null && solar_to_load_kwh > 0) { text += "☀ Solar → Load: " + solar_to_load_kwh.toFixed(3) + " kWh"; if (tariff != null) text += " (" + (solar_to_load_kwh * tariff).toFixed(2) + "p saved)
"; else text += "
"; } - if (solar_to_battery_kwh != null) { + if (solar_to_battery_kwh != null && solar_to_battery_kwh > 0) { text += "☀ Solar → Battery: " + solar_to_battery_kwh.toFixed(3) + " kWh"; if (tariff != null) text += " (" + (solar_to_battery_kwh * tariff).toFixed(2) + "p saved)
"; else text += "
"; } - if (solar_to_grid_kwh != null) { + if (solar_to_grid_kwh != null && solar_to_grid_kwh > 0) { text += "☀ Solar → Grid: " + solar_to_grid_kwh.toFixed(3) + " kWh"; if (outgoing != null) text += " (" + (solar_to_grid_kwh * outgoing).toFixed(2) + "p gained)
"; else text += "
"; } - if (battery_to_load_kwh != null) { + if (battery_to_load_kwh != null && battery_to_load_kwh > 0) { text += "🔋 Battery → Load: " + battery_to_load_kwh.toFixed(3) + " kWh"; if (tariff != null) text += " (" + (battery_to_load_kwh * tariff).toFixed(2) + "p saved)
"; else text += "
"; } - if (battery_to_grid_kwh != null) { + if (battery_to_grid_kwh != null && battery_to_grid_kwh > 0) { text += "🔋 Battery → Grid: " + battery_to_grid_kwh.toFixed(3) + " kWh"; if (outgoing != null) text += " (" + (battery_to_grid_kwh * outgoing).toFixed(2) + "p gained)
"; else text += "
"; } - if (grid_to_load_kwh != null) { + if (grid_to_load_kwh != null && grid_to_load_kwh > 0) { text += "💡 Grid → Load: " + grid_to_load_kwh.toFixed(3) + " kWh"; if (tariff != null) text += " (" + (grid_to_load_kwh * tariff).toFixed(2) + "p cost)
"; else text += "
"; } - if (grid_to_battery_kwh != null) { + if (grid_to_battery_kwh != null && grid_to_battery_kwh > 0) { text += "💡 Grid → Battery: " + grid_to_battery_kwh.toFixed(3) + " kWh"; if (tariff != null) text += " (" + (grid_to_battery_kwh * tariff).toFixed(2) + "p cost)
"; else text += "
"; diff --git a/apps/OpenEnergyMonitor/octopus/tariff_explorer.php b/apps/OpenEnergyMonitor/octopus/tariff_explorer.php index 2ec0e0c7..d55931b5 100644 --- a/apps/OpenEnergyMonitor/octopus/tariff_explorer.php +++ b/apps/OpenEnergyMonitor/octopus/tariff_explorer.php @@ -65,6 +65,8 @@ << > < + - + + @@ -117,17 +121,6 @@ - - - - - - - - - -
Energy flowEnergyValue / CostUnit price
-
Show energy and costs based on Octopus smart meter data where available From 3a73268dc4b4b5950cddb8ab4bada9c938419221 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Fri, 10 Apr 2026 08:53:45 +0100 Subject: [PATCH 086/110] fix autogen config visibility and nodename where feed names are not standard --- Lib/appconf/appconf.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Lib/appconf/appconf.js b/Lib/appconf/appconf.js index 63a9a01b..62ba0108 100644 --- a/Lib/appconf/appconf.js +++ b/Lib/appconf/appconf.js @@ -34,10 +34,12 @@ var config = { if (session_write == undefined) { config.showConfig(); // Show setup block config.UI(); // Populate setup UI options + config.autogen.render_feed_list(); } else { if (session_write) { config.showConfig(); // Show setup block config.UI(); // Populate setup UI options + config.autogen.render_feed_list(); } else { alert("Invalid app configuration"); } @@ -312,7 +314,12 @@ var config = { // Return the node tag string used for all auto-generated feeds node_name: function() { - return config.app.autogenerate_nodename.value; + var node_name = config.app.autogenerate_nodename.default; + if (config.app.autogenerate_nodename.value != undefined) { + node_name = config.app.autogenerate_nodename.value; + } + + return node_name.trim(); }, // Set autogenerate node name when the user clicks "Set node" button From c769c2420f3f0fa30495767066d4ed119165a09b Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Fri, 10 Apr 2026 14:40:47 +0100 Subject: [PATCH 087/110] easier to read tooltip, hide legend on mobile, fixed co2 intensity --- .../octopus/tariff_explorer.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js index 2b01add6..775b319c 100644 --- a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js +++ b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js @@ -772,7 +772,8 @@ function draw_tables() { var out = ""; if (show_carbonintensity) { - var window_co2_intensity = 1000 * total.co2 / total.import_kwh; + var total_import_kwh = total.grid_to_load_kwh + total.grid_to_battery_kwh; + var window_co2_intensity = total_import_kwh > 0 ? 1000 * total.co2 / total_import_kwh : 0; $("#carbonintensity_result").html("Total CO2: " + (total.co2).toFixed(1) + "kgCO2, Consumption intensity: " + window_co2_intensity.toFixed(0) + " gCO2/kWh") } @@ -1101,7 +1102,7 @@ function graph_draw() { mode: "x" }, legend: { - show: true, + show: $('#placeholder').width() > 500, position: "NW", noColumns: 1 } @@ -1380,17 +1381,19 @@ $('#placeholder').bind("plothover", function(event, pos, item) { text += "
"; + if (tariff != null) { + text += " Import Tariff: " + tariff.toFixed(2) + " p/kWh (inc VAT)
"; + } + if (outgoing != null) { - text += "Export Tariff: " + outgoing.toFixed(2) + " p/kWh (inc VAT)
"; + text += " Export Tariff: " + outgoing.toFixed(2) + " p/kWh (inc VAT)
"; } if (show_carbonintensity && carbonintensity != null) { - text += "Carbon Intensity: " + carbonintensity.toFixed(0) + " gCO2/kWh
"; + text += "🌱 Carbon Intensity: " + carbonintensity.toFixed(0) + " gCO2/kWh
"; } - if (tariff != null) { - text += config.app.tariff.value+": " + tariff.toFixed(2) + " p/kWh (inc VAT)
"; - } + tooltip(item.pageX, item.pageY, text, "#fff", "#000"); } From 516b27c86b02dad5117de41a996e196be4c14b5f Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Fri, 10 Apr 2026 15:27:37 +0100 Subject: [PATCH 088/110] fix battery soc change --- .../mysolarpvbattery/mysolarpvbattery.js | 15 +++++++++++++-- .../mysolarpvbattery/mysolarpvbattery.php | 2 +- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index 48d3e256..d5ee2052 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -771,6 +771,9 @@ function load_powergraph() { var interval = view.interval; var power_to_kwh = interval / 3600000.0; + var battery_soc_start = null; + var battery_soc_end = null; + for (var z=0; z0) sign = "+"; + var sign = ""; if (soc_change>=0) sign = "+"; $(".battery_soc_change").html(sign+soc_change.toFixed(1)); powerseries = []; diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 1bc42c15..3f14a6f8 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -202,4 +202,4 @@ function getTranslations(){ - + From 58dd03aaf1c419a1bcf3e65af3f4756e61668197 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Fri, 10 Apr 2026 16:46:49 +0100 Subject: [PATCH 089/110] for testing --- .../mysolarpvbattery/mysolarpvbattery_controller.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php index 4022be96..60a71a2e 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php @@ -67,6 +67,9 @@ function mysolarpvbattery_app_controller($route,$app,$appconfig,$apikey) "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"), + // For testing + // "solar_kwh" => $feed->exists_tag_name($userid, $tag, "solar_kwh"), + "process_mode" => "all", "process_start" => 0, "process" => "solarbatterykwh" From 0a32897158110e570b684b3e183dcaed563137c6 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Fri, 10 Apr 2026 17:38:07 +0100 Subject: [PATCH 090/110] set consistent colors --- .../mysolarpvbattery/mysolarpvbattery.js | 44 ++++++++++++++----- .../mysolarpvbattery/mysolarpvbattery.php | 2 +- .../octopus/tariff_explorer.js | 38 ++++++++++------ 3 files changed, 57 insertions(+), 27 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index d5ee2052..a176e05c 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -14,6 +14,26 @@ $(window).ready(function(){ }); if (!sessionwrite) $(".openconfig").hide(); +var flow_colors_old = { + "solar_to_load": "#abddff", + "solar_to_battery": "#fba050", + "solar_to_grid": "#dccc1f", + "battery_to_load": "#ffd08e", + "battery_to_grid": "#fabb68", + "grid_to_load": "#82cbfc", + "grid_to_battery": "#fb7b50" +}; + +var flow_colors = { + "solar_to_load": "#bec745", + "solar_to_battery": "#a3d977", + "solar_to_grid": "#dccc1f", + "battery_to_load": "#fbb450", + "battery_to_grid": "#f0913a", + "grid_to_load": "#44b3e2", + "grid_to_battery": "#82cbfc" +}; + // ---------------------------------------------------------------------- // Configuration // ---------------------------------------------------------------------- @@ -843,13 +863,13 @@ function load_powergraph() { $(".battery_soc_change").html(sign+soc_change.toFixed(1)); powerseries = []; - 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_battery_data, label: "Solar to Battery", color: "#fba050", stack: 1, lines: {lineWidth: 0, fill: 0.8}}); - powerseries.push({data: solar_to_grid_data, label: "Solar to Grid", color: "#dccc1f", stack: 1, lines: {lineWidth: 0, fill: 1.0}}); - 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}}); + powerseries.push({data: solar_to_load_data, label: "Solar to Load", color: flow_colors["solar_to_load"], stack: 1, lines: {lineWidth: 0, fill: 0.8}}); + powerseries.push({data: solar_to_battery_data, label: "Solar to Battery", color: flow_colors["solar_to_battery"], stack: 1, lines: {lineWidth: 0, fill: 0.8}}); + powerseries.push({data: solar_to_grid_data, label: "Solar to Grid", color: flow_colors["solar_to_grid"], stack: 1, lines: {lineWidth: 0, fill: 1.0}}); + powerseries.push({data: battery_to_load_data, label: "Battery to Load", color: flow_colors["battery_to_load"], stack: 1, lines: {lineWidth: 0, fill: 0.8}}); + powerseries.push({data: battery_to_grid_data, label: "Battery to Grid", color: flow_colors["battery_to_grid"], stack: 1, lines: {lineWidth: 0, fill: 0.8}}); + powerseries.push({data: grid_to_load_data, label: "Grid to Load", color: flow_colors["grid_to_load"], stack: 1, lines: {lineWidth: 0, fill: 0.8}}); + powerseries.push({data: grid_to_battery_data, label: "Grid to Battery", color: flow_colors["grid_to_battery"], stack: 1, lines: {lineWidth: 0, fill: 0.8}}); if (battery_soc_available) { // only add if time period is less or equall to 1 month @@ -1084,12 +1104,12 @@ function load_bargraph() { // Series definitions: label, color, stack (1=positive/load, 0=negative/export) var series_defs = [ // Stack 1: onsite use breakdown (positive bars above zero) - { key: 'solar_to_load', label: "Solar to Load", color: "#dccc1f", stack: 1, invert: false }, - { key: 'battery_to_load', label: "Battery to Load", color: "#fbb450", stack: 1, invert: false }, - { key: 'grid_to_load', label: "Grid to Load", color: "#82cbfc", stack: 1, invert: false }, + { key: 'solar_to_load', label: "Solar to Load", color: flow_colors["solar_to_load"], stack: 1, invert: false }, + { key: 'battery_to_load', label: "Battery to Load", color: flow_colors["battery_to_load"], stack: 1, invert: false }, + { key: 'grid_to_load', label: "Grid to Load", color: flow_colors["grid_to_load"], stack: 1, invert: false }, // Stack 0: exports (negative bars below zero) - { key: 'solar_to_grid', label: "Solar to Grid", color: "#dccc1f", stack: 0, invert: true }, - { key: 'battery_to_grid', label: "Battery to Grid", color: "#fbb450", stack: 0, invert: true } + { key: 'solar_to_grid', label: "Solar to Grid", color: flow_colors["solar_to_grid"], stack: 0, invert: true }, + { key: 'battery_to_grid', label: "Battery to Grid", color: flow_colors["battery_to_grid"], stack: 0, invert: true } ]; historyseries = []; diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 3f14a6f8..ef2ab927 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -202,4 +202,4 @@ function getTranslations(){ - + diff --git a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js index 775b319c..a91a00cf 100644 --- a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js +++ b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js @@ -10,6 +10,16 @@ var profile_mode = false; var show_carbonintensity = $("#show_carbonintensity")[0].checked; +var flow_colors = { + "solar_to_load": "#bec745", + "solar_to_battery": "#a3d977", + "solar_to_grid": "#dccc1f", + "battery_to_load": "#fbb450", + "battery_to_grid": "#f0913a", + "grid_to_load": "#44b3e2", + "grid_to_battery": "#82cbfc" +}; + // ---------------------------------------------------------------------- // Display // ---------------------------------------------------------------------- @@ -808,13 +818,13 @@ function draw_tables() { return r; } - out += flow_row("☀ Solar → Load", total.solar_to_load_kwh, total.tariff.solar_to_load_value * 1.05, "avoided import cost", "#bec745"); - out += flow_row("☀ Solar → Battery", total.solar_to_battery_kwh, total.tariff.solar_to_battery_value * 1.05, "avoided import cost", "#a3d977"); - out += flow_row("☀ Solar → Grid (export)",total.solar_to_grid_kwh, total.tariff.solar_to_grid_value * 1.05, "earned at export tariff", "#dccc1f"); - out += flow_row("🔋 Battery → Load", total.battery_to_load_kwh, total.tariff.battery_to_load_value * 1.05, "avoided import cost", "#fbb450"); - out += flow_row("🔋 Battery → Grid (export)", total.battery_to_grid_kwh, total.tariff.battery_to_grid_value * 1.05, "earned at export tariff", "#f0913a"); - out += flow_row("💡 Grid → Load", total.grid_to_load_kwh, (total.tariff.grid_to_load_cost * 1.05), "import cost", "#44b3e2"); - out += flow_row("💡 Grid → Battery", total.grid_to_battery_kwh, (total.tariff.grid_to_battery_cost * 1.05),"import cost", "#82cbfc"); + out += flow_row("☀ Solar → Load", total.solar_to_load_kwh, total.tariff.solar_to_load_value * 1.05, "avoided import cost", flow_colors.solar_to_load); + out += flow_row("☀ Solar → Battery", total.solar_to_battery_kwh, total.tariff.solar_to_battery_value * 1.05, "avoided import cost", flow_colors.solar_to_battery); + out += flow_row("☀ Solar → Grid (export)",total.solar_to_grid_kwh, total.tariff.solar_to_grid_value * 1.05, "earned at export tariff", flow_colors.solar_to_grid); + out += flow_row("🔋 Battery → Load", total.battery_to_load_kwh, total.tariff.battery_to_load_value * 1.05, "avoided import cost", flow_colors.battery_to_load); + out += flow_row("🔋 Battery → Grid (export)", total.battery_to_grid_kwh, total.tariff.battery_to_grid_value * 1.05, "earned at export tariff", flow_colors.battery_to_grid); + out += flow_row("💡 Grid → Load", total.grid_to_load_kwh, (total.tariff.grid_to_load_cost * 1.05), "import cost", flow_colors.grid_to_load); + out += flow_row("💡 Grid → Battery", total.grid_to_battery_kwh, (total.tariff.grid_to_battery_cost * 1.05),"import cost", flow_colors.grid_to_battery); // Summary row: net cost = grid costs - earnings, unit cost = net cost / total consumption var net_cost_gbp = ( @@ -1006,13 +1016,13 @@ function graph_draw() { graph_series = []; // All 7 disaggregated flows stacked as positive bars - graph_series.push({ label: "Solar to Load", data: data["solar_to_load"], yaxis: 1, color: "#bec745", stack: true, bars: bars }); - graph_series.push({ label: "Solar to Battery", data: data["solar_to_battery"], yaxis: 1, color: "#a3d977", stack: true, bars: bars }); - graph_series.push({ label: "Solar to Grid", data: data["solar_to_grid"], yaxis: 1, color: "#dccc1f", stack: true, bars: bars }); - graph_series.push({ label: "Battery to Load", data: data["battery_to_load"], yaxis: 1, color: "#fbb450", stack: true, bars: bars }); - graph_series.push({ label: "Battery to Grid", data: data["battery_to_grid"], yaxis: 1, color: "#f0913a", stack: true, bars: bars }); - graph_series.push({ label: "Grid to Load", data: data["grid_to_load"], yaxis: 1, color: "#44b3e2", stack: true, bars: bars }); - graph_series.push({ label: "Grid to Battery", data: data["grid_to_battery"], yaxis: 1, color: "#82cbfc", stack: true, bars: bars }); + graph_series.push({ label: "Solar to Load", data: data["solar_to_load"], yaxis: 1, color: flow_colors.solar_to_load, stack: true, bars: bars }); + graph_series.push({ label: "Solar to Battery", data: data["solar_to_battery"], yaxis: 1, color: flow_colors.solar_to_battery, stack: true, bars: bars }); + graph_series.push({ label: "Solar to Grid", data: data["solar_to_grid"], yaxis: 1, color: flow_colors.solar_to_grid, stack: true, bars: bars }); + graph_series.push({ label: "Battery to Load", data: data["battery_to_load"], yaxis: 1, color: flow_colors.battery_to_load, stack: true, bars: bars }); + graph_series.push({ label: "Battery to Grid", data: data["battery_to_grid"], yaxis: 1, color: flow_colors.battery_to_grid, stack: true, bars: bars }); + graph_series.push({ label: "Grid to Load", data: data["grid_to_load"], yaxis: 1, color: flow_colors.grid_to_load, stack: true, bars: bars }); + graph_series.push({ label: "Grid to Battery", data: data["grid_to_battery"], yaxis: 1, color: flow_colors.grid_to_battery, stack: true, bars: bars }); // price signals graph_series.push({ From 0ff2d6f58c3fe0b631571305de72b5ae6f751369 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Fri, 10 Apr 2026 17:41:10 +0100 Subject: [PATCH 091/110] show colors in tooltip --- .../mysolarpvbattery/mysolarpvbattery.js | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index a176e05c..0cfa9909 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -683,13 +683,13 @@ function solar_battery_visibility() { for (var id in boxColors) $(id).css("background-color", boxColors[id]); var arrowColors = { - "#solar-to-grid-box": s ? "#999" : "#333", - "#solar-to-load-box": s ? "#999" : "#333", - "#solar-to-battery-box": s && b ? "#999" : "#333", - "#battery-to-load-box": b ? "#999" : "#333", - "#battery-to-grid-box": b ? "#999" : "#333", - "#grid-to-battery-box": b ? "#999" : "#333", - "#grid-to-load-box": "#999" + "#solar-to-grid-box": s ? flow_colors["solar_to_grid"] : "#333", + "#solar-to-load-box": s ? flow_colors["solar_to_load"] : "#333", + "#solar-to-battery-box": s && b ? flow_colors["solar_to_battery"] : "#333", + "#battery-to-load-box": b ? flow_colors["battery_to_load"] : "#333", + "#battery-to-grid-box": b ? flow_colors["battery_to_grid"] : "#333", + "#grid-to-battery-box": b ? flow_colors["grid_to_battery"] : "#333", + "#grid-to-load-box": flow_colors["grid_to_load"] }; for (var id in arrowColors) $(id).css("--statsbox-color", arrowColors[id]); @@ -981,13 +981,13 @@ function powergraph_events() { var series = powerseries[i]; if (series.data[item.dataIndex]!=undefined && series.data[item.dataIndex][1]!=null) { if (series.label.toUpperCase()=="SOC") { - tooltip_items.push([series.label.toUpperCase(), series.data[item.dataIndex][1].toFixed(1), "%"]); + tooltip_items.push([series.label.toUpperCase(), series.data[item.dataIndex][1].toFixed(1), "%", series.color]); } else { if (series.data[item.dataIndex][1] != 0) { if ( series.data[item.dataIndex][1] >= 1000) { - tooltip_items.push([series.label.toUpperCase(), (series.data[item.dataIndex][1]/1000.0).toFixed(1) , "kW"]); + tooltip_items.push([series.label.toUpperCase(), (series.data[item.dataIndex][1]/1000.0).toFixed(1) , "kW", series.color]); } else { - tooltip_items.push([series.label.toUpperCase(), series.data[item.dataIndex][1].toFixed(0), "W"]); + tooltip_items.push([series.label.toUpperCase(), series.data[item.dataIndex][1].toFixed(0), "W", series.color]); } } } @@ -1266,7 +1266,8 @@ function show_tooltip(x, y, values) { for (i = 0; i < values.length; i++) { var value = values[i]; var row = $('').appendTo(table); - $(''+value[0]+'').appendTo(row); + var swatch = value[3] ? '' : ''; + $(''+swatch+''+value[0]+'').appendTo(row); $(''+value[1]+' '+value[2]+'').appendTo(row); } From 69f831e4eac6f8cd36eebbecee9f9c0bc8c286a2 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Fri, 10 Apr 2026 17:42:49 +0100 Subject: [PATCH 092/110] force cache reset --- .../mysolarpvbattery/mysolarpvbattery_controller.php | 2 +- apps/OpenEnergyMonitor/octopus/octopus_controller.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php index 60a71a2e..8e94604d 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php @@ -16,7 +16,7 @@ function mysolarpvbattery_app_controller($route,$app,$appconfig,$apikey) { global $path, $session, $settings, $mysqli, $redis, $user, $linked_modules_dir; - $v = 3; + $v = 4; // ---------------------------------------------------- // Main app view route diff --git a/apps/OpenEnergyMonitor/octopus/octopus_controller.php b/apps/OpenEnergyMonitor/octopus/octopus_controller.php index dfef198b..585bd9d2 100644 --- a/apps/OpenEnergyMonitor/octopus/octopus_controller.php +++ b/apps/OpenEnergyMonitor/octopus/octopus_controller.php @@ -16,7 +16,7 @@ function octopus_app_controller($route,$app,$appconfig,$apikey) { global $path, $session, $settings, $mysqli, $redis, $user, $linked_modules_dir; - $v = 3; + $v = 4; // ---------------------------------------------------- // Main app view route From aecd9b6d3124069f393e642e6cf093c126d0282c Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Fri, 10 Apr 2026 17:45:31 +0100 Subject: [PATCH 093/110] limit solar and use values below 0 --- apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js | 3 +++ apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index 0cfa9909..f5dd3a0d 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -454,6 +454,9 @@ function flow_derive_missing(input) { var battery = input.battery; var grid = input.grid; + if (solar<0) solar = 0; + if (use<0) use = 0; + if (assume_zero_solar) solar = 0; if (assume_zero_battery) battery = 0; diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index ef2ab927..49b8028a 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -202,4 +202,4 @@ function getTranslations(){ - + From 008187d0aec28b954ba62312d3b10ae4a03b4bd2 Mon Sep 17 00:00:00 2001 From: TrystanLea Date: Fri, 10 Apr 2026 20:03:03 +0100 Subject: [PATCH 094/110] fix auto update moving window --- Lib/timeseries.js | 31 +++++++++++++++++-- .../mysolarpvbattery/mysolarpvbattery.js | 4 +-- .../mysolarpvbattery/mysolarpvbattery.php | 2 +- .../mysolarpvbattery_controller.php | 12 +++++-- 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/Lib/timeseries.js b/Lib/timeseries.js index 118008de..aa24b16d 100644 --- a/Lib/timeseries.js +++ b/Lib/timeseries.js @@ -19,7 +19,7 @@ var timeseries = { datastore[name].interval = (datastore[name].data[1][0] - datastore[name].data[0][0])*0.001; }, - append: function (name,time,value) + append: function (name,time,value,join=false) { if (datastore[name]==undefined) { app_log("ERROR","timeseries.append datastore["+name+"] is undefined"); @@ -35,6 +35,31 @@ var timeseries = { var pos = (time - start) / interval; // 3. get last position from data length var last_pos = datastore[name].data.length - 1; + + var padd_value = null; + + if (join) { + let last_value_position = false; + // Itterate through the data points in reverse order to find the last non-null value for padding (limit lookup to 5 minutes) + var lookback = Math.ceil(300 / interval); + var min_i = Math.max(0, datastore[name].data.length - 1 - lookback); + for (var i = datastore[name].data.length - 1; i >= min_i; i--) { + if (datastore[name].data[i][1] !== null) { + padd_value = datastore[name].data[i][1]; + last_value_position = i; + break; + } + } + + // Fill null values in existing data after last known good value up to the new time + if (padd_value !== null) { + for (var j = last_value_position + 1; j < datastore[name].data.length; j++) { + if (datastore[name].data[j][1] === null) { + datastore[name].data[j][1] = padd_value; + } + } + } + } // if the datapoint is newer than the last: if (pos > last_pos) @@ -42,11 +67,11 @@ var timeseries = { var npadding = (pos - last_pos)-1; // padding - if (npadding>0 && npadding<12) { + if (npadding>0 && npadding<1000000) { for (var padd = 0; padd diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php index 8e94604d..43d592d3 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php @@ -16,7 +16,7 @@ function mysolarpvbattery_app_controller($route,$app,$appconfig,$apikey) { global $path, $session, $settings, $mysqli, $redis, $user, $linked_modules_dir; - $v = 4; + $v = 6; // ---------------------------------------------------- // Main app view route @@ -29,7 +29,15 @@ function mysolarpvbattery_app_controller($route,$app,$appconfig,$apikey) $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)); + $result .= view($dir.$app->app.".php",array( + "id"=>$app->id, + "name"=>$app->name, + "public"=>$app->public, + "appdir"=>$dir, + "config"=>$app->config, + "apikey"=>$apikey, + "v"=>$v + )); return $result; } From fefab07840471e8fa9e6c328d6b2f4a16065cc8e Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sat, 11 Apr 2026 08:40:26 +0100 Subject: [PATCH 095/110] start of mysolarpvbattery css refactor --- .../mysolarpvbattery/mysolarpvbattery.css | 255 +++++++++++++++--- .../mysolarpvbattery/mysolarpvbattery.js | 4 +- .../mysolarpvbattery/mysolarpvbattery.php | 116 ++++---- .../mysolarpvbattery_controller.php | 5 +- 4 files changed, 289 insertions(+), 91 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css index f8cb74f4..11388328 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css @@ -1,3 +1,155 @@ +/* ===================================================================== + Dark theme + (previously in Modules/app/Views/css/dark.css) + ===================================================================== */ + +body { + background-color: #222; +} + +.content-container { + max-width: 1150px; +} + +#app-block { + padding: 0; +} + +.app-top-bar { + padding: .5rem 0; + border-bottom: 1px solid #333; + margin-bottom: .4rem; +} + +/* btn-list: replaces Bootstrap .nav .nav-pills */ +.btn-list { + list-style: none; + padding: 0; + margin: 0; + display: flex; + align-items: center; +} + +/* app-btn: replaces Bootstrap btn + btn-large + btn-link + btn-inverse. + Explicitly resets Bootstrap properties so it is self-contained. */ +.app-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.25em; + padding: 0.25rem 0.5rem; + font-size: 1rem; + line-height: 1.5; + font-family: inherit; + cursor: pointer; + text-align: center; + white-space: nowrap; + background: transparent; + border: none; + border-radius: 0; + box-shadow: none; + text-shadow: none; + color: #aaa; + opacity: .6; + transition: opacity .3s linear; + text-decoration: none; +} +.app-btn:hover, +.app-btn:active { + opacity: 1; + text-decoration: none; +} +.app-btn.active { + color: white; + opacity: .8; + font-weight: bold; + text-decoration: underline; +} +.app-btn[disabled], +.app-btn[disabled]:hover { + color: #616161; + opacity: .8; + background: rgba(6, 153, 250, 0.06); +} + +/* History button: short label by default, full label on wider screens */ +.app-btn .label-short { display: inline; } +.app-btn .label-full { display: none; } +@media (min-width: 480px) { + .app-btn .label-short { display: none; } + .app-btn .label-full { display: inline; } +} + +.visnavblock { + color: #0699fa; + font-size: 18px; +} + +.visnav { + background-color: rgba(6,153,250,0.1); + flex-grow: 1; +} + +.visnav:focus, +.visnav:active, +.visnav:hover { + color: #0699fa; + text-decoration: none!important; +} + +#footer { + margin: 0; + background-color: #181818; + color: #999; +} + +.ajax-loader { + background: #222 + url(../../../images/ajax-loader.gif) + center + no-repeat; +} + +@media (min-width: 576px) { + .visnav { + flex-grow: 0; + min-width: 3em; + } +} + + +/* ===================================================================== + Power display + ===================================================================== */ + +.power-title { + margin: 0; + font-size: 1rem; + color: #aaa; +} + +.power-value { + margin: 0; + line-height: 1.2; + font-size: 1.5rem; +} + +@media (min-width: 768px) { + .power-title { font-size: 1.34rem; } + .power-value { font-size: 4.5rem; } +} + +/* kW mode: scale down power values so larger numbers still fit */ +#app-block.in_kw .power-value { font-size: 1rem; } +@media (min-width: 768px) { #app-block.in_kw .power-value { font-size: 3rem; } } +@media (min-width: 992px) { #app-block.in_kw .power-value { font-size: 4.4rem; } } +@media (min-width: 1200px) { #app-block.in_kw .power-value { font-size: 4.5rem; } } + + +/* ===================================================================== + Stats table + ===================================================================== */ + .statstable { width: 100%; border-spacing: 10px; @@ -12,7 +164,8 @@ --statsbox-color: #333; } -.statsbox-inner-unit { +.statsbox-inner-unit, +.statsbox-inner-arrow { color: var(--statsbox-color); } @@ -20,10 +173,6 @@ padding: 10px; } -.statsbox-inner-arrow { - color: var(--statsbox-color); -} - .statsbox-title { font-weight: bold; font-size: 20px; @@ -76,17 +225,17 @@ } .statsbox-arrow-right:after { - left: 100%; - top: 50%; - border: solid transparent; - content: " "; - width: 0; - height: 0; - position: absolute; - pointer-events: none; - border-left-color: var(--statsbox-color); - border-width: 16px; - margin-top: -16px; + left: 100%; + top: 50%; + border: solid transparent; + content: " "; + width: 0; + height: 0; + position: absolute; + pointer-events: none; + border-left-color: var(--statsbox-color); + border-width: 16px; + margin-top: -16px; } .statsbox-arrow-left { @@ -95,20 +244,17 @@ } .statsbox-arrow-left:before { - right: 100%; - top: 60%; - border: solid transparent; - content: " "; - width: 0; - height: 0; - position: absolute; - pointer-events: none; - border-right-color: var(--statsbox-color); - border-width: 16px; - margin-top: -16px; -} - -.tooltip-item { + right: 100%; + top: 60%; + border: solid transparent; + content: " "; + width: 0; + height: 0; + position: absolute; + pointer-events: none; + border-right-color: var(--statsbox-color); + border-width: 16px; + margin-top: -16px; } .tooltip-title { @@ -129,7 +275,49 @@ font-size:10px; } -/*Small devices (landscape phones, 576px and up)*/ +/* Box background colours (solar/battery overridden by JS when feeds unavailable) */ +#solar-box { background: #dccc1f; } +#grid-box { background: #d52e2e; } +#battery-box { background: #fb7b50; } +#house-box { background: #82cbfc; } + +/* Text alignment for battery flow label boxes */ +#grid-to-battery-box { text-align: left; } +#battery-to-grid-box { text-align: right; } + +/* Battery flow label boxes: padding adjusted to leave room for the arrow */ +#battery_import .statsbox-arrow-left { padding: 10px 0 0 10px; } +#battery_export .statsbox-arrow-right { padding: 10px 10px 0 0; } + +/* Value font size in battery flow label boxes */ +#grid-to-battery-box .statsbox-value, +#battery-to-grid-box .statsbox-value { font-size: 22px; } + +/* Rounded corners on first and last graph-nav buttons */ +#graph-nav .app-btn:first-child { border-radius: 0.375rem 0 0 0.375rem; } +#graph-nav .app-btn:last-child { border-radius: 0 0.375rem 0.375rem 0; } + +/* Faint dividing lines between buttons */ +#graph-nav .app-btn + .app-btn { border-left: 1px solid rgba(255,255,255,0.12); } + +/* Graph placeholder */ +#placeholder_bound { width: 100%; min-height: 300px; } +#placeholder { height: 100%; } + +/* Solar generation box: percentage overlays */ +#statsbox-generation { position: relative; } +.prc-solar-to-battery { position: absolute; width: 50%; left: 0; bottom: 0; } +.prc-solar-direct { position: absolute; width: 50%; left: 50%; bottom: 0; } +.prc-solar-export { position: absolute; height: 100%; right: 0; top: 0; + display: flex; align-items: center; } + +/* House box: percentage overlays */ +#house-box .statsbox-padded { position: relative; } +.prc-battery-to-house { position: absolute; width: 0; left: 3px; top: 40%; } +.prc-solar-to-house { position: absolute; width: 33.33333%; left: 0; top: 0; } +.prc-grid-to-house { position: absolute; width: 33.33333%; left: 66.66667%; top: 0; } + +/* Mobile (max 576px) */ @media (max-width: 576px) { #app-block { padding: 0; } #statsbox-generation { padding-bottom:18px; } @@ -168,8 +356,3 @@ min-width: 0; } -@media (max-width: 576px) { - .stats-grid { - grid-template-columns: repeat(3, 1fr); - } -} \ No newline at end of file diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index 3c4b4eb7..1e9855da 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -949,8 +949,8 @@ function draw_powergraph() { var options = { lines: { fill: false }, xaxis: { mode: "time", timezone: "browser", min: view.start, max: view.end}, - yaxes: [{ min: 0 },{ min: 0, max: 100 }], - grid: { hoverable: true, clickable: true }, + yaxes: [{ min: 0, reserveSpace: false },{ min: 0, max: 100, reserveSpace: false }], + grid: { hoverable: true, clickable: true, borderWidth: 0 }, selection: { mode: "x" }, legend: { show: false } } diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index 3b52cac3..a74637f5 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -3,8 +3,6 @@ global $path, $session; ?> - - @@ -16,75 +14,92 @@ - + -
+
- + diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php index 43d592d3..d33ea135 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php @@ -16,15 +16,14 @@ function mysolarpvbattery_app_controller($route,$app,$appconfig,$apikey) { global $path, $session, $settings, $mysqli, $redis, $user, $linked_modules_dir; - $v = 6; + $v = 7; // ---------------------------------------------------- // Main app view route // ---------------------------------------------------- if ($route->action == "view" || $route->action == "") { $route->format = "html"; - $result = "\n"; - $result .= "\n" . ''; + $result = "\n"; $result .= "\n" . ''; $result .= "\n\n \n"; From 69e189b72130ae2350a49f6a99d5a9f41f70f2c3 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sat, 11 Apr 2026 08:42:42 +0100 Subject: [PATCH 096/110] line up solar % figures --- apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css | 4 ++-- .../mysolarpvbattery/mysolarpvbattery_controller.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css index 11388328..c2fbc140 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css @@ -306,8 +306,8 @@ body { /* Solar generation box: percentage overlays */ #statsbox-generation { position: relative; } -.prc-solar-to-battery { position: absolute; width: 50%; left: 0; bottom: 0; } -.prc-solar-direct { position: absolute; width: 50%; left: 50%; bottom: 0; } +.prc-solar-to-battery { position: absolute; width: 33.3%; left: 0; bottom: 0; } +.prc-solar-direct { position: absolute; width: 33.3%; left: 66.66%; bottom: 0; } .prc-solar-export { position: absolute; height: 100%; right: 0; top: 0; display: flex; align-items: center; } diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php index d33ea135..1d10ea10 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php @@ -16,7 +16,7 @@ function mysolarpvbattery_app_controller($route,$app,$appconfig,$apikey) { global $path, $session, $settings, $mysqli, $redis, $user, $linked_modules_dir; - $v = 7; + $v = 8; // app version for cache busting // ---------------------------------------------------- // Main app view route From 616a89ac09b51ad1f4a8ad52bac9cc1454e041bd Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sat, 11 Apr 2026 08:52:49 +0100 Subject: [PATCH 097/110] fix chart height --- .../mysolarpvbattery/mysolarpvbattery.css | 3 +-- .../mysolarpvbattery/mysolarpvbattery.js | 20 ++++++++++++++----- .../mysolarpvbattery/mysolarpvbattery.php | 2 +- .../mysolarpvbattery_controller.php | 2 +- 4 files changed, 18 insertions(+), 9 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css index c2fbc140..66fa6e20 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css @@ -301,8 +301,7 @@ body { #graph-nav .app-btn + .app-btn { border-left: 1px solid rgba(255,255,255,0.12); } /* Graph placeholder */ -#placeholder_bound { width: 100%; min-height: 300px; } -#placeholder { height: 100%; } +#placeholder_bound { width: 100%;} /* Solar generation box: percentage overlays */ #statsbox-generation { position: relative; } diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index 1e9855da..47df519d 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -24,7 +24,7 @@ var flow_colors_old = { "grid_to_battery": "#fb7b50" }; -var flow_colors = { +var flow_colors_tariff_app = { "solar_to_load": "#bec745", "solar_to_battery": "#a3d977", "solar_to_grid": "#dccc1f", @@ -34,6 +34,16 @@ var flow_colors = { "grid_to_battery": "#82cbfc" }; +var flow_colors = { + "solar_to_load": "#F5C518", // bright amber – direct solar use + "solar_to_battery": "#C8A000", // darker gold – solar into storage + "solar_to_grid": "#FFE066", // light yellow – solar export + "battery_to_load": "#4ADE80", // bright green – battery discharge + "battery_to_grid": "#86EFAC", // soft green – battery export + "grid_to_load": "#38BDF8", // sky blue – grid import + "grid_to_battery": "#7DD3FC" // light blue – grid charging battery +}; + // ---------------------------------------------------------------------- // Configuration // ---------------------------------------------------------------------- @@ -542,12 +552,12 @@ function resize() var height = $(window).height() - offset_top - bottom_margin; // In landscape cap at 60% of window width to avoid an overly tall chart - var is_landscape = $(window).height() < $(window).width(); - if (is_landscape) height = Math.min(height, width * 0.6); + //var is_landscape = $(window).height() < $(window).width(); + //if (is_landscape) height = Math.min(height, width * 0.6); // min size to avoid flot errors - if (height < 180) height = 180; - if (width < 200) width = 200; + if (height < 100) height = 100; + if (width < 100) width = 100; placeholder.width(width); placeholder_bound.height(height); diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index a74637f5..e28ed51a 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -77,7 +77,7 @@
- +
diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php index 1d10ea10..7d52e6f8 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php @@ -16,7 +16,7 @@ function mysolarpvbattery_app_controller($route,$app,$appconfig,$apikey) { global $path, $session, $settings, $mysqli, $redis, $user, $linked_modules_dir; - $v = 8; // app version for cache busting + $v = 11; // version number for cache busting of js and css // ---------------------------------------------------- // Main app view route From 80ad867c84a87c1906e5bc648fe77f7b90136fee Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sat, 11 Apr 2026 08:55:35 +0100 Subject: [PATCH 098/110] fix hours and mins left --- .../OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js index 47df519d..8a9ab2f9 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js @@ -726,7 +726,12 @@ function battery_time_left({ capacity, soc, battery_power }) { const hours_left = Math.floor(time_left_hours); const mins_left = Math.floor((time_left_hours*60) % 60); - return `${hours_left}h ${mins_left}m`; + + let time_left_str = ""; + if (hours_left > 0) time_left_str += `${hours_left}h `; + if (hours_left < 10) time_left_str += `${mins_left}m`; // show minutes only if less than 10h left + + return time_left_str.trim(); } function draw(load) { From 6073117999712305220e81660e7f62c5c9a652c5 Mon Sep 17 00:00:00 2001 From: Trystan Lea Date: Sat, 11 Apr 2026 09:21:45 +0100 Subject: [PATCH 099/110] further refinements --- .../mysolarpvbattery/mysolarpvbattery.css | 55 ++++++++++++------- .../mysolarpvbattery/mysolarpvbattery.php | 5 +- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css index 66fa6e20..870cb245 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css @@ -72,12 +72,19 @@ body { background: rgba(6, 153, 250, 0.06); } -/* History button: short label by default, full label on wider screens */ -.app-btn .label-short { display: inline; } -.app-btn .label-full { display: none; } -@media (min-width: 480px) { - .app-btn .label-short { display: none; } - .app-btn .label-full { display: inline; } +/* Display utilities */ +.d-none { display: none !important; } +.d-inline { display: inline !important; } +.d-block { display: block !important; } +@media (min-width: 576px) { + .d-sm-none { display: none !important; } + .d-sm-inline { display: inline !important; } + .d-sm-block { display: block !important; } +} +@media (min-width: 768px) { + .d-md-none { display: none !important; } + .d-md-inline { display: inline !important; } + .d-md-block { display: block !important; } } .visnavblock { @@ -124,26 +131,30 @@ body { .power-title { margin: 0; - font-size: 1rem; + font-size: 0.9rem; color: #aaa; } .power-value { margin: 0; - line-height: 1.2; - font-size: 1.5rem; + line-height: 1.6; + font-size: 1.7rem; } @media (min-width: 768px) { - .power-title { font-size: 1.34rem; } - .power-value { font-size: 4.5rem; } + .power-title { font-size: 1.0rem; } + .power-value { font-size: 2.5rem; } +} + +@media (min-width: 992px) { + .power-title { font-size: 1.2rem; } + .power-value { font-size: 3.5rem; } } /* kW mode: scale down power values so larger numbers still fit */ -#app-block.in_kw .power-value { font-size: 1rem; } -@media (min-width: 768px) { #app-block.in_kw .power-value { font-size: 3rem; } } -@media (min-width: 992px) { #app-block.in_kw .power-value { font-size: 4.4rem; } } -@media (min-width: 1200px) { #app-block.in_kw .power-value { font-size: 4.5rem; } } +#app-block.in_kw .power-value { font-size: 0.9rem; } +@media (min-width: 768px) { #app-block.in_kw .power-value { font-size: 2.5rem; } } +@media (min-width: 992px) { #app-block.in_kw .power-value { font-size: 3.5rem; } } /* ===================================================================== @@ -316,17 +327,15 @@ body { .prc-solar-to-house { position: absolute; width: 33.33333%; left: 0; top: 0; } .prc-grid-to-house { position: absolute; width: 33.33333%; left: 66.66667%; top: 0; } -/* Mobile (max 576px) */ -@media (max-width: 576px) { +/* Mobile (max 768px): */ +@media (max-width: 768px) { #app-block { padding: 0; } - #statsbox-generation { padding-bottom:18px; } .statstable { border-spacing: 4px; } .statsbox-padded { padding: 4px; } - .statsbox-title { font-size: 12px; padding-bottom: 4px; } /* 20px */ + .statsbox-title { font-size: 16px; padding-bottom: 4px; } /* 20px */ .statsbox-flow-title { font-size: 12px; padding-bottom: 2px; line-height:12px; } /* 20px */ .statsbox-value { font-size: 20px; } /* 36px */ - .statsbox-units { display: none; } /* hide units */ .statsbox-prc { font-size: 12px; } /* 16px */ .statsbox-arrow-down:after { border-width: 8px; @@ -342,6 +351,12 @@ body { } } +/* Mobile (max 576px) */ +@media (max-width: 576px) { + .statsbox-title { font-size: 12px; padding-bottom: 4px; } /* 20px */ + .statsbox-units { display: none; } /* hide units */ +} + .stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php index e28ed51a..766e39d8 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -23,8 +23,7 @@