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 += ""; - for (f in feedsbygroup[group]) { - out += ""; - } - 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. +

+ + +
+ +
+ + +
+
+ + + + + + + + + + + + + + + + +
Feed nameNodeStatus
{{ feed.name }}{{ feed.node }} + ✓ exists + ✗ missing +
+ +
+ + + + {{ autogen_status }} +
+
+ + + + +
+
+ + +
+ App name (menu) +
+ +
+ +
+ Public +
Make app public + +
+ +
+ +
+ + + + + + + + +
+ +
+
+ + +
+ +
+
+
+
+
+ + 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 += '
'; - out += '
'+available_apps[z].title+'
'; - out += '
'+available_apps[z].description+'
'; - out += '
'; -} + +var app_list = new Vue({ + el: '#available-apps', + data: { + apps: available_apps + } +}); + + $(function() { - $("#available-apps").html(out); - $(".app-item").first().css("border-top","1px solid #ccc"); + $(".app-group").each(function() { $(this).find(".app-item").first().css("border-top","1px solid #ccc"); }); $(".app-item").click(function(){ if (app_new_enable) { diff --git a/Views/css/dark.css b/Views/css/dark.css index daa6716f..f3d1f937 100644 --- a/Views/css/dark.css +++ b/Views/css/dark.css @@ -1,34 +1,3 @@ -/* ------------------------------------------------------------------- - Config ---------------------------------------------------------------------*/ -.app-config-box { - border: 1px solid #fff; - 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"); -} - -.app-config .feed-auto { - color: #ccc; -} - /* ------------------------------------------------------------------- Applications --------------------------------------------------------------------*/ diff --git a/Views/css/light.css b/Views/css/light.css index d0a5f904..52678ba0 100644 --- a/Views/css/light.css +++ b/Views/css/light.css @@ -1,41 +1,3 @@ -/* ------------------------------------------------------------------- - 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; -} - /* ------------------------------------------------------------------- Application --------------------------------------------------------------------*/ diff --git a/app_controller.php b/app_controller.php index b6997fe3..96ea02bb 100644 --- a/app_controller.php +++ b/app_controller.php @@ -16,7 +16,7 @@ function app_controller() { global $mysqli,$redis,$path,$session,$route,$user,$settings,$v; // Force cache reload of css and javascript - $v = 39; + $v = 43; $result = false; @@ -42,8 +42,12 @@ function app_controller() // -------------------------------------------------------------- if ($route->action == "list" && $session['read']) { - $route->format = "json"; - return $appconfig->get_list($session['userid']); + if ($route->format == "html") { + return view("Modules/app/Views/app_list.php"); + } else { + $route->format = "json"; + return $appconfig->get_list($session['userid']); + } } // List of available apps json diff --git a/app_menu.php b/app_menu.php index 2436107b..0afcffe4 100644 --- a/app_menu.php +++ b/app_menu.php @@ -72,6 +72,13 @@ "icon"=>"plus", "order"=>$_i ); + + $l2['list'] = array( + "name"=>tr('List'), + "href"=>"app/list", + "icon"=>"format_list_bulleted", + "order"=>$_i+1 + ); } } diff --git a/app_model.php b/app_model.php index 676ee815..b468775e 100644 --- a/app_model.php +++ b/app_model.php @@ -107,7 +107,7 @@ public function get_list($userid) } $apps = array(); - $result = $this->mysqli->query("SELECT `id`, `app`, `name`, `public` FROM app WHERE `userid`='$userid'"); + $result = $this->mysqli->query("SELECT `id`, `app`, `name`, `public` FROM app WHERE `userid`='$userid' ORDER BY `id` ASC"); while ($row = $result->fetch_object()) { $apps[] = $row; } diff --git a/apps/OpenEnergyMonitor/co2monitor/co2monitor.php b/apps/OpenEnergyMonitor/co2monitor/co2monitor.php index d1eb32ce..72a7e20b 100644 --- a/apps/OpenEnergyMonitor/co2monitor/co2monitor.php +++ b/apps/OpenEnergyMonitor/co2monitor/co2monitor.php @@ -2,10 +2,7 @@ defined('EMONCMS_EXEC') or die('Restricted access'); global $path, $session, $v; ?> - - - @@ -165,21 +162,10 @@ - -
-
-
-
-
-

-

Calculate room air change rates form CO2 decay curves.

- -
-
-
-
-
-
+ +
@@ -188,6 +174,7 @@ var apikey = ""; var sessionwrite = ; + config.app_name = "CO2 Monitor"; config.id = ; config.name = ""; config.public = ; diff --git a/apps/OpenEnergyMonitor/costcomparison/costcomparison.php b/apps/OpenEnergyMonitor/costcomparison/costcomparison.php index 25bf0fad..4cf0bfdb 100644 --- a/apps/OpenEnergyMonitor/costcomparison/costcomparison.php +++ b/apps/OpenEnergyMonitor/costcomparison/costcomparison.php @@ -2,10 +2,8 @@ defined('EMONCMS_EXEC') or die('Restricted access'); global $path, $session, $v; ?> - - @@ -102,22 +100,11 @@
-
- -
-
-
-

-

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

-

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

-

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

-

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

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

-

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

-

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

-

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

-
-
-
-
-
+ + +
@@ -138,6 +127,7 @@ } }; +config.app_name = "Feed-in"; config.id = ; config.name = ""; config.public = ; diff --git a/apps/OpenEnergyMonitor/myboiler/myboiler.php b/apps/OpenEnergyMonitor/myboiler/myboiler.php index 5beb5c69..7ad145db 100644 --- a/apps/OpenEnergyMonitor/myboiler/myboiler.php +++ b/apps/OpenEnergyMonitor/myboiler/myboiler.php @@ -2,11 +2,9 @@ defined('EMONCMS_EXEC') or die('Restricted access'); global $path, $session, $v; ?> - - @@ -258,26 +256,12 @@ -
- -
-
-
-
-

-

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

-

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

-

Cumulative kWh feeds can be created from power feeds using the power_to_kwh input processor, which converts power data (measured in watts) into energy consumption data (measured in kWh).

-

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

-

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

- -
-
-
-
-
-
- + +
@@ -285,6 +269,8 @@ var apikey = ""; var session_write = ; + config.app_name = "My Boiler"; + config.app_name_color = "#fb5e50"; config.id = ; config.name = ""; config.public = ; diff --git a/apps/OpenEnergyMonitor/myelectric/myelectric.php b/apps/OpenEnergyMonitor/myelectric/myelectric.php index 5f05708f..084eefed 100644 --- a/apps/OpenEnergyMonitor/myelectric/myelectric.php +++ b/apps/OpenEnergyMonitor/myelectric/myelectric.php @@ -2,10 +2,7 @@ defined('EMONCMS_EXEC') or die('Restricted access'); global $path, $session, $v; ?> - - - @@ -110,24 +107,10 @@ - -
- -
-
-
-
-

-

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

-

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

-

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

- -
-
-
-
-
-
+ +
@@ -175,6 +158,7 @@ function getTranslations(){ "kw":{"type":"checkbox", "default":0, "name": "Show kW", "description":tr("Display power as kW")} }; +config.app_name = "My Electric"; config.id = ; config.name = ""; config.public = ; diff --git a/apps/OpenEnergyMonitor/myelectric2/myelectric2.php b/apps/OpenEnergyMonitor/myelectric2/myelectric2.php index 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.

- -
-
-
-
-
-
+ + + +
@@ -205,8 +192,9 @@ "use_kwh":{"type":"feed", "autoname":"use_kwh"}, "unitcost":{"type":"value", "default":0.1508, "name": "Unit cost", "description":"Unit cost of electricity £/kWh"}, "currency":{"type":"value", "default":"£", "name": "Currency", "description":"Currency symbol (£,$..)"}, - "showcomparison":{"type":"checkbox", "default":true, "name": "Show comparison", "description":"Energy stack comparison"} + "showcomparison":{"type":"checkbox", "default":false, "name": "Show comparison", "description":"Energy stack comparison"} }; +config.app_name = "My Electric v2"; config.id = ; config.name = ""; config.public = ; diff --git a/apps/OpenEnergyMonitor/myenergy/myenergy.php b/apps/OpenEnergyMonitor/myenergy/myenergy.php index c04cbe04..8abfb7db 100644 --- a/apps/OpenEnergyMonitor/myenergy/myenergy.php +++ b/apps/OpenEnergyMonitor/myenergy/myenergy.php @@ -2,10 +2,8 @@ defined('EMONCMS_EXEC') or die('Restricted access'); global $path, $session, $v; ?> - - @@ -93,22 +91,11 @@ -
- -
-
-
-

&

-

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

-

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

-

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

-

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

- -
-
-
-
-
+
+

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

+

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

+
+
@@ -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.

- - - - - -
-
-
-
-
-
- + +
- + diff --git a/apps/OpenEnergyMonitor/mysolarpv/mysolarpv.php b/apps/OpenEnergyMonitor/mysolarpv/mysolarpv.php index ce43e373..06a2cf50 100644 --- a/apps/OpenEnergyMonitor/mysolarpv/mysolarpv.php +++ b/apps/OpenEnergyMonitor/mysolarpv/mysolarpv.php @@ -2,7 +2,6 @@ defined('EMONCMS_EXEC') or die('Restricted access'); global $path, $session, $v; ?> - - @@ -57,30 +55,32 @@ - + - -
- -
-
-
-
-

-

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

-

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

-

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

-

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

- -
-
-
-
-
-
- + +
@@ -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 += ""; - out += ""; - out += "" + total.import_tariff_A.kwh.toFixed(1) + " kWh"; - out += "£" + (total.import_tariff_A.cost * 1.05).toFixed(2) + ""; - out += "" + (unit_cost_import_tariff_A * 100 * 1.05).toFixed(1) + "p/kWh (inc VAT)"; - out += ""; - - out += ""; - out += ""; - out += "" + total.import_tariff_B.kwh.toFixed(1) + " kWh"; - out += "£" + (total.import_tariff_B.cost * 1.05).toFixed(2) + ""; - out += "" + (unit_cost_import_tariff_B * 100 * 1.05).toFixed(1) + "p/kWh (inc VAT)"; - out += ""; if (show_carbonintensity) { - var window_co2_intensity = 1000 * total.co2 / total.import_kwh; + var total_import_kwh = total.grid_to_load_kwh + total.grid_to_battery_kwh; + var window_co2_intensity = total_import_kwh > 0 ? 1000 * total.co2 / total_import_kwh : 0; $("#carbonintensity_result").html("Total CO2: " + (total.co2).toFixed(1) + "kgCO2, Consumption intensity: " + window_co2_intensity.toFixed(0) + " gCO2/kWh") } - if (solarpv_mode || battery_mode) { - var unit_cost_export = (total.export_tariff.cost / total.export_tariff.kwh); - out += ""; - out += "Export"; - out += "" + total.export_tariff.kwh.toFixed(1) + " kWh"; - out += "£" + total.export_tariff.cost.toFixed(2) + ""; - out += "" + (unit_cost_export * 100 * 1.05).toFixed(1) + "p/kWh (inc VAT)"; - out += ""; - } + // Helper: one table row per energy flow + function flow_row(label, kwh, value_gbp, value_label, color, rowStyle) { + if (kwh === 0) return ""; // skip zero rows for clarity + + var value_color; + if (value_label.indexOf("avoided") !== -1) { + value_color = "#888"; + } else if (value_label.indexOf("earned") !== -1) { + value_color = "#4a9e4a"; + } else { + value_color = "#c0392b"; + } + + var r = ""; + r += "" + label + ""; + r += "" + kwh.toFixed(2) + " kWh"; + r += "" + (value_gbp !== null ? (value_gbp >= 0 ? "\u00a3" : "-\u00a3") + Math.abs(value_gbp).toFixed(2) : "—") + ""; + + // unit price value_gbp / kwh, only if kwh > 0 and value_gbp is not null + if (value_gbp !== null && kwh > 0) { + let unit_price = value_gbp / kwh; + r += "" + (unit_price * 100).toFixed(1) + " p/kWh"; + } else { + r += "—"; + } - if (solarpv_mode) { - var unit_cost_solar_used = (total.solar_used.cost / total.solar_used.kwh); - out += ""; - out += "Solar self consumption"; - out += "" + total.solar_used.kwh.toFixed(1) + " kWh"; - out += "£" + total.solar_used.cost.toFixed(2) + ""; - out += "" + (unit_cost_solar_used * 100 * 1.05).toFixed(1) + "p/kWh (inc VAT)"; - out += ""; - - var unit_cost_solar_combined = ((total.solar_used.cost + total.export_tariff.cost) / (total.solar_used.kwh + total.export_tariff.kwh)); - out += ""; - out += "Solar + Export"; - out += "" + (total.solar_used.kwh + total.export_tariff.kwh).toFixed(1) + " kWh"; - out += "£" + (total.solar_used.cost + total.export_tariff.cost).toFixed(2) + ""; - out += "" + (unit_cost_solar_combined * 100 * 1.05).toFixed(1) + "p/kWh (inc VAT)"; - out += ""; + r += "" + value_label + ""; + r += ""; + return r; } + out += flow_row("☀ Solar → Load", total.solar_to_load_kwh, total.tariff.solar_to_load_value * 1.05, "avoided import cost", flow_colors.solar_to_load); + out += flow_row("☀ Solar → Battery", total.solar_to_battery_kwh, total.tariff.solar_to_battery_value * 1.05, "avoided import cost", flow_colors.solar_to_battery); + out += flow_row("☀ Solar → Grid (export)",total.solar_to_grid_kwh, total.tariff.solar_to_grid_value * 1.05, "earned at export tariff", flow_colors.solar_to_grid); + out += flow_row("🔋 Battery → Load", total.battery_to_load_kwh, total.tariff.battery_to_load_value * 1.05, "avoided import cost", flow_colors.battery_to_load); + out += flow_row("🔋 Battery → Grid (export)", total.battery_to_grid_kwh, total.tariff.battery_to_grid_value * 1.05, "earned at export tariff", flow_colors.battery_to_grid); + out += flow_row("💡 Grid → Load", total.grid_to_load_kwh, (total.tariff.grid_to_load_cost * 1.05), "import cost", flow_colors.grid_to_load); + out += flow_row("💡 Grid → Battery", total.grid_to_battery_kwh, (total.tariff.grid_to_battery_cost * 1.05),"import cost", flow_colors.grid_to_battery); + + // Summary row: net cost = grid costs - earnings, unit cost = net cost / total consumption + var net_cost_gbp = ( + (total.tariff.grid_to_load_cost + total.tariff.grid_to_battery_cost) - + (total.tariff.solar_to_grid_value + total.tariff.battery_to_grid_value) + ) * 1.05; + var total_consumption_kwh = total.solar_to_load_kwh + total.battery_to_load_kwh + total.grid_to_load_kwh; + + // spacer row + out += flow_row("Net result", total_consumption_kwh, net_cost_gbp, "grid costs minus export earnings", "#000", + "font-weight:bold;background-color:#e8e8e8"); + $("#show_profile").show(); $("#octopus_totals").html(out); - // Set tariff_A - $("#tariff_A").val(config.app.tariff_A.value); - $("#tariff_B").val(config.app.tariff_B.value); var months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; - // Populate monthly data if more than one month of data + // Populate monthly data table if more than one month of data if (Object.keys(monthly_data).length > 1) { + + // Update table headers with selected tariff names + var tariff_name = config.app.tariff.value; + var has_baseline = baseline_monthly_summary != undefined && Object.keys(baseline_monthly_summary).length > 0; + + var tariff_label = tariff_name + (has_baseline ? " (A)" : ""); + var baseline_label = (baseline_tariff_name || "Baseline") + " (B)"; + + var heading = "Month" + + "Consumption (kWh)" + + "" + tariff_label + ""; + + if (has_baseline) { + heading += "" + baseline_label + "" + + "Cheaper tariff"; + } + + heading += ""; + + + $("#monthly-data thead tr").html(heading); + var monthly_out = ""; - var monthly_sum_kwh = 0; - var monthly_sum_kwh_tariff_A = 0; - var monthly_sum_kwh_tariff_B = 0; - var monthly_sum_cost_import_tariff_A = 0; - var monthly_sum_cost_import_tariff_B = 0; + var sum_consumption_kwh = 0; + var sum_net_cost_tariff = 0; + var sum_net_cost_baseline = 0; + + // Saves this monthly summary for use in base-line comparison. + monthly_summary = {}; for (var month in monthly_data) { + var md = monthly_data[month]; var d = new Date(parseInt(month)); + var vat = 1.05; - let vat = 1.05; + // Net cost = grid import costs - export earnings, with VAT + var net_cost = ( + (md.tariff.grid_to_load_cost + md.tariff.grid_to_battery_cost) - + (md.tariff.solar_to_grid_value + md.tariff.battery_to_grid_value) + ) * vat; - let tariff_A_kwh = monthly_data[month]["import_tariff_A"]; - let tariff_B_kwh = monthly_data[month]["import_tariff_B"]; - let tariff_A_cost = monthly_data[month]["cost_import_tariff_A"]*vat; - let tariff_B_cost = monthly_data[month]["cost_import_tariff_B"]*vat; - let tariff_A_unit_cost = 100*(tariff_A_cost / tariff_A_kwh); - let tariff_B_unit_cost = 100*(tariff_B_cost / tariff_B_kwh); + // Effective unit rate against total consumption + var consumption = md.solar_to_load_kwh + md.battery_to_load_kwh + md.grid_to_load_kwh; + var unit_rate = consumption > 0 ? (net_cost / consumption) * 100 : NaN; monthly_out += ""; monthly_out += "" + d.getFullYear() + " " + months[d.getMonth()] + ""; - monthly_out += "" + monthly_data[month]["import"].toFixed(1) + " kWh"; + monthly_out += "" + consumption.toFixed(1) + " kWh"; - monthly_out += "£" + tariff_A_cost.toFixed(2) + ""; - if (!isNaN(tariff_A_unit_cost)) { - monthly_out += "" + tariff_A_unit_cost.toFixed(1) + " p/kWh"; + // Tariff A: cost + rate merged + if (!isNaN(unit_rate)) { + monthly_out += "" + (net_cost >= 0 ? "\u00a3" : "-\u00a3") + Math.abs(net_cost).toFixed(2) + " " + unit_rate.toFixed(1) + " p/kWh"; } else { - monthly_out += ""; - } - - monthly_out += "£" + tariff_B_cost.toFixed(2) + ""; - if (!isNaN(tariff_B_unit_cost)) { - monthly_out += "" + tariff_B_unit_cost.toFixed(1) + " p/kWh"; - } else { - monthly_out += ""; + monthly_out += "" + (net_cost >= 0 ? "\u00a3" : "-\u00a3") + Math.abs(net_cost).toFixed(2) + ""; } - // A, B = - if (tariff_A_unit_cost < tariff_B_unit_cost) { - monthly_out += "A"; - } else if (tariff_A_unit_cost > tariff_B_unit_cost) { - monthly_out += "B"; - } else { - monthly_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 += "" + (baseline_net_cost >= 0 ? "\u00a3" : "-\u00a3") + Math.abs(baseline_net_cost).toFixed(2) + " " + baseline_unit_rate.toFixed(1) + " p/kWh"; + } else { + monthly_out += "" + (baseline_net_cost >= 0 ? "\u00a3" : "-\u00a3") + Math.abs(baseline_net_cost).toFixed(2) + ""; + } + + // Which tariff is cheaper this month + if (!isNaN(unit_rate) && !isNaN(baseline_unit_rate)) { + if (unit_rate < baseline_unit_rate) { + monthly_out += "A"; + } else if (baseline_unit_rate < unit_rate) { + monthly_out += "B"; + } else { + monthly_out += "="; + } + } else { + monthly_out += "—"; + } + + sum_net_cost_baseline += baseline_net_cost; } - // link icon that zooms to month - monthly_out += ""; + // Link icon to zoom to this month + monthly_out += ""; monthly_out += ""; - monthly_sum_kwh += monthly_data[month]["import"]; - monthly_sum_kwh_tariff_A += tariff_A_kwh; - monthly_sum_kwh_tariff_B += tariff_B_kwh; - monthly_sum_cost_import_tariff_A += tariff_A_cost; - monthly_sum_cost_import_tariff_B += tariff_B_cost; + sum_consumption_kwh += consumption; + sum_net_cost_tariff += net_cost; + + monthly_summary[month] = { + consumption_kwh: consumption, + net_cost_tariff: net_cost, + unit_rate_tariff: unit_rate + }; } - var tariff_A_unit_cost = 100*(monthly_sum_cost_import_tariff_A / monthly_sum_kwh_tariff_A); - var tariff_B_unit_cost = 100*(monthly_sum_cost_import_tariff_B / monthly_sum_kwh_tariff_B); + // Totals row + var total_unit_rate = sum_consumption_kwh > 0 ? (sum_net_cost_tariff / sum_consumption_kwh) * 100 : NaN; - // add totals line in bold - monthly_out += ""; + monthly_out += ""; monthly_out += "Total"; - monthly_out += "" + monthly_sum_kwh.toFixed(1) + " kWh"; - monthly_out += "£" + monthly_sum_cost_import_tariff_A.toFixed(2) + ""; - monthly_out += "" + (tariff_A_unit_cost).toFixed(1) + " p/kWh"; - monthly_out += "£" + monthly_sum_cost_import_tariff_B.toFixed(2) + ""; - monthly_out += "" + (tariff_B_unit_cost).toFixed(1) + " p/kWh"; + monthly_out += "" + sum_consumption_kwh.toFixed(1) + " kWh"; + if (!isNaN(total_unit_rate)) { + monthly_out += "" + (sum_net_cost_tariff >= 0 ? "\u00a3" : "-\u00a3") + Math.abs(sum_net_cost_tariff).toFixed(2) + " " + total_unit_rate.toFixed(1) + " p/kWh"; + } else { + monthly_out += "" + (sum_net_cost_tariff >= 0 ? "\u00a3" : "-\u00a3") + Math.abs(sum_net_cost_tariff).toFixed(2) + ""; + } + + if (baseline_monthly_summary != undefined && Object.keys(baseline_monthly_summary).length > 0) { + var baseline_total_unit_rate = sum_consumption_kwh > 0 ? (sum_net_cost_baseline / sum_consumption_kwh) * 100 : NaN; + if (!isNaN(baseline_total_unit_rate)) { + monthly_out += "" + (sum_net_cost_baseline >= 0 ? "\u00a3" : "-\u00a3") + Math.abs(sum_net_cost_baseline).toFixed(2) + " " + baseline_total_unit_rate.toFixed(1) + " p/kWh"; + } else { + monthly_out += "" + (sum_net_cost_baseline >= 0 ? "\u00a3" : "-\u00a3") + Math.abs(sum_net_cost_baseline).toFixed(2) + ""; + } + } + + + monthly_out += ""; monthly_out += ""; $("#monthly-data-body").html(monthly_out); $("#monthly-data").show(); + } else { + $("#monthly-data").hide(); } } @@ -808,21 +981,24 @@ function graph_draw() { profile_mode = false; $("#history-title").html("HISTORY"); + if (this_halfhour_index != -1) { + + // let kwh_grid_to_load = get_data_value_at_index("grid_to_load", this_halfhour_index); + // let kwh_grid_to_battery = get_data_value_at_index("grid_to_battery", this_halfhour_index); + // let kwh_last_halfhour = (kwh_grid_to_load != null ? kwh_grid_to_load : 0) + // + (kwh_grid_to_battery != null ? kwh_grid_to_battery : 0); - let kwh_last_halfhour = data["import"][this_halfhour_index][1]; - - if (kwh_last_halfhour != null) { - $("#kwh_halfhour").html(kwh_last_halfhour.toFixed(2) + "kWh"); - } else { - $("#kwh_halfhour").html("N/A"); - } + // $("#kwh_halfhour").html(kwh_last_halfhour.toFixed(2) + "kWh"); - let cost_last_halfhour = data["import_cost_tariff_A"][this_halfhour_index][1] * 100; - $("#cost_halfhour").html("(" + cost_last_halfhour.toFixed(2) + "p)"); + let tariff_unit = get_data_value_at_index("tariff", this_halfhour_index); + if (tariff_unit != null) { + // let cost_last_halfhour = kwh_last_halfhour * tariff_unit; + // $("#cost_halfhour").html("(" + cost_last_halfhour.toFixed(2) + "p)"); - let unit_price = data["tariff_A"][this_halfhour_index][1] * 1.05; - $("#unit_price").html(unit_price.toFixed(2) + "p"); + let unit_price = tariff_unit * 1.05; + $("#unit_price").html(unit_price.toFixed(2) + "p"); + } $(".last_halfhour_stats").show(); } else { @@ -839,67 +1015,19 @@ function graph_draw() { graph_series = []; - // Solar used data - if (solarpv_mode) { - graph_series.push({ - label: "Solar direct", - data: data["solar_direct"], - yaxis: 1, - color: "#bec745", - stack: true, - bars: bars - }); - } - - if (solarpv_mode && battery_mode) { - graph_series.push({ - label: "Solar to Battery", - data: data["solar_to_battery"], - yaxis: 1, - color: "#a3d977", - stack: true, - bars: bars - }); - } - - // Import data - graph_series.push({ - label: "Import", - data: data["import"], - yaxis: 1, - color: "#44b3e2", - stack: true, - bars: bars - }); - - // Export data - if (solarpv_mode || battery_mode) { - graph_series.push({ - label: "Export", - data: data["export"], - yaxis: 1, - color: "#dccc1f", - stack: false, - bars: bars - }); - } - - // Smart meter data - if (smart_meter_data && !solarpv_mode) { - graph_series.push({ - label: "Import Actual", - data: data["meter_kwh_hh"], - yaxis: 1, - color: "#1d8dbc", - stack: false, - bars: bars - }); - } + // All 7 disaggregated flows stacked as positive bars + graph_series.push({ label: "Solar to Load", data: data["solar_to_load"], yaxis: 1, color: flow_colors.solar_to_load, stack: true, bars: bars }); + graph_series.push({ label: "Solar to Battery", data: data["solar_to_battery"], yaxis: 1, color: flow_colors.solar_to_battery, stack: true, bars: bars }); + graph_series.push({ label: "Solar to Grid", data: data["solar_to_grid"], yaxis: 1, color: flow_colors.solar_to_grid, stack: true, bars: bars }); + graph_series.push({ label: "Battery to Load", data: data["battery_to_load"], yaxis: 1, color: flow_colors.battery_to_load, stack: true, bars: bars }); + graph_series.push({ label: "Battery to Grid", data: data["battery_to_grid"], yaxis: 1, color: flow_colors.battery_to_grid, stack: true, bars: bars }); + graph_series.push({ label: "Grid to Load", data: data["grid_to_load"], yaxis: 1, color: flow_colors.grid_to_load, stack: true, bars: bars }); + graph_series.push({ label: "Grid to Battery", data: data["grid_to_battery"], yaxis: 1, color: flow_colors.grid_to_battery, stack: true, bars: bars }); // price signals graph_series.push({ - label: config.app.tariff_A.value, - data: data["tariff_A"], + label: config.app.tariff.value, + data: data["tariff"], yaxis: 2, color: "#fb1a80", lines: { @@ -910,20 +1038,18 @@ function graph_draw() { } }); - if (solarpv_mode) { - graph_series.push({ - label: "Outgoing", - data: data["outgoing"], - yaxis: 2, - color: "#941afb", - lines: { - show: true, - steps: true, - align: "center", - lineWidth: 1 - } - }); - } + graph_series.push({ + label: "Outgoing", + data: data["outgoing"], + yaxis: 2, + color: "#941afb", + lines: { + show: true, + steps: true, + align: "center", + lineWidth: 1 + } + }); if (show_carbonintensity) { graph_series.push({ @@ -940,19 +1066,6 @@ function graph_draw() { }); } - graph_series.push({ - label: config.app.tariff_B.value, - data: data["tariff_B"], - yaxis: 2, - color: "#7c1a80", - lines: { - show: true, - steps: true, - align: "left", - lineWidth: 1 - } - }); - var options = { xaxis: { mode: "time", @@ -999,8 +1112,9 @@ function graph_draw() { mode: "x" }, legend: { + show: $('#placeholder').width() > 500, position: "NW", - noColumns: 6 + noColumns: 1 } } $.plot($('#placeholder'), graph_series, options); @@ -1021,9 +1135,17 @@ function resize() { var width = placeholder_bound.width(); var height = window_height - topblock - 250; - if (height < 250) height = 250; if (height > 500) height = 500; + + // min size to avoid flot errors + if (height<180) height = 180; + if (width<200) width = 200; + + if (height > width*0.8) { + height = width*0.8; + } + placeholder.width(width); placeholder_bound.height(height); placeholder.height(height - top_offset); @@ -1041,6 +1163,14 @@ function resize() { $(".power-value").css("font-size", "50px"); $(".halfhour-value").css("font-size", "40px"); } + + if (width <= 500) { + $("#zoomout").hide(); + $("#zoomin").hide(); + } else { + $("#zoomout").show(); + $("#zoomin").show(); + } } $(function() { @@ -1129,23 +1259,6 @@ function getdataremote(id, start, end, interval) { return data; } - -function convert_cumulative_kwh_to_kwh_hh(cumulative_kwh_data, limit_positive=false) { - var kwh_hh_data = []; - - for (var z = 1; z < cumulative_kwh_data.length; z++) { - let time = cumulative_kwh_data[z - 1][0]; - let kwh_hh = 0; - if (cumulative_kwh_data[z][1] != null && cumulative_kwh_data[z - 1][1] != null) { - kwh_hh = cumulative_kwh_data[z][1] - cumulative_kwh_data[z - 1][1]; - } - if (limit_positive && kwh_hh < 0.0) kwh_hh = 0.0; - kwh_hh_data.push([time, kwh_hh]); - } - - return kwh_hh_data; -} - function calibration_line_of_best_fit(import_kwh, meter_kwh_hh) { if (import_kwh.length != meter_kwh_hh.length) { @@ -1196,21 +1309,14 @@ function calibration_line_of_best_fit(import_kwh, meter_kwh_hh) // ------------------------------------------------------------------------------- $('#placeholder').bind("plothover", function(event, pos, item) { if (item) { - var z = item.dataIndex; - - var isStepped = item.series.lines && item.series.lines.steps; - - if (isStepped) { - z = Math.floor(z / 2); - } - if (previousPoint != item.datapoint) { previousPoint = item.datapoint; $("#tooltip").remove(); var itemTime = item.datapoint[0]; - var itemValue = item.datapoint[1]; + + var z = time_to_index_map[itemTime]; var d = new Date(itemTime); var days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; @@ -1235,55 +1341,69 @@ $('#placeholder').bind("plothover", function(event, pos, item) { } } - let import_kwh = get_data_value_at_index("import", z); - let solar_used_kwh = get_data_value_at_index("solar_used", z); - let export_kwh = get_data_value_at_index("export", z); - let tariff_A = get_data_value_at_index("tariff_A", z); - let tariff_B = get_data_value_at_index("tariff_B", z); + let tariff = get_data_value_at_index("tariff", z); let outgoing = get_data_value_at_index("outgoing", z); let carbonintensity = get_data_value_at_index("carbonintensity", z); - if (import_kwh != null) { - text += "Import: " + import_kwh.toFixed(3) + " kWh"; - if (tariff_A != null) { - let cost = import_kwh * tariff_A; - text += " (" + cost.toFixed(2) + "p cost)"; - } + let solar_to_load_kwh = get_data_value_at_index("solar_to_load", z); + let solar_to_grid_kwh = get_data_value_at_index("solar_to_grid", z); + let solar_to_battery_kwh = get_data_value_at_index("solar_to_battery", z); + let battery_to_load_kwh = get_data_value_at_index("battery_to_load", z); + let battery_to_grid_kwh = get_data_value_at_index("battery_to_grid", z); + let grid_to_load_kwh = get_data_value_at_index("grid_to_load", z); + let grid_to_battery_kwh = get_data_value_at_index("grid_to_battery", z); + + if (solar_to_load_kwh != null && solar_to_load_kwh > 0) { + text += "☀ Solar → Load: " + solar_to_load_kwh.toFixed(3) + " kWh"; + if (tariff != null) text += " (" + (solar_to_load_kwh * tariff).toFixed(2) + "p saved)
"; + else text += "
"; } - - if (solar_used_kwh != null) { - text += "
Used Solar: " + solar_used_kwh.toFixed(3) + " kWh"; - if (tariff_A != null) { - let cost = solar_used_kwh * tariff_A; - text += " (" + cost.toFixed(2) + "p saved)"; - } + if (solar_to_battery_kwh != null && solar_to_battery_kwh > 0) { + text += "☀ Solar → Battery: " + solar_to_battery_kwh.toFixed(3) + " kWh"; + if (tariff != null) text += " (" + (solar_to_battery_kwh * tariff).toFixed(2) + "p saved)
"; + else text += "
"; } - - if (export_kwh != null) { - text += "
Export: " + (export_kwh * -1).toFixed(3) + " kWh"; - if (outgoing != null) { - let cost = export_kwh * outgoing; - text += " (" + cost.toFixed(2) + "p gained)"; - } + if (solar_to_grid_kwh != null && solar_to_grid_kwh > 0) { + text += "☀ Solar → Grid: " + solar_to_grid_kwh.toFixed(3) + " kWh"; + if (outgoing != null) text += " (" + (solar_to_grid_kwh * outgoing).toFixed(2) + "p gained)
"; + else text += "
"; + } + if (battery_to_load_kwh != null && battery_to_load_kwh > 0) { + text += "🔋 Battery → Load: " + battery_to_load_kwh.toFixed(3) + " kWh"; + if (tariff != null) text += " (" + (battery_to_load_kwh * tariff).toFixed(2) + "p saved)
"; + else text += "
"; + } + if (battery_to_grid_kwh != null && battery_to_grid_kwh > 0) { + text += "🔋 Battery → Grid: " + battery_to_grid_kwh.toFixed(3) + " kWh"; + if (outgoing != null) text += " (" + (battery_to_grid_kwh * outgoing).toFixed(2) + "p gained)
"; + else text += "
"; + } + if (grid_to_load_kwh != null && grid_to_load_kwh > 0) { + text += "💡 Grid → Load: " + grid_to_load_kwh.toFixed(3) + " kWh"; + if (tariff != null) text += " (" + (grid_to_load_kwh * tariff).toFixed(2) + "p cost)
"; + else text += "
"; + } + if (grid_to_battery_kwh != null && grid_to_battery_kwh > 0) { + text += "💡 Grid → Battery: " + grid_to_battery_kwh.toFixed(3) + " kWh"; + if (tariff != null) text += " (" + (grid_to_battery_kwh * tariff).toFixed(2) + "p cost)
"; + else text += "
"; } text += "
"; + if (tariff != null) { + text += " Import Tariff: " + tariff.toFixed(2) + " p/kWh (inc VAT)
"; + } + if (outgoing != null) { - text += "Export Tariff: " + outgoing.toFixed(2) + " p/kWh (inc VAT)
"; + text += " Export Tariff: " + outgoing.toFixed(2) + " p/kWh (inc VAT)
"; } if (show_carbonintensity && carbonintensity != null) { - text += "Carbon Intensity: " + carbonintensity.toFixed(0) + " gCO2/kWh
"; + text += "🌱 Carbon Intensity: " + carbonintensity.toFixed(0) + " gCO2/kWh
"; } - if (tariff_A != null) { - text += config.app.tariff_A.value+": " + tariff_A.toFixed(2) + " p/kWh (inc VAT)
"; - } - if (tariff_B != null) { - text += config.app.tariff_B.value+": " + tariff_B.toFixed(2) + " p/kWh (inc VAT)
"; - } tooltip(item.pageX, item.pageY, text, "#fff", "#000"); } @@ -1386,28 +1506,25 @@ $("#monthly-data").on("click", ".zoom-to-month", function() { return false; }); -$("#octopus_totals").on("change", "select", function() { - config.app.tariff_A.value = $("#tariff_A").val(); - config.app.tariff_B.value = $("#tariff_B").val(); - - config.db.tariff_A = config.app.tariff_A.value; - config.db.tariff_B = config.app.tariff_B.value; +$("#tariff").on("change", function() { + config.app.tariff.value = $("#tariff").val(); + config.db.tariff = config.app.tariff.value; config.set(); - graph_load(); + graph_load(false); graph_draw(); }); $("#use_meter_kwh_hh").click(function() { use_meter_kwh_hh = $(this)[0].checked; - graph_load(); + graph_load(false); graph_draw(); }); $("#show_carbonintensity").click(function() { show_carbonintensity = $(this)[0].checked; - graph_load(); + graph_load(false); graph_draw(); if (!show_carbonintensity) $("#carbonintensity_result").html(""); }); @@ -1416,11 +1533,7 @@ $("#download-csv").click(function() { var csv = []; - if (solarpv_mode) { - keys = ["tariff_A", "tariff_B", "outgoing", "use", "import", "import_cost_tariff_A", "import_cost_tariff_B", "export", "export_cost", "solar_used", "solar_used_cost", "meter_kwh_hh", "meter_kwh_hh_cost"] - } else { - keys = ["tariff_A", "tariff_B", "import", "import_cost_tariff_A", "import_cost_tariff_B", "meter_kwh_hh", "meter_kwh_hh_cost"] - } + keys = ["tariff", "outgoing", "solar_to_load", "solar_to_battery", "solar_to_grid", "battery_to_load", "battery_to_grid", "grid_to_load", "grid_to_battery", "import", "import_cost_tariff", "export", "export_cost", "solar_used", "solar_used_cost", "meter_kwh_hh"] csv.push("time," + keys.join(",")) @@ -1478,4 +1591,36 @@ $('#datetimepicker2').on("changeDate", function(e) { graph_load(); graph_draw(); $(".time-select").val("C"); -}); \ No newline at end of file +}); + +// Save monthly data as baseline to be compared against tariff change +$("#save-baseline").click(function() { + baseline_monthly_summary = JSON.parse(JSON.stringify(monthly_summary)); + baseline_tariff_name = config.app.tariff.value; + draw_tables(); +}); + + +// ---------------------------------------------------------------------- +// Helper: return array of feeds that should be auto-generated +// (delegates to config.autogen.get_feeds in appconf.js) +// ---------------------------------------------------------------------- +function get_autogen_feeds() { + return config.autogen.get_feeds(); +} + +// ---------------------------------------------------------------------- +// Auto-generate feed list +// (delegates to config.autogen.render_feed_list in appconf.js) +// ---------------------------------------------------------------------- +function render_autogen_feed_list() { + config.autogen.render_feed_list(); +} + +// ---------------------------------------------------------------------- +// Auto-generate feed actions +// (delegate to config.autogen.* in appconf.js) +// ---------------------------------------------------------------------- +function create_missing_feeds() { config.autogen.create_missing_feeds(); } +function start_post_processor() { config.autogen.start_post_processor(); } +function reset_feeds() { config.autogen.reset_feeds(); } \ No newline at end of file diff --git a/apps/OpenEnergyMonitor/octopus/octopus.php b/apps/OpenEnergyMonitor/octopus/tariff_explorer.php similarity index 65% rename from apps/OpenEnergyMonitor/octopus/octopus.php rename to apps/OpenEnergyMonitor/octopus/tariff_explorer.php index ea1f1aa2..adf103e0 100644 --- a/apps/OpenEnergyMonitor/octopus/octopus.php +++ b/apps/OpenEnergyMonitor/octopus/tariff_explorer.php @@ -2,12 +2,10 @@ defined('EMONCMS_EXEC') or die('Restricted access'); global $path, $session, $v; ?> - - @@ -34,25 +32,16 @@
- - - - - - - - - -
-
IMPORT NOW
-
0
-
-
CURRENT PRICE
-
-
-
CURRENT HALF HOUR
-
-
+
+
+ IMPORT NOW +
0
+
+
+ CURRENT PRICE +
+
+
@@ -67,6 +56,8 @@ << > < + - + + + + +
@@ -132,20 +133,11 @@
- - - - - - - - - - - - +
MonthEnergy (kWh)Tariff A Cost (£)Tariff B Cost (£)
+ +
@@ -154,34 +146,22 @@ -
- -
-
-
-
-

-

Explore Octopus Agile tariff costs 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.

-

Import & Import kWh The standard naming for electricity imported from the grid in a household without solar PV is 'use' and 'use_kwh', this app expects 'import' and 'import_kwh' in order to provide compatibility with the Solar PV option as well. Select relevant house consumption feeds using the dropdown feed selectors as required. Feeds 'use_kwh' and 'solar_kwh' are optional.

-

Cumulative kWh feeds can be generated from power feeds with the power_to_kwh input processor. To create cumulative kWh feeds from historic power data try the postprocess module.

-

meter_kwh_hh If you have half hourly Octopus smart meter data available select the applicable feed.

-

Optional: Octopus Outgoing Include total house consumption (use_kwh) and solar PV (solar_kwh) feeds to explore octopus outgoing feed-in-tariff potential.

- -
-
-
-
-
-
+ + +
@@ -53,18 +50,13 @@

- - - + + +
@@ -238,15 +236,10 @@ - - - -
- -
-
-
-

-

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

-

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

-

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

-
-
-
-
-
- - + +
@@ -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

- -
-

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.

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

-
-
-
-
-
+ +
@@ -249,11 +237,11 @@ "cl_use":{"type":"feed", "autoname":"cl_use", "engine":5, "description":"Controlled Load power feed (W)"}, "cl_kwh":{"type":"feed", "autoname":"cl_kwh", "engine":5, "description":"Controlled Load accumulated kWh"}, "cl_cost":{"type":"value", "default":"0.17", "name":"Controlled Load Cost", - "description":"Cost of the controlled Load accumulated kWh, currency/kWh."}, - - "public":{"type":"checkbox", "name": "Public", "default": 0, "optional":true, "description":"Make app public"} + "description":"Cost of the controlled Load accumulated kWh, currency/kWh."} }; +config.app_name = "Time of Use - flexible + CL"; + config.id = ; config.name = ""; config.public = ; diff --git a/apps/OpenEnergyMonitor/ukgrid/ukgrid.php b/apps/OpenEnergyMonitor/ukgrid/ukgrid.php index d8412b70..020b73a6 100644 --- a/apps/OpenEnergyMonitor/ukgrid/ukgrid.php +++ b/apps/OpenEnergyMonitor/ukgrid/ukgrid.php @@ -5,10 +5,7 @@ global $path, $session, $v; ?> - - - @@ -100,20 +97,10 @@

- -
-
-
-
-
-

-

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

-
-
-
-
-
-
+ +
@@ -132,15 +119,8 @@ // ---------------------------------------------------------------------- // Configuration // ---------------------------------------------------------------------- - config.app = { - "public": { - "type": "checkbox", - "name": "Public", - "default": 0, - "optional": true, - "description": "Make app public" - } - }; + config.app = {}; + config.app_name = "UK Grid Visualisation"; config.id = ; config.name = ""; config.public = ; diff --git a/apps/template/app.json b/apps/template/app.json index 90378e5e..5a59c409 100644 --- a/apps/template/app.json +++ b/apps/template/app.json @@ -1,5 +1,5 @@ { "title" : "Example 1", "description" : "A basic app example useful for developing new apps.", - "order" : 15 + "order" : 19 } diff --git a/apps/template/template.php b/apps/template/template.php index a9e2f28a..db4f318d 100644 --- a/apps/template/template.php +++ b/apps/template/template.php @@ -2,10 +2,7 @@ defined('EMONCMS_EXEC') or die('Restricted access'); global $path, $session, $v; ?> - - - @@ -71,22 +68,10 @@
- - -
-
-
-
-
-

-

A basic app example useful for developing new apps.

- -
-
-
-
-
-
+ +
@@ -94,6 +79,7 @@ // Transfer php variables to javascript var apikey = ""; var sessionwrite = ; + config.app_name = "Template App"; config.id = ; config.name = ""; config.public = ;