Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .cspell/custom-dictionary-workspace.txt
Original file line number Diff line number Diff line change
Expand Up @@ -209,8 +209,8 @@ hadashboard
hahistory
hainterface
halfhourly
Hanchu
hanchu
Hanchu
hanchuess
hanres
HAOS
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,8 @@ it
# Runtime-generated manifest
apps/predbat/manifest.yaml

# Dev-deploy/standalone commit marker (see coverage/deploy, hass.py)
apps/predbat/git_version.txt

# Claude Code local worktrees
.claude/worktrees/
36 changes: 36 additions & 0 deletions apps/predbat/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,40 @@ def remove_file_quietly(filepath):
print("Warn: Failed to remove {}: {}".format(filepath, e))


def read_deploy_git_version(this_path):
"""
Read the git_version.txt marker, if present, written by a dev deploy
(coverage/deploy) or a standalone launch (hass.py) from a git checkout, so the
running code can show which commit is actually installed instead of just the
release tag baked into THIS_VERSION.

Args:
this_path (str): Directory to look for git_version.txt in, alongside predbat.py.
Returns:
str or None: The marker contents, or None if the file is absent or unreadable.
"""
filepath = os.path.join(this_path, "git_version.txt")
try:
if os.path.exists(filepath):
with open(filepath, "r") as f:
return f.read().strip() or None
except Exception as e:
print("Warn: Failed to read git_version.txt: {}".format(e))
return None


def clear_deploy_git_version(this_path):
"""
Remove the git_version.txt dev marker after a real update has installed official
release files, so a stale commit marker from an earlier dev deploy doesn't linger
and get shown as the running version once it's no longer accurate.

Args:
this_path (str): Directory the marker lives in, alongside predbat.py.
"""
remove_file_quietly(os.path.join(this_path, "git_version.txt"))


def remove_staged_files(this_path, files, tag):
"""
Remove staged files, used to clean up after an aborted update.
Expand Down Expand Up @@ -566,7 +600,9 @@ def predbat_update_move(version, files):
for file in files:
cmd += "mv -f {} {} && ".format(os.path.join(this_path, file + "." + tag), os.path.join(this_path, file))
cmd += "echo 'Update complete'"
clear_deploy_git_version(this_path)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might want to be before the os.system(cmd) as touching the files triggers a restart

os.system(cmd)

return True
return False

Expand Down
4 changes: 2 additions & 2 deletions apps/predbat/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from ha import run_async
from download import DEFAULT_PREDBAT_REPOSITORY, resolve_predbat_repository
from utils import dp1
from predbat import THIS_VERSION
from predbat import THIS_VERSION, THIS_VERSION_DISPLAY


class GitHub:
Expand Down Expand Up @@ -152,7 +152,7 @@ def download_predbat_releases(self):
self.releases["latest_beta_body"] = release.get("body", "Unknown")
found_latest_beta = True

self.log("Predbat {} repository {} version {} currently running, latest version is {}, latest beta is {}".format(__file__, repository, self.releases["this"], self.releases["latest"], self.releases["latest_beta"]))
self.log("Predbat {} repository {} version {} currently running, latest version is {}, latest beta is {}".format(__file__, repository, THIS_VERSION_DISPLAY, self.releases["latest"], self.releases["latest_beta"]))
PREDBAT_UPDATE_OPTIONS = ["main"]
this_tag = THIS_VERSION
new_version = False
Expand Down
27 changes: 26 additions & 1 deletion apps/predbat/hass.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,38 @@
import yaml
import sys
import asyncio
import os
import subprocess


def write_git_version_marker():
"""
Best-effort: when running from a git checkout - directly, or via the symlinked
.py files that coverage/standalone_ha sets up for a live-HA dev run - record the
commit as git_version.txt next to this file (predbat.py resolves its own __file__
to the same directory) so predbat.py can show it instead of just the release tag.
Runs before predbat is imported, since that's when predbat.py reads the marker.
Silently does nothing if git isn't available or this isn't a checkout.
"""
this_dir = os.path.dirname(os.path.abspath(__file__))
repo_dir = os.path.dirname(os.path.realpath(__file__))
try:
commit = subprocess.check_output(["git", "-C", repo_dir, "rev-parse", "--short", "HEAD"], stderr=subprocess.DEVNULL).decode().strip()
dirty = bool(subprocess.check_output(["git", "-C", repo_dir, "status", "--porcelain"], stderr=subprocess.DEVNULL).decode().strip())
with open(os.path.join(this_dir, "git_version.txt"), "w") as f:
f.write(commit + ("-dirty" if dirty else ""))
except Exception:
pass


write_git_version_marker()

import predbat
import time
from datetime import datetime, timedelta
from multiprocessing import set_start_method
import concurrent.futures
import threading
import os
import traceback


Expand Down
8 changes: 4 additions & 4 deletions apps/predbat/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@
import math
import copy
from html import escape as escape_html
from datetime import datetime, timedelta
from config import THIS_VERSION
from datetime import timedelta
from predbat import THIS_VERSION_DISPLAY
from const import TIME_FORMAT, PREDICT_STEP, EXPORT_LIMIT_FREEZE, EXPORT_LIMIT_IDLE, MINUTE_WATT
from utils import dp0, dp1, dp2, dp3, calc_percent_limit, minute_data, minute_data_state, find_charge_rate
from prediction import Prediction
Expand Down Expand Up @@ -2588,9 +2588,9 @@ def record_status(self, message, debug="", had_errors=False, notify=False, extra
"friendly_name": "Status",
"detail": extra,
"icon": "mdi:information",
"last_updated": str(datetime.now()),
"last_updated": self.now_utc_real.strftime(TIME_FORMAT),
"debug": debug,
"version": THIS_VERSION,
"version": THIS_VERSION_DISPLAY,
"error": (had_errors or self.had_errors),
"error_count": error_count,
},
Expand Down
13 changes: 10 additions & 3 deletions apps/predbat/predbat.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,19 @@
import asyncio

THIS_VERSION = "v8.51.1"
THIS_VERSION_DISPLAY = THIS_VERSION

from download import predbat_update_move, predbat_update_download, check_install, DEFAULT_PREDBAT_REPOSITORY
from download import predbat_update_move, predbat_update_download, check_install, read_deploy_git_version, DEFAULT_PREDBAT_REPOSITORY
from const import MINUTE_WATT

# Only do the self-install/self-update logic if we are NOT compiled.
if not IS_COMPILED:
# Show the actual commit for a dev deploy (coverage/deploy) or standalone git
# checkout (hass.py) rather than just the release tag - see git_version.txt
git_version = read_deploy_git_version(os.path.dirname(__file__))
if git_version:
THIS_VERSION_DISPLAY = "{} ({})".format(THIS_VERSION, git_version)

# Sanity check the install and re-download if corrupted
passed, modified = check_install(THIS_VERSION, repository=DEFAULT_PREDBAT_REPOSITORY)
if not passed:
Expand All @@ -53,7 +60,7 @@
elif modified:
print("Warn: Predbat files are installed but have modifications")
else:
print("Predbat files are installed correctly for version {}".format(THIS_VERSION))
print("Predbat files are installed correctly for version {}".format(THIS_VERSION_DISPLAY))
else:
# In compiled mode, we skip the entire self-update logic
print("Running in compiled mode; skipping local file checks and auto-update.")
Expand Down Expand Up @@ -1626,7 +1633,7 @@ def is_running(self):
return False

# Check if the last updated time is within the last 15 minutes
if (datetime.now() - predbat_last_updated).total_seconds() > 15 * 60:
if (datetime.now(timezone.utc) - predbat_last_updated).total_seconds() > 15 * 60:
return False
return True

Expand Down
4 changes: 2 additions & 2 deletions apps/predbat/userinterface.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
PREDBAT_MODE_MONITOR,
)
from config import CONFIG_API_OVERRIDE
from predbat import THIS_VERSION
from predbat import THIS_VERSION, THIS_VERSION_DISPLAY

DEBUG_EXCLUDE_LIST = [
"ha_interface",
Expand Down Expand Up @@ -817,7 +817,7 @@ def create_entity_list(self):
"""

text = ""
text += "# Predbat Dashboard - {}\n".format(THIS_VERSION)
text += "# Predbat Dashboard - {}\n".format(THIS_VERSION_DISPLAY)
text += "type: entities\n"
text += "Title: Predbat\n"
text += "entities:\n"
Expand Down
13 changes: 9 additions & 4 deletions apps/predbat/web.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@

from utils import calc_percent_limit, str2time, dp0, dp2, dp4, format_time_ago, get_override_time_from_string, history_attribute, prune_today, mask_secret_args
from const import TIME_FORMAT, TIME_FORMAT_DAILY, TIME_FORMAT_HA
from predbat import THIS_VERSION
from predbat import THIS_VERSION_DISPLAY
from component_base import ComponentBase
from config import APPS_SCHEMA
from web_annual import AnnualPage
Expand Down Expand Up @@ -808,6 +808,11 @@ def get_status_html(self, version):

status_entity = self.prefix + ".status"
last_updated = self.get_state_wrapper(status_entity, attribute="last_updated", default=None)
if last_updated:
try:
last_updated = str2time(last_updated).replace(tzinfo=None, microsecond=0)
except (ValueError, TypeError) as e:
self.log("Warn: Failed to parse last_updated time {}: {}".format(last_updated, e))
status = self.get_state_wrapper(status_entity, default="Unknown")
detail = self.get_state_wrapper(status_entity, attribute="detail", default="")
debug = self.get_state_wrapper(status_entity, attribute="debug", default="")
Expand Down Expand Up @@ -1607,7 +1612,7 @@ def get_header(self, title, refresh=0, codemirror=False):
if self.base.update_pending:
calculating = True
self.update_success_timestamp()
return get_header_html(title, calculating, self.default_page, self.arg_errors, THIS_VERSION, self.get_battery_status_icon(), refresh, codemirror=codemirror)
return get_header_html(title, calculating, self.default_page, self.arg_errors, THIS_VERSION_DISPLAY, self.get_battery_status_icon(), refresh, codemirror=codemirror)

def get_chart_series(self, name, results, chart_type, color):
"""
Expand Down Expand Up @@ -2831,7 +2836,7 @@ async def html_dash_content(self, request):
"""
Return just the dashboard body content for AJAX refresh (preserves scroll position)
"""
text = self.get_status_html(THIS_VERSION)
text = self.get_status_html(THIS_VERSION_DISPLAY)
return web.Response(content_type="text/html", text=text)

async def html_dash(self, request):
Expand Down Expand Up @@ -2880,7 +2885,7 @@ async def html_dash(self, request):
"""
text += "<body>\n"
text += '<div id="dash-content-container">\n'
text += self.get_status_html(THIS_VERSION)
text += self.get_status_html(THIS_VERSION_DISPLAY)
text += "</div>\n"
text += "</body></html>\n"
return web.Response(content_type="text/html", text=text)
Expand Down
5 changes: 5 additions & 0 deletions coverage/deploy
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
GIT_HASH=$(git -C .. rev-parse --short HEAD)
if [ -n "$(git -C .. status --porcelain)" ]; then
GIT_HASH="${GIT_HASH}-dirty"
fi
echo "$GIT_HASH" > /Volumes/addon_configs/6adb4f0d_predbat/git_version.txt
cp ../apps/predbat/*.py /Volumes/addon_configs/6adb4f0d_predbat
cp ../apps/predbat/*.proto /Volumes/addon_configs/6adb4f0d_predbat
cp ../apps/predbat/*.so /Volumes/addon_configs/6adb4f0d_predbat
Loading