From cb57c50a2fc6ea8f21d026b4d778fac67aa55f3c Mon Sep 17 00:00:00 2001 From: Trefor Southwell Date: Sun, 8 Feb 2026 14:52:55 +0000 Subject: [PATCH 1/6] New savings chart in web UI Update Axle event sensor every minute - https://github.com/springfall2008/batpred/issues/3318 --- apps/predbat/axle.py | 2 ++ apps/predbat/web.py | 81 +++++++++++++++++++++++--------------------- 2 files changed, 44 insertions(+), 39 deletions(-) diff --git a/apps/predbat/axle.py b/apps/predbat/axle.py index 25bc83c50..e24da25b8 100644 --- a/apps/predbat/axle.py +++ b/apps/predbat/axle.py @@ -307,6 +307,8 @@ async def run(self, seconds, first): except Exception as e: self.log(f"Warn: Axle API: Exception during fetch: {e}") self.failures_total += 1 + elif (seconds % 60) == 0: # Every minute, update state to reflect if event is active or not + self.publish_axle_event() return True diff --git a/apps/predbat/web.py b/apps/predbat/web.py index 155c00e71..6d0d9016d 100644 --- a/apps/predbat/web.py +++ b/apps/predbat/web.py @@ -2679,6 +2679,38 @@ def get_chart(self, chart): ] text += self.render_chart(series_data, "kW", "ML Load & PV Power with Temperature", now_str, extra_yaxis=secondary_axis) + elif chart == "Savings": + # Get daily savings data (historical) + savings_predbat_hist = history_attribute(self.get_history_wrapper(self.prefix + ".savings_yesterday_predbat", 28, required=False), daily=True, offset_days=-1, pounds=True) + savings_pvbat_hist = history_attribute(self.get_history_wrapper(self.prefix + ".savings_yesterday_pvbat", 28, required=False), daily=True, offset_days=-1, pounds=True) + cost_yesterday_hist = history_attribute(self.get_history_wrapper(self.prefix + ".cost_yesterday", 28, required=False), daily=True, offset_days=-1, pounds=True) + + # Get cumulative/total savings over time (historical) + savings_total_predbat_hist = history_attribute(self.get_history_wrapper(self.prefix + ".savings_total_predbat", 28, required=False), daily=True, pounds=True) + savings_total_pvbat_hist = history_attribute(self.get_history_wrapper(self.prefix + ".savings_total_pvbat", 28, required=False), daily=True, pounds=True) + + series_data = [ + # Daily savings (bars) on primary axis + {"name": "Daily Predbat Saving", "data": savings_predbat_hist, "opacity": "1.0", "stroke_width": "2", "chart_type": "bar", "color": "#f5a442", "unit": self.currency_symbols[0]}, + {"name": "Daily PV/Battery Saving", "data": savings_pvbat_hist, "opacity": "1.0", "stroke_width": "2", "chart_type": "bar", "color": "#3291a8", "unit": self.currency_symbols[0]}, + {"name": "Daily Actual Cost", "data": cost_yesterday_hist, "opacity": "1.0", "stroke_width": "2", "chart_type": "bar", "color": "#eb2323", "unit": self.currency_symbols[0]}, + # Cumulative savings (lines) on secondary axis + {"name": "Total Predbat Saving", "data": savings_total_predbat_hist, "opacity": "1.0", "stroke_width": "3", "stroke_curve": "smooth", "color": "#f5c43d", "unit": self.currency_symbols[0]}, + {"name": "Total PV/Battery Saving", "data": savings_total_pvbat_hist, "opacity": "1.0", "stroke_width": "3", "stroke_curve": "smooth", "color": "#15eb8b", "unit": self.currency_symbols[0]}, + ] + + # Configure secondary axis for cumulative totals + secondary_axis = [ + { + "title": f"Total Savings ({self.currency_symbols[0]})", + "series_names": ["Total Predbat Saving", "Total PV/Battery Saving"], + "decimals": 0, + "opposite": True, + "labels_formatter": f"return val.toFixed(0);", + } + ] + + text += self.render_chart(series_data, f"Daily Savings ({self.currency_symbols[0]})", "Cost Savings Analysis", now_str, daily_chart=False, extra_yaxis=secondary_axis) else: text += "

Unknown chart type

" @@ -2695,49 +2727,20 @@ async def html_charts(self, request): text += "\n" text += get_charts_css() - # Define which chart is active - active_battery = "" - active_power = "" - active_cost = "" - active_rates = "" - active_inday = "" - active_pv = "" - active_pv7 = "" - active_loadml = "" - active_loadmlpower = "" - - if chart == "Battery": - active_battery = "active" - elif chart == "Power": - active_power = "active" - elif chart == "Cost": - active_cost = "active" - elif chart == "Rates": - active_rates = "active" - elif chart == "InDay": - active_inday = "active" - elif chart == "PV": - active_pv = "active" - elif chart == "PV7": - active_pv7 = "active" - elif chart == "LoadML": - active_loadml = "active" - elif chart == "LoadMLPower": - active_loadmlpower = "active" - text += '
' text += "

Charts

" - text += f'Battery' - text += f'Power' - text += f'Cost' - text += f'Rates' - text += f'InDay' - text += f'PV' - text += f'PV7' + text += f'Battery' + text += f'Power' + text += f'Cost' + text += f'Rates' + text += f'InDay' + text += f'PV' + text += f'PV7' + text += f'Savings' # Only show LoadML chart if ML is enabled if self.base.get_arg("load_ml_enable", False): - text += f'LoadML' - text += f'LoadMLPower' + text += f'LoadML' + text += f'LoadMLPower' text += "
" text += '
' From bc985baaf3bba8b68ba289c0a4fa680e555aa346 Mon Sep 17 00:00:00 2001 From: Trefor Southwell Date: Sun, 8 Feb 2026 15:35:57 +0000 Subject: [PATCH 2/6] Component sorting, editor detect changes --- apps/predbat/web.py | 100 ++++++++++++++++++++++++++++++++++--- apps/predbat/web_helper.py | 52 +++++++++++++++++++ 2 files changed, 146 insertions(+), 6 deletions(-) diff --git a/apps/predbat/web.py b/apps/predbat/web.py index 6d0d9016d..f691f1335 100644 --- a/apps/predbat/web.py +++ b/apps/predbat/web.py @@ -25,6 +25,7 @@ import threading import io from io import StringIO +import hashlib from ruamel.yaml import YAML from ruamel.yaml.scalarstring import DoubleQuotedScalarString @@ -127,6 +128,7 @@ async def start(self): app.router.add_post("/compare", self.html_compare_post) app.router.add_get("/apps_editor", self.html_apps_editor) app.router.add_post("/apps_editor", self.html_apps_editor_post) + app.router.add_get("/apps_editor_checksum", self.html_apps_editor_checksum) app.router.add_post("/plan_override", self.html_plan_override) app.router.add_post("/rate_override", self.html_rate_override) app.router.add_post("/restart", self.html_restart) @@ -3338,6 +3340,9 @@ async def html_apps_editor(self, request): except Exception as e: file_error = f"Error reading apps.yaml: {str(e)}" + # Calculate MD5 checksum of the content for external change detection + file_checksum = hashlib.md5(apps_yaml_content.encode("utf-8")).hexdigest() if apps_yaml_content else "" + text += get_editor_css() text += """ @@ -3347,10 +3352,10 @@ async def html_apps_editor(self, request):
""" - text += """ + text += f""" -
+