Web UI improvements, Axle update sensor every minute#3333
Web UI improvements, Axle update sensor every minute#3333springfall2008 merged 6 commits intomainfrom
Conversation
Update Axle event sensor every minute - #3318
There was a problem hiding this comment.
Pull request overview
This PR enhances Predbat’s built-in web UI by improving the Apps.yaml editor experience (detecting external file changes), adding a new “Savings” chart, and making the Components page easier to scan/filter.
Changes:
- Add Apps.yaml external-change detection via checksum polling and a new
/apps_editor_checksumendpoint. - Add a new “Savings” chart and simplify charts menu active-state handling.
- Improve Components page status summary/sorting and add a “show disabled components” toggle; expand web interface endpoint testing.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/predbat/web_helper.py | Adds JS polling logic to detect external Apps.yaml changes and warn/auto-reload. |
| apps/predbat/web.py | Adds checksum endpoint, fixes /api/state POST parsing, adds “Savings” chart, improves charts/components UI. |
| apps/predbat/tests/test_web_if.py | Expands web interface tests to hit more endpoints and runs in a temp directory. |
| apps/predbat/ha.py | Extends service name parsing to support both domain/service and domain.service. |
| apps/predbat/axle.py | Publishes Axle event state every minute to keep “active” status current between API polls. |
| # /api/ping returns 500 when Predbat isn't fully initialized (expected in test) | ||
| # Other endpoints may return 400 for missing optional params, which is fine | ||
| if res.status_code in [200, 400, 404, 500]: | ||
| accessed_endpoints.add(("GET", page)) | ||
| else: | ||
| print("ERROR: Unexpected status from {} got {} value {}".format(address, res.status_code, res.text)) | ||
| failed = 1 | ||
|
|
There was a problem hiding this comment.
The GET endpoint loop treats 404 (and 500) as acceptable for all GET endpoints, which can mask real regressions (missing routes should fail the test). It would be more effective to fail on 404 by default and only allow specific non-200 statuses for the few endpoints where they’re expected (e.g., /api/ping returning 500 in this test harness).
| elif "." in service: | ||
| domain, service = service.split(".") | ||
| else: | ||
| domain = "" |
There was a problem hiding this comment.
If service contains neither '/' nor '.', this now sets domain = "" and continues, which can hide invalid inputs (e.g. from the web API) and lead to confusing failures downstream. Prefer to reject/raise on invalid service formats (or return an explicit error) rather than calling HA/loopback with an empty domain.
| domain = "" | |
| # Invalid service format: must be 'domain/service' or 'domain.service' | |
| raise ValueError("Invalid service format: {}. Expected 'domain/service' or 'domain.service'".format(service)) |
| Return the current checksum and content of apps.yaml for external change detection | ||
| """ | ||
| try: | ||
| apps_yaml_path = "apps.yaml" | ||
| with open(apps_yaml_path, "r") as f: | ||
| content = f.read() | ||
| checksum = hashlib.md5(content.encode("utf-8")).hexdigest() | ||
| return web.json_response({"checksum": checksum, "content": content}) |
There was a problem hiding this comment.
/apps_editor_checksum returns the full apps.yaml content on every poll. Since the editor JS calls this endpoint on an interval, this can generate unnecessary disk reads and network traffic for large files. Consider returning only the checksum by default (or supporting conditional requests like ETag/If-None-Match) and only returning content when the client indicates its checksum is stale (or requests ?include_content=1).
| Return the current checksum and content of apps.yaml for external change detection | |
| """ | |
| try: | |
| apps_yaml_path = "apps.yaml" | |
| with open(apps_yaml_path, "r") as f: | |
| content = f.read() | |
| checksum = hashlib.md5(content.encode("utf-8")).hexdigest() | |
| return web.json_response({"checksum": checksum, "content": content}) | |
| Return the current checksum (and optionally content) of apps.yaml for external change detection | |
| """ | |
| try: | |
| # By default we preserve existing behaviour and return both checksum and content. | |
| # Callers that only need the checksum can pass ?include_content=0 to reduce payload size. | |
| include_content = request.rel_url.query.get("include_content", "1") | |
| apps_yaml_path = "apps.yaml" | |
| with open(apps_yaml_path, "r") as f: | |
| content = f.read() | |
| checksum = hashlib.md5(content.encode("utf-8")).hexdigest() | |
| response_data = {"checksum": checksum} | |
| # Treat common "false" values as a request to omit content | |
| if include_content.lower() not in ("0", "false", "no"): | |
| response_data["content"] = content | |
| return web.json_response(response_data) |
apps/predbat/web_helper.py
Outdated
| // Check every 2 seconds for external changes | ||
| setInterval(checkForExternalChanges, 2000); |
There was a problem hiding this comment.
The editor polls for external changes every 2 seconds, which can create a steady stream of requests (and currently downloads the full apps.yaml content each time). Consider increasing the interval and/or pausing polling when the tab is hidden, and combine with a checksum-only endpoint so polling is lightweight.
| # Test /component_restart POST | ||
| print("Test POST /component_restart") | ||
| address = "http://127.0.0.1:5052/component_restart" | ||
| data = {"component_name": "web"} | ||
| res = requests.post(address, data=data) | ||
| if res.status_code in [200, 302, 303, 400]: | ||
| accessed_endpoints.add(("POST", "/component_restart")) | ||
| else: | ||
| print("ERROR: Unexpected response from /component_restart: {} - {}".format(res.status_code, res.text)) | ||
| failed = 1 |
There was a problem hiding this comment.
This POST test uses form field component_name, but the /component_restart handler reads component (see component_name = data.get("component") in web.py). As written the test will always hit the error path (400) and won’t validate the happy-path behavior. Update the test payload to send the expected field name so the endpoint is meaningfully exercised.
No description provided.