diff --git a/apps/predbat/environment.py b/apps/predbat/environment.py new file mode 100644 index 000000000..c9341bab6 --- /dev/null +++ b/apps/predbat/environment.py @@ -0,0 +1,19 @@ +def is_package_installed(package_name: str) -> bool: + try: + import importlib + + module = importlib.import_module(package_name) + return True + except ImportError: + return False + + +def is_jinja2_installed() -> bool: + return is_package_installed("jinja2") + + +def is_appdaemon_environment() -> bool: + if is_package_installed("appdaemon"): + return True + else: + return False diff --git a/apps/predbat/predbat.py b/apps/predbat/predbat.py index 25e3da2af..ba5510ed3 100644 --- a/apps/predbat/predbat.py +++ b/apps/predbat/predbat.py @@ -17,6 +17,7 @@ from datetime import datetime, timedelta import hashlib import traceback +from environment import is_appdaemon_environment # Import AppDaemon or our standalone wrapper try: @@ -10994,9 +10995,10 @@ def initialize(self): self.ha_interface = HAInterface(self) self.web_interface = None self.web_interface_task = None - self.log("Starting web interface") - self.web_interface = WebInterface(self) - self.web_interface_task = self.create_task(self.web_interface.start()) + if not is_appdaemon_environment(): + self.log("Starting web interface") + self.web_interface = WebInterface(self) + self.web_interface_task = self.create_task(self.web_interface.start()) # Printable config root self.config_root_p = self.config_root diff --git a/apps/predbat/templates/apps.html b/apps/predbat/templates/apps.html new file mode 100644 index 000000000..63999d198 --- /dev/null +++ b/apps/predbat/templates/apps.html @@ -0,0 +1,13 @@ +{% extends "layout.html" %} + +{% block title %}Home{% endblock %} + +{% block content %} +

Predbat Apps.yaml

+ +
+ {% autoescape false %} + {{ apps_html }} + {% endautoescape %} +
+{% endblock %} diff --git a/apps/predbat/templates/charts.html b/apps/predbat/templates/charts.html new file mode 100644 index 000000000..ccb281486 --- /dev/null +++ b/apps/predbat/templates/charts.html @@ -0,0 +1,19 @@ +{% extends "layout.html" %} + +{% block title %}Home{% endblock %} + +{% block content %} +

{{ chart_title }} Chart

+ - Battery + Power + Cost + Rates + InDay + PV + PV7 +
+ {% autoescape false %} + {{ chart_html }} + {% endautoescape %} +
+{% endblock %} diff --git a/apps/predbat/templates/config.html b/apps/predbat/templates/config.html new file mode 100644 index 000000000..8dd1cd6d3 --- /dev/null +++ b/apps/predbat/templates/config.html @@ -0,0 +1,13 @@ +{% extends "layout.html" %} + +{% block title %}Home{% endblock %} + +{% block content %} +

Predbat Config

+ +
+ {% autoescape false %} + {{ config_html }} + {% endautoescape %} +
+{% endblock %} diff --git a/apps/predbat/templates/dash.html b/apps/predbat/templates/dash.html new file mode 100644 index 000000000..f1f387f93 --- /dev/null +++ b/apps/predbat/templates/dash.html @@ -0,0 +1,10 @@ +{% extends "layout.html" %} + +{% block title %}Home{% endblock %} + + +{% block content %} +{% autoescape false %} +{{ dash_html }} +{% endautoescape %} +{% endblock %} diff --git a/apps/predbat/templates/docs.html b/apps/predbat/templates/docs.html new file mode 100644 index 000000000..1a0551105 --- /dev/null +++ b/apps/predbat/templates/docs.html @@ -0,0 +1,9 @@ +{% extends "layout.html" %} + +{% block title %}Home{% endblock %} + +{% block content %} +
+ +
+{% endblock %} diff --git a/apps/predbat/templates/layout.html b/apps/predbat/templates/layout.html new file mode 100644 index 000000000..8b034bf5a --- /dev/null +++ b/apps/predbat/templates/layout.html @@ -0,0 +1,89 @@ + + + + Predbat Web Interface + + + + {% if refresh is defined %} + + {% endif %} + + + +
+ + + + + + + + + + + +

Predbat

DashPlanChartsConfigapps.yamlLogDocs
+
+ {% block content %}{% endblock %} + + diff --git a/apps/predbat/templates/logs.html b/apps/predbat/templates/logs.html new file mode 100644 index 000000000..8a5788a7a --- /dev/null +++ b/apps/predbat/templates/logs.html @@ -0,0 +1,16 @@ +{% extends "layout.html" %} + +{% block title %}Home{% endblock %} + + +{% block content %} +

Logfile ({% if errors %}Errors{% elif warnings %}Warnings{% else %}All{% endif %})

+ - All Warnings Errors
+ + {% for line in lines %} + + {% endfor %} +
{{ line.line_no}}{{ line.start_line }} {{ line.rest_line }}
+ {% autoescape false %} + {% endautoescape %} +{% endblock %} diff --git a/apps/predbat/templates/plan.html b/apps/predbat/templates/plan.html new file mode 100644 index 000000000..d3a5e4865 --- /dev/null +++ b/apps/predbat/templates/plan.html @@ -0,0 +1,11 @@ +{% extends "layout.html" %} + +{% block title %}Home{% endblock %} + + +{% block content %} +Plan +{% autoescape false %} +{{ plan_html }} +{% endautoescape %} +{% endblock %} diff --git a/apps/predbat/web.py b/apps/predbat/web.py index 980ed327a..3670e85b7 100644 --- a/apps/predbat/web.py +++ b/apps/predbat/web.py @@ -9,6 +9,7 @@ from config import CONFIG_ITEMS from utils import calc_percent_limit from config import TIME_FORMAT, TIME_FORMAT_SECONDS +from environment import is_jinja2_installed class WebInterface: @@ -16,10 +17,18 @@ def __init__(self, base) -> None: self.abort = False self.base = base self.log = base.log - self.default_page = "./dash" self.pv_power_hist = {} self.pv_forecast_hist = {} + # Set up Jinja2 templating (as long as installed) + if is_jinja2_installed(): + from jinja2 import Environment, FileSystemLoader, select_autoescape + + self.template_env = Environment(loader=FileSystemLoader("templates"), autoescape=select_autoescape()) + + # Disable autoescaping for the HTML plan + self.template_env.filters["raw"] = lambda value: value + def history_attribute(self, history, state_key="state", last_updated_key="last_updated", scale=1.0): results = {} if history: @@ -60,16 +69,24 @@ def history_update(self): async def start(self): # Start the web server on port 5052 - app = web.Application() - app.router.add_get("/", self.html_index) - app.router.add_get("/plan", self.html_plan) - app.router.add_get("/log", self.html_log) - app.router.add_get("/menu", self.html_menu) - app.router.add_get("/apps", self.html_apps) - app.router.add_get("/charts", self.html_charts) - app.router.add_get("/config", self.html_config) - app.router.add_post("/config", self.html_config_post) - app.router.add_get("/dash", self.html_dash) + # TODO: Turn off debugging + app = web.Application(debug=True) + + # Check if required dependencies are present + if is_jinja2_installed(): + app.router.add_get("/", self.html_dash) + app.router.add_get("/plan", self.html_plan) + app.router.add_get("/log", self.html_log) + app.router.add_get("/menu", self.html_menu) + app.router.add_get("/apps", self.html_apps) + app.router.add_get("/charts", self.html_charts) + app.router.add_get("/config", self.html_config) + app.router.add_post("/config", self.html_config_post) + app.router.add_get("/docs", self.html_docs) + else: + # Display the missing dependencies/update message + app.router.add_get("/", self.update_message) + runner = web.AppRunner(app) await runner.setup() site = web.TCPSite(runner, "0.0.0.0", 5052) @@ -141,82 +158,6 @@ def get_status_html(self, level, status): return text - def get_header(self, title, refresh=0): - """ - Return the HTML header for a page - """ - text = "Predbat Web Interface" - - text += """ - - -" - - if refresh: - text += ''.format(refresh) - text += "\n" - return text - def get_entity_detailedForecast(self, entity, subitem="pv_estimate"): results = {} detailedForecast = self.base.dashboard_values.get(entity, {}).get("attributes", {}).get("detailedForecast", {}) @@ -374,11 +315,15 @@ async def html_plan(self, request): """ Return the Predbat plan as an HTML page """ - self.default_page = "./plan" - html_plan = self.base.html_plan - text = self.get_header("Predbat Plan", refresh=60) - text += "{}\n".format(html_plan) - return web.Response(content_type="text/html", text=text) + template = self.template_env.get_template("plan.html") + + context = { + "plan_html": self.base.html_plan, + "refresh": 60, + } + + rendered_template = template.render(context) + return web.Response(text=rendered_template, content_type="text/html") async def html_log(self, request): """ @@ -386,7 +331,6 @@ async def html_log(self, request): """ logfile = "predbat.log" logdata = "" - self.default_page = "./log" if os.path.exists(logfile): with open(logfile, "r") as f: logdata = f.read() @@ -397,31 +341,18 @@ async def html_log(self, request): warnings = False if "errors" in args: errors = True - self.default_page = "./log?errors" if "warnings" in args: warnings = True - self.default_page = "./log?warnings" loglines = logdata.split("\n") - text = self.get_header("Predbat Log", refresh=10) - text += "" - - if errors: - text += "

Logfile (errors)

\n" - elif warnings: - text += "

Logfile (Warnings)

\n" - else: - text += "

Logfile (All)

\n" - text += '- All ' - text += 'Warnings ' - text += 'Errors
\n' - - text += "\n" + text = "" + text = "
\n" total_lines = len(loglines) count_lines = 0 lineno = total_lines - 1 + line_data = [] while count_lines < 1024 and lineno >= 0: line = loglines[lineno] line_lower = line.lower() @@ -430,28 +361,34 @@ async def html_log(self, request): start_line = line[0:27] rest_line = line[27:] - if "error" in line_lower: - text += "\n".format(lineno, start_line, rest_line) - count_lines += 1 - continue - elif (not errors) and ("warn" in line_lower): - text += "\n".format(lineno, start_line, rest_line) + if "error" in line_lower or ((not errors) and ("warn" in line_lower)) or (line and (not errors) and (not warnings)): + line_data.append( + { + "line_no": lineno, + "start_line": start_line, + "rest_line": rest_line, + } + ) count_lines += 1 - continue - if line and (not errors) and (not warnings): - text += "\n".format(lineno, start_line, rest_line) - count_lines += 1 + template = self.template_env.get_template("logs.html") - text += "
{}{} {}
{}{} {}
{}{} {}
" - text += "\n" - return web.Response(content_type="text/html", text=text) + context = { + "errors": errors, + "warnings": warnings, + "lines": line_data, + "refresh": 10, + } + + rendered_template = template.render(context) + return web.Response(text=rendered_template, content_type="text/html") async def html_config_post(self, request): """ Save the Predbat config from an HTML page """ postdata = await request.post() + self.log("Post data: {}".format(postdata)) for pitem in postdata: new_value = postdata[pitem] if pitem: @@ -539,13 +476,17 @@ async def html_dash(self, request): """ Render apps.yaml as an HTML page """ - self.default_page = "./dash" - text = self.get_header("Predbat Dashboard") - text += "\n" soc_perc = calc_percent_limit(self.base.soc_kw, self.base.soc_max) - text += self.get_status_html(soc_perc, self.base.current_status) - text += "\n" - return web.Response(content_type="text/html", text=text) + text = self.get_status_html(soc_perc, self.base.current_status) + + template = self.template_env.get_template("dash.html") + + context = { + "dash_html": text, + } + + rendered_template = template.render(context) + return web.Response(text=rendered_template, content_type="text/html") def prune_today(self, data, prune=True, group=15): """ @@ -691,31 +632,25 @@ async def html_charts(self, request): """ args = request.query chart = args.get("chart", "Battery") - self.default_page = "./charts?chart={}".format(chart) - text = self.get_header("Predbat Config") - text += "\n" - text += "

{} Chart

\n".format(chart) - text += '- Battery ' - text += 'Power ' - text += 'Cost ' - text += 'Rates ' - text += 'InDay ' - text += 'PV ' - text += 'PV7 ' - - text += '
' + + text = '
' text += self.get_chart(chart=chart) - text += "\n" - return web.Response(content_type="text/html", text=text) + + template = self.template_env.get_template("charts.html") + + context = { + "chart_html": text, + "chart_title": chart, + } + + rendered_template = template.render(context) + return web.Response(text=rendered_template, content_type="text/html") async def html_apps(self, request): """ Render apps.yaml as an HTML page """ - self.default_page = "./apps" - text = self.get_header("Predbat Config") - text += "\n" - text += "\n" + text = "
\n" text += "\n'.format(arg, self.render_type(arg, value)) text += "
NameValue\n" args = self.base.args @@ -730,18 +665,21 @@ async def html_apps(self, request): text += '
{}{}
" - text += "\n" - return web.Response(content_type="text/html", text=text) + + template = self.template_env.get_template("apps.html") + + context = { + "apps_html": text, + } + + rendered_template = template.render(context) + return web.Response(text=rendered_template, content_type="text/html") async def html_config(self, request): """ Return the Predbat config as an HTML page """ - - self.default_page = "./config" - text = self.get_header("Predbat Config", refresh=60) - text += "\n" - text += '
\n' + text = '\n' text += "\n" text += "\n" @@ -797,36 +735,49 @@ async def html_config(self, request): text += "
NameEntityTypeCurrentDefaultSelect
" text += "
" - text += "\n" - return web.Response(content_type="text/html", text=text) + + template = self.template_env.get_template("config.html") + + context = { + "config_html": text, + "refresh": 60, + } + + rendered_template = template.render(context) + return web.Response(text=rendered_template, content_type="text/html") async def html_menu(self, request): """ Return the Predbat Menu page as an HTML page """ - text = self.get_header("Predbat Menu") - text += "\n" - text += "\n" - text += '\n' - text += '\n' - text += '\n' - text += '\n' - text += '\n' - text += '\n' - text += '\n' - text += '\n' - text += "

Predbat

DashPlanChartsConfigapps.yamlLogDocs
\n" - return web.Response(content_type="text/html", text=text) + template = self.template_env.get_template("menu.html") + + context = {} + + rendered_template = template.render(context) + return web.Response(text=rendered_template, content_type="text/html") async def html_index(self, request): """ Return the Predbat index page as an HTML page """ - text = self.get_header("Predbat Index") - text += "\n" - text += '
\n' - text += '\n' - text += '\n'.format(self.default_page) - text += "
\n" - text += "\n" - return web.Response(content_type="text/html", text=text) + template = self.template_env.get_template("index.html") + + context = {} + + rendered_template = template.render(context) + return web.Response(text=rendered_template, content_type="text/html") + + async def html_docs(self, request): + """ + Returns the Github documentation for Predbat + """ + template = self.template_env.get_template("docs.html") + + context = {} + + rendered_template = template.render(context) + return web.Response(text=rendered_template, content_type="text/html") + + async def update_message(self, request): + return web.Response(text="Please update to the latest version of the add-on or Dockerfile, as you have missing dependencies", content_type="text/html") diff --git a/requirements.txt b/requirements.txt index d5dc8c4d5..419478eda 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ adbase appdaemon +Jinja2 mkdocs pre-commit pytz