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 2e9736d8..2ff89766 100644 --- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php @@ -272,7 +272,10 @@
- + +
+
BATTERY TO GRID
0 kWh
+
@@ -328,9 +331,43 @@

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).

+ +
+

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
+ +
+ + + + +
+
+
@@ -353,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; @@ -372,35 +409,39 @@ function getTranslations(){ // Configuration // ---------------------------------------------------------------------- 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"}, - // 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 %"}, - - // 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"}, + // == 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_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 (energy flow breakdown from solarbatterykwh post-processor) + "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"}, - - "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 = ; -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()}; @@ -442,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; @@ -464,7 +506,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 { @@ -520,7 +564,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(); } @@ -531,6 +577,19 @@ 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); + } + }); + */ } function resize() @@ -573,8 +632,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 +649,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 +663,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') { @@ -657,8 +722,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; @@ -702,10 +767,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 +778,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 +949,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 +958,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 +973,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"}); } @@ -967,19 +1073,22 @@ 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 - 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); + // 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, + 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 < earliest_start_time) earliest_start_time = m.start_time; + } + latest_start_time = earliest_start_time; view.first_data = latest_start_time * 1000; } @@ -987,91 +1096,95 @@ 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; - // 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 = []; - if (solar_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)
"); } // ------------------------------------------------------------------------------------------ @@ -1122,45 +1235,54 @@ function bargraph_events() { if (item) { var z = item.dataIndex; - var total_solar_kwh = solar_kwhd_data[z][1]; - var total_use_kwh = use_kwhd_data[z][1]; - var total_solar_export_kwh = export_kwhd_data[z][1]*-1; - var total_solar_direct_kwh = solar_direct_kwhd_data[z][1]; - var total_battery_charge_kwh = battery_charge_kwhd_data[z][1]; - var total_battery_discharge_kwh = battery_discharge_kwhd_data[z][1]; - var total_import_kwh = import_kwhd_data[z][1]; - - 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; - + // 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_import_direct_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_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)+"%"); + $(".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_direct_kwh/total_use_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_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)); - $(".use_from_battery_prc").html((100*total_battery_discharge_kwh/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_import_for_battery_kwh>=0.1) { + 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 { @@ -1177,7 +1299,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); @@ -1260,4 +1382,186 @@ 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(); + 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); + }); +} + +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; + $("#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 new file mode 100644 index 00000000..e565bdd2 --- /dev/null +++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php @@ -0,0 +1,78 @@ +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(); + + $tag = "app_mysolarpvbattery_".$app->id; + + $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, + + "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" + ); + + // 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 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..ea76c1cf --- /dev/null +++ b/apps/OpenEnergyMonitor/solartemplate/solartemplate.js @@ -0,0 +1,494 @@ +// 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. +// 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", + "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 = discharge, negative = charge)" + }, + "grid": { + "type": "feed", + "autoname": "grid", + "engine": "5", + "description": "Grid power in watts (positive = import, negative = export)" + }, + "battery_soc": { + "type": "feed", + "autoname": "battery_soc", + "engine": "5", + "description": "Battery state of charge in percent (%)" + } +}; + +// Fetch user feed list (used by the config UI to populate feed pickers) +config.feeds = feed.list(); + +// 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 +// ---------------------------------------------------------------------- + +// 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: { + mode: "time", + timezone: "browser" + }, + yaxes: [ + { min: 0 }, // yaxis 1: power — never show negative + { min: 0, max: 100, position: "right" } // yaxis 2: SOC % + ], + grid: { + show: true, + color: "#aaa", + borderWidth: 0, + hoverable: true + }, + legend: { + show: false + }, + selection: { + mode: "x", + color: "#555" + } +}; + +// 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(); + + // 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; + } + view.start = view.end - (3600000 * 24.0); + + load(); +} + +function load() { + view.calc_interval(1500); + + // 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; + }); + + // 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 all feed data in a single batch request + feed.getdata(feedids, view.start, view.end, view.interval, average, 0, skipmissing, limitinterval, function (all_data) { + + // 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); + } + }); + + + // Conversion factor: multiply instantaneous watts by this to get kWh for one interval + let power_to_kwh = view.interval / 3600000.0; + + let use_kwh = 0; + let solar_kwh = 0; + let import_kwh = 0; + let export_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"] = []; + + data["battery_to_load"] = []; + data["battery_to_grid"] = []; + + data["grid_to_load"] = []; + data["grid_to_battery"] = []; + + 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 = discharging, negative = charging + let battery_power = data["battery_power"][z][1]; + // positive grid = importing, negative = exporting + let grid = data["grid"][z][1]; + + // Skip data points where any feed has a null value + if (use == null || solar == null || battery_power == null || grid == null) { + continue; + } + + // ------------------------------------------------------------------------------------------------ + // 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; + + 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); + + let solar_to_battery = 0; + if (battery_power < 0) { + // 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 FLOWS + // Only relevant when discharging (battery_power > 0) + // Priority: battery → load first, remainder → grid + // ------------------------------------------------------------------------------------------------ + let battery_to_load = 0; + let battery_to_grid = 0; + if (battery_power > 0) { + battery_to_load = Math.min(battery_power, use - solar_to_load); + battery_to_grid = battery_power - battery_to_load; + } + + // ------------------------------------------------------------------------------------------------ + // 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_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 + // ------------------------------------------------------------------------------------------------ + 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; + + // ------------------------------------------------------------------------------------------------ + // STORE TIME-SERIES DATA POINTS FOR GRAPH + // ------------------------------------------------------------------------------------------------ + 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]); + } + + // 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)); + $("#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 — 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; + + 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: "#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"); + + +} + +function reset_series() { + powergraph_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; + + // 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() { + 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(window.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: show the series label and value when hovering over the graph +$('#graph').bind("plothover", function (event, pos, item) { + if (item) { + if (previousPoint != item.datapoint) { + previousPoint = item.datapoint; + $("#tooltip").remove(); + + let itemTime = item.datapoint[0]; + let itemValue = item.datapoint[1]; + let label = item.series.label || ""; + + 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(); + previousPoint = null; + } +}); + +$('#graph').bind("plotselected", function (event, ranges) { + view.start = ranges.xaxis.from; + view.end = ranges.xaxis.to; + load(); +}); + +$(window).resize(function () { + resize(); +}); + +// ---------------------------------------------------------------------- +// App log — simple wrapper; extend for production logging if needed +// ---------------------------------------------------------------------- +function app_log(level, message) { + if (level === "ERROR") alert(level + ": " + message); + console.log(level + ": " + message); +} + +// 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; +} \ 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..986f264e --- /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 }