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 0kWh
+
@@ -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 name
+
Node
+
Status
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
0W
+
+
+
+
0W
+
+
+
+
0W
+
+
+
+
0W
+
+
+
+
--%
+
+
+
+
+
+
+
+
+
+
+
+
+
---kWh
+
+
+
+
---kWh
+
+
+
+
---kWh
+
+
+
+
---kWh
+
+
+
+
+
+
+
---kWh
+
+
+
+
---kWh
+
+
+
+
---kWh
+
+
+
+
---kWh
+
+
+
+
---kWh
+
+
+
+
---kWh
+
+
+
+
---kWh
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
}