diff --git a/Lib/appconf.js b/Lib/appconf.js
deleted file mode 100644
index 0ad56304..00000000
--- a/Lib/appconf.js
+++ /dev/null
@@ -1,533 +0,0 @@
-var config = {
-
- initialized: false,
-
- id: false,
- name: "",
- db: {},
- app: {},
- feeds: {},
- feedsbyid: {},
- feedsbyname: {},
-
- init: function() {
- // console.log("CONFIG: Init");
-
- for (var z in config.feeds) config.feedsbyname[config.feeds[z].name] = config.feeds[z];
- for (var z in config.feeds) config.feedsbyid[config.feeds[z].id] = config.feeds[z];
-
- // Check that the config is complete first otherwise show config interface
- if (!config.check()) {
- if (!public_userid) {
- if (session_write == undefined) {
- config.showConfig(); // Show setup block
- config.UI(); // Populate setup UI options
- } else {
- if (session_write) {
- config.showConfig(); // Show setup block
- config.UI(); // Populate setup UI options
- } else {
- alert("Invalid app configuration");
- }
- }
- } else {
- $("#app-block").show(); // Show app block
- }
- $(".ajax-loader").hide(); // Hide AJAX loader
- } else {
- $("#app-block").show(); // Show app block
- $(".ajax-loader").show(); // Show AJAX loader
-
- config.load(); // Merge db config into app config
- config.initapp();
- config.initialized = true; // Init app
- config.showapp();
- }
-
- $("body").on("click",".config-open", function() {
- config.showConfig();
- config.UI();
- });
-
- // don't save and just show app
- $("body").on("click",".config-close", function(event) {
- config.closeConfig();
- });
-
- // save and show app
- $("body").on("click",".app-launch",function() {
- $(".ajax-loader").show();
- config.closeConfig();
- config.load();
- if (!config.initialized) {
- config.initapp();
- config.initialized = true;
- }
- config.showapp();
- });
-
- $("body").on("click",".app-delete",function(){
- console.log("delete: "+config.id);
- $.ajax({
- url: path+"app/remove",
- data: "id="+config.id,
- dataType: 'text',
- async: false,
- success: function(result){
- try {
- result = JSON.parse(result);
- if (result.success != undefined && !result.success) appLog("ERROR", result.message);
- window.location = path+"app/view";
- } catch (e) {
- app.log("ERROR","Could not parse /setconfig reply, error: "+e);
- }
- }
- });
- });
- },
-
- /**
- * hide the app config window and show the app.
- * enable the buttons in the app header
- */
- closeConfig: function () {
- $("#app-block").show();
- $("#app-setup").hide();
-
- $('.config-open').show();
- $('.config-close').hide();
- $('#buttons #tabs .btn').attr('disabled',false).css('opacity',1);
- // allow app to react to closing config window
- $('body').trigger('config.closed')
- },
-
- /**
- * hide the app window and show the config window.
- * disable the buttons in the app header
- */
- showConfig: function () {
- $("#app-block").hide();
- $("#app-setup").show();
-
- $('.config-open').hide();
- $('.config-close').show();
- $('#buttons #tabs .btn').attr('disabled',true).css('opacity',.2);
- },
-
- UI: function() {
- $(".app-config").html("");
- $("body").css('background-color','#222');
- $("#footer").css('background-color','#181818');
- $("#footer").css('color','#999');
-
- // Remove old config items that are no longer used/described in new config definition
- if (config.db.constructor===Array) config.db = {};
-
- for (var z in config.db) {
- if (config.db[z]==undefined) delete config.db[z];
- }
-
- // Draw the config interface from the config object:
- var out = "";
-
- // Option to set app name
- out += "
";
- out += " App name (menu)";
- out += " ";
- out += "";
- out += "
";
- // Option to set public or private
- out += "
";
- out += " Public";
- out += " Make app public";
- var checked = ""; if (config.public) checked = "checked";
- out += " ";
- out += "
";
-
- out += " ";
-
- for (var z in config.app) {
- out += "
";
- if (config.app[z].type=="feed") {
-
- var selection_mode = "AUTO";
- if (config.db[z]=="disable") selection_mode = "DISABLED";
-
- out += " "+config.app[z].autoname+" ["+selection_mode+"]";
- out += " ";
- out += "";
- } else if (config.app[z].type=="value") {
- out += " "+config.app[z].name+"";
- out += " ";
- out += "";
- } else if (config.app[z].type=="checkbox") {
- out += " "+config.app[z].name+"";
- out += " ";
- var checked = ""; if (config.app[z].default) checked = "checked";
- out += " ";
- } else if (config.app[z].type=="select") {
- out += " "+config.app[z].name+"";
- out += "";
- }
- out += "
";
- }
-
- out += " ";
-
- $(".app-config").html(out);
-
- for (var z in config.app) {
- var configItem = $(".app-config-box[key="+z+"]");
-
- if (config.app[z].type=="feed") {
- // Create list of feeds that satisfy engine requirement
- var out = "" +
- "" +
- ""
-
- var feedsbygroup = [];
- for (var f in config.feedsbyid) {
- if (config.engine_check(config.feedsbyid[f], config.app[z])) {
- var group = (config.feedsbyid[f].tag === null ? "NoGroup" : config.feedsbyid[f].tag);
- if (group != "Deleted") {
- if (!feedsbygroup[group]) feedsbygroup[group] = []
- feedsbygroup[group].push(config.feedsbyid[f]);
- }
- }
- }
- for (group in feedsbygroup) {
- out += "";
- }
- configItem.find(".feed-select").html(out);
-
- var feedvalid = false;
- // Check for existing configuration
- if (config.db[z]!=undefined) {
- var feedid = config.db[z];
- if (config.feedsbyid[feedid]!=undefined && config.engine_check(config.feedsbyid[feedid],config.app[z])) {
- var keyappend = ""; if (z!=config.feedsbyid[feedid].name) keyappend = z+": ";
- configItem.find(".feed-name").html(keyappend+config.feedsbyid[feedid].name);
- configItem.find(".feed-select").val(feedid);
- configItem.find(".feed-select-div").hide();
- feedvalid = true;
- } else {
- // Invalid feedid remove from configuration
- delete config.db[z];
- }
- }
-
- // Important that this is not an else here as an invalid feedid
- // in the section above will call delete making the item undefined again
- if (config.db[z]==undefined) {
- // Auto match feeds that follow the naming convention
- for (var n in config.feedsbyid) {
- if (config.feedsbyid[n].name==config.app[z].autoname && config.engine_check(config.feedsbyid[n],config.app[z])) {
- configItem.find(".feed-select").val("auto");
- configItem.find(".feed-select-div").hide();
- feedvalid = true;
- }
- }
- }
-
- // Indicator icon to show if setup correctly or not
- if (!feedvalid) {
- configItem.find(".status").removeClass("icon-ok-sign");
- configItem.find(".status").addClass("icon-remove-circle");
- }
- }
-
- if (config.app[z].type=="value") {
- if (config.db[z]!=undefined) configItem.find(".app-config-value").val(config.db[z]);
- }
-
- if (config.app[z].type=="checkbox") {
- if (config.db[z]!=undefined) configItem.find(".app-config-value")[0].checked = config.db[z];
- }
-
- if (config.app[z].type=="select") {
- if (config.db[z]!=undefined) configItem.find(".app-config-value").val(config.db[z]);
- }
-
- // Set description
- configItem.find(".app-config-info").html(config.app[z].description);
- }
-
- if (config.check()) {
- $(".app-launch").show();
- }
-
- // Brings up the feed selector if the pencil item is clicked
- $(".app-config-edit").unbind("click");
- $(".app-config-edit").click(function(){
- var key = $(this).parent().attr("key");
- var configItem = $(".app-config-box[key="+key+"]");
-
- if (config.app[key].type=="feed") {
- configItem.find(".feed-select-div").show();
- }
- });
-
- $(".feed-select-ok").unbind("click");
- $(".feed-select-ok").click(function(){
- var key = $(this).parent().parent().attr("key");
- var configItem = $(".app-config-box[key="+key+"]");
-
- var feedid = $(this).parent().find(".feed-select").val();
-
- if (feedid!="auto" && feedid!=0 && feedid!="disable") {
- config.db[key] = feedid;
- var keyappend = ""; if (key!=config.feedsbyid[feedid].name) keyappend = key+": ";
- configItem.find(".feed-name").html(keyappend+config.feedsbyid[feedid].name);
- configItem.find(".status").addClass("icon-ok-sign");
- configItem.find(".status").removeClass("icon-remove-circle");
- // Save config
- }
-
- if (feedid=="auto") {
- delete config.db[key];
- configItem.find(".feed-name").html(config.app[key].autoname+" [AUTO]");
- }
-
- if (feedid=="disable") {
- config.db[key] = "disable"
- configItem.find(".feed-name").html(config.app[key].autoname+" [DISABLED]");
- }
-
- if (feedid!=0 ) {
- configItem.find(".feed-select-div").hide();
- config.set();
-
- if (config.check()) {
- $(".app-launch").show();
- } else {
- $(".app-launch").hide();
- }
- }
- });
-
- $(".app-config-value").unbind("click");
- $(".app-config-value").change(function(){
- var value = false;
- var key = $(this).parent().attr("key");
- var configItem = $(".app-config-box[key="+key+"]");
-
- if (config.app[key].type=="value") {
- value = $(this).val();
- } else if (config.app[key].type=="checkbox") {
- value = $(this)[0].checked;
- } else if (config.app[key].type=="select") {
- value = $(this).val();
- }
-
- config.db[key] = value;
- config.set();
- });
-
- $(".app-config-name").unbind("change");
- $(".app-config-name").change(function(){
- var value = $(this).val();
- config.name = value;
- config.set_name();
- });
-
- $(".app-config-public").unbind("change");
- $(".app-config-public").change(function(){
- var value = $(this)[0].checked;
- config.public = 1*value;
- config.set_public();
- });
- },
-
- check: function()
- {
- var valid = {};
- for (var key in config.app) {
- if (config.app[key].type=="feed") {
- if (config.app[key].optional!=undefined && config.app[key].optional) {
-
- } else {
- valid[key] = false;
-
- if (config.db[key]==undefined) {
- // Check if feeds match naming convention and engine
- var autoname = config.app[key].autoname;
- if (config.feedsbyname[autoname]!=undefined) {
- if (config.engine_check(config.feedsbyname[autoname],config.app[key])) valid[key] = true;
- }
- } else {
- // Overwrite with any user set feeds if applicable
- for (var z in config.feedsbyid) {
- // Check that the feed exists
- // config will be shown if a previous valid feed has been deleted
- var feedid = config.feedsbyid[z].id;
- if (config.db[key] == feedid) {
- if (config.engine_check(config.feedsbyid[z],config.app[key])) valid[key] = true;
- break;
- }
- }
- }
- }
- }
- }
-
- for (var key in valid) {
- if (valid[key]==false) return false;
- }
-
- return true;
- },
-
- load: function()
- {
- var auto_conf_to_save = false;
-
- for (var key in config.app) {
-
- if (config.app[key].type=="feed") {
- config.app[key].value = false;
-
-
- // Overwrite with any user set feeds if applicable
- if (config.db[key]!=undefined) {
- config.app[key].value = config.db[key];
- } else {
- // Check if feeds match naming convention
- var autoname = config.app[key].autoname;
- if (config.feedsbyname[autoname]!=undefined) {
- config.app[key].value = config.feedsbyname[autoname].id;
- config.db[key] = config.feedsbyname[autoname].id;
- auto_conf_to_save = true;
- }
- }
- }
-
- if (config.app[key].type=="value") {
- if (config.db[key]!=undefined) {
- config.app[key].value = config.db[key];
- } else {
- config.app[key].value = config.app[key].default;
- }
- }
-
- if (config.app[key].type=="checkbox") {
- if (config.db[key]!=undefined) {
- config.app[key].value = config.db[key];
- } else {
- config.app[key].value = config.app[key].default;
- }
- }
-
- if (config.app[key].type=="select") {
- if (config.db[key]!=undefined) {
- config.app[key].value = config.db[key];
- } else {
- config.app[key].value = config.app[key].default;
- }
- }
- }
-
- if (auto_conf_to_save) {
- config.set();
- }
-
- return config.app;
- },
-
- engine_check: function(feed,conf)
- {
- if (typeof conf.engine === 'undefined') {
- return true;
- }
- if (isNaN(conf.engine)) {
- var engines = conf.engine.split(",");
- if (engines.length>0) {
- for (var z in engines) {
- if (feed.engine==engines[z]) return true;
- }
- }
- } else {
- if (feed.engine*1==conf.engine*1) return true;
- }
- return false;
- },
-
- set: function()
- {
- $.ajax({
- url: path+"app/setconfig",
- data: "id="+config.id+"&config="+JSON.stringify(config.db),
- dataType: 'text',
- async: false,
- success: function(result){
- try {
- result = JSON.parse(result);
- if (result.success != undefined && !result.success) appLog("ERROR", result.message);
- } catch (e) {
- try {
- app.log("ERROR","Could not parse /setconfig reply, error: "+e);
- } catch (e2) {
- console.log(e,e2);
- }
- }
- }
- });
- },
-
- set_name: function()
- {
- $.ajax({
- url: path+"app/setname",
- data: "id="+config.id+"&name="+config.name,
- dataType: 'text',
- async: false,
- success: function(result){
- try {
- result = JSON.parse(result);
- if (result.success != undefined && !result.success) appLog("ERROR", result.message);
- } catch (e) {
- try {
- app.log("ERROR","Could not parse /setname reply, error: "+e);
- } catch (e2) {
- console.log(e,e2);
- }
- }
- }
- });
- },
-
- set_public: function()
- {
- $.ajax({
- url: path+"app/setpublic",
- data: "id="+config.id+"&public="+config.public,
- dataType: 'text',
- async: false,
- success: function(result){
- try {
- result = JSON.parse(result);
- if (result.success != undefined && !result.success) appLog("ERROR", result.message);
- } catch (e) {
- try {
- app.log("ERROR","Could not parse /setpublic reply, error: "+e);
- } catch (e2) {
- console.log(e,e2);
- }
- }
- }
- });
- },
-
- sortByLongname: function(a, b){
- var aName = a.longname.toLowerCase();
- var bName = b.longname.toLowerCase();
- return ((aName < bName) ? -1 : ((aName > bName) ? 1 : 0));
- }
-}
diff --git a/Views/css/config.css b/Lib/appconf/appconf.css
similarity index 51%
rename from Views/css/config.css
rename to Lib/appconf/appconf.css
index 0f769b04..400cce67 100644
--- a/Views/css/config.css
+++ b/Lib/appconf/appconf.css
@@ -1,4 +1,42 @@
+/* -------------------------------------------------------------------
+ Config
+--------------------------------------------------------------------*/
+.app-config-box {
+ border: 1px solid #fff;
+ color: #fff;
+}
+
+.app-config-box-main {
+ border: 1px solid #aaa;
+ color: #fff;
+}
+
+.app-config-description-inner {
+ padding-right: 20px;
+ color: #ccc;
+}
+
+.app-config select {
+ background-color: #333;
+ color: #fff;
+}
+.app-config select optgroup {
+ background-color: #666;
+}
+.app-config input[type=text] {
+ background-color: #333;
+ color: #fff;
+}
+.app-config .icon-app-config {
+ background-image: url("../../../../Lib/bootstrap/img/glyphicons-halflings-white.png");
+ margin-top:5px;
+}
+
+.app-config .feed-auto {
+ color: #ccc;
+}
+
.app-config-box {
margin-bottom:8px;
padding: 8px 10px 10px 10px;
diff --git a/Lib/appconf/appconf.js b/Lib/appconf/appconf.js
new file mode 100644
index 00000000..3054647d
--- /dev/null
+++ b/Lib/appconf/appconf.js
@@ -0,0 +1,835 @@
+
+
+
+var config = {
+
+ initialized: false,
+
+ id: false,
+ name: "",
+ db: {},
+ app: {},
+ feeds: {},
+ feedsbyid: {},
+ feedsbyname: {},
+ app_name: "",
+ // Common options:
+ // Blue: #44b3e2, Green: #5cb85c, Yellow: #f0ad4e, Red: #d9534f, Grey: #aaa
+ app_name_color: "#44b3e2",
+
+ init: function() {
+ // console.log("CONFIG: Init");
+
+ vue_config.app_name = config.app_name;
+ vue_config.app_name_color = config.app_name_color || "#44b3e2";
+ let html = $("#appconf-description").html();
+ vue_config.app_description = html ? html : "";
+
+ for (var z in config.feeds) config.feedsbyname[config.feeds[z].name] = config.feeds[z];
+ for (var z in config.feeds) config.feedsbyid[config.feeds[z].id] = config.feeds[z];
+
+ // Check that the config is complete first otherwise show config interface
+ if (!config.check()) {
+ if (!public_userid) {
+ if (session_write == undefined) {
+ config.showConfig(); // Show setup block
+ config.UI(); // Populate setup UI options
+ config.autogen.render_feed_list();
+ } else {
+ if (session_write) {
+ config.showConfig(); // Show setup block
+ config.UI(); // Populate setup UI options
+ config.autogen.render_feed_list();
+ } else {
+ alert("Invalid app configuration");
+ }
+ }
+ } else {
+ $("#app-block").show(); // Show app block
+ }
+ $(".ajax-loader").hide(); // Hide AJAX loader
+ } else {
+ $("#app-block").show(); // Show app block
+ $(".ajax-loader").show(); // Show AJAX loader
+
+ config.load(); // Merge db config into app config
+ config.initapp();
+ config.initialized = true; // Init app
+ config.showapp();
+ }
+
+ $("body").on("click",".config-open", function() {
+ config.showConfig();
+ config.UI();
+ });
+
+ // don't save and just show app
+ $("body").on("click",".config-close", function(event) {
+ config.closeConfig();
+ });
+ },
+
+ /**
+ * hide the app config window and show the app.
+ * enable the buttons in the app header
+ */
+ closeConfig: function () {
+ $("#app-block").show();
+ $("#app-setup").hide();
+
+ $('.config-open').show();
+ $('.config-close').hide();
+ $('#buttons #tabs .btn').attr('disabled',false).css('opacity',1);
+ // allow app to react to closing config window
+ $('body').trigger('config.closed')
+ },
+
+ /**
+ * hide the app window and show the config window.
+ * disable the buttons in the app header
+ */
+ showConfig: function () {
+ $("#app-block").hide();
+ $("#app-setup").show();
+
+ $('.config-open').hide();
+ $('.config-close').show();
+ $('#buttons #tabs .btn').attr('disabled',true).css('opacity',.2);
+ },
+
+ UI: function() {
+ $("body").css('background-color','#222');
+ $("#footer").css('background-color','#181818');
+ $("#footer").css('color','#999');
+ vue_config.renderUI();
+ },
+
+ check: function()
+ {
+ var valid = {};
+ for (var key in config.app) {
+ if (config.app[key].type=="feed") {
+ if (config.app[key].optional!=undefined && config.app[key].optional) {
+
+ } else {
+ valid[key] = false;
+
+ if (config.db[key]==undefined) {
+ // Check if feeds match naming convention and engine
+ var autoname = config.app[key].autoname;
+ if (config.feedsbyname[autoname]!=undefined) {
+ if (config.engine_check(config.feedsbyname[autoname],config.app[key])) valid[key] = true;
+ }
+ } else {
+ // Overwrite with any user set feeds if applicable
+ for (var z in config.feedsbyid) {
+ // Check that the feed exists
+ // config will be shown if a previous valid feed has been deleted
+ var feedid = config.feedsbyid[z].id;
+ if (config.db[key] == feedid) {
+ if (config.engine_check(config.feedsbyid[z],config.app[key])) valid[key] = true;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ for (var key in valid) {
+ if (valid[key]==false) return false;
+ }
+
+ return true;
+ },
+
+ load: function()
+ {
+ var auto_conf_to_save = false;
+
+ for (var key in config.app) {
+
+ if (config.app[key].type=="feed") {
+ config.app[key].value = false;
+
+
+ // Overwrite with any user set feeds if applicable
+ if (config.db[key]!=undefined) {
+ config.app[key].value = config.db[key];
+ } else {
+ // Check if feeds match naming convention
+ var autoname = config.app[key].autoname;
+ if (config.feedsbyname[autoname]!=undefined) {
+ config.app[key].value = config.feedsbyname[autoname].id;
+ config.db[key] = config.feedsbyname[autoname].id;
+ auto_conf_to_save = true;
+ }
+ }
+ }
+
+ if (config.app[key].type=="value") {
+ if (config.db[key]!=undefined) {
+ config.app[key].value = config.db[key];
+ } else {
+ config.app[key].value = config.app[key].default;
+ }
+ }
+
+ if (config.app[key].type=="checkbox") {
+ if (config.db[key]!=undefined) {
+ config.app[key].value = config.db[key];
+ } else {
+ config.app[key].value = config.app[key].default;
+ }
+ }
+
+ if (config.app[key].type=="select") {
+ if (config.db[key]!=undefined) {
+ config.app[key].value = config.db[key];
+ } else {
+ config.app[key].value = config.app[key].default;
+ }
+ }
+ }
+
+ if (auto_conf_to_save) {
+ config.set();
+ }
+
+ return config.app;
+ },
+
+ engine_check: function(feed,conf)
+ {
+ if (typeof conf.engine === 'undefined') {
+ return true;
+ }
+ if (isNaN(conf.engine)) {
+ var engines = conf.engine.split(",");
+ if (engines.length>0) {
+ for (var z in engines) {
+ if (feed.engine==engines[z]) return true;
+ }
+ }
+ } else {
+ if (feed.engine*1==conf.engine*1) return true;
+ }
+ return false;
+ },
+
+ set: function()
+ {
+ $.ajax({
+ url: path+"app/setconfig",
+ data: "id="+config.id+"&config="+JSON.stringify(config.db),
+ dataType: 'text',
+ async: false,
+ success: function(result){
+ try {
+ result = JSON.parse(result);
+ if (result.success != undefined && !result.success) appLog("ERROR", result.message);
+
+ // If success update config.app with latest from config.db
+ for (var key in config.app) {
+ if (config.app[key].type == "feed" || config.app[key].type == "value" || config.app[key].type == "checkbox" || config.app[key].type == "select") {
+ if (config.db[key] != undefined) {
+ config.app[key].value = config.db[key];
+ // console.log("Updated config.app["+key+"].value to "+config.db[key]);
+ }
+ }
+ }
+
+ } catch (e) {
+ try {
+ app.log("ERROR","Could not parse /setconfig reply, error: "+e);
+ } catch (e2) {
+ console.log(e,e2);
+ }
+ }
+ }
+ });
+ },
+
+ set_name: function()
+ {
+ $.ajax({
+ url: path+"app/setname",
+ data: "id="+config.id+"&name="+config.name,
+ dataType: 'text',
+ async: false,
+ success: function(result){
+ try {
+ result = JSON.parse(result);
+ if (result.success != undefined && !result.success) appLog("ERROR", result.message);
+ } catch (e) {
+ try {
+ app.log("ERROR","Could not parse /setname reply, error: "+e);
+ } catch (e2) {
+ console.log(e,e2);
+ }
+ }
+ }
+ });
+ },
+
+ set_public: function()
+ {
+ $.ajax({
+ url: path+"app/setpublic",
+ data: "id="+config.id+"&public="+config.public,
+ dataType: 'text',
+ async: false,
+ success: function(result){
+ try {
+ result = JSON.parse(result);
+ if (result.success != undefined && !result.success) appLog("ERROR", result.message);
+ } catch (e) {
+ try {
+ app.log("ERROR","Could not parse /setpublic reply, error: "+e);
+ } catch (e2) {
+ console.log(e,e2);
+ }
+ }
+ }
+ });
+ },
+
+ sortByLongname: function(a, b){
+ var aName = a.longname.toLowerCase();
+ var bName = b.longname.toLowerCase();
+ return ((aName < bName) ? -1 : ((aName > bName) ? 1 : 0));
+ },
+
+ // -----------------------------------------------------------------------
+ // autogen: helpers for apps that declare feeds with autogenerate:true
+ //
+ // Apps opt in by setting before calling config.init():
+ //
+ // config.autogen_node_prefix = "app_myappname";
+ // The tag used for auto-generated feeds becomes
+ // config.autogen_node_prefix + "_" + config.id
+ // Defaults to "app_" + config.id when not set.
+ //
+ // config.autogen_feed_defaults = { datatype: 1, engine: 5, options: { interval: 1800 } };
+ // Merged into each feed/create.json request.
+ // Defaults to { datatype: 1, engine: 5, options: { interval: 1800 } } when not set.
+ //
+ // config.autogen_feeds_by_tag_name = feed.by_tag_and_name(config.feeds);
+ // Lookup used to check which feeds already exist.
+ // Must be kept up to date by the app after refreshing config.feeds.
+ // -----------------------------------------------------------------------
+ autogen: {
+
+ post_process_already_running: false,
+ post_process_run_count: 0,
+
+ // Return the node tag string used for all auto-generated feeds
+ node_name: function() {
+ var node_name = config.app.autogenerate_nodename.default;
+ if (config.app.autogenerate_nodename.value != undefined) {
+ node_name = config.app.autogenerate_nodename.value;
+ }
+
+ return node_name.trim();
+ },
+
+ // Set autogenerate node name when the user clicks "Set node" button
+ set_node: function() {
+ var node_name = vue_config.autogen_node.trim();
+ if (node_name) {
+ config.db['autogenerate_nodename'] = node_name;
+ config.app.autogenerate_nodename.value = node_name;
+ config.set();
+ config.autogen.render_feed_list();
+ } else {
+ alert("Please enter a valid node name");
+ }
+ },
+
+ // Return array of { key, name, feedid } for every feed marked autogenerate:true
+ get_feeds: function() {
+ var node_name = config.autogen.node_name();
+ var lookup = config.autogen_feeds_by_tag_name || {};
+ var feeds = [];
+
+ for (var key in config.app) {
+ if (config.app.hasOwnProperty(key) && config.app[key].autogenerate) {
+ // Skip hidden feeds (e.g. battery feeds when has_battery is off)
+ if (config.app[key].hidden === true) continue;
+
+ var feed_name = config.app[key].autoname || key;
+ var feedid = false;
+
+ if (lookup[node_name] != undefined) {
+ if (lookup[node_name][feed_name] != undefined) {
+ feedid = lookup[node_name][feed_name]['id'];
+ }
+ }
+
+ feeds.push({ key: key, name: feed_name, feedid: feedid });
+ }
+ }
+ return feeds;
+ },
+
+ // Render the autogen feed list — delegates to vue_config
+ render_feed_list: function() {
+ config.autogen.refresh_autogen_feed_references();
+ vue_config.renderAutogenFeedList();
+ },
+
+ // Create any feeds that are missing
+ create_missing_feeds: function() {
+ var node_name = config.autogen.node_name();
+ var missing = config.autogen.get_feeds().filter(function(f) { return !f.feedid; });
+ var defaults = config.autogen_feed_defaults || { datatype: 1, engine: 5, options: { interval: 1800 } };
+ var created = 0, errors = 0;
+
+ vue_config.autogen_status = "Creating feeds...";
+ vue_config.autogen_status_color = "#aaa";
+
+ missing.forEach(function(item) {
+ $.ajax({
+ url: path + "feed/create.json",
+ data: $.extend({}, defaults, {
+ tag: node_name,
+ name: item.name,
+ options: JSON.stringify(defaults.options || { interval: 1800 }),
+ apikey: apikey,
+ unit: "kWh"
+ }),
+ dataType: "json",
+ async: false,
+ success: function(res) { (res && res.feedid) ? created++ : errors++; },
+ error: function() { errors++; }
+ });
+ });
+
+ // Refresh feed lookups
+ config.feeds = feed.list();
+ config.feedsbyname = {};
+ config.feedsbyid = {};
+ for (var z in config.feeds) config.feedsbyname[config.feeds[z].name] = config.feeds[z];
+ for (var z in config.feeds) config.feedsbyid[config.feeds[z].id] = config.feeds[z];
+ config.autogen_feeds_by_tag_name = feed.by_tag_and_name(config.feeds);
+
+ // Populate config.app[key].value for the newly created autogen feeds
+ config.autogen.refresh_autogen_feed_references();
+ config.load();
+
+ config.autogen.render_feed_list();
+
+ var statusText = errors === 0
+ ? "Created " + created + " feed(s) successfully."
+ : "Created " + created + " feed(s), " + errors + " error(s).";
+ vue_config.autogen_status = statusText;
+ vue_config.autogen_status_color = errors === 0 ? "#5cb85c" : "#f0ad4e";
+ },
+
+ start_post_processor: function() {
+ if (config.autogen.post_process_already_running) {
+ vue_config.autogen_status = "Post-processor is already running, please wait...";
+ vue_config.autogen_status_color = "#f0ad4e";
+ return false;
+ } else {
+ config.autogen.post_process_already_running = true;
+ }
+ config.autogen.post_process_run_count = 0;
+ config.autogen.run_post_processor();
+ },
+
+ // Trigger the app post-processor via app/process
+ run_post_processor: function() {
+
+ if (config.autogen.post_process_run_count == 0) {
+ vue_config.autogen_status = "Starting post-processor...";
+ vue_config.autogen_status_color = "#aaa";
+ } else {
+ vue_config.autogen_status = "Post-processor is still running, please wait...";
+ vue_config.autogen_status_color = "#f0ad4e";
+ }
+
+ config.autogen.post_process_run_count++;
+
+ $.ajax({
+ url: path + "app/process",
+ data: { id: config.id, apikey: apikey },
+ dataType: "json",
+ timeout: 120000,
+ success: function(result) {
+ if (result && result.success) {
+
+ if (result.more_to_process) {
+ vue_config.autogen_status = "Post-processor is still running, please wait...";
+ vue_config.autogen_status_color = "#f0ad4e";
+ // run post processor again but only a maximum of 30 times (5 mins) to prevent infinite loops in case of an issue
+ if (config.autogen.post_process_run_count < 30) {
+ setTimeout(function() {
+ config.autogen.run_post_processor();
+ }, 1000);
+ } else {
+ vue_config.autogen_status = "Post-processor is taking longer than expected, please check the app logs.";
+ vue_config.autogen_status_color = "#f0ad4e";
+ config.autogen.post_process_already_running = false;
+ }
+ } else {
+ vue_config.autogen_status = "Post-processor completed successfully.";
+ vue_config.autogen_status_color = "#5cb85c";
+ config.autogen.post_process_already_running = false;
+ }
+
+
+ } else {
+ var msg = (result && result.message) ? result.message : "Unknown response";
+ vue_config.autogen_status = "Post-processor: " + msg;
+ vue_config.autogen_status_color = "#f0ad4e";
+ config.autogen.post_process_already_running = false;
+ }
+ },
+ error: function(xhr) {
+ vue_config.autogen_status = "Post-processor failed: " + xhr.statusText;
+ vue_config.autogen_status_color = "#d9534f";
+ config.autogen.post_process_already_running = false;
+ },
+ complete: function() { }
+ });
+ },
+
+ // Clear (reset) all auto-generated feeds after confirmation
+ reset_feeds: function() {
+ var autogen_feeds = config.autogen.get_feeds();
+ var count = autogen_feeds.filter(function(f) { return f.feedid; }).length;
+
+ if (!confirm("Are you sure you want to clear all " + count + " auto-generated feed(s)? This cannot be undone.")) return;
+
+ var feed_ids = autogen_feeds.filter(function(f) { return f.feedid; }).map(function(f) { return f.feedid; });
+
+ if (feed_ids.length === 0) {
+ vue_config.autogen_status = "No matching feeds found to clear.";
+ vue_config.autogen_status_color = "#f0ad4e";
+ return;
+ }
+
+ vue_config.autogen_status = "Clearing feeds...";
+ vue_config.autogen_status_color = "#aaa";
+
+ var cleared = 0, errors = 0;
+
+ feed_ids.forEach(function(id) {
+ $.ajax({
+ url: path + "feed/clear.json",
+ data: { id: id, apikey: apikey },
+ dataType: "json",
+ async: false,
+ success: function(res) { (res && res.success) ? cleared++ : errors++; },
+ error: function() { errors++; }
+ });
+ });
+
+ // Refresh feed list
+ config.feeds = feed.list();
+ config.autogen_feeds_by_tag_name = feed.by_tag_and_name(config.feeds);
+ config.autogen.render_feed_list();
+
+ var statusText = errors === 0
+ ? "Cleared " + cleared + " feed(s) successfully."
+ : "Cleared " + cleared + " feed(s), " + errors + " error(s).";
+ vue_config.autogen_status = statusText;
+ vue_config.autogen_status_color = errors === 0 ? "#5cb85c" : "#f0ad4e";
+ },
+
+ // Update config.app[key].value for each autogen feed based on current feedid lookups so that the UI shows correct feed selections
+ refresh_autogen_feed_references: function() {
+ var feeds = config.autogen.get_feeds();
+ for (var i in feeds) {
+ var f = feeds[i];
+ if (f.feedid) {
+ config.app[f.key].value = f.feedid;
+ config.db[f.key] = f.feedid;
+ } else {
+ delete config.db[f.key];
+ config.app[f.key].value = false;
+ }
+ }
+ }
+ },
+
+ // Reset daily data
+ reset_daily_data: function() {
+ $.ajax({
+ url: path + "app/cleardaily",
+ data: { id: config.id, apikey: apikey },
+ async: true,
+ dataType: "json",
+ success: function (result) {
+ if (result.success) {
+ alert("Daily data cleared, please refresh the page to reload data");
+ app_log("INFO", "Daily data cleared");
+ } else {
+ alert("Failed to clear daily data");
+ app_log("ERROR", "Failed to clear daily data");
+ }
+ }
+ });
+ }
+}
+
+
+var vue_config = new Vue({
+ el: '#vue-config',
+ data: {
+ app_name: "App Name",
+ app_name_color: "#44b3e2",
+ app_description: "",
+ app_instructions: "",
+ config_name: "",
+ config_public: false,
+ config_items: [],
+ config_valid: false,
+ autogen_node: "",
+ autogen_feeds: [],
+ autogen_all_present: false,
+ autogen_status: "",
+ autogen_status_color: "#aaa",
+
+ // Button only currently used by myheatpump app.
+ enable_process_daily: false
+ },
+ methods: {
+
+ // Build config_items from config.app and refresh all derived state.
+ // Called by config.UI() every time the panel needs to (re-)render.
+ renderUI: function() {
+ if (typeof config.ui_before_render === 'function') config.ui_before_render();
+
+ if (config.db.constructor === Array) config.db = {};
+ for (var z in config.db) { if (config.db[z] == undefined) delete config.db[z]; }
+
+ this.config_name = config.name;
+ this.config_public = !!config.public;
+
+ // config.app.enable_process_daily.value
+ if (config.app.enable_process_daily != undefined && config.app.enable_process_daily.value) {
+ this.enable_process_daily = true;
+ } else {
+ this.enable_process_daily = false;
+ }
+
+ var items = [];
+ for (var z in config.app) {
+ if (config.app[z].autogenerate != undefined && config.app[z].autogenerate) continue;
+ if (config.app[z].hidden === true) continue;
+
+ var item = {
+ key: z,
+ type: config.app[z].type,
+ label: config.app[z].name || config.app[z].autoname || z,
+ description: config.app[z].description || "",
+ // feed-specific
+ displayName: config.app[z].autoname || z,
+ selectionMode: "AUTO",
+ isValid: false,
+ showSelector: false,
+ derivable: config.app[z].derivable || false,
+ feedGroups: [],
+ selectedFeedId: 0,
+ // value / checkbox / select
+ inputValue: config.app[z].default !== undefined ? config.app[z].default : "",
+ selectOptions: config.app[z].options || []
+ };
+
+ if (item.type === "feed") {
+ // Build grouped feed options
+ var byGroup = {};
+ for (var f in config.feedsbyid) {
+ if (config.engine_check(config.feedsbyid[f], config.app[z])) {
+ var grp = (config.feedsbyid[f].tag === null ? "NoGroup" : config.feedsbyid[f].tag);
+ if (grp !== "Deleted") {
+ if (!byGroup[grp]) byGroup[grp] = [];
+ byGroup[grp].push(config.feedsbyid[f]);
+ }
+ }
+ }
+ var feedGroups = [];
+ for (var g in byGroup) feedGroups.push({ name: g, feeds: byGroup[g] });
+ item.feedGroups = feedGroups;
+
+ // Resolve current selection
+ var feedvalid = false;
+ if (config.db[z] != undefined) {
+ if (config.db[z] === "disable") {
+ item.selectionMode = "DISABLED";
+ item.selectedFeedId = "disable";
+ } else if (config.db[z] === "derive") {
+ item.selectionMode = "DERIVE";
+ item.selectedFeedId = "derive";
+ } else {
+ var feedid = config.db[z];
+ if (config.feedsbyid[feedid] != undefined && config.engine_check(config.feedsbyid[feedid], config.app[z])) {
+ var keyappend = (z != config.feedsbyid[feedid].name) ? z + ": " : "";
+ item.displayName = keyappend + config.feedsbyid[feedid].name;
+ item.selectionMode = "";
+ item.selectedFeedId = feedid * 1;
+ feedvalid = true;
+ } else {
+ delete config.db[z];
+ }
+ }
+ }
+ // Auto-match by name if nothing is configured
+ if (config.db[z] == undefined) {
+ for (var n in config.feedsbyid) {
+ if (config.feedsbyid[n].name == config.app[z].autoname && config.engine_check(config.feedsbyid[n], config.app[z])) {
+ item.selectedFeedId = "auto";
+ item.selectionMode = "AUTO";
+ feedvalid = true;
+ }
+ }
+ }
+ item.isValid = feedvalid;
+
+ } else if (item.type === "value" || item.type === "checkbox" || item.type === "select") {
+ if (config.db[z] != undefined) item.inputValue = config.db[z];
+ }
+
+ items.push(item);
+ }
+
+ this.config_items = items;
+ this.config_valid = config.check();
+ },
+
+ editFeed: function(key) {
+ var item = this.config_items.find(function(i) { return i.key === key; });
+ item.showSelector = true;
+ },
+
+ selectFeed: function(key, feedid) {
+ if (feedid == 0) return;
+ var item = this.config_items.find(function(i) { return i.key === key; });
+
+ if (feedid !== "auto" && feedid !== "disable" && feedid !== "derive") {
+ config.db[key] = feedid;
+ var keyappend = (key != config.feedsbyid[feedid].name) ? key + ": " : "";
+ item.displayName = keyappend + config.feedsbyid[feedid].name;
+ item.selectionMode = "";
+ item.isValid = true;
+ }
+ if (feedid === "auto") {
+ delete config.db[key];
+ item.displayName = config.app[key].autoname;
+ item.selectionMode = "AUTO";
+ item.isValid = true;
+ }
+ if (feedid === "disable") {
+ config.db[key] = "disable";
+ item.displayName = config.app[key].autoname;
+ item.selectionMode = "DISABLED";
+ item.isValid = false;
+ }
+ if (feedid === "derive") {
+ config.db[key] = "derive";
+ item.displayName = config.app[key].autoname;
+ item.selectionMode = "DERIVE";
+ item.isValid = true;
+ }
+
+ item.showSelector = false;
+ config.set();
+ this.config_valid = config.check();
+ if (typeof config.ui_after_value_change === 'function') config.ui_after_value_change(key);
+ },
+
+ changeValue: function(key, value) {
+ var item = this.config_items.find(function(i) { return i.key === key; });
+ item.inputValue = value;
+ config.db[key] = value;
+ config.set();
+ if (typeof config.ui_after_value_change === 'function') config.ui_after_value_change(key);
+ },
+
+ changeName: function(event) {
+ var value = event.target.value;
+ this.config_name = value;
+ config.name = value;
+ config.set_name();
+ },
+
+ changePublic: function(event) {
+ var value = event.target.checked;
+ this.config_public = value;
+ config.public = value ? 1 : 0;
+ config.set_public();
+ },
+
+ launchApp: function() {
+ $(".ajax-loader").show();
+ config.closeConfig();
+ config.load();
+ if (!config.initialized) {
+ config.initapp();
+ config.initialized = true;
+ }
+ config.showapp();
+ },
+
+ deleteApp: function() {
+ console.log("delete: " + config.id);
+ $.ajax({
+ url: path + "app/remove",
+ data: "id=" + config.id,
+ dataType: 'text',
+ async: false,
+ success: function(result) {
+ try {
+ result = JSON.parse(result);
+ if (result.success != undefined && !result.success) appLog("ERROR", result.message);
+ window.location = path + "app/view";
+ } catch (e) {
+ console.log("Could not parse /remove reply, error: " + e);
+ }
+ }
+ });
+ },
+
+ setNode: function() {
+ config.autogen.set_node();
+ },
+
+ // Rebuild the autogen feed list rows and button visibility from config.autogen state.
+ // Replaces config.autogen.render_feed_list().
+ renderAutogenFeedList: function() {
+ var autogen_feeds = config.autogen.get_feeds();
+ var node_name = config.autogen.node_name();
+
+ this.autogen_node = node_name;
+
+ var missing_count = 0;
+ var rows = [];
+ for (var j = 0; j < autogen_feeds.length; j++) {
+ if (!autogen_feeds[j].feedid) missing_count++;
+ rows.push({ name: autogen_feeds[j].name, node: node_name, feedid: autogen_feeds[j].feedid });
+ }
+ this.autogen_feeds = rows;
+ this.autogen_all_present = (missing_count === 0);
+ this.autogen_status = "";
+ this.autogen_status_color = "#aaa";
+ },
+
+ createMissingFeeds: function() {
+ config.autogen.create_missing_feeds();
+ },
+
+ runPostProcessor: function() {
+ config.autogen.start_post_processor();
+ },
+
+ resetFeeds: function() {
+ config.autogen.reset_feeds();
+ },
+
+ reloadDailyData: function() {
+ config.reset_daily_data();
+ }
+ }
+});
\ No newline at end of file
diff --git a/Lib/appconf/appconf.php b/Lib/appconf/appconf.php
new file mode 100644
index 00000000..146fb16d
--- /dev/null
+++ b/Lib/appconf/appconf.php
@@ -0,0 +1,152 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ app_name }}
+
+
+
Auto configure: This app can auto-configure connecting to emoncms feeds with the names shown on the right, alternatively feeds can be selected by clicking on the edit button.
+
+
+
+
Auto generate kWh flow feeds
+
+ The following feeds are required for historic half-hourly and daily analysis. They are generated from the power feeds using feed post-processing.
+
+
+
diff --git a/Lib/timeseries.js b/Lib/timeseries.js
index faeb5d83..aa24b16d 100644
--- a/Lib/timeseries.js
+++ b/Lib/timeseries.js
@@ -19,7 +19,7 @@ var timeseries = {
datastore[name].interval = (datastore[name].data[1][0] - datastore[name].data[0][0])*0.001;
},
- append: function (name,time,value)
+ append: function (name,time,value,join=false)
{
if (datastore[name]==undefined) {
app_log("ERROR","timeseries.append datastore["+name+"] is undefined");
@@ -35,6 +35,31 @@ var timeseries = {
var pos = (time - start) / interval;
// 3. get last position from data length
var last_pos = datastore[name].data.length - 1;
+
+ var padd_value = null;
+
+ if (join) {
+ let last_value_position = false;
+ // Itterate through the data points in reverse order to find the last non-null value for padding (limit lookup to 5 minutes)
+ var lookback = Math.ceil(300 / interval);
+ var min_i = Math.max(0, datastore[name].data.length - 1 - lookback);
+ for (var i = datastore[name].data.length - 1; i >= min_i; i--) {
+ if (datastore[name].data[i][1] !== null) {
+ padd_value = datastore[name].data[i][1];
+ last_value_position = i;
+ break;
+ }
+ }
+
+ // Fill null values in existing data after last known good value up to the new time
+ if (padd_value !== null) {
+ for (var j = last_value_position + 1; j < datastore[name].data.length; j++) {
+ if (datastore[name].data[j][1] === null) {
+ datastore[name].data[j][1] = padd_value;
+ }
+ }
+ }
+ }
// if the datapoint is newer than the last:
if (pos > last_pos)
@@ -42,11 +67,11 @@ var timeseries = {
var npadding = (pos - last_pos)-1;
// padding
- if (npadding>0 && npadding<12) {
+ if (npadding>0 && npadding<1000000) {
for (var padd = 0; padd
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ app.id }}
+
{{ app.app }}
+
+ {{ app.name }}
+
+
+
+
+ {{ app.public ? '' : '' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Views/app_view.php b/Views/app_view.php
index 42ab00d6..18642b01 100644
--- a/Views/app_view.php
+++ b/Views/app_view.php
@@ -1,11 +1,33 @@
+
Available Apps
Create a new instance of an app by clicking on one of the apps below.
-
+
+
+
Featured apps:
+
+
+
+
{{ app.title }}
+
{{ app.description || "no description..." }}
+
+
+
+
+
All apps:
+
+
+
+
{{ app.title }}
+
{{ app.description || "no description..." }}
+
+
+
+
@@ -36,18 +58,17 @@
var selected_app = "";
var app_new_enable = true;
-var out = "";
-for (var z in available_apps) {
- if (available_apps[z].description=="")
- available_apps[z].description = "no description...";
- out += '
The Cost Comparison app allows you to compare your energy usage against energy suppliers tariffs including new time of use tariffs.
-
Auto configure: This app can auto-configure connecting to emoncms feeds with the names shown on the right, alternatively feeds can be selected by clicking on the edit button.
-
Note: If you have solar or renewable energy generation then use the import_kwh feed to view the actual cost based on energy brought from the grid
-
Cumulative kWh feeds can be generated from power feeds with the power_to_kwh input processor.
-
-
-
-
-
-
+
+
The Cost Comparison app allows you to compare your energy usage against energy suppliers tariffs including new time of use tariffs.
+
Note: If you have solar or renewable energy generation then use the import_kwh feed to view the actual cost based on energy brought from the grid
+
+
@@ -46,20 +44,11 @@
-
-
-
-
-
-
-
The feed-in tariff app is a simple home energy monitoring app to explore onsite energy generation, feed-in and self-consumption, as well as the buildings overall consumption and cost.
-
Auto configure: This app can auto-configure connecting to emoncms feeds with the names shown on the right, alternatively feeds can be selected by clicking on the edit button.
-
Cumulative kWh feeds can be generated from power feeds with the power_to_kwh input processor.
-
-
-
-
-
+
+
The feed-in tariff app is a simple home energy monitoring app to explore onsite energy generation, feed-in and self-consumption, as well as the buildings overall consumption and cost.
The My Boiler app can be used to explore the performance of a boiler including, fuel input, electricity consumption, heat output, efficiency and system temperatures.
-
Auto configure: This app can auto-configure connecting to emoncms feeds with the names shown on the right, alternatively feeds can be selected by clicking on the edit button.
-
Cumulative kWh feeds can be created from power feeds using the power_to_kwh input processor, which converts power data (measured in watts) into energy consumption data (measured in kWh).
-
Share publicly: Check the "public" check box if you want to share your dashboard publicly, and ensure that the associated feeds are also made public by adjusting their settings on the feeds page.
-
Start date: To modify the start date for cumulative total fuel and electricity consumption, heat output and efficiency, input a unix timestamp corresponding to your desired starting date and time.
-
-
-
-
-
-
-
-
+
+
The My Boiler app can be used to explore the performance of a boiler including, fuel input, electricity consumption, heat output, efficiency and system temperatures.
+
Share publicly: Check the "public" check box if you want to share your dashboard publicly, and ensure that the associated feeds are also made public by adjusting their settings on the feeds page.
+
Start date: To modify the start date for cumulative total electricity consumption, heat output and SCOP, input a unix timestamp corresponding to your desired starting date and time.
The My Electric app is a simple home energy monitoring app for exploring home or building electricity consumption over time. It includes a real-time view and a historic kWh per day bar graph.
-
Auto configure: This app can auto-configure connecting to emoncms feeds with the names shown on the right, alternatively feeds can be selected by clicking on the edit button.
-
Cumulative kWh feeds can be generated from power feeds with the power_to_kwh input processor.
-
-
-
-
-
-
-
+
+
The My Electric app is a simple home energy monitoring app for exploring home or building electricity consumption over time. It includes a real-time view and a historic kWh per day bar graph.
+
+
@@ -175,6 +158,7 @@ function getTranslations(){
"kw":{"type":"checkbox", "default":0, "name": "Show kW", "description":tr("Display power as kW")}
};
+config.app_name = "My Electric";
config.id = ;
config.name = "";
config.public = ;
diff --git a/apps/OpenEnergyMonitor/myelectric2/myelectric2.php b/apps/OpenEnergyMonitor/myelectric2/myelectric2.php
index ef12d8be..5b9a280c 100644
--- a/apps/OpenEnergyMonitor/myelectric2/myelectric2.php
+++ b/apps/OpenEnergyMonitor/myelectric2/myelectric2.php
@@ -2,11 +2,9 @@
defined('EMONCMS_EXEC') or die('Restricted access');
global $path, $session, $v;
?>
-
-
-
+
@@ -155,23 +153,12 @@
-
-
-
-
-
-
-
-
The My Electric app is a simple home energy monitoring app for exploring home or building electricity consumption over time.
-
Auto configure: This app can auto-configure connecting to emoncms feeds with the names shown on the right, alternatively feeds can be selected by clicking on the edit button.
-
Cumulative kWh feeds can be generated from power feeds with the power_to_kwh input processor.
-
-
-
-
-
-
-
+
+
+
The My Electric app is a simple home energy monitoring app for exploring home or building electricity consumption over time.
This app extends the My Solar app by adding in a 'share of UK wind' estimate.
-
The share of wind estimate is calculated by using real-time electricity data from wind power in the uk and then scaling it so that the annual wind generation matches a percentage of annual household consumption. The default estimate assumes 60% or near 2000 kWh annually. This is close to the fuel mix quoted by two of the UK's leading green electricity suppliers.
-
Auto configure: This app can auto-configure connecting to emoncms feeds with the names shown on the right, alternatively feeds can be selected by clicking on the edit button.
-
Cumulative kWh feeds can be generated from power feeds with the power_to_kwh input processor.
-
-
-
-
-
-
+
+
This app extends the My Solar app by adding in a 'share of UK wind' estimate.
+
The share of wind estimate is calculated by using real-time electricity data from wind power in the uk and then scaling it so that the annual wind generation matches a percentage of annual household consumption. The default estimate assumes 60% or near 2000 kWh annually. This is close to the fuel mix quoted by two of the UK's leading green electricity suppliers.
+
+
@@ -153,6 +140,9 @@ function getTranslations(){
"kw":{"type":"checkbox", "default":0, "name": "Show kW", "description":tr("Display power as kW")}
};
+config.app_name = "My Energy";
+config.app_name_color = "#5cb85c";
+
config.id = ;
config.name = "";
config.public = ;
@@ -255,6 +245,7 @@ function resize()
if (height>width) height = width;
if (height<180) height = 180;
+ if (width<200) width = 200;
placeholder.width(width);
placeholder_bound.height(height);
diff --git a/apps/OpenEnergyMonitor/myheatpump/app.json b/apps/OpenEnergyMonitor/myheatpump/app.json
index 59aae96a..72b6254e 100644
--- a/apps/OpenEnergyMonitor/myheatpump/app.json
+++ b/apps/OpenEnergyMonitor/myheatpump/app.json
@@ -1,5 +1,6 @@
{
"title" : "My Heatpump",
"description" : "Explore heatpump performance: daily electricity consumption, heat output and COP. Zoom in for detailed temperature, power, heat graphs.",
- "order" : 7
+ "order" : 2,
+ "primary" : true
}
diff --git a/apps/OpenEnergyMonitor/myheatpump/myheatpump.js b/apps/OpenEnergyMonitor/myheatpump/myheatpump.js
index a99bdd27..74863c73 100644
--- a/apps/OpenEnergyMonitor/myheatpump/myheatpump.js
+++ b/apps/OpenEnergyMonitor/myheatpump/myheatpump.js
@@ -129,12 +129,10 @@ function show() {
if (!config.app.enable_process_daily.value) {
$(".bargraph_mode").hide();
- $("#clear-daily-data").hide();
bargraph_mode = "combined";
} else {
$(".bargraph_mode").show();
- $("#clear-daily-data").show();
}
$("body").css('background-color', 'WhiteSmoke');
@@ -580,24 +578,6 @@ $('#placeholder').bind("plotselected", function (event, ranges) {
setTimeout(function () { panning = false; }, 100);
});
-$("#clear-daily-data").click(function () {
- $.ajax({
- url: path + "app/cleardaily",
- data: { id: config.id, apikey: apikey },
- async: true,
- dataType: "json",
- success: function (result) {
- if (result.success) {
- alert("Daily data cleared, please refresh the page to reload data");
- app_log("INFO", "Daily data cleared");
- } else {
- alert("Failed to clear daily data");
- app_log("ERROR", "Failed to clear daily data");
- }
- }
- });
-});
-
$("#show_dhw_temp").click(function () {
if ($("#show_dhw_temp")[0].checked) {
show_dhw_temp = true;
diff --git a/apps/OpenEnergyMonitor/myheatpump/myheatpump.php b/apps/OpenEnergyMonitor/myheatpump/myheatpump.php
index c826cff8..3d0a699e 100644
--- a/apps/OpenEnergyMonitor/myheatpump/myheatpump.php
+++ b/apps/OpenEnergyMonitor/myheatpump/myheatpump.php
@@ -2,11 +2,10 @@
defined('EMONCMS_EXEC') or die('Restricted access');
global $path, $session, $v;
?>
-
-
+
@@ -373,43 +372,29 @@
-
-
-
-
-
-
-
-
The My Heatpump app can be used to explore the performance of a heatpump including, electricity consumption, heat output, COP and system temperatures.
-
Auto configure: This app can auto-configure connecting to emoncms feeds with the names shown on the right, alternatively feeds can be selected by clicking on the edit button.
-
Cumulative kWh feeds can be created from power feeds using the power_to_kwh input processor, which converts power data (measured in watts) into energy consumption data (measured in kWh).
-
Share publicly: Check the "public" check box if you want to share your dashboard publicly, and ensure that the associated feeds are also made public by adjusting their settings on the feeds page.
-
Start date: To modify the start date for cumulative total electricity consumption, heat output and SCOP, input a unix timestamp corresponding to your desired starting date and time.
-
-
-
-
-
-
-
-
-
-
-
-
+
+
The My Heatpump app can be used to explore the performance of a heatpump including, electricity consumption, heat output, COP and system temperatures.
+
Share publicly: Check the "public" check box if you want to share your dashboard publicly, and ensure that the associated feeds are also made public by adjusting their settings on the feeds page.
+
Start date: To modify the start date for cumulative total electricity consumption, heat output and SCOP, input a unix timestamp corresponding to your desired starting date and time.
The My Solar app can be used to explore onsite solar generation, self consumption, export and building consumption both in realtime with a moving power graph view and historically with a daily and monthly bargraph.
-
Auto configure: This app can auto-configure connecting to emoncms feeds with the names shown on the right, alternatively feeds can be selected by clicking on the edit button.
-
Cumulative kWh feeds can be generated from power feeds with the power_to_kwh input processor.
-
-
-
-
-
-
-
+
+
The My Solar app can be used to explore onsite solar generation, self consumption, export and building consumption both in realtime with a moving power graph view and historically with a daily and monthly bargraph.
+
+
@@ -218,6 +203,9 @@ function getTranslations(){
//"import_unitcost":{"type":"value", "default":0.1508, "name": "Import unit cost", "description":"Unit cost of imported grid electricity"}
};
+config.app_name = "My Solar PV";
+config.app_name_color = "#dccc1f";
+
config.id = "";
config.name = "";
config.public = ;
diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/app.json b/apps/OpenEnergyMonitor/mysolarpvbattery/app.json
index dc465234..daed7811 100644
--- a/apps/OpenEnergyMonitor/mysolarpvbattery/app.json
+++ b/apps/OpenEnergyMonitor/mysolarpvbattery/app.json
@@ -1,5 +1,6 @@
{
"title" : "My Solar Battery",
"description" : "Explore solar generation, household consumption and a home battery system",
- "order" : 4
+ "order" : 1,
+ "primary" : true
}
diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css
new file mode 100644
index 00000000..43b3a8b7
--- /dev/null
+++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.css
@@ -0,0 +1,371 @@
+/* =====================================================================
+ Dark theme
+ (previously in Modules/app/Views/css/dark.css)
+ ===================================================================== */
+
+body {
+ background-color: #222;
+}
+
+.content-container {
+ max-width: 1150px;
+}
+
+#app-block {
+ padding: 0;
+}
+
+.app-top-bar {
+ padding: .5rem 0;
+ border-bottom: 1px solid #333;
+ margin-bottom: .4rem;
+}
+
+/* btn-list: replaces Bootstrap .nav .nav-pills */
+.btn-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: flex;
+ align-items: center;
+}
+
+/* app-btn: replaces Bootstrap btn + btn-large + btn-link + btn-inverse.
+ Explicitly resets Bootstrap properties so it is self-contained. */
+.app-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.25em;
+ padding: 0.25rem 0.5rem;
+ font-size: 1rem;
+ line-height: 1.5;
+ font-family: inherit;
+ cursor: pointer;
+ text-align: center;
+ white-space: nowrap;
+ background: transparent;
+ border: none;
+ border-radius: 0;
+ box-shadow: none;
+ text-shadow: none;
+ color: #aaa;
+ opacity: .6;
+ transition: opacity .3s linear;
+ text-decoration: none;
+}
+.app-btn:hover,
+.app-btn:active {
+ opacity: 1;
+ text-decoration: none;
+}
+.app-btn.active {
+ color: white;
+ opacity: .8;
+ font-weight: bold;
+ text-decoration: underline;
+}
+.app-btn[disabled],
+.app-btn[disabled]:hover {
+ color: #616161;
+ opacity: .8;
+ background: rgba(6, 153, 250, 0.06);
+}
+
+/* Display utilities */
+.d-none { display: none !important; }
+.d-inline { display: inline !important; }
+.d-block { display: block !important; }
+@media (min-width: 576px) {
+ .d-sm-none { display: none !important; }
+ .d-sm-inline { display: inline !important; }
+ .d-sm-block { display: block !important; }
+}
+@media (min-width: 768px) {
+ .d-md-none { display: none !important; }
+ .d-md-inline { display: inline !important; }
+ .d-md-block { display: block !important; }
+}
+
+.visnavblock {
+ color: #0699fa;
+ font-size: 18px;
+}
+
+.visnav {
+ background-color: rgba(6,153,250,0.1);
+ flex-grow: 1;
+}
+
+.visnav:focus,
+.visnav:active,
+.visnav:hover {
+ color: #0699fa;
+ text-decoration: none!important;
+}
+
+#footer {
+ margin: 0;
+ background-color: #181818;
+ color: #999;
+}
+
+.ajax-loader {
+ background: #222
+ url(../../../images/ajax-loader.gif)
+ center
+ no-repeat;
+}
+
+@media (min-width: 576px) {
+ .visnav {
+ flex-grow: 0;
+ min-width: 3em;
+ }
+}
+
+
+/* =====================================================================
+ Power display
+ ===================================================================== */
+
+.power-title {
+ margin: 0;
+ font-size: 0.9rem;
+ color: #aaa;
+}
+
+.power-value {
+ margin: 0;
+ line-height: 1.6;
+ font-size: 1.7rem;
+}
+
+.power-unit {
+ /*font-weight: normal;*/
+}
+
+@media (min-width: 768px) {
+ .power-title { font-size: 1.0rem; }
+ .power-value { font-size: 2.5rem; }
+}
+
+@media (min-width: 992px) {
+ .power-title { font-size: 1.2rem; }
+ .power-value { font-size: 3.5rem; }
+}
+
+/* =====================================================================
+ Stats table
+ ===================================================================== */
+
+.statstable {
+ width: 100%;
+ border-spacing: 10px;
+ border-collapse: separate;
+}
+
+.statsbox {
+ width: 20%;
+ text-align: center;
+ vertical-align: middle;
+ background: #282828;
+ --statsbox-color: #333;
+}
+
+.statsbox-inner-unit,
+.statsbox-inner-arrow {
+ color: var(--statsbox-color);
+}
+
+.statsbox-padded {
+ padding: 10px;
+}
+
+.statsbox-title {
+ font-weight: bold;
+ font-size: 20px;
+ padding-bottom: 15px;
+}
+
+.statsbox-flow-title {
+ font-size: 16px;
+}
+
+.statsbox-value {
+ font-weight: bold;
+ font-size: 36px;
+ color: var(--statsbox-color);
+}
+
+.statsbox-units {
+ font-weight: bold;
+ font-size: 16px;
+ color: var(--statsbox-color);
+}
+
+.statsbox-prc {
+ font-weight: normal;
+ font-size: 16px;
+}
+
+.statsbox-arrow-down {
+ position: relative;
+ margin-bottom: 16px;
+}
+
+.statsbox-arrow-down:after {
+ top: 100%;
+ left: 50%;
+ border: solid transparent;
+ content: " ";
+ width: 0;
+ height: 0;
+ position: absolute;
+ pointer-events: none;
+ border-top-color: var(--statsbox-color);
+ border-width: 16px;
+ margin-left: -16px;
+}
+
+.statsbox-arrow-right {
+ position: relative;
+ margin-right: 16px;
+}
+
+.statsbox-arrow-right:after {
+ left: 100%;
+ top: 50%;
+ border: solid transparent;
+ content: " ";
+ width: 0;
+ height: 0;
+ position: absolute;
+ pointer-events: none;
+ border-left-color: var(--statsbox-color);
+ border-width: 16px;
+ margin-top: -16px;
+}
+
+.statsbox-arrow-left {
+ position: relative;
+ margin-left: 16px;
+}
+
+.statsbox-arrow-left:before {
+ right: 100%;
+ top: 60%;
+ border: solid transparent;
+ content: " ";
+ width: 0;
+ height: 0;
+ position: absolute;
+ pointer-events: none;
+ border-right-color: var(--statsbox-color);
+ border-width: 16px;
+ margin-top: -16px;
+}
+
+.tooltip-title {
+ color: #aaa;
+ font-weight:bold;
+ font-size:12px;
+}
+
+.tooltip-value {
+ color: #fff;
+ font-weight:bold;
+ font-size:14px;
+}
+
+.tooltip-units {
+ color: #fff;
+ font-weight:bold;
+ font-size:10px;
+}
+
+/* Box background colours (solar/battery overridden by JS when feeds unavailable) */
+#solar-box { background: #dccc1f; }
+#grid-box { background: #d52e2e; }
+#battery-box { background: #fb7b50; }
+#house-box { background: #82cbfc; }
+
+/* Text alignment for battery flow label boxes */
+#grid-to-battery-box { text-align: left; }
+#battery-to-grid-box { text-align: right; }
+
+/* Battery flow label boxes: padding adjusted to leave room for the arrow */
+#battery_import .statsbox-arrow-left { padding: 10px 0 0 10px; }
+#battery_export .statsbox-arrow-right { padding: 10px 10px 0 0; }
+
+/* Value font size in battery flow label boxes */
+#grid-to-battery-box .statsbox-value,
+#battery-to-grid-box .statsbox-value { font-size: 22px; }
+
+/* Rounded corners on first and last graph-nav buttons */
+#graph-nav .app-btn:first-child { border-radius: 0.375rem 0 0 0.375rem; }
+#graph-nav .app-btn:last-child { border-radius: 0 0.375rem 0.375rem 0; }
+
+/* Faint dividing lines between buttons */
+#graph-nav .app-btn + .app-btn { border-left: 1px solid rgba(255,255,255,0.12); }
+
+/* Graph placeholder */
+#placeholder_bound { width: 100%;}
+#placeholder { height:100px; }
+
+/* Solar generation box: percentage overlays */
+#statsbox-generation { position: relative; }
+.prc-solar-to-battery { position: absolute; width: 33.3%; left: 0; bottom: 0; }
+.prc-solar-direct { position: absolute; width: 33.3%; left: 66.66%; bottom: 0; }
+.prc-solar-export { position: absolute; height: 100%; right: 0; top: 0;
+ display: flex; align-items: center; }
+
+/* House box: percentage overlays */
+#house-box .statsbox-padded { position: relative; }
+.prc-battery-to-house { position: absolute; width: 0; left: 3px; top: 40%; }
+.prc-solar-to-house { position: absolute; width: 33.33333%; left: 0; top: 0; }
+.prc-grid-to-house { position: absolute; width: 33.33333%; left: 66.66667%; top: 0; }
+
+/* Mobile (max 768px): */
+@media (max-width: 768px) {
+ #app-block { padding: 0; }
+ .statstable { border-spacing: 4px; }
+ .statsbox-padded { padding: 4px; }
+ .statsbox-title { font-size: 16px; padding-bottom: 4px; } /* 20px */
+ .statsbox-flow-title { font-size: 12px; padding-bottom: 2px; line-height:12px; } /* 20px */
+
+ .statsbox-value { font-size: 20px; } /* 36px */
+ .statsbox-prc { font-size: 12px; } /* 16px */
+ .statsbox-arrow-down:after {
+ border-width: 8px;
+ margin-left: -8px;
+ }
+ .statsbox-arrow-right:after {
+ border-width: 8px;
+ margin-top: -8px;
+ }
+ .statsbox-arrow-left:before {
+ border-width: 8px;
+ margin-top: -8px;
+ }
+}
+
+/* Mobile (max 576px) */
+@media (max-width: 576px) {
+ .statsbox-title { font-size: 12px; padding-bottom: 4px; } /* 20px */
+ .statsbox-units { display: none; } /* hide units */
+}
+
+.stats-grid {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ grid-template-rows: auto auto;
+ gap: 0.5rem 1rem;
+ padding: 0.5rem 0;
+}
+
+.stats-grid > div {
+ text-align: center;
+ min-width: 0;
+}
+
diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js
new file mode 100644
index 00000000..d3d83e8c
--- /dev/null
+++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.js
@@ -0,0 +1,888 @@
+// ----------------------------------------------------------------------
+// Globals
+// ----------------------------------------------------------------------
+feed.apikey = apikey;
+feed.public_userid = public_userid;
+feed.public_username = public_username;
+// ----------------------------------------------------------------------
+// Display
+// ----------------------------------------------------------------------
+$("body").css('background-color','#222');
+$(window).ready(function(){
+ $("#footer").css('background-color','#181818');
+ $("#footer").css('color','#999');
+});
+if (!sessionwrite) $(".openconfig").hide();
+
+var flow_colors_old = {
+ "solar_to_load": "#abddff",
+ "solar_to_battery": "#fba050",
+ "solar_to_grid": "#dccc1f",
+ "battery_to_load": "#ffd08e",
+ "battery_to_grid": "#fabb68",
+ "grid_to_load": "#82cbfc",
+ "grid_to_battery": "#fb7b50"
+};
+
+var flow_colors_tariff_app = {
+ "solar_to_load": "#bec745",
+ "solar_to_battery": "#a3d977",
+ "solar_to_grid": "#dccc1f",
+ "battery_to_load": "#fbb450",
+ "battery_to_grid": "#f0913a",
+ "grid_to_load": "#44b3e2",
+ "grid_to_battery": "#82cbfc"
+};
+
+var flow_colors_contrast = {
+ "solar_to_load": "#F5C518", // bright amber – direct solar use
+ "solar_to_battery": "#C8A000", // darker gold – solar into storage
+ "solar_to_grid": "#FFE066", // light yellow – solar export
+ "battery_to_load": "#4ADE80", // bright green – battery discharge
+ "battery_to_grid": "#86EFAC", // soft green – battery export
+ "grid_to_load": "#38BDF8", // sky blue – grid import
+ "grid_to_battery": "#7DD3FC" // light blue – grid charging battery
+};
+
+// flow_colors_blend: each flow is 66% source + 33% destination, blended from 4 base colors:
+// Solar=#FFD700 (yellow), Battery=#FF7700 (orange), Grid=#E03030 (red), Home=#3399DD (blue)
+var flow_colors_blend = {
+ "solar_to_load": "#B9C049", // 66% yellow + 33% blue
+ "solar_to_battery": "#FFB500", // 66% yellow + 33% orange
+ "solar_to_grid": "#F29E10", // 66% yellow + 33% red
+ "battery_to_load": "#B98149", // 66% orange + 33% blue
+ "battery_to_grid": "#F25E10", // 66% orange + 33% red
+ "grid_to_load": "#A55269", // 66% red + 33% blue
+ "grid_to_battery": "#E84720" // 66% red + 33% orange
+};
+
+var flow_colors = flow_colors_tariff_app;
+
+// ----------------------------------------------------------------------
+// Configuration
+// ----------------------------------------------------------------------
+config.app = {
+ // == System configuration ==
+ // Select which metering points are present on the system.
+ // When has_solar=false, the solar feed is hidden and solar power is treated as 0.
+ // When has_battery=false, the battery feed is hidden and battery power is treated as 0.
+ // In each mode, conservation of energy allows one feed to be derived from the others:
+ // Full (solar+battery): GRID=USE-SOLAR-BATTERY, USE=GRID+SOLAR+BATTERY, SOLAR=USE-GRID-BATTERY, BATTERY=USE-GRID-SOLAR
+ // Solar only: GRID=USE-SOLAR, USE=GRID+SOLAR, SOLAR=USE-GRID
+ // Battery only: GRID=USE-BATTERY, USE=GRID+BATTERY, BATTERY=USE-GRID
+ // Consumption only: no derivation; only USE (or GRID) feed needed
+ "has_solar":{"type":"checkbox", "default":1, "name":"Has solar PV", "description":"Does the system have solar PV generation?"},
+ "has_battery":{"type":"checkbox", "default":1, "name":"Has battery", "description":"Does the system have a battery?"},
+
+ // == Key power feeds ==
+ // All four feeds are optional at the config level; the custom check() below enforces the
+ // correct minimum set depending on the has_solar / has_battery mode.
+ // Any single missing feed will be derived from the other three (or two in solar/battery-only modes).
+ "use":{"optional":true, "type":"feed", "derivable":true, "autoname":"use", "description":"House or building use in watts"},
+ "solar":{"optional":true, "type":"feed", "derivable":true, "autoname":"solar", "description":"Solar generation in watts"},
+ "battery":{"optional":true, "type":"feed", "derivable":true, "autoname":"battery_power", "description":"Battery power in watts, positive for discharge, negative for charge (only shown when has_battery is enabled)"},
+ "grid":{"optional":true, "type":"feed", "derivable":true, "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 % (only shown when has_battery is enabled)"},
+
+ // History feeds (energy flow breakdown from solarbatterykwh post-processor)
+
+ // Node name for auto-generated feeds, common with mysolarpvbattery app.
+ "autogenerate_nodename": {
+ "hidden": true,
+ "type": "value",
+ "default": "solar_battery_kwh_flows",
+ "name": "Auto-generate feed node name",
+ "description": ""
+ },
+
+ // Auto-generated cumulative kWh feeds
+ "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 (used for time-remaining estimate; only used when has_battery is enabled)"}
+};
+
+// ----------------------------------------------------------------------
+// Custom check: enforce the correct minimum set of feeds based on mode.
+// This overrides the appconf.js default check() which just tests optional flags.
+// Rules:
+// has_solar + has_battery: at least 3 of (use, solar, battery, grid) must be configured
+// has_solar only: at least 2 of (use, solar, grid) must be configured
+// has_battery only: at least 2 of (use, battery, grid) must be configured
+// consumption only: at least 1 of (use, grid) must be configured
+// ----------------------------------------------------------------------
+config.check = function() {
+ // Read mode from db (persisted) or app default
+ var has_solar = (config.db.has_solar !== undefined) ? (config.db.has_solar != 0) : (config.app.has_solar.default != 0);
+ var has_battery = (config.db.has_battery !== undefined) ? (config.db.has_battery != 0) : (config.app.has_battery.default != 0);
+
+ // Helper: is a feed key resolved (either auto-matched by name or explicitly set in db)?
+ function feed_resolved(key) {
+ if (config.db[key] == "disable") return false; // explicitly disabled
+ if (config.db[key] != undefined) {
+ // user-set: check the feed id still exists
+ return config.feedsbyid[config.db[key]] !== undefined;
+ }
+ // auto-match by name
+ var autoname = config.app[key] && config.app[key].autoname;
+ return autoname && config.feedsbyname[autoname] !== undefined;
+ }
+
+ var use_ok = feed_resolved("use");
+ var solar_ok = feed_resolved("solar");
+ var bat_ok = feed_resolved("battery");
+ var grid_ok = feed_resolved("grid");
+
+ if (has_solar && has_battery) {
+ // Need at least 3 of the 4 feeds
+ return [use_ok, solar_ok, bat_ok, grid_ok].filter(Boolean).length >= 3;
+ } else if (has_solar && !has_battery) {
+ // Need at least 2 of (use, solar, grid)
+ return [use_ok, solar_ok, grid_ok].filter(Boolean).length >= 2;
+ } else if (!has_solar && has_battery) {
+ // Need at least 2 of (use, battery, grid)
+ return [use_ok, bat_ok, grid_ok].filter(Boolean).length >= 2;
+ } else {
+ // Consumption only: need at least 1 of (use, grid)
+ return use_ok || grid_ok;
+ }
+};
+
+config.feeds = feed.list();
+
+var feeds_by_tag_name = feed.by_tag_and_name(config.feeds);
+
+config.autogen_node_prefix = "solar_battery_kwh_flows";
+config.autogen_feed_defaults = { datatype: 1, engine: 5, options: { interval: 1800 } };
+config.autogen_feeds_by_tag_name = feeds_by_tag_name;
+
+config.initapp = function(){init()};
+config.showapp = function(){show()};
+config.hideapp = function(){hide()};
+
+// ----------------------------------------------------------------------
+// Config UI helpers: hide/show feeds based on the current mode
+// ----------------------------------------------------------------------
+function get_mode() {
+ var has_solar = (config.db.has_solar !== undefined) ? (config.db.has_solar != 0) : (config.app.has_solar.default != 0);
+ var has_battery = (config.db.has_battery !== undefined) ? (config.db.has_battery != 0) : (config.app.has_battery.default != 0);
+ return { has_solar: has_solar, has_battery: has_battery };
+}
+
+// Called by appconf.js before rendering the config UI
+config.ui_before_render = function() {
+ var mode = get_mode();
+
+ // solar feed: only relevant if has_solar is on
+ config.app.solar.hidden = !mode.has_solar;
+ // battery feeds: only relevant if has_battery is on
+ config.app.battery.hidden = !mode.has_battery;
+ config.app.battery_soc.hidden = !mode.has_battery;
+ config.app.battery_capacity_kwh.hidden = !mode.has_battery;
+ // autogenerate feeds: hide battery-specific ones if no battery, solar-specific if no solar
+ config.app.solar_to_load_kwh.hidden = !mode.has_solar;
+ config.app.solar_to_grid_kwh.hidden = !mode.has_solar;
+ config.app.solar_to_battery_kwh.hidden = !mode.has_battery || !mode.has_solar;
+ config.app.battery_to_load_kwh.hidden = !mode.has_battery;
+ config.app.battery_to_grid_kwh.hidden = !mode.has_battery;
+ config.app.grid_to_battery_kwh.hidden = !mode.has_battery;
+};
+
+// Called by appconf.js after any config value is changed; re-renders UI when a mode checkbox changes
+config.ui_after_value_change = function(key) {
+ if (key === 'has_solar' || key === 'has_battery' || key === 'use' || key === 'solar' || key === 'battery' || key === 'grid') {
+ config.UI();
+ }
+ render_autogen_feed_list();
+};
+
+// ----------------------------------------------------------------------
+// APPLICATION
+// ----------------------------------------------------------------------
+var feeds = {};
+
+var live = false;
+var autoupdate = true;
+var lastupdate = +new Date;
+var viewmode = "powergraph";
+var historyseries = [];
+var powerseries = [];
+var latest_start_time = 0;
+var panning = false;
+var bargraph_initialized = false;
+var bargraph_loaded = false;
+var kwhd_data = {};
+
+
+// == Flow decomposition control variables ==
+
+// Which feeds are actually available
+var available = {
+ use: false,
+ solar: false,
+ battery: false,
+ grid: false
+};
+var battery_soc_available = false;
+
+// which feed to derive (if any) based on the config; false = no derivation needed
+var derive = false;
+
+// which feeds to assume zero
+var assume_zero_solar = false;
+var assume_zero_battery = false;
+
+// == End of flow decomposition control variables ==
+
+// ----------------------------------------------------------------------
+// check_history_feeds: return true if the right kWh flow feeds are available
+// for the current mode (needed to enable the History bargraph view).
+// ----------------------------------------------------------------------
+function check_history_feeds(mode) {
+ // Core grid-load feed is always needed
+ if (!config.app.grid_to_load_kwh.value) return false;
+ if (!config.app.solar_to_grid_kwh.value && mode.has_solar) return false;
+ if (!config.app.solar_to_load_kwh.value && mode.has_solar) return false;
+
+ if (mode.has_battery) {
+ if (!config.app.solar_to_battery_kwh.value && mode.has_solar) return false;
+ if (!config.app.battery_to_load_kwh.value) return false;
+ if (!config.app.battery_to_grid_kwh.value) return false;
+ if (!config.app.grid_to_battery_kwh.value) return false;
+ }
+ return true;
+}
+
+
+
+var timeWindow = (3600000*24.0*30);
+var history_end = +new Date;
+var history_start = history_end - timeWindow;
+
+timeWindow = (3600000*6.0*1);
+var power_end = +new Date;
+var power_start = power_end - timeWindow;
+
+var live_timerange = timeWindow;
+
+var meta = {};
+var power_graph_end_time = 0;
+
+config.init();
+
+// App start function
+function init()
+{
+ app_log("INFO","solar & battery init");
+
+ var mode = get_mode();
+
+ // Apply hidden flags (also used by autogen feed list and config UI)
+ config.ui_before_render();
+
+ render_autogen_feed_list();
+
+ view.end = power_end;
+ view.start = power_start;
+
+ // Load metadata from whatever feeds are actually configured to find data end time
+ var feeds_to_check = ["use", "solar", "battery", "grid"];
+ for (var i = 0; i < feeds_to_check.length; i++) {
+ var key = feeds_to_check[i];
+ if (config.app[key].value) {
+ meta[key] = feed.getmeta(config.app[key].value);
+ if (meta[key].end_time > power_graph_end_time) power_graph_end_time = meta[key].end_time;
+ }
+ }
+
+ // If the feed is more than 1 hour behind then start the view at the end of the feed
+ if ((view.end*0.001-power_graph_end_time)>3600) {
+ view.end = power_graph_end_time*1000;
+ autoupdate = false;
+ }
+ view.start = view.end - timeWindow;
+ live_timerange = timeWindow;
+
+ // Show history bargraph button only when all required kWh flow feeds are available for the current mode
+ var has_history = check_history_feeds(mode);
+ if (has_history) init_bargraph();
+ $(".viewhistory").toggle(has_history);
+
+ // The buttons for these powergraph events are hidden when in historic mode
+ // The events are loaded at the start here and dont need to be unbinded and binded again.
+ $("#zoomout").click(function () {view.zoomout(); autoupdate = false; draw(true);});
+ $("#zoomin").click(function () {view.zoomin(); autoupdate = false; draw(true);});
+ $('#right').click(function () {view.panright(); autoupdate = false; draw(true);});
+ $('#left').click(function () {view.panleft(); autoupdate = false; draw(true);});
+
+ $('.time').click(function () {
+ view.timewindow($(this).attr("time")/24.0);
+ autoupdate = true;
+ live_timerange = view.end - view.start;
+ draw(true);
+ });
+
+ $(".viewhistory").click(function () {
+ $btn = $(this);
+ $btn.toggleClass('active');
+
+ $('.balanceline').attr('disabled', $btn.is('.active'));
+ viewmode = $btn.is('.active') ? 'bargraph' : 'powergraph';
+
+ if (viewmode=="bargraph") {
+ power_start = view.start
+ power_end = view.end
+ view.start = history_start
+ view.end = history_end
+ if (bargraph_loaded) {
+ draw(false);
+ } else {
+ bargraph_loaded = true;
+ draw(true);
+ }
+ bargraph_events();
+ } else {
+ history_start = view.start
+ history_end = view.end
+ view.start = power_start
+ view.end = power_end
+ draw(false);
+ powergraph_events();
+ }
+ });
+}
+
+function show()
+{
+ app_log("INFO","solar & battery show");
+
+ var mode = get_mode();
+
+
+ flow_available();
+ solar_battery_visibility();
+
+ if (check_history_feeds(mode)) {
+ if (!bargraph_initialized) init_bargraph();
+ }
+
+ draw(true);
+ powergraph_events();
+
+ livefn();
+ live = setInterval(livefn,5000);
+
+ // Trigger process here
+ setTimeout(function() {
+ start_post_processor();
+ }, 1000);
+
+ // resize after a delay to ensure the DOM is fully rendered and dimensions are correct
+ setTimeout(resize, 100);
+}
+
+// -------------------------------------------------------------------------------------------------------
+// Flow decomposition
+// Conservation of energy: use = solar + battery + grid
+// battery: positive = discharge, negative = charge
+// grid: positive = import, negative = export
+// -------------------------------------------------------------------------------------------------------
+
+function flow_available() {
+
+ // 4 feeds: solar, use, battery, grid
+ // 3 feeds
+ // 2 feeds (need at least use or grid, second can be solar or battery)
+ // 1 feed (use or grid)
+
+
+ // Availability
+ let feedids = {};
+ let feeds_to_check = ["use", "solar", "battery", "grid"];
+ for (let i = 0; i < feeds_to_check.length; i++) {
+ let key = feeds_to_check[i];
+ if (config.app[key].value != "disable" && config.app[key].value != "derive") {
+ feedids[key] = config.app[key].value*1;
+ } else {
+ feedids[key] = false;
+ }
+ }
+
+ available = {
+ use: false,
+ solar: false,
+ battery: false,
+ grid: false
+ };
+
+ derive = false;
+
+ // Availability
+ if (config.app.has_solar.value && feedids['solar']) available.solar = true;
+ if (feedids['use']) available.use = true;
+ if (config.app.has_battery.value && feedids['battery']) available.battery = true;
+ if (feedids['grid']) available.grid = true;
+
+ var number_of_feeds = 0;
+ if (available.solar) number_of_feeds++;
+ if (available.use) number_of_feeds++;
+ if (available.battery) number_of_feeds++;
+ if (available.grid) number_of_feeds++;
+
+ // 3 Feeds: Just find the one that is missing
+ if (number_of_feeds === 3) {
+ if (!available.grid) derive = "grid";
+ else if (!available.use) derive = "use";
+ else if (!available.solar) derive = "solar";
+ else if (!available.battery) {
+ if (config.app.has_battery.value) {
+ derive = "battery";
+ } else {
+ // If all feeds are preset but battery is disabled by config, assume battery=0 and recalculate grid from use and solar
+ derive = "grid";
+ }
+ }
+ }
+
+ // 2 Feeds: Specific logic based on your priority rules
+ else if (number_of_feeds === 2) {
+
+ if (available.solar && available.battery) {
+ // We cant derive in this scenario
+ }
+
+ if (available.solar) {
+ if (available.use) derive = "grid";
+ else if (available.grid) derive = "use";
+ assume_zero_battery = true; // if battery feed is missing, assume no battery power (solar-only mode)
+ }
+
+ if (available.battery) {
+ if (available.use) derive = "grid";
+ else if (available.grid) derive = "use";
+ assume_zero_solar = true; // if solar feed is missing, assume no solar generation (battery-only mode)
+ }
+ }
+
+ // 1 Feed (dervice use from grid or vice versa, assume zero solar and battery)
+ else if (number_of_feeds === 1) {
+ if (available.use) derive = "grid";
+ else if (available.grid) derive = "use";
+ assume_zero_solar = true;
+ assume_zero_battery = true;
+ }
+
+ // Battery state of charge feed availability
+ if ((available.battery || derive == "battery") && config.app.battery_soc.value) {
+ battery_soc_available = true;
+ }
+
+ return {
+ has_solar: config.app.has_solar.value,
+ has_battery: config.app.has_battery.value,
+ number_of_feeds: number_of_feeds,
+ available: available,
+ derive: derive,
+ assume_zero_solar: assume_zero_solar,
+ assume_zero_battery: assume_zero_battery
+ }
+}
+
+function flow_derive_missing(input) {
+ var solar = input.solar;
+ var use = input.use;
+ var battery = input.battery;
+ var grid = input.grid;
+
+ if (solar<0) solar = 0;
+ if (use<0) use = 0;
+
+ if (assume_zero_solar) solar = 0;
+ if (assume_zero_battery) battery = 0;
+
+ if (derive === "grid") {
+ grid = use - solar - battery;
+ } else if (derive === "use") {
+ use = solar + battery + grid;
+ } else if (derive === "solar") {
+ solar = use - battery - grid;
+ } else if (derive === "battery") {
+ battery = use - solar - grid;
+ }
+
+ return {
+ solar: solar,
+ use: use,
+ battery: battery,
+ grid: grid
+ }
+}
+
+function flow_calculation(input) {
+
+ var solar = input.solar;
+ var use = input.use;
+ var battery = input.battery;
+ var grid = input.grid;
+
+ // Import/export split: positive grid = import, negative grid = export
+ var import_power = grid > 0 ? grid : 0;
+
+ // SOLAR flows
+ var solar_to_load = Math.min(solar, use);
+ var solar_to_battery = 0;
+ if (battery < 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);
+ }
+ 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 > 0) {
+ // Battery is discharging
+ battery_to_load = Math.min(battery, use - solar_to_load);
+ battery_to_grid = battery - 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 < 0 ? -battery - solar_to_battery : 0);
+ }
+
+ return {
+ solar_to_load: solar_to_load,
+ solar_to_battery: solar_to_battery,
+ solar_to_grid: solar_to_grid,
+ battery_to_load: battery_to_load,
+ battery_to_grid: battery_to_grid,
+ grid_to_load: grid_to_load,
+ grid_to_battery: grid_to_battery
+ }
+}
+
+
+function resize()
+{
+ app_log("INFO","solar & battery resize");
+
+ var placeholder_bound = $('#placeholder_bound');
+ var placeholder = $('#placeholder');
+
+ var width = placeholder_bound.width();
+
+ // Calculate height from the top of the chart to the bottom of the viewport,
+ // leaving enough room for the stats table below to remain visible.
+ var bottom_margin = $('.statstable').outerHeight(true) + 64;
+ var offset_top = placeholder_bound.offset().top - $(window).scrollTop();
+ var height = $(window).height() - offset_top - bottom_margin;
+
+ // In landscape cap at 60% of window width to avoid an overly tall chart
+ //var is_landscape = $(window).height() < $(window).width();
+ //if (is_landscape) height = Math.min(height, width * 0.6);
+
+ // min size to avoid flot errors
+ if (height < 200) height = 200;
+ if (width < 200) width = 200;
+
+ placeholder.width(width);
+ placeholder_bound.height(height);
+ placeholder.height(height);
+
+ draw(false)
+}
+
+function hide()
+{
+ clearInterval(live);
+}
+
+function livefn()
+{
+ // Check if the updater ran in the last 60s if it did not the app was sleeping
+ // and so the data needs a full reload.
+ var reload = false;
+ var now = +new Date();
+ if ((now-lastupdate)>60000) reload = true;
+ lastupdate = now;
+ var powerUnit = config.app && config.app.kw && config.app.kw.value===true ? 'kW' : 'W';
+
+ var feeds = feed.listbyid();
+ if (feeds === null) { return; }
+
+ var input = {};
+ for (const key in available) {
+ // if feed is available use its value, otherwise null
+ input[key] = available[key] && feeds[config.app[key].value]!=undefined ? parseFloat(feeds[config.app[key].value].value) : null;
+ }
+
+ input = flow_derive_missing(input);
+
+ var battery_soc_now = "---";
+ if (battery_soc_available && feeds[config.app.battery_soc.value] != undefined) {
+ battery_soc_now = parseInt(feeds[config.app.battery_soc.value].value);
+ }
+
+ if (autoupdate) {
+
+ var updatetime = false;
+
+ // Find and update time based on the first available.
+ for (const key in available) {
+ if (available[key] && feeds[config.app[key].value]!=undefined) {
+ updatetime = feeds[config.app[key].value].time;
+ break;
+ }
+ }
+
+ if (updatetime) {
+ // Append new data to timeseries for each available feed, and trim old data outside of view
+ for (const key in available) {
+ if (available[key]) {
+ timeseries.append(key, updatetime, input[key], true);
+ timeseries.trim_start(key, view.start * 0.001);
+ }
+ }
+
+ // add soc if available
+ if (battery_soc_now !== "---") {
+ timeseries.append("battery_soc", updatetime, battery_soc_now);
+ timeseries.trim_start("battery_soc", view.start * 0.001);
+ }
+
+ // Advance view
+ view.end = now;
+ view.start = now - live_timerange;
+ }
+ }
+
+ // Calculate time left
+ $(".battery_time_left").html(battery_time_left({
+ capacity: config.app.battery_capacity_kwh.value,
+ soc: battery_soc_now,
+ battery_power: input.battery
+ }));
+
+ $('.power-unit').text(powerUnit);
+
+ let scale = powerUnit === 'kW' ? 0.001 : 1;
+ let dp = powerUnit === 'kW' ? 1 : 0;
+
+ $(".solar-now").html(toFixed(input.solar * scale, dp));
+ $(".use-now").html(toFixed(input.use * scale, dp));
+ $(".battery_soc").html(battery_soc_now);
+
+ // Grid import/export status
+ let grid = toFixed(Math.abs(input.grid) * scale, dp);
+
+ if (input.grid > 0) {
+ $(".balance-label").html("IMPORTING");
+ $(".grid-now").parent().css("color","#d52e2e");
+ } else if (input.grid < 0) {
+ $(".balance-label").html("EXPORTING");
+ $(".grid-now").parent().css("color","#2ed52e");
+ } else {
+ $(".balance-label").html("BALANCED");
+ $(".grid-now").parent().css("color", "#89ae65");
+ $(".grid-now").siblings('.power-unit').text("");
+ grid = "--";
+ }
+ $(".grid-now").html(grid);
+
+ // Battery charge/discharge status
+ let battery = toFixed(Math.abs(input.battery) * scale, dp);
+
+ if (input.battery > 0) {
+ $(".battery_now_title").html("DISCHARGING");
+ } else if (input.battery < 0) {
+ $(".battery_now_title").html("CHARGING");
+ } else {
+ $(".battery_now_title").html("POWER");
+ }
+ $(".battery-now").html(battery);
+
+ // Only redraw the graph if its the power graph and auto update is turned on
+ if (viewmode=="powergraph" && (autoupdate || reload)) process_and_draw_power_graph();
+}
+
+function solar_battery_visibility() {
+ var s = available.solar;
+ var b = available.battery;
+
+ $("#live-solar-title").toggleClass("text-light", s);
+ $("#live-solar-value").toggleClass("text-warning", s);
+
+ var boxColors = {
+ "#solar-box": s ? "#dccc1f" : "#282828",
+ "#battery-box": b ? "#fb7b50" : "#282828"
+ };
+ for (var id in boxColors) $(id).css("background-color", boxColors[id]);
+
+ var arrowColors = {
+ "#solar-to-grid-box": s ? flow_colors["solar_to_grid"] : "#333",
+ "#solar-to-load-box": s ? flow_colors["solar_to_load"] : "#333",
+ "#solar-to-battery-box": s && b ? flow_colors["solar_to_battery"] : "#333",
+ "#battery-to-load-box": b ? flow_colors["battery_to_load"] : "#333",
+ "#battery-to-grid-box": b ? flow_colors["battery_to_grid"] : "#333",
+ "#grid-to-battery-box": b ? flow_colors["grid_to_battery"] : "#333",
+ "#grid-to-load-box": flow_colors["grid_to_load"]
+ };
+ for (var id in arrowColors) $(id).css("--statsbox-color", arrowColors[id]);
+
+ $(".prc-solar").toggle(s);
+ $(".prc-battery").toggle(b);
+ $(".prc-solar-battery").toggle(s && b);
+
+ $(".battery-section").toggle(b);
+}
+
+function toFixed(num, dp) {
+ if (num === null || num === undefined || isNaN(num)) return "--";
+ return parseFloat(num).toFixed(dp);
+}
+
+// Capacity in kWh, power in W, returns time left as string "Xh Ym"
+function battery_time_left({ capacity, soc, battery_power }) {
+ if (capacity <= 0 || soc < 0 || soc>100 || soc=="---" || battery_power === 0 || battery_power === null) return "--";
+
+ // if discharging, soc_part is soc; if charging, soc_part is 100-soc (time to full charge)
+ let soc_part = battery_power>0 ? soc : (100 - soc);
+ let energy_remaining_kwh = (capacity * soc_part) / 100;
+
+ let battery_power_kw = Math.abs(battery_power * 0.001); // convert W to kW
+ let time_left_hours = energy_remaining_kwh / battery_power_kw;
+
+ const hours_left = Math.floor(time_left_hours);
+ const mins_left = Math.floor((time_left_hours*60) % 60);
+
+ let time_left_str = "";
+ if (hours_left > 0) time_left_str += `${hours_left}h `;
+ if (hours_left < 10) time_left_str += `${mins_left}m`; // show minutes only if less than 10h left
+
+ return time_left_str.trim();
+}
+
+function draw(load) {
+ if (viewmode=="powergraph") {
+ if (load) {
+ load_process_draw_power_graph();
+ } else {
+ draw_powergraph();
+ }
+ }
+ if (viewmode=="bargraph") {
+ if (load) {
+ // draw called from load
+ load_bargraph();
+ } else {
+ draw_bargraph();
+ }
+
+ }
+}
+
+
+// ----------------------------------------------------------------------
+// updateStats: write all stats-box DOM values from a flat flow data object.
+// Keys match the flow naming convention used throughout the app.
+// ----------------------------------------------------------------------
+function updateStats(d) {
+
+ // Reconstruct aggregate totals
+ var solar_kwh = d.solar_to_load + d.solar_to_grid + d.solar_to_battery;
+ var use_kwh = d.solar_to_load + d.battery_to_load + d.grid_to_load;
+ var import_kwh = d.grid_to_load + d.grid_to_battery;
+ var export_kwh = d.solar_to_grid + d.battery_to_grid;
+ var grid_balance_kwh = import_kwh - export_kwh;
+
+ var use_from_import_prc = use_kwh > 0 ? (100 * d.grid_to_load / use_kwh).toFixed(0) + "%" : "";
+ var solar_export_prc = solar_kwh > 0 ? (100 * d.solar_to_grid / solar_kwh).toFixed(0) + "%" : "";
+ var solar_to_load_prc = solar_kwh > 0 ? (100 * d.solar_to_load / solar_kwh).toFixed(0) + "%" : "";
+ var solar_to_battery_prc = solar_kwh > 0 ? (100 * d.solar_to_battery / solar_kwh).toFixed(0) + "%" : "";
+ var use_from_solar_prc = use_kwh > 0 ? (100 * d.solar_to_load / use_kwh).toFixed(0) + "%" : "";
+ var use_from_battery_prc = use_kwh > 0 ? (100 * d.battery_to_load / use_kwh).toFixed(0) + "%" : "";
+
+ $(".total_solar_kwh").html(solar_kwh.toFixed(1));
+ $(".total_use_kwh").html(use_kwh.toFixed(1));
+ $(".total_import_direct_kwh").html(d.grid_to_load.toFixed(1));
+ $(".total_grid_balance_kwh").html(grid_balance_kwh.toFixed(1));
+ $(".use_from_import_prc").html(use_from_import_prc);
+
+ $(".total_solar_direct_kwh").html(d.solar_to_load.toFixed(1));
+ $(".total_solar_export_kwh").html(d.solar_to_grid.toFixed(1));
+ $(".solar_export_prc").html(solar_export_prc);
+ $(".solar_direct_prc").html(solar_to_load_prc);
+ $(".solar_to_battery_prc").html(solar_to_battery_prc);
+ $(".use_from_solar_prc").html(use_from_solar_prc);
+
+ $(".total_battery_charge_from_solar_kwh").html(d.solar_to_battery.toFixed(1));
+ $(".total_import_for_battery_kwh").html(d.grid_to_battery.toFixed(1));
+ $(".total_battery_discharge_kwh").html(d.battery_to_load.toFixed(1));
+ $(".total_battery_to_grid_kwh").html(d.battery_to_grid.toFixed(1));
+ $(".use_from_battery_prc").html(use_from_battery_prc);
+
+ toggleBatteryFlowVisibility(d.grid_to_battery, d.battery_to_grid);
+}
+
+// ----------------------------------------------------------------------
+// toggleBatteryFlowVisibility: show/hide the battery import/export rows
+// based on whether the flow values are significant (>= 0.1 kWh).
+// ----------------------------------------------------------------------
+function toggleBatteryFlowVisibility(grid_to_battery, battery_to_grid) {
+ $("#battery_import").toggle(grid_to_battery >= 0.1);
+ $("#battery_export").toggle(battery_to_grid >= 0.1);
+}
+
+$(function() {
+ $(document).on('window.resized hidden.sidebar.collapse shown.sidebar.collapse', function(){
+ resize()
+ })
+})
+
+// ----------------------------------------------------------------------
+// App log
+// ----------------------------------------------------------------------
+function app_log (level, message) {
+ // if (level=="ERROR") alert(level+": "+message);
+ console.log(level+": "+message);
+}
+
+// ----------------------------------------------------------------------
+// Helper: return array of feeds that should be auto-generated
+// (delegates to config.autogen.get_feeds in appconf.js)
+// ----------------------------------------------------------------------
+function get_autogen_feeds() {
+ return config.autogen.get_feeds();
+}
+
+// ----------------------------------------------------------------------
+// Auto-generate feed list
+// (delegates to config.autogen.render_feed_list in appconf.js)
+// ----------------------------------------------------------------------
+function render_autogen_feed_list() {
+ config.autogen.render_feed_list();
+
+ //let result = flow_available();
+ //vue_config.app_instructions = JSON.stringify(result, null, 2);
+}
+
+// ----------------------------------------------------------------------
+// Auto-generate feed actions
+// (delegate to config.autogen.* in appconf.js)
+// ----------------------------------------------------------------------
+function create_missing_feeds() { config.autogen.create_missing_feeds(); }
+function start_post_processor() { config.autogen.start_post_processor(); }
+function reset_feeds() { config.autogen.reset_feeds(); }
diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php
index 2e9736d8..19099072 100644
--- a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php
+++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery.php
@@ -1,12 +1,8 @@
-
-
-
-
@@ -17,235 +13,104 @@
+
+
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
-
+
0W
-
-
-
+
+
+
-
-
-
-
-
-
+
+
+
--
-
-
-
-
+
+
+
--
-
-
-
-%
+
+
+
-%
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
-
+
-
+
SOLAR
0kWh
-
- 0
+
+ 0
-
- 0
+
+ 0
-
-
-
- 0
-
-
+
+ 0
-
+
0kWh
-
+
GRID
0kWh
@@ -254,28 +119,31 @@
-
+
0kWh
-
+
-
GRID CHARGE 0kWh
+
GRID CHARGE
0kWh
-
+
0kWh
-
+
+
+
BATTERY TO GRID
0kWh
+
-
+
0kWh
@@ -283,981 +151,89 @@
-
+
BATTERY
0%
-
+
0kWh
-
+
-
+
HOUSE
0kWh
-
-
0
+
+ 0
-
-
0
+
+ 0
-
-
0
+
+ 0
-
-
-
-
-
-
-
-
-
-
-
- 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.
-
-
-
-
-
+
+
This app can be used to explore onsite solar generation, self consumption, battery integration, export and building consumption.
+
Derive missing feed: If you do not have one out of the selectable power feeds, this app can derive this data from the others using conservation of energy.
+
+
-
+
-
- // Auto click through to power graph
- $('#placeholder').bind("plotclick", function (event, pos, item)
- {
- if (item && !panning) {
- var z = item.dataIndex;
-
- history_start = view.start
- history_end = view.end
- view.start = solar_kwhd_data[z][0];
- view.end = view.start + 86400*1000;
+
').appendTo(tooltip);
+function load_js_auto_version($scriptname) {
+ global $path;
+ $script_path = "Modules/app/apps/OpenEnergyMonitor/mysolarpvbattery/".$scriptname;
- for (i = 0; i < values.length; i++) {
- var value = values[i];
- var row = $('
').appendTo(table);
- $('
'+value[0]+'
').appendTo(row);
- $('
'+value[1]+''+value[2]+'
').appendTo(row);
+ $version_string = "";
+ if (file_exists($script_path)) {
+ $last_updated = filemtime($script_path);
+ $version_string = "?v=".$last_updated;
}
-
- tooltip
- .css({
- left: x,
- top: y
- })
- .show();
+ echo '';
}
-
-function hide_tooltip() {
- $('#tooltip').hide();
-}
-
-$(function() {
- $(document).on('window.resized hidden.sidebar.collapse shown.sidebar.collapse', function(){
- resize()
- })
-})
-
-// ----------------------------------------------------------------------
-// App log
-// ----------------------------------------------------------------------
-function app_log (level, message) {
- // if (level=="ERROR") alert(level+": "+message);
- console.log(level+": "+message);
-}
-
+?>
\ 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..6d266ac4
--- /dev/null
+++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_controller.php
@@ -0,0 +1,91 @@
+action == "view" || $route->action == "") {
+ $route->format = "html";
+ $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,
+ "v"=>$v
+ ));
+ 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();
+
+ if (!isset($app->config->autogenerate_nodename)) {
+ return array("success"=>false, "message"=>"Feed node name not set");
+ }
+ $tag = $app->config->autogenerate_nodename;
+
+ $process_conf = (object) array(
+ "solar" => (int) isset($app->config->solar) ? $app->config->solar : 0,
+ "use" => (int) isset($app->config->use) ? $app->config->use : 0,
+ "grid" => (int) isset($app->config->grid) ? $app->config->grid : 0,
+ "battery" => (int) isset($app->config->battery) ? $app->config->battery : 0,
+
+ "solar_to_load_kwh" => $feed->exists_tag_name($userid, $tag, "solar_to_load_kwh"),
+ "solar_to_grid_kwh" => $feed->exists_tag_name($userid, $tag, "solar_to_grid_kwh"),
+ "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"),
+
+ // For testing
+ // "solar_kwh" => $feed->exists_tag_name($userid, $tag, "solar_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;
+ }
+}
diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_daily.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_daily.js
new file mode 100644
index 00000000..3a50fd29
--- /dev/null
+++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_daily.js
@@ -0,0 +1,235 @@
+
+// ======================================================================================
+// PART 2: BAR GRAPH PAGE
+// ======================================================================================
+
+// --------------------------------------------------------------------------------------
+// INIT BAR GRAPH
+// - load cumulative kWh feeds
+// - calculate used solar, solar, used and exported kwh/d
+// --------------------------------------------------------------------------------------
+function init_bargraph() {
+ bargraph_initialized = true;
+ // Fetch the earliest start_time from grid_to_load
+ var m = feed.getmeta(config.app.grid_to_load_kwh.value);
+ var earliest_start_time = m.start_time;
+ latest_start_time = earliest_start_time;
+ view.first_data = latest_start_time * 1000;
+}
+
+function load_bargraph() {
+ var interval = 3600*24;
+ var intervalms = interval * 1000;
+ var mode = get_mode();
+
+ var end = view.end;
+ var start = view.start;
+
+ end = Math.ceil(end/intervalms)*intervalms;
+ start = Math.floor(start/intervalms)*intervalms;
+
+ // Feed definitions: key -> { guard, feedkey }
+ // guard: condition under which this flow feed is applicable to the current mode
+ var flow_defs = [
+ { key: 'grid_to_load', guard: true, },
+ { key: 'solar_to_load', guard: mode.has_solar, },
+ { key: 'solar_to_grid', guard: mode.has_solar, },
+ { key: 'solar_to_battery', guard: mode.has_solar && mode.has_battery },
+ { key: 'battery_to_load', guard: mode.has_battery, },
+ { key: 'battery_to_grid', guard: mode.has_battery, },
+ { key: 'grid_to_battery', guard: mode.has_battery, }
+ ];
+
+ var keys_to_load = [];
+ var feedids = [];
+ flow_defs.forEach(function(d) {
+ if (d.guard && config.app[d.key + "_kwh"] && config.app[d.key + "_kwh"].value) {
+ keys_to_load.push(d.key);
+ feedids.push(config.app[d.key + "_kwh"].value);
+ }
+ });
+
+ // Load raw daily delta data for each applicable flow
+ feed.getdata(feedids, start, end, "daily", 0, 1, 0, 0, function (all_data) {
+
+ // if success false
+ if (all_data.success === false) {
+ historyseries = [];
+ draw_bargraph();
+ return;
+ }
+
+ var raw = {};
+ var idx = 0;
+ keys_to_load.forEach(function(key) {
+ raw[key] = all_data[idx].data;
+ idx++;
+ });
+
+ // Per-day arrays for graph and hover access (global so bargraph_events can read them)
+ kwhd_data = {};
+ flow_defs.forEach(function(d) { kwhd_data[d.key] = []; });
+
+ for (var day = 0; day < raw['grid_to_load'].length; day++) {
+ var time = raw['grid_to_load'][day][0];
+
+ // Only skip days where both reference feeds are null
+ // var required_ok = (raw['grid_to_load'][day] && raw['grid_to_load'][day][1] !== null) ||
+ // (raw['solar_to_load'][day] && raw['solar_to_load'][day][1] !== null);
+ // if (!required_ok) continue;
+
+ flow_defs.forEach(function(d) {
+ kwhd_data[d.key].push([time, kwhd_val(raw[d.key], day)]);
+ });
+ }
+
+ // Series definitions: label, color, stack (1=positive/load, 0=negative/export)
+ var series_defs = [
+ // Stack 1: onsite use breakdown (positive bars above zero)
+ { key: 'solar_to_load', label: "Solar to Load", color: flow_colors["solar_to_load"], stack: 1, invert: false },
+ { key: 'battery_to_load', label: "Battery to Load", color: flow_colors["battery_to_load"], stack: 1, invert: false },
+ { key: 'grid_to_load', label: "Grid to Load", color: flow_colors["grid_to_load"], stack: 1, invert: false },
+ // Stack 0: exports (negative bars below zero)
+ { key: 'solar_to_grid', label: "Solar to Grid", color: flow_colors["solar_to_grid"], stack: 0, invert: true },
+ { key: 'battery_to_grid', label: "Battery to Grid", color: flow_colors["battery_to_grid"], stack: 0, invert: true }
+ ];
+
+ historyseries = [];
+
+ series_defs.forEach(function(def) {
+ var data = kwhd_data[def.key];
+ if (!data.length) return;
+ historyseries.push({
+ data: def.invert ? invert_kwhd_data(data) : data,
+ label: def.label,
+ color: def.color,
+ bars: { show: true, align: "center", barWidth: 0.8 * 3600 * 24 * 1000, fill: 0.9, lineWidth: 0 },
+ stack: def.stack
+ });
+ });
+
+ draw_bargraph();
+
+ }, false);
+}
+
+// Invert kWh/d data for export flows so they appear as negative bars on the graph
+function invert_kwhd_data(data) {
+ var neg_data = [];
+ for (var i = 0; i < data.length; i++) {
+ neg_data.push([data[i][0], -1 * data[i][1]]);
+ }
+ return neg_data;
+}
+
+// kwhd_val: safely read a daily kWh value from a feed data array.
+// Returns 0 when the entry or its value is null/undefined.
+function kwhd_val(arr, idx) {
+ if (arr === null || arr === undefined) return 0;
+ if (arr[idx] === undefined) return 0;
+
+ return (arr[idx] && arr[idx][1] !== null) ? arr[idx][1] : 0;
+}
+
+// ------------------------------------------------------------------------------------------
+// DRAW BAR GRAPH
+// Because the data for the bargraph only needs to be loaded once at the start we seperate out
+// the data loading part to init and the draw part here just draws the bargraph to the flot
+// placeholder overwritting the power graph as the view is changed.
+// ------------------------------------------------------------------------------------------
+function draw_bargraph()
+{
+ var markings = [];
+ markings.push({ color: "#ccc", lineWidth: 1, yaxis: { from: 0, to: 0 } });
+
+ var options = {
+ xaxis: { mode: "time", timezone: "browser", minTickSize: [1, "day"] },
+ grid: { hoverable: true, clickable: true, markings: markings, borderWidth: 0 },
+ selection: { mode: "x" },
+ legend: { show: false }
+ };
+
+ var plot = $.plot($('#placeholder'),historyseries,options);
+
+ $('#placeholder').append("
Above: Onsite Use & Total Use
");
+ $('#placeholder').append("
Below: Total export (solar + battery to grid)
");
+}
+
+// ------------------------------------------------------------------------------------------
+// BAR GRAPH EVENTS
+// - show bar values on hover
+// - click through to power graph
+// ------------------------------------------------------------------------------------------
+function bargraph_events() {
+ $(".visnav[time=1], .visnav[time=3], .visnav[time=6], .visnav[time=24]").hide();
+
+ $('#placeholder').unbind("plotclick");
+ $('#placeholder').unbind("plothover");
+ $('#placeholder').unbind("plotselected");
+ $('.bargraph-viewall').unbind("click");
+
+ // Show day's figures on the bottom of the page
+
+ $('#placeholder').bind("plothover", function (event, pos, item)
+ {
+ if (item) {
+ var z = item.dataIndex;
+ var mode = get_mode();
+
+ // Read directly from the fine-grained flow feed data arrays (0 when not applicable in mode)
+ updateStats({
+ solar_to_load: kwhd_val(kwhd_data['solar_to_load'], z),
+ solar_to_grid: kwhd_val(kwhd_data['solar_to_grid'], z),
+ solar_to_battery: kwhd_val(kwhd_data['solar_to_battery'], z),
+ battery_to_load: kwhd_val(kwhd_data['battery_to_load'], z),
+ battery_to_grid: kwhd_val(kwhd_data['battery_to_grid'], z),
+ grid_to_load: kwhd_val(kwhd_data['grid_to_load'], z),
+ grid_to_battery: kwhd_val(kwhd_data['grid_to_battery'], z)
+ });
+ $(".battery_soc_change").html("---");
+
+ } else {
+ // Hide tooltip
+ hide_tooltip();
+ }
+ });
+
+ // Auto click through to power graph
+ $('#placeholder').bind("plotclick", function (event, pos, item)
+ {
+ if (item && !panning) {
+ var z = item.dataIndex;
+
+ history_start = view.start;
+ history_end = view.end;
+ // Use whichever per-day data array has data
+ var ref_day_data = kwhd_data['grid_to_load'].length ? kwhd_data['grid_to_load'] : kwhd_data['solar_to_load'];
+ view.start = ref_day_data[z][0];
+ view.end = view.start + 86400*1000;
+
+ $(".balanceline").attr('disabled',false);
+ $(".viewhistory").toggleClass('active');
+
+ reload = true;
+ autoupdate = false;
+ viewmode = "powergraph";
+
+ draw(true);
+ powergraph_events();
+ }
+ });
+
+
+ $('#placeholder').bind("plotselected", function (event, ranges) {
+ view.start = ranges.xaxis.from;
+ view.end = ranges.xaxis.to;
+ draw(true);
+ panning = true; setTimeout(function() {panning = false; }, 100);
+ });
+
+ $('.bargraph-viewall').click(function () {
+ view.start = latest_start_time * 1000;
+ view.end = +new Date;
+ draw(true);
+ });
+}
\ No newline at end of file
diff --git a/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_powergraph.js b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_powergraph.js
new file mode 100644
index 00000000..bdafb295
--- /dev/null
+++ b/apps/OpenEnergyMonitor/mysolarpvbattery/mysolarpvbattery_powergraph.js
@@ -0,0 +1,293 @@
+// -------------------------------------------------------------------------------------------------------
+// MySolarPVBattery Power Graph: load, process, and draw the power flow graph
+// -------------------------------------------------------------------------------------------------------
+
+// Fetch raw feed data for the current view window. Only requests feeds that are
+// actually configured; any missing feeds will be derived later in processing.
+// On success, loads each feed into the timeseries store then triggers processing.
+function load_process_draw_power_graph() {
+ view.calc_interval(1500); // npoints = 1500;
+
+ var feeds = [
+ { key: "solar", cond: available.solar, avg: 1 },
+ { key: "use", cond: available.use, avg: 1 },
+ { key: "battery", cond: available.battery, avg: 1 },
+ { key: "grid", cond: available.grid, avg: 1 },
+ { key: "battery_soc", cond: battery_soc_available, avg: 0 },
+ ].filter(f => f.cond);
+
+ var feedids = feeds.map(f => config.app[f.key].value);
+ var averages = feeds.map(f => f.avg);
+ var deltas = feeds.map(() => 0);
+
+ feed.getdata(feedids, view.start, view.end, view.interval, averages.join(","), deltas.join(","), 0, 0, function (all_data) {
+ if (all_data.success === false) {
+ feeds.forEach(f => timeseries.load(f.key, []));
+ } else {
+ feeds.forEach((f, idx) => timeseries.load(f.key, remove_null_values(all_data[idx].data, view.interval)));
+ console.log("Data loaded for feeds: " + feeds.map(f => f.key).join(", "));
+ }
+ process_and_draw_power_graph();
+ }, false, "notime");
+}
+
+// Iterates over the loaded timeseries data, derives any missing power flows using
+// flow_derive_missing / flow_calculation, accumulates kWh totals for the stats
+// panel, builds the flot series arrays, and then calls draw_powergraph().
+function process_and_draw_power_graph() {
+
+ // -------------------------------------------------------------------------------------------------------
+
+ // Determine which feed we use as the time axis reference (any loaded feed will do)
+ var ts_ref = ["use", "grid", "solar", "battery"].find(key => available[key]) || false;
+ console.log("Time reference feed: " + ts_ref);
+
+ 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 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;
+
+ let battery_soc_now = null;
+
+ var datastart = timeseries.start_time(ts_ref);
+ var interval = view.interval;
+ var power_to_kwh = interval / 3600000.0;
+
+ var battery_soc_start = null;
+ var battery_soc_end = null;
+
+ for (var z=0; z=0) sign = "+";
+ $(".battery_soc_change").html(sign+soc_change.toFixed(1));
+
+ powerseries = [];
+ powerseries.push({data: solar_to_load_data, label: "Solar to Load", color: flow_colors["solar_to_load"], stack: 1, lines: {lineWidth: 0, fill: 0.8}});
+ powerseries.push({data: solar_to_battery_data, label: "Solar to Battery", color: flow_colors["solar_to_battery"], stack: 1, lines: {lineWidth: 0, fill: 0.8}});
+ powerseries.push({data: solar_to_grid_data, label: "Solar to Grid", color: flow_colors["solar_to_grid"], stack: 1, lines: {lineWidth: 0, fill: 1.0}});
+ powerseries.push({data: battery_to_load_data, label: "Battery to Load", color: flow_colors["battery_to_load"], stack: 1, lines: {lineWidth: 0, fill: 0.8}});
+ powerseries.push({data: battery_to_grid_data, label: "Battery to Grid", color: flow_colors["battery_to_grid"], stack: 1, lines: {lineWidth: 0, fill: 0.8}});
+ powerseries.push({data: grid_to_load_data, label: "Grid to Load", color: flow_colors["grid_to_load"], stack: 1, lines: {lineWidth: 0, fill: 0.8}});
+ powerseries.push({data: grid_to_battery_data, label: "Grid to Battery", color: flow_colors["grid_to_battery"], stack: 1, lines: {lineWidth: 0, fill: 0.8}});
+
+ if (battery_soc_available) {
+ // only add if time period is less or equall to 1 month
+ if ((view.end - view.start) <= 3600000*24*32) {
+ powerseries.push({data:battery_soc_data, label: "SOC", yaxis:2, color: "#888"});
+ }
+ }
+
+ draw_powergraph();
+}
+
+// Renders the power-flow stacked area chart (and optional SOC line) using flot,
+// fitted to the current view.start / view.end time range.
+function draw_powergraph() {
+
+ var options = {
+ lines: { fill: false },
+ xaxis: { mode: "time", timezone: "browser", min: view.start, max: view.end},
+ yaxes: [{ min: 0, reserveSpace: false },{ min: 0, max: 100, reserveSpace: false }],
+ grid: { hoverable: true, clickable: true, borderWidth: 0 },
+ selection: { mode: "x" },
+ legend: { show: false }
+ }
+
+ options.xaxis.min = view.start;
+ options.xaxis.max = view.end;
+ $.plot($('#placeholder'),powerseries,options);
+ $(".ajax-loader").hide();
+}
+
+// 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.
+// Forward-fills null gaps that are shorter than 15 minutes so the graph
+// doesn't show false breaks for brief outages. Longer gaps remain null.
+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) { // 900 seconds = 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;
+}
+
+// Binds flot interaction events (hover tooltip, drag-to-zoom selection) to the
+// chart placeholder. Safe to call on every redraw — unbinds before rebinding.
+function powergraph_events() {
+ $(".visnav[time=1], .visnav[time=3], .visnav[time=6], .visnav[time=24]").show();
+
+ $('#placeholder').unbind("plotclick");
+ $('#placeholder').unbind("plothover");
+ $('#placeholder').unbind("plotselected");
+
+ $('#placeholder').bind("plothover", function (event, pos, item)
+ {
+ if (item) {
+ // Show tooltip
+ var tooltip_items = [];
+
+ var date = new Date(item.datapoint[0]);
+ tooltip_items.push(["TIME", dateFormat(date, 'HH:MM'), ""]);
+
+ for (i = 0; i < powerseries.length; i++) {
+ var series = powerseries[i];
+ if (series.data[item.dataIndex]!=undefined && series.data[item.dataIndex][1]!=null) {
+ if (series.label.toUpperCase()=="SOC") {
+ tooltip_items.push([series.label.toUpperCase(), series.data[item.dataIndex][1].toFixed(1), "%", series.color]);
+ } else {
+ if (series.data[item.dataIndex][1] != 0) {
+ if ( series.data[item.dataIndex][1] >= 1000) {
+ tooltip_items.push([series.label.toUpperCase(), (series.data[item.dataIndex][1]/1000.0).toFixed(1) , "kW", series.color]);
+ } else {
+ tooltip_items.push([series.label.toUpperCase(), series.data[item.dataIndex][1].toFixed(0), "W", series.color]);
+ }
+ }
+ }
+ }
+ }
+ show_tooltip(pos.pageX+10, pos.pageY+5, tooltip_items);
+ } else {
+ // Hide tooltip
+ hide_tooltip();
+ }
+ });
+
+ $('#placeholder').bind("plotselected", function (event, ranges) {
+ view.start = ranges.xaxis.from;
+ view.end = ranges.xaxis.to;
+
+ autoupdate = false;
+ reload = true;
+
+ var now = +new Date();
+ if (Math.abs(view.end-now)<30000) {
+ autoupdate = true;
+ live_timerange = view.end - view.start;
+ }
+
+ draw(true);
+ });
+}
+
+// Builds and positions the hover tooltip. Each entry in `values` is
+// [label, value, units, swatchColor?]. Creates the tooltip element on first call.
+function show_tooltip(x, y, values) {
+ var tooltip = $('#tooltip');
+ if (!tooltip[0]) {
+ tooltip = $('')
+ .css({
+ position: "absolute",
+ display: "none",
+ border: "1px solid #545454",
+ padding: "8px",
+ "background-color": "#333",
+ })
+ .appendTo("body");
+ }
+
+ tooltip.html('');
+ var table = $('
').appendTo(tooltip);
+
+ for (i = 0; i < values.length; i++) {
+ var value = values[i];
+ var row = $('
').appendTo(row);
+ }
+
+ tooltip
+ .css({
+ left: x,
+ top: y
+ })
+ .show();
+}
+
+// Hides the hover tooltip when the cursor moves off a data point.
+function hide_tooltip() {
+ $('#tooltip').hide();
+}
\ No newline at end of file
diff --git a/apps/OpenEnergyMonitor/mysolarpvdivert/mysolarpvdivert.php b/apps/OpenEnergyMonitor/mysolarpvdivert/mysolarpvdivert.php
index 030e5c1d..2f68f577 100644
--- a/apps/OpenEnergyMonitor/mysolarpvdivert/mysolarpvdivert.php
+++ b/apps/OpenEnergyMonitor/mysolarpvdivert/mysolarpvdivert.php
@@ -3,10 +3,7 @@
global $path, $session, $v;
?>
-
-
-
@@ -296,28 +293,13 @@
-
-
-
-
-
-
-
-
-
- The My Solar with Divert app can be used to explore onsite solar (and optionally wind) generation, self consumption, export and building consumption.
-
It is designed for users who divert some or all of their excess generated power to something. For example an immersion heater or electric car. It shows all of this both in realtime with a moving power graph view and historically with a daily and monthly bargraph.
-
-
Auto configure: This app can auto-configure connecting to emoncms feeds with the names shown on the right, alternatively feeds can be selected by clicking on the edit button.
-
Cumulative kWh feeds can be generated from power feeds with the power_to_kwh input processor.
-
-
-
-
-
-
-
-
+
+
+The My Solar with Divert app can be used to explore onsite solar (and optionally wind) generation, self consumption, export and building consumption.
+
It is designed for users who divert some or all of their excess generated power to something. For example an immersion heater or electric car. It shows all of this both in realtime with a moving power graph view and historically with a daily and monthly bargraph.
+
+
+
@@ -363,11 +345,13 @@ function getTranslations(){
"wind_kwh":{"optional":true, "type":"feed", "autoname":"wind_kwh", "description":"Cumulative wind generation in kWh"},
"divert_kwh":{"optional":true, "type":"feed", "autoname":"divert_kwh", "description":"Cumulative divert energy in kWh"},
"import_kwh":{"optional":true, "type":"feed", "autoname":"import_kwh", "description":"Cumulative grid import in kWh"},
- "kw":{"type":"checkbox", "default":0, "name": "Show kW", "description":tr("Display power as kW")},
+ "kw":{"type":"checkbox", "default":0, "name": "Show kW", "description":tr("Display power as kW")}
//"import_unitcost":{"type":"value", "default":0.1508, "name": "Import unit cost", "description":"Unit cost of imported grid electricity"}
- "public":{"type":"checkbox", "name": "Public", "default": 0, "optional":true, "description":"Make app public"}
}
+config.app_name = "My Solar PV Divert";
+config.app_name_color = "#dccc1f";
+
config.id = ;
config.name = "";
config.public = ;
diff --git a/apps/OpenEnergyMonitor/octopus/app.json b/apps/OpenEnergyMonitor/octopus/app.json
index a7216b5f..c0b64ce2 100644
--- a/apps/OpenEnergyMonitor/octopus/app.json
+++ b/apps/OpenEnergyMonitor/octopus/app.json
@@ -1,5 +1,6 @@
{
- "title" : "Octopus Agile",
- "description" : "Explore Octopus Agile tariff energy costs",
- "order" : 8
+ "title" : "Tariff Explorer",
+ "description" : "Explore time of use tariff energy costs",
+ "order" : 3,
+ "primary" : true
}
diff --git a/apps/OpenEnergyMonitor/octopus/octopus_controller.php b/apps/OpenEnergyMonitor/octopus/octopus_controller.php
new file mode 100644
index 00000000..585bd9d2
--- /dev/null
+++ b/apps/OpenEnergyMonitor/octopus/octopus_controller.php
@@ -0,0 +1,81 @@
+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."tariff_explorer.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();
+
+ if (!isset($app->config->autogenerate_nodename)) {
+ return array("success"=>false, "message"=>"Feed node name not set");
+ }
+ $tag = $app->config->autogenerate_nodename;
+
+ $process_conf = (object) array(
+ "solar" => (int) isset($app->config->solar) ? $app->config->solar : 0,
+ "use" => (int) isset($app->config->use) ? $app->config->use : 0,
+ "grid" => (int) isset($app->config->grid) ? $app->config->grid : 0,
+ "battery" => (int) isset($app->config->battery) ? $app->config->battery : 0,
+
+ "solar_to_load_kwh" => $feed->exists_tag_name($userid, $tag, "solar_to_load_kwh"),
+ "solar_to_grid_kwh" => $feed->exists_tag_name($userid, $tag, "solar_to_grid_kwh"),
+ "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/octopus/profile.js b/apps/OpenEnergyMonitor/octopus/profile.js
index ac347903..3630acb7 100644
--- a/apps/OpenEnergyMonitor/octopus/profile.js
+++ b/apps/OpenEnergyMonitor/octopus/profile.js
@@ -35,7 +35,7 @@ function profile_draw() {
});
graph_series.push({
- label: config.app.tariff_A.value,
+ label: config.app.tariff.value,
data: profile_unitprice,
yaxis: 2,
color: "#fb1a80",
diff --git a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js
index c9efce2e..a91a00cf 100644
--- a/apps/OpenEnergyMonitor/octopus/tariff_explorer.js
+++ b/apps/OpenEnergyMonitor/octopus/tariff_explorer.js
@@ -10,6 +10,16 @@ var profile_mode = false;
var show_carbonintensity = $("#show_carbonintensity")[0].checked;
+var flow_colors = {
+ "solar_to_load": "#bec745",
+ "solar_to_battery": "#a3d977",
+ "solar_to_grid": "#dccc1f",
+ "battery_to_load": "#fbb450",
+ "battery_to_grid": "#f0913a",
+ "grid_to_load": "#44b3e2",
+ "grid_to_battery": "#82cbfc"
+};
+
// ----------------------------------------------------------------------
// Display
// ----------------------------------------------------------------------
@@ -52,36 +62,92 @@ config.app = {
"name": "Title",
"description": "Optional title for app"
},
- "import": {
+
+ // == System configuration ==
+ // Select which metering points are present on the system.
+ // When has_solar=false, the solar feed is hidden and solar power is treated as 0.
+ // When has_battery=false, the battery feed is hidden and battery power is treated as 0.
+ // In each mode, conservation of energy allows one feed to be derived from the others:
+ // Full (solar+battery): GRID=USE-SOLAR-BATTERY, USE=GRID+SOLAR+BATTERY, SOLAR=USE-GRID-BATTERY, BATTERY=USE-GRID-SOLAR
+ // Solar only: GRID=USE-SOLAR, USE=GRID+SOLAR, SOLAR=USE-GRID
+ // Battery only: GRID=USE-BATTERY, USE=GRID+BATTERY, BATTERY=USE-GRID
+ // Consumption only: no derivation; only USE (or GRID) feed needed
+ "has_solar":{"type":"checkbox", "default":1, "name":"Has solar PV", "description":"Does the system have solar PV generation?"},
+ "has_battery":{"type":"checkbox", "default":1, "name":"Has battery", "description":"Does the system have a battery?"},
+
+ // == Key power feeds ==
+ // All four feeds are optional at the config level; the custom check() below enforces the
+ // correct minimum set depending on the has_solar / has_battery mode.
+ // Any single missing feed will be derived from the other three (or two in solar/battery-only modes).
+ // These power feeds are used to auto-generate the cumulative kWh feeds below.
+ "use":{"optional":true, "type":"feed", "autoname":"use", "description":"House or building use in watts"},
+ "solar":{"optional":true, "type":"feed", "autoname":"solar", "description":"Solar generation in watts (only shown when has_solar is enabled)"},
+ "battery":{"optional":true, "type":"feed", "autoname":"battery_power", "description":"Battery power in watts, positive for discharge, negative for charge (only shown when has_battery is enabled)"},
+ "grid":{"optional":true, "type":"feed", "autoname":"grid", "description":"Grid power in watts (positive for import, negative for export)"},
+
+ // We actually use this cumulative kWh feeds to generate the half hourly data
+ // the power feeds above are used to auto-generate these feeds.
+
+ // Node name for auto-generated feeds, common with mysolarpvbattery app.
+ "autogenerate_nodename": {
+ "hidden": true,
+ "type": "value",
+ "default": "solar_battery_kwh_flows",
+ "name": "Auto-generate feed node name",
+ "description": ""
+ },
+
+ // Auto-generated cumulative kWh feeds
+ "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": "import"
+ "autoname": "solar_to_grid_kwh",
+ "description": "Cumulative solar to grid (export) energy in kWh"
},
- "import_kwh": {
+ "solar_to_battery_kwh": {
+ "autogenerate":true,
"optional": true,
"type": "feed",
- "autoname": "import_kwh"
+ "autoname": "solar_to_battery_kwh",
+ "description": "Cumulative solar to battery energy in kWh"
},
- "use_kwh": {
+ "battery_to_load_kwh": {
+ "autogenerate":true,
"optional": true,
"type": "feed",
- "autoname": "use_kwh"
+ "autoname": "battery_to_load_kwh",
+ "description": "Cumulative battery to load energy in kWh"
},
- "solar_kwh": {
+ "battery_to_grid_kwh": {
+ "autogenerate":true,
"optional": true,
"type": "feed",
- "autoname": "solar_kwh"
+ "autoname": "battery_to_grid_kwh",
+ "description": "Cumulative battery to grid energy in kWh"
},
- "battery_charge_kwh": {
+ "grid_to_load_kwh": {
+ "autogenerate":true,
"optional": true,
"type": "feed",
- "autoname": "battery_charge_kwh"
+ "autoname": "grid_to_load_kwh",
+ "description": "Cumulative grid to load energy in kWh"
},
- "battery_discharge_kwh": {
+ "grid_to_battery_kwh": {
+ "autogenerate":true,
"optional": true,
"type": "feed",
- "autoname": "battery_discharge_kwh"
+ "autoname": "grid_to_battery_kwh",
+ "description": "Cumulative grid to battery energy in kWh"
},
+
"meter_kwh_hh": {
"optional": true,
"type": "feed",
@@ -95,41 +161,106 @@ config.app = {
"options": ["A_Eastern_England", "B_East_Midlands", "C_London", "E_West_Midlands", "D_Merseyside_and_Northern_Wales", "F_North_Eastern_England", "G_North_Western_England", "H_Southern_England", "J_South_Eastern_England", "K_Southern_Wales", "L_South_Western_England", "M_Yorkshire", "N_Southern_Scotland", "P_Northern_Scotland"]
},
- "tariff_A": {
+ "tariff": {
"type": "select",
- "name": "Select tariff A:",
+ "name": "Select tariff:",
"default": "AGILE-23-12-06",
"options": tariff_options
- },
+ }
+};
- "tariff_B": {
- "type": "select",
- "name": "Select tariff B:",
- "default": "INTELLI-VAR-22-10-14",
- "options": tariff_options
- },
- "public": {
- "type": "checkbox",
- "name": "Public",
- "default": 0,
- "optional": true,
- "description": "Make app public"
+// ----------------------------------------------------------------------
+// Custom check: enforce the correct minimum set of feeds based on mode.
+// This overrides the appconf.js default check() which just tests optional flags.
+// Rules:
+// has_solar + has_battery: at least 3 of (use, solar, battery, grid) must be configured
+// has_solar only: at least 2 of (use, solar, grid) must be configured
+// has_battery only: at least 2 of (use, battery, grid) must be configured
+// consumption only: at least 1 of (use, grid) must be configured
+// ----------------------------------------------------------------------
+config.check = function() {
+ // Read mode from db (persisted) or app default
+ var has_solar = (config.db.has_solar !== undefined) ? (config.db.has_solar != 0) : (config.app.has_solar.default != 0);
+ var has_battery = (config.db.has_battery !== undefined) ? (config.db.has_battery != 0) : (config.app.has_battery.default != 0);
+
+ // Helper: is a feed key resolved (either auto-matched by name or explicitly set in db)?
+ function feed_resolved(key) {
+ if (config.db[key] == "disable") return false; // explicitly disabled
+ if (config.db[key] != undefined) {
+ // user-set: check the feed id still exists
+ return config.feedsbyid[config.db[key]] !== undefined;
+ }
+ // auto-match by name
+ var autoname = config.app[key] && config.app[key].autoname;
+ return autoname && config.feedsbyname[autoname] !== undefined;
}
+ var use_ok = feed_resolved("use");
+ var solar_ok = feed_resolved("solar");
+ var bat_ok = feed_resolved("battery");
+ var grid_ok = feed_resolved("grid");
+
+ if (has_solar && has_battery) {
+ // Need at least 3 of the 4 feeds
+ return [use_ok, solar_ok, bat_ok, grid_ok].filter(Boolean).length >= 3;
+ } else if (has_solar && !has_battery) {
+ // Need at least 2 of (use, solar, grid)
+ return [use_ok, solar_ok, grid_ok].filter(Boolean).length >= 2;
+ } else if (!has_solar && has_battery) {
+ // Need at least 2 of (use, battery, grid)
+ return [use_ok, bat_ok, grid_ok].filter(Boolean).length >= 2;
+ } else {
+ // Consumption only: need at least 1 of (use, grid)
+ return use_ok || grid_ok;
+ }
};
config.feeds = feed.list();
-config.initapp = function() {
- init()
-};
-config.showapp = function() {
- show()
+var feeds_by_tag_name = feed.by_tag_and_name(config.feeds);
+
+config.autogen_feed_defaults = { datatype: 1, engine: 5, options: { interval: 1800 } };
+config.autogen_feeds_by_tag_name = feeds_by_tag_name;
+
+
+config.initapp = function(){init()};
+config.showapp = function(){show()};
+config.hideapp = function(){hide()};
+
+// ----------------------------------------------------------------------
+// Config UI helpers: hide/show feeds based on the current mode
+// ----------------------------------------------------------------------
+function get_mode() {
+ var has_solar = (config.db.has_solar !== undefined) ? (config.db.has_solar != 0) : (config.app.has_solar.default != 0);
+ var has_battery = (config.db.has_battery !== undefined) ? (config.db.has_battery != 0) : (config.app.has_battery.default != 0);
+ return { has_solar: has_solar, has_battery: has_battery };
+}
+
+// Called by appconf.js before rendering the config UI
+config.ui_before_render = function() {
+ var mode = get_mode();
+
+ // solar feed: only relevant if has_solar is on
+ config.app.solar.hidden = !mode.has_solar;
+ // battery feeds: only relevant if has_battery is on
+ config.app.battery.hidden = !mode.has_battery;
+ // autogenerate feeds: hide battery-specific ones if no battery, solar-specific if no solar
+ config.app.solar_to_load_kwh.hidden = !mode.has_solar;
+ config.app.solar_to_grid_kwh.hidden = !mode.has_solar;
+ config.app.solar_to_battery_kwh.hidden = !mode.has_battery || !mode.has_solar;
+ config.app.battery_to_load_kwh.hidden = !mode.has_battery;
+ config.app.battery_to_grid_kwh.hidden = !mode.has_battery;
+ config.app.grid_to_battery_kwh.hidden = !mode.has_battery;
};
-config.hideapp = function() {
- hide()
+
+// Called by appconf.js after any config value is changed; re-renders UI when a mode checkbox changes
+config.ui_after_value_change = function(key) {
+ if (key === 'has_solar' || key === 'has_battery') {
+ config.UI();
+ }
+ render_autogen_feed_list();
};
var octopus_feed_list = {};
@@ -156,6 +287,7 @@ var regions_outgoing = {
// ----------------------------------------------------------------------
var feeds = {};
var data = {};
+var time_to_index_map = {};
var graph_series = [];
var previousPoint = false;
var panning = false;
@@ -164,18 +296,27 @@ var updaterinst = false;
var this_halfhour_index = -1;
// disable x axis limit
view.limit_x = false;
-var cumulative_import_data = false;
-var solarpv_mode = false;
-var battery_mode = false;
var smart_meter_data = false;
var use_meter_kwh_hh = false;
var profile_kwh = {};
var profile_cost = {};
+var monthly_summary = {};
+var baseline_monthly_summary = {};
+var baseline_tariff_name = "";
+
config.init();
function init() {
+
+ var mode = get_mode();
+
+ // Apply hidden flags (also used by autogen feed list and config UI)
+ config.ui_before_render();
+
+ render_autogen_feed_list();
+
$("#datetimepicker1").datetimepicker({
language: 'en-EN'
});
@@ -199,9 +340,6 @@ function show() {
if (config.app[key].value) feeds[key] = config.feedsbyid[config.app[key].value];
}
- solarpv_mode = false;
- battery_mode = false;
-
resize();
$.ajax({
@@ -218,12 +356,18 @@ function show() {
});
setPeriod('168');
+ $(".time-select").val('168');
graph_load();
graph_draw();
updater();
updaterinst = setInterval(updater, 5000);
$(".ajax-loader").hide();
+
+ // Trigger process here
+ setTimeout(function() {
+ start_post_processor();
+ }, 1000);
}
function setPeriod(period) {
@@ -299,13 +443,21 @@ function updater() {
if (config.app[key].value) feeds[key] = result[config.app[key].value];
}
- if (feeds["import"] != undefined) {
- if (feeds["import"].value < 10000) {
- $("#power_now").html(Math.round(feeds["import"].value) + "W");
- } else {
- $("#power_now").html((feeds["import"].value * 0.001).toFixed(1) + "kW");
+ // Update live stats value for current half-hour if feeds are present
+ // use grid feed for import reference.
+ if (config.app.grid.value != undefined) {
+ var grid_feed_id = config.app.grid.value;
+ var grid_value = result[grid_feed_id] != undefined ? result[grid_feed_id].value : null;
+ if (grid_value != null) {
+ if (grid_value < 0) {
+ $("#import_export").html("EXPORT NOW");
+ } else {
+ $("#import_export").html("IMPORT NOW");
+ }
+ $("#power_now").html(Math.abs(grid_value) + " W");
}
}
+
});
}
@@ -324,7 +476,7 @@ function get_data_value_at_index(key, index) {
// - graph_draw
// - resize
-function graph_load() {
+function graph_load(time_window_changed = true) {
$(".power-graph-footer").show();
var interval = 1800;
@@ -341,95 +493,75 @@ function graph_load() {
datetimepicker2.setStartDate(new Date(view.start));
}
- // Determine if required solar PV feeds are available
- if (feeds["use_kwh"] != undefined && feeds["solar_kwh"] != undefined) {
- solarpv_mode = true;
- }
-
- // Determine if required battery feeds are available
- if (feeds["battery_charge_kwh"] != undefined && feeds["battery_discharge_kwh"] != undefined && feeds["use_kwh"] != undefined) {
- battery_mode = true;
- }
-
- if (feeds["meter_kwh_hh"] != undefined) {
- smart_meter_data = true;
+ smart_meter_data = feeds["meter_kwh_hh"] != undefined;
+ if (smart_meter_data) {
$("#use_meter_kwh_hh_bound").show();
}
- if (feeds["import_kwh"] != undefined) {
- cumulative_import_data = true;
- }
- var import_kwh = [];
- var use_kwh = [];
- var solar_kwh = [];
- var battery_charge_kwh = [];
- var battery_discharge_kwh = [];
- var meter_kwh_hh = [];
- if (cumulative_import_data) {
- import_kwh = feed.getdata(feeds["import_kwh"].id, view.start, view.end, interval);
- }
+ if (time_window_changed) {
+ // Load energy flow feeds (cumulative kWh, delta=1 returns half-hourly differences directly)
+ solar_to_load_kwh_data = [];
+ solar_to_grid_kwh_data = [];
+ solar_to_battery_kwh_data = [];
+ battery_to_load_kwh_data = [];
+ battery_to_grid_kwh_data = [];
+ grid_to_load_kwh_data = [];
+ grid_to_battery_kwh_data = [];
+ meter_kwh_hh = [];
- if (solarpv_mode || battery_mode) {
- use_kwh = feed.getdata(feeds["use_kwh"].id, view.start, view.end, interval);
- }
- if (solarpv_mode) {
- solar_kwh = feed.getdata(feeds["solar_kwh"].id, view.start, view.end, interval);
- }
+ if (feeds["solar_to_load_kwh"]!=undefined) {
+ solar_to_load_kwh_data = feed.getdata(feeds["solar_to_load_kwh"].id, view.start, view.end, interval, 0, 1);
+ }
+ if (feeds["solar_to_grid_kwh"]!=undefined) {
+ solar_to_grid_kwh_data = feed.getdata(feeds["solar_to_grid_kwh"].id, view.start, view.end, interval, 0, 1);
+ }
+ if (feeds["solar_to_battery_kwh"]!=undefined) {
+ solar_to_battery_kwh_data = feed.getdata(feeds["solar_to_battery_kwh"].id, view.start, view.end, interval, 0, 1);
+ }
+ if (feeds["battery_to_load_kwh"]!=undefined) {
+ battery_to_load_kwh_data = feed.getdata(feeds["battery_to_load_kwh"].id, view.start, view.end, interval, 0, 1);
+ }
+ if (feeds["battery_to_grid_kwh"]!=undefined) {
+ battery_to_grid_kwh_data = feed.getdata(feeds["battery_to_grid_kwh"].id, view.start, view.end, interval, 0, 1);
+ }
+ if (feeds["grid_to_load_kwh"]!=undefined) {
+ grid_to_load_kwh_data = feed.getdata(feeds["grid_to_load_kwh"].id, view.start, view.end, interval, 0, 1);
+ }
+ if (feeds["grid_to_battery_kwh"]!=undefined) {
+ grid_to_battery_kwh_data = feed.getdata(feeds["grid_to_battery_kwh"].id, view.start, view.end, interval, 0, 1);
+ }
- if (battery_mode) {
- battery_charge_kwh = feed.getdata(feeds["battery_charge_kwh"].id, view.start, view.end, interval);
- battery_discharge_kwh = feed.getdata(feeds["battery_discharge_kwh"].id, view.start, view.end, interval);
+ if (smart_meter_data) meter_kwh_hh = feed.getdata(feeds["meter_kwh_hh"].id, view.start, view.end, interval);
}
- if (smart_meter_data) meter_kwh_hh = feed.getdata(feeds["meter_kwh_hh"].id, view.start, view.end, interval);
-
- // Add last half hour of current day to cumulative data if missing
+ // Detect current half-hour index for live stats (use grid_to_load feed or meter as reference)
this_halfhour_index = -1;
- var this_halfhour = Math.floor((new Date()).getTime() / 1800000) * 1800000
- for (var z = 1; z < import_kwh.length; z++) {
- if (import_kwh[z][0] == this_halfhour) {
- import_kwh[z + 1] = [this_halfhour + 1800000, feeds["import_kwh"].value]
- this_halfhour_index = z
-
- if (solarpv_mode || battery_mode) {
- use_kwh[z + 1] = [this_halfhour + 1800000, feeds["use_kwh"].value]
- }
-
- if (solarpv_mode) {
- solar_kwh[z + 1] = [this_halfhour + 1800000, feeds["solar_kwh"].value]
- }
-
- if (battery_mode) {
- battery_charge_kwh[z + 1] = [this_halfhour + 1800000, feeds["battery_charge_kwh"].value]
- battery_discharge_kwh[z + 1] = [this_halfhour + 1800000, feeds["battery_discharge_kwh"].value]
- }
+ var ref_data = grid_to_load_kwh_data;
+ var this_halfhour = Math.floor((new Date()).getTime() / 1800000) * 1800000;
+ for (var z = 0; z < ref_data.length; z++) {
+ if (ref_data[z][0] == this_halfhour) {
+ this_halfhour_index = z;
break;
}
}
data = {};
- data["tariff_A"] = []
- data["tariff_B"] = []
+ data["tariff"] = []
data["outgoing"] = []
data["carbonintensity"] = []
// Tariff A
- if (config.app.region != undefined && octopus_feed_list[config.app.tariff_A.value] != undefined && octopus_feed_list[config.app.tariff_A.value][config.app.region.value] != undefined) {
- data["tariff_A"] = getdataremote(octopus_feed_list[config.app.tariff_A.value][config.app.region.value], view.start, view.end, interval);
- }
-
- // Tariff B
- if (config.app.region != undefined && octopus_feed_list[config.app.tariff_B.value] != undefined && octopus_feed_list[config.app.tariff_B.value][config.app.region.value] != undefined) {
- data["tariff_B"] = getdataremote(octopus_feed_list[config.app.tariff_B.value][config.app.region.value], view.start, view.end, interval);
+ if (config.app.region != undefined && octopus_feed_list[config.app.tariff.value] != undefined && octopus_feed_list[config.app.tariff.value][config.app.region.value] != undefined) {
+ data["tariff"] = getdataremote(octopus_feed_list[config.app.tariff.value][config.app.region.value], view.start, view.end, interval);
}
- // Outgoing
- if (config.app.region != undefined && (solarpv_mode || battery_mode)) {
+ // Outgoing (export tariff) - only needed in flow mode
+ if (config.app.region != undefined) {
data["outgoing"] = getdataremote(regions_outgoing[config.app.region.value], view.start, view.end, interval);
- // Invert export tariff
+ // Invert export tariff so it reads as a positive earning rate
for (var z in data["outgoing"]) data["outgoing"][z][1] *= -1;
}
@@ -438,16 +570,14 @@ function graph_load() {
data["carbonintensity"] = getdataremote(428391, view.start, view.end, interval);
}
- data["use"] = [];
- data["import"] = [];
- data["import_cost_tariff_A"] = [];
- data["import_cost_tariff_B"] = [];
- data["export"] = [];
- data["export_cost"] = [];
- data["solar_direct"] = [];
+ data["solar_to_load"] = [];
+ data["solar_to_grid"] = [];
data["solar_to_battery"] = [];
- data["solar_used"] = []
- data["solar_used_cost"] = [];
+ data["battery_to_load"] = [];
+ data["battery_to_grid"] = [];
+ data["grid_to_load"] = [];
+ data["grid_to_battery"] = [];
+
data["meter_kwh_hh"] = meter_kwh_hh;
data["meter_kwh_hh_cost"] = [];
@@ -465,342 +595,385 @@ function graph_load() {
profile_cost[hh] = [profile_time, 0.0]
}
- var total = {
- import_kwh: 0,
- export_kwh: 0,
-
- import_tariff_A: { kwh: 0, cost: 0 },
- import_tariff_B: { kwh: 0, cost: 0 },
- export_tariff: { kwh: 0, cost: 0 },
- solar_used: { kwh: 0, cost: 0 },
+ var total_template = {
+
+ // Per-flow kWh totals
+ solar_to_load_kwh: 0,
+ solar_to_grid_kwh: 0,
+ solar_to_battery_kwh: 0,
+ battery_to_load_kwh: 0,
+ battery_to_grid_kwh: 0,
+ grid_to_load_kwh: 0,
+ grid_to_battery_kwh: 0,
+
+ // Per-flow value at tariff A (avoided cost / earned)
+ tariff: {
+ solar_to_load_value: 0,
+ solar_to_grid_value: 0,
+ solar_to_battery_value: 0,
+ battery_to_load_value: 0,
+ battery_to_grid_value: 0,
+ grid_to_load_cost: 0,
+ grid_to_battery_cost: 0,
+ },
co2: 0
}
- var monthly_data = {};
+ // assign global
+ total = JSON.parse(JSON.stringify(total_template)); // deep copy
+ monthly_data = {};
- // Convert cumulative import data to half hourly kwh
- // Core import data conversion
- var import_kwh_hh = convert_cumulative_kwh_to_kwh_hh(import_kwh, true);
-
- // Solar PV mode data conversions
- var use_kwh_hh = convert_cumulative_kwh_to_kwh_hh(use_kwh, true);
- var solar_kwh_hh = convert_cumulative_kwh_to_kwh_hh(solar_kwh, true);
- var battery_charge_kwh_hh = convert_cumulative_kwh_to_kwh_hh(battery_charge_kwh, true);
- var battery_discharge_kwh_hh = convert_cumulative_kwh_to_kwh_hh(battery_discharge_kwh, true);
-
- var data_length = 0;
- if (cumulative_import_data) data_length = import_kwh_hh.length;
- else if (smart_meter_data) data_length = meter_kwh_hh.length;
+ // Determine data length and primary time reference
+ var data_length = grid_to_load_kwh_data.length;
for (var z = 0; z < data_length; z++) {
- let time = 0;
- if (cumulative_import_data) time = import_kwh[z][0];
- else if (smart_meter_data) time = meter_kwh_hh[z][0];
+ let time = grid_to_load_kwh_data[z][0];
d.setTime(time)
let hh = d.getHours() * 2 + d.getMinutes() / 30
// get start of month timestamp to calculate monthly data
let startOfMonth = new Date(d.getFullYear(), d.getMonth(), 1).getTime();
-
- let kwh_import = 0;
- let kwh_export = 0;
- let kwh_use = 0;
- let kwh_solar = 0;
- let kwh_battery_charge = 0;
- let kwh_battery_discharge = 0;
- let balance = 0;
-
- // Use cumulative import data if available by default
- if (cumulative_import_data) {
- kwh_import = import_kwh_hh[z][1];
-
- // If we dont have cumulative import data, but we have smart meter data, use that instead
- } else if (smart_meter_data) {
- kwh_import = meter_kwh_hh[z][1];
- }
-
- // If solar or battery mode fetch kwh_use
- if (solarpv_mode || battery_mode) {
- kwh_use = use_kwh_hh[z][1];
- balance += kwh_use;
- }
-
- // If solar mode, fetch kwh_solar
- if (solarpv_mode) {
- kwh_solar = solar_kwh_hh[z][1];
- balance -= kwh_solar;
- }
-
- // if battery mode, fetch battery charge and discharge
- if (battery_mode) {
- kwh_battery_charge = battery_charge_kwh_hh[z][1];
- kwh_battery_discharge = battery_discharge_kwh_hh[z][1];
- balance += kwh_battery_charge;
- balance -= kwh_battery_discharge;
- }
- // If solar mode and no battery, calculate import from use and solar
- if (solarpv_mode || battery_mode) {
- if (balance >= 0) {
- kwh_import = balance;
- kwh_export = 0;
- } else {
- kwh_import = 0;
- kwh_export = balance * -1;
- }
- }
+ let kwh_import = 0;
- // Alternatively use meter data in place of cumulative import data if user selected
+ let kwh_solar_to_load = 0;
+ let kwh_solar_to_grid = 0;
+ let kwh_solar_to_battery = 0;
+ let kwh_battery_to_load = 0;
+ let kwh_battery_to_grid = 0;
+ let kwh_grid_to_load = 0;
+ let kwh_grid_to_battery = 0;
+
+ // Read half-hourly energy flow values directly from post-processed feeds (delta=1)
+ // Clamp negatives to zero for safety
+ kwh_solar_to_load = Math.max(0, get_value_at_index(solar_to_load_kwh_data, z, 0));
+ kwh_solar_to_grid = Math.max(0, get_value_at_index(solar_to_grid_kwh_data, z, 0));
+ kwh_solar_to_battery = Math.max(0, get_value_at_index(solar_to_battery_kwh_data, z, 0));
+ kwh_battery_to_load = Math.max(0, get_value_at_index(battery_to_load_kwh_data, z, 0));
+ kwh_battery_to_grid = Math.max(0, get_value_at_index(battery_to_grid_kwh_data, z, 0));
+ kwh_grid_to_load = Math.max(0, get_value_at_index(grid_to_load_kwh_data, z, 0));
+ kwh_grid_to_battery = Math.max(0, get_value_at_index(grid_to_battery_kwh_data, z, 0));
+
+ // Derive aggregate values from flows
+ kwh_import = kwh_grid_to_load + kwh_grid_to_battery;
+ kwh_export = kwh_solar_to_grid + kwh_battery_to_grid;
+ kwh_use = kwh_solar_to_load + kwh_battery_to_load + kwh_grid_to_load;
+
+ // Alternatively use meter data in place of flow import data if user selected
if (smart_meter_data && use_meter_kwh_hh) {
- kwh_import = meter_kwh_hh[z][1];
+ kwh_import = meter_kwh_hh[z][1] != null ? meter_kwh_hh[z][1] : 0;
+ kwh_grid_to_load = kwh_import;
}
- data["import"].push([time, kwh_import]);
- total.import_kwh += kwh_import;
-
// Unit and import cost on tariff A
- let unitcost_tariff_A = null;
- let hh_cost_tariff_A = null;
- if (data.tariff_A[z][1] != null) {
- unitcost_tariff_A = data.tariff_A[z][1] * 0.01;
- hh_cost_tariff_A = kwh_import * unitcost_tariff_A;
-
- total.import_tariff_A.kwh += kwh_import
- total.import_tariff_A.cost += hh_cost_tariff_A
+ let unitcost_tariff = null;
+ if (data.tariff[z] != undefined && data.tariff[z][1] != null) {
+ unitcost_tariff = data.tariff[z][1] * 0.01;
// Generate profile
profile_kwh[hh][1] += kwh_import
- profile_cost[hh][1] += hh_cost_tariff_A
- }
-
- // Unit and import cost on tariff B
- let unitcost_tariff_B = null;
- let hh_cost_tariff_B = null;
- if (data.tariff_B[z][1] != null) {
- unitcost_tariff_B = data.tariff_B[z][1] * 0.01;
- hh_cost_tariff_B = kwh_import * unitcost_tariff_B;
-
- total.import_tariff_B.kwh += kwh_import
- total.import_tariff_B.cost += hh_cost_tariff_B
- }
-
- data["import_cost_tariff_A"].push([time, hh_cost_tariff_A]);
- data["import_cost_tariff_B"].push([time, hh_cost_tariff_B]);
-
- // Calculate monthly data
- if (monthly_data[startOfMonth] == undefined) {
- monthly_data[startOfMonth] = {
- "import": 0,
- "import_tariff_A": 0,
- "import_tariff_B": 0,
- "cost_import_tariff_A": 0,
- "cost_import_tariff_B": 0
- }
- }
-
- monthly_data[startOfMonth]["import"] += kwh_import
-
- if (hh_cost_tariff_A != null) {
- monthly_data[startOfMonth]["import_tariff_A"] += kwh_import
- monthly_data[startOfMonth]["cost_import_tariff_A"] += hh_cost_tariff_A
- }
-
- if (hh_cost_tariff_B != null) {
- monthly_data[startOfMonth]["import_tariff_B"] += kwh_import
- monthly_data[startOfMonth]["cost_import_tariff_B"] += hh_cost_tariff_B
+ profile_cost[hh][1] += kwh_import * unitcost_tariff;
}
// Carbon Intensity
if (show_carbonintensity) {
- let co2intensity = data.carbonintensity[z][1];
- let co2_hh = kwh_import * (co2intensity * 0.001)
- total.co2 += co2_hh
+ let co2intensity = data.carbonintensity[z] != undefined ? data.carbonintensity[z][1] : null;
+ if (co2intensity != null) {
+ let co2_hh = kwh_import * (co2intensity * 0.001)
+ total.co2 += co2_hh
+ }
}
- // We may explort in battery mode
- if (solarpv_mode || battery_mode) {
- data["use"].push([time, kwh_use]);
- data["export"].push([time, kwh_export * -1]);
- total.export_tariff.kwh += kwh_export
- let cost_export = data.outgoing[z][1] * 0.01 * -1;
- data["export_cost"].push([time, kwh_export * cost_export * -1]);
- total.export_tariff.cost += kwh_export * cost_export
+ // All 7 disaggregated flow data arrays
+ data["solar_to_load"].push([time, kwh_solar_to_load]);
+ data["solar_to_grid"].push([time, kwh_solar_to_grid]);
+ data["solar_to_battery"].push([time, kwh_solar_to_battery]);
+ data["battery_to_load"].push([time, kwh_battery_to_load]);
+ data["battery_to_grid"].push([time, kwh_battery_to_grid]);
+ data["grid_to_load"].push([time, kwh_grid_to_load]);
+ data["grid_to_battery"].push([time, kwh_grid_to_battery]);
+
+ let outgoing_unit = (data.outgoing[z] != undefined && data.outgoing[z][1] != null)
+ ? data.outgoing[z][1] * 0.01 * -1 // already inverted, so this is positive p/kWh
+ : null;
+
+ var flows = {
+ solar_to_load: kwh_solar_to_load,
+ solar_to_grid: kwh_solar_to_grid,
+ solar_to_battery: kwh_solar_to_battery,
+ battery_to_load: kwh_battery_to_load,
+ battery_to_grid: kwh_battery_to_grid,
+ grid_to_load: kwh_grid_to_load,
+ grid_to_battery: kwh_grid_to_battery
+ };
+
+ accumulate_flows(total, flows, outgoing_unit, unitcost_tariff);
+
+ // Accumulate monthly data
+ if (monthly_data[startOfMonth] == undefined) {
+ monthly_data[startOfMonth] = JSON.parse(JSON.stringify(total_template)); // deep copy
}
+ var m = monthly_data[startOfMonth];
+ accumulate_flows(m, flows, outgoing_unit, unitcost_tariff);
+ }
- // Solar used calculation
- if (solarpv_mode) {
+ // if (smart_meter_data && !flow_mode) {
+ // calibration_line_of_best_fit(data["import"], meter_kwh_hh);
+ // }
- let solar_direct = Math.min(kwh_solar, kwh_use);
- data["solar_direct"].push([time, solar_direct]);
- // Any additional solar used to charge battery
- let solar_to_battery = Math.min(Math.max(0, kwh_solar - solar_direct), kwh_battery_charge);
- data["solar_to_battery"].push([time, solar_to_battery]);
+ // Create time to index map using grid_to_load feed as reference (should be present in all modes)
+ time_to_index_map = {};
+ for (var z = 0; z < grid_to_load_kwh_data.length; z++) {
+ time_to_index_map[grid_to_load_kwh_data[z][0]] = z;
+ }
- let kwh_solar_used = kwh_solar - kwh_export;
- data["solar_used"].push([time, kwh_solar_used]);
- total.solar_used.kwh += kwh_solar_used
- data["solar_used_cost"].push([time, kwh_solar_used * unitcost_tariff_A]);
- total.solar_used.cost += kwh_solar_used * unitcost_tariff_A
- }
+ // Clear baseline summary if time window changed (as this may affect the selected baseline period)
+ if (time_window_changed) {
+ baseline_monthly_summary = {};
}
-
- // --------------------------------------------------------------------------------------
+ draw_tables();
+}
- if (smart_meter_data) {
- calibration_line_of_best_fit(import_kwh_hh, meter_kwh_hh);
+function get_value_at_index(data_array, index, default_value = null) {
+ if (data_array[index] != undefined && data_array[index][1] != null) {
+ return data_array[index][1];
}
+ return default_value;
+}
- draw_tables(total, monthly_data);
+function accumulate_flows(bucket, flows, outgoing_unit, unitcost_tariff) {
+ bucket.solar_to_load_kwh += flows.solar_to_load;
+ bucket.solar_to_grid_kwh += flows.solar_to_grid;
+ bucket.solar_to_battery_kwh += flows.solar_to_battery;
+ bucket.battery_to_load_kwh += flows.battery_to_load;
+ bucket.battery_to_grid_kwh += flows.battery_to_grid;
+ bucket.grid_to_load_kwh += flows.grid_to_load;
+ bucket.grid_to_battery_kwh += flows.grid_to_battery;
+
+ if (outgoing_unit != null) {
+ bucket.tariff.solar_to_grid_value += flows.solar_to_grid * outgoing_unit;
+ bucket.tariff.battery_to_grid_value += flows.battery_to_grid * outgoing_unit;
+ }
+ if (unitcost_tariff != null) {
+ bucket.tariff.solar_to_load_value += flows.solar_to_load * unitcost_tariff;
+ bucket.tariff.solar_to_battery_value += flows.solar_to_battery * unitcost_tariff;
+ bucket.tariff.battery_to_load_value += flows.battery_to_load * unitcost_tariff;
+ bucket.tariff.grid_to_load_cost += flows.grid_to_load * unitcost_tariff;
+ bucket.tariff.grid_to_battery_cost += flows.grid_to_battery * unitcost_tariff;
+ }
}
-function draw_tables(total, monthly_data) {
+function draw_tables() {
- var unit_cost_import_tariff_A = (total.import_tariff_A.cost / total.import_tariff_A.kwh);
- var unit_cost_import_tariff_B = (total.import_tariff_B.cost / total.import_tariff_B.kwh);
+ // Populate standalone tariff selectors (built once, then just set value)
+ ["tariff"].forEach(function(id) {
+ var sel = $("#" + id);
+ if (sel.find("option").length === 0) {
+ for (var key in tariff_options) {
+ sel.append("");
+ }
+ }
+ });
+ $("#tariff").val(config.app.tariff.value);
var out = "";
- out += "
";
+
+ // unit price value_gbp / kwh, only if kwh > 0 and value_gbp is not null
+ if (value_gbp !== null && kwh > 0) {
+ let unit_price = value_gbp / kwh;
+ r += "
" + (unit_price * 100).toFixed(1) + " p/kWh
";
+ } else {
+ r += "
—
";
+ }
- if (solarpv_mode) {
- var unit_cost_solar_used = (total.solar_used.cost / total.solar_used.kwh);
- out += "
";
+ // Baseline comparison if data exists
+ if (baseline_monthly_summary[month] != undefined) {
+ // index will match as monthly_summary is built in chronological order
+ var baseline_net_cost = baseline_monthly_summary[month].net_cost_tariff;
+ var baseline_unit_rate = baseline_monthly_summary[month].unit_rate_tariff;
+
+ // Baseline: cost + rate merged
+ if (!isNaN(baseline_unit_rate)) {
+ monthly_out += "
The My Electric app is a simple home energy monitoring app for exploring home or building electricity consumption over time.
-
Auto configure: This app can auto-configure connecting to emoncms feeds with the names shown on the right, alternatively feeds can be selected by clicking on the edit button.
-
Cumulative kWh feeds can be generated from power feeds with the power_to_kwh input processor.
-
-
-
-
-
-
-
+
+
The My Electric app is a simple home energy monitoring app for exploring home or building electricity consumption over time.
+
+
@@ -222,11 +207,10 @@
"unitcost_day":{"type":"value", "default":0.15, "name": "Day time unit cost", "description":"Day time unit cost of electricity £/kWh"},
"unitcost_night":{"type":"value", "default":0.07, "name": "Night time unit cost", "description":"Night time unit cost of electricity £/kWh"},
- "currency":{"type":"value", "default":"£", "name": "Currency", "description":"Currency symbol (£,$..)"},
-
- "public":{"type":"checkbox", "name": "Public", "default": 0, "optional":true, "description":"Make app public"}
+ "currency":{"type":"value", "default":"£", "name": "Currency", "description":"Currency symbol (£,$..)"}
};
+config.app_name = "Time of Use";
config.id = ;
config.name = "";
config.public = ;
diff --git a/apps/OpenEnergyMonitor/timeofuse2/timeofuse2.php b/apps/OpenEnergyMonitor/timeofuse2/timeofuse2.php
index 4f3336a7..29b7f707 100644
--- a/apps/OpenEnergyMonitor/timeofuse2/timeofuse2.php
+++ b/apps/OpenEnergyMonitor/timeofuse2/timeofuse2.php
@@ -2,11 +2,10 @@
defined('EMONCMS_EXEC') or die('Restricted access');
global $path, $session, $v;
?>
-
-
+
@@ -160,67 +159,59 @@
+
+
The "Time of Use - flexible" app is a simple home energy monitoring app for exploring home or building electricity consumption and cost over time. It allows you to track multiple electricity tariffs as used in Australia.
+
Cumulative kWh
+
feeds can be generated from power feeds with the power_to_kwh input processor.
+
+
As the number of configuration options for this are quite large, a shorthand has been used to specify
+the tiers, days and times they apply and the respective costs.
+
+
Assumptions
+
+
Any number of tariffs can be defined, but they must be consistent across weekdays or weekends.
+
One cost must be defined per tariff tier.
+
Each weekday (Monday to Friday) has the same tiers and times for each tier.
+
Each weekend day (Saturday and Sunday) has the same tiers and times for each tier.
+
Public Holidays are treated the same as a weekend day.
+
+
+
Shorthand
+
Tier names and tariffs are specified as a comma separated, colon separated list. If there are three
+tariffs, Off Peak, Shoulder and Peak, costing 16.5c/kWh, 25.3c/kWh and 59.4c/kWh respectively, they
+are specified as:
+
OffPeak:0.165,Shoulder:0.253,Peak:0.594
+
Tier start times are split into two definitions, weekday and weekend. They both use the same format,
+<start hour>:<tier>,<start hour>:<tier>,...
+<tier>
+is the tier number defined above, numbered from 0
To specify the public holidays that should be treated the same as weekends, specify a comma separated
+list of days of the year (from 1-365/366) per year.
+
+
+
Example:
+
for public holiays 2017: Jan 2, Apr 14, Apr 17, Apr 25, Jun 12, Oct 2, Dec 25, Dec 26; and 2018: Jan 1 you would specify:
The "Time of Use - flexible" app is a simple home energy monitoring app for exploring home or building electricity consumption and cost over time. It allows you to track multiple electricity tariffs as used in Australia.
-
Cumulative kWh
-
feeds can be generated from power feeds with the power_to_kwh input processor.
-
-
As the number of configuration options for this are quite large, a shorthand has been used to specify
- the tiers, days and times they apply and the respective costs.
-
-
Assumptions
-
-
Any number of tariffs can be defined, but they must be consistent across weekdays or weekends.
-
One cost must be defined per tariff tier.
-
Each weekday (Monday to Friday) has the same tiers and times for each tier.
-
Each weekend day (Saturday and Sunday) has the same tiers and times for each tier.
-
Public Holidays are treated the same as a weekend day.
-
-
-
Shorthand
-
Tier names and tariffs are specified as a comma separated, colon separated list. If there are three
- tariffs, Off Peak, Shoulder and Peak, costing 16.5c/kWh, 25.3c/kWh and 59.4c/kWh respectively, they
- are specified as:
-
OffPeak:0.165,Shoulder:0.253,Peak:0.594
-
Tier start times are split into two definitions, weekday and weekend. They both use the same format,
- <start hour>:<tier>,<start hour>:<tier>,...
- <tier>
- is the tier number defined above, numbered from 0
To specify the public holidays that should be treated the same as weekends, specify a comma separated
- list of days of the year (from 1-365/366) per year.
-
-
-
Example:
-
for public holiays 2017: Jan 2, Apr 14, Apr 17, Apr 25, Jun 12, Oct 2, Dec 25, Dec 26; and 2018: Jan 1 you would specify:
-
@@ -263,11 +254,10 @@
"description":"List of weekend tier start times. See description on the left for details"},
"ph_days":{"type":"value", "default":"2017:2,104,107,115,163,275,359,360;2018:1",
"name":"Public Holiday days",
- "description":"List of public holidays. See description on the left for details"},
-
- "public":{"type":"checkbox", "name": "Public", "default": 0, "optional":true, "description":"Make app public"}
+ "description":"List of public holidays. See description on the left for details"}
};
+config.app_name = "Time of Use - flexible";
config.id = ;
config.name = "";
config.public = ;
diff --git a/apps/OpenEnergyMonitor/timeofusecl/timeofusecl.php b/apps/OpenEnergyMonitor/timeofusecl/timeofusecl.php
index f6302eb6..d2846de4 100644
--- a/apps/OpenEnergyMonitor/timeofusecl/timeofusecl.php
+++ b/apps/OpenEnergyMonitor/timeofusecl/timeofusecl.php
@@ -2,11 +2,9 @@
defined('EMONCMS_EXEC') or die('Restricted access');
global $path, $session, $v;
?>
-
-
@@ -159,48 +157,38 @@
-
-
-
-
-
-
-
-
-
The "Time of Use - flexible + CL" app is a simple home energy monitoring app for exploring home or building electricity consumption and cost over time.
-
It allows you to track multiple electricity tariffs as used in Australia. This version adds a daily supply charge and a separately monitored controlled load (such as off-peak hot water).
-
Cumulative kWh feeds can be generated from power feeds with the power_to_kwh input processor.
-
-
As the number of configuration options for this are quite large, a shorthand has been used to specify
- the tiers, days and times they apply and the respective costs.
-
Assumptions:
-
-
Any number of tariffs can be defined, but they must be consistent across weekdays or weekends.
-
One cost must be defined per tariff tier.
-
Each weekday (Monday to Friday) has the same tiers and times for each tier.
-
Each weekend day (Saturday and Sunday) has the same tiers and times for each tier.
-
Public Holidays are treated the same as a weekend day.
-
-
Shorthand
-
Tier names and tariffs are specified as a comma separated, colon separated list. If there are three
- tariffs, Off Peak, Shoulder and Peak, costing 16.5c/kWh, 25.3c/kWh and 59.4c/kWh respectively, they
- are specified as OffPeak:0.165,Shoulder:0.253,Peak:0.594
-
Tier start times are split into two definitions, weekday and weekend. They both use the same format,
- <start hour>:<tier>,<start hour>:<tier>,...
- <tier> is the tier number defined above, numbered from 0
- Example: A weekday with the following tariff times: OffPeak: 00:00 - 06:59, Shoulder: 07:00
- - 13:59, Peak: 14:00 - 19:59, Shoulder: 20:00 - 21:59, OffPeak: 22:00 - 23:59 would be defined as
- 0:0,7:1,14:2,20:1,22:0
-
To specify the public holidays that should be treated the same as weekends, specify a comma separated
- list of days of the year (from 1-365/366) per year. Example: for public holiays 2017: Jan 2, Apr 14,
- Apr 17, Apr 25, Jun 12, Oct 2, Dec 25, Dec 26; and 2018: Jan 1 you would specify
- 2017:2,104,107,115,163,275,359,360;2018:1
- https://www.epochconverter.com/days provides an easy reference.
-
-
-
-
-
+
+
The "Time of Use - flexible + CL" app is a simple home energy monitoring app for exploring home or building electricity consumption and cost over time.
+
It allows you to track multiple electricity tariffs as used in Australia. This version adds a daily supply charge and a separately monitored controlled load (such as off-peak hot water).
+
Cumulative kWh feeds can be generated from power feeds with the power_to_kwh input processor.
+
+
As the number of configuration options for this are quite large, a shorthand has been used to specify
+the tiers, days and times they apply and the respective costs.
+
Assumptions:
+
+
Any number of tariffs can be defined, but they must be consistent across weekdays or weekends.
+
One cost must be defined per tariff tier.
+
Each weekday (Monday to Friday) has the same tiers and times for each tier.
+
Each weekend day (Saturday and Sunday) has the same tiers and times for each tier.
+
Public Holidays are treated the same as a weekend day.
+
+
Shorthand
+
Tier names and tariffs are specified as a comma separated, colon separated list. If there are three
+tariffs, Off Peak, Shoulder and Peak, costing 16.5c/kWh, 25.3c/kWh and 59.4c/kWh respectively, they
+are specified as OffPeak:0.165,Shoulder:0.253,Peak:0.594
+
Tier start times are split into two definitions, weekday and weekend. They both use the same format,
+<start hour>:<tier>,<start hour>:<tier>,...
+<tier> is the tier number defined above, numbered from 0
+Example: A weekday with the following tariff times: OffPeak: 00:00 - 06:59, Shoulder: 07:00
+- 13:59, Peak: 14:00 - 19:59, Shoulder: 20:00 - 21:59, OffPeak: 22:00 - 23:59 would be defined as
+0:0,7:1,14:2,20:1,22:0
+
To specify the public holidays that should be treated the same as weekends, specify a comma separated
+list of days of the year (from 1-365/366) per year. Example: for public holiays 2017: Jan 2, Apr 14,
+Apr 17, Apr 25, Jun 12, Oct 2, Dec 25, Dec 26; and 2018: Jan 1 you would specify
+2017:2,104,107,115,163,275,359,360;2018:1
+https://www.epochconverter.com/days provides an easy reference.