Skip to content

Commit 551077f

Browse files
Add a git_version file written by deploy and hass to indicate what the running version is rather than a tag
Also fix record_status() writing the status entity's last_updated attribute via str(datetime.now()) - a naive, sub-second-precision string in a format str2time() can't parse - so the web dashboard's Last Updated field rendered raw microseconds instead of a clean timestamp. Now written via TIME_FORMAT like last_started already is, and parsed/rounded to whole seconds for display in web.py. Uses self.now_utc_real rather than self.now_utc: the latter is deliberately snapped to the PREDICT_STEP grid for the simulation (see update_time()), so using it here made Last Updated show a rounded, past minute instead of the actual time record_status() ran. now_utc_real is the same true wall-clock source last_started already uses (self.started_time = self.now_utc_real). Also fixes is_running(): it reads the same last_updated attribute, and its naive datetime.now() comparison broke once the attribute became tz-aware ("can't subtract offset-naive and offset-aware datetimes"), which was showing as Unhealthy on the web dashboard. Now compares against datetime.now(timezone.utc). Reorders predbat_update_move() per review: clear_deploy_git_version() now runs before the mv, so the marker is gone before any new process (including one started by a hot-reload the mv itself triggers) could read it.
1 parent aaba48f commit 551077f

9 files changed

Lines changed: 97 additions & 16 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,5 +112,8 @@ it
112112
# Runtime-generated manifest
113113
apps/predbat/manifest.yaml
114114

115+
# Dev-deploy/standalone commit marker (see coverage/deploy, hass.py)
116+
apps/predbat/git_version.txt
117+
115118
# Claude Code local worktrees
116119
.claude/worktrees/

apps/predbat/download.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,40 @@ def remove_file_quietly(filepath):
145145
print("Warn: Failed to remove {}: {}".format(filepath, e))
146146

147147

148+
def read_deploy_git_version(this_path):
149+
"""
150+
Read the git_version.txt marker, if present, written by a dev deploy
151+
(coverage/deploy) or a standalone launch (hass.py) from a git checkout, so the
152+
running code can show which commit is actually installed instead of just the
153+
release tag baked into THIS_VERSION.
154+
155+
Args:
156+
this_path (str): Directory to look for git_version.txt in, alongside predbat.py.
157+
Returns:
158+
str or None: The marker contents, or None if the file is absent or unreadable.
159+
"""
160+
filepath = os.path.join(this_path, "git_version.txt")
161+
try:
162+
if os.path.exists(filepath):
163+
with open(filepath, "r") as f:
164+
return f.read().strip() or None
165+
except Exception as e:
166+
print("Warn: Failed to read git_version.txt: {}".format(e))
167+
return None
168+
169+
170+
def clear_deploy_git_version(this_path):
171+
"""
172+
Remove the git_version.txt dev marker after a real update has installed official
173+
release files, so a stale commit marker from an earlier dev deploy doesn't linger
174+
and get shown as the running version once it's no longer accurate.
175+
176+
Args:
177+
this_path (str): Directory the marker lives in, alongside predbat.py.
178+
"""
179+
remove_file_quietly(os.path.join(this_path, "git_version.txt"))
180+
181+
148182
def remove_staged_files(this_path, files, tag):
149183
"""
150184
Remove staged files, used to clean up after an aborted update.
@@ -566,7 +600,9 @@ def predbat_update_move(version, files):
566600
for file in files:
567601
cmd += "mv -f {} {} && ".format(os.path.join(this_path, file + "." + tag), os.path.join(this_path, file))
568602
cmd += "echo 'Update complete'"
603+
clear_deploy_git_version(this_path)
569604
os.system(cmd)
605+
570606
return True
571607
return False
572608

apps/predbat/github.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from ha import run_async
1414
from download import DEFAULT_PREDBAT_REPOSITORY, resolve_predbat_repository
1515
from utils import dp1
16-
from predbat import THIS_VERSION
16+
from predbat import THIS_VERSION, THIS_VERSION_DISPLAY
1717

1818

1919
class GitHub:
@@ -152,7 +152,7 @@ def download_predbat_releases(self):
152152
self.releases["latest_beta_body"] = release.get("body", "Unknown")
153153
found_latest_beta = True
154154

155-
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"]))
155+
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"]))
156156
PREDBAT_UPDATE_OPTIONS = ["main"]
157157
this_tag = THIS_VERSION
158158
new_version = False

apps/predbat/hass.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,38 @@
1010
import yaml
1111
import sys
1212
import asyncio
13+
import os
14+
import subprocess
15+
16+
17+
def write_git_version_marker():
18+
"""
19+
Best-effort: when running from a git checkout - directly, or via the symlinked
20+
.py files that coverage/standalone_ha sets up for a live-HA dev run - record the
21+
commit as git_version.txt next to this file (predbat.py resolves its own __file__
22+
to the same directory) so predbat.py can show it instead of just the release tag.
23+
Runs before predbat is imported, since that's when predbat.py reads the marker.
24+
Silently does nothing if git isn't available or this isn't a checkout.
25+
"""
26+
this_dir = os.path.dirname(os.path.abspath(__file__))
27+
repo_dir = os.path.dirname(os.path.realpath(__file__))
28+
try:
29+
commit = subprocess.check_output(["git", "-C", repo_dir, "rev-parse", "--short", "HEAD"], stderr=subprocess.DEVNULL).decode().strip()
30+
dirty = bool(subprocess.check_output(["git", "-C", repo_dir, "status", "--porcelain"], stderr=subprocess.DEVNULL).decode().strip())
31+
with open(os.path.join(this_dir, "git_version.txt"), "w") as f:
32+
f.write(commit + ("-dirty" if dirty else ""))
33+
except Exception:
34+
pass
35+
36+
37+
write_git_version_marker()
38+
1339
import predbat
1440
import time
1541
from datetime import datetime, timedelta
1642
from multiprocessing import set_start_method
1743
import concurrent.futures
1844
import threading
19-
import os
2045
import traceback
2146

2247

apps/predbat/output.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@
1919
import math
2020
import copy
2121
from html import escape as escape_html
22-
from datetime import datetime, timedelta
23-
from config import THIS_VERSION
22+
from datetime import timedelta
23+
from predbat import THIS_VERSION_DISPLAY
2424
from const import TIME_FORMAT, PREDICT_STEP, EXPORT_LIMIT_FREEZE, EXPORT_LIMIT_IDLE, MINUTE_WATT
2525
from utils import dp0, dp1, dp2, dp3, calc_percent_limit, minute_data, minute_data_state, find_charge_rate
2626
from prediction import Prediction
@@ -2588,9 +2588,9 @@ def record_status(self, message, debug="", had_errors=False, notify=False, extra
25882588
"friendly_name": "Status",
25892589
"detail": extra,
25902590
"icon": "mdi:information",
2591-
"last_updated": str(datetime.now()),
2591+
"last_updated": self.now_utc_real.strftime(TIME_FORMAT),
25922592
"debug": debug,
2593-
"version": THIS_VERSION,
2593+
"version": THIS_VERSION_DISPLAY,
25942594
"error": (had_errors or self.had_errors),
25952595
"error_count": error_count,
25962596
},

apps/predbat/predbat.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,19 @@
3636
import asyncio
3737

3838
THIS_VERSION = "v8.51.1"
39+
THIS_VERSION_DISPLAY = THIS_VERSION
3940

40-
from download import predbat_update_move, predbat_update_download, check_install, DEFAULT_PREDBAT_REPOSITORY
41+
from download import predbat_update_move, predbat_update_download, check_install, read_deploy_git_version, DEFAULT_PREDBAT_REPOSITORY
4142
from const import MINUTE_WATT
4243

4344
# Only do the self-install/self-update logic if we are NOT compiled.
4445
if not IS_COMPILED:
46+
# Show the actual commit for a dev deploy (coverage/deploy) or standalone git
47+
# checkout (hass.py) rather than just the release tag - see git_version.txt
48+
git_version = read_deploy_git_version(os.path.dirname(__file__))
49+
if git_version:
50+
THIS_VERSION_DISPLAY = "{} ({})".format(THIS_VERSION, git_version)
51+
4552
# Sanity check the install and re-download if corrupted
4653
passed, modified = check_install(THIS_VERSION, repository=DEFAULT_PREDBAT_REPOSITORY)
4754
if not passed:
@@ -53,7 +60,7 @@
5360
elif modified:
5461
print("Warn: Predbat files are installed but have modifications")
5562
else:
56-
print("Predbat files are installed correctly for version {}".format(THIS_VERSION))
63+
print("Predbat files are installed correctly for version {}".format(THIS_VERSION_DISPLAY))
5764
else:
5865
# In compiled mode, we skip the entire self-update logic
5966
print("Running in compiled mode; skipping local file checks and auto-update.")
@@ -1626,7 +1633,7 @@ def is_running(self):
16261633
return False
16271634

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

apps/predbat/userinterface.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
PREDBAT_MODE_MONITOR,
3131
)
3232
from config import CONFIG_API_OVERRIDE
33-
from predbat import THIS_VERSION
33+
from predbat import THIS_VERSION, THIS_VERSION_DISPLAY
3434

3535
DEBUG_EXCLUDE_LIST = [
3636
"ha_interface",
@@ -817,7 +817,7 @@ def create_entity_list(self):
817817
"""
818818

819819
text = ""
820-
text += "# Predbat Dashboard - {}\n".format(THIS_VERSION)
820+
text += "# Predbat Dashboard - {}\n".format(THIS_VERSION_DISPLAY)
821821
text += "type: entities\n"
822822
text += "Title: Predbat\n"
823823
text += "entities:\n"

apps/predbat/web.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@
7171

7272
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
7373
from const import TIME_FORMAT, TIME_FORMAT_DAILY, TIME_FORMAT_HA
74-
from predbat import THIS_VERSION
74+
from predbat import THIS_VERSION_DISPLAY
7575
from component_base import ComponentBase
7676
from config import APPS_SCHEMA
7777
from web_annual import AnnualPage
@@ -808,6 +808,11 @@ def get_status_html(self, version):
808808

809809
status_entity = self.prefix + ".status"
810810
last_updated = self.get_state_wrapper(status_entity, attribute="last_updated", default=None)
811+
if last_updated:
812+
try:
813+
last_updated = str2time(last_updated).replace(tzinfo=None, microsecond=0)
814+
except (ValueError, TypeError) as e:
815+
self.log("Warn: Failed to parse last_updated time {}: {}".format(last_updated, e))
811816
status = self.get_state_wrapper(status_entity, default="Unknown")
812817
detail = self.get_state_wrapper(status_entity, attribute="detail", default="")
813818
debug = self.get_state_wrapper(status_entity, attribute="debug", default="")
@@ -1607,7 +1612,7 @@ def get_header(self, title, refresh=0, codemirror=False):
16071612
if self.base.update_pending:
16081613
calculating = True
16091614
self.update_success_timestamp()
1610-
return get_header_html(title, calculating, self.default_page, self.arg_errors, THIS_VERSION, self.get_battery_status_icon(), refresh, codemirror=codemirror)
1615+
return get_header_html(title, calculating, self.default_page, self.arg_errors, THIS_VERSION_DISPLAY, self.get_battery_status_icon(), refresh, codemirror=codemirror)
16111616

16121617
def get_chart_series(self, name, results, chart_type, color):
16131618
"""
@@ -2831,7 +2836,7 @@ async def html_dash_content(self, request):
28312836
"""
28322837
Return just the dashboard body content for AJAX refresh (preserves scroll position)
28332838
"""
2834-
text = self.get_status_html(THIS_VERSION)
2839+
text = self.get_status_html(THIS_VERSION_DISPLAY)
28352840
return web.Response(content_type="text/html", text=text)
28362841

28372842
async def html_dash(self, request):
@@ -2880,7 +2885,7 @@ async def html_dash(self, request):
28802885
"""
28812886
text += "<body>\n"
28822887
text += '<div id="dash-content-container">\n'
2883-
text += self.get_status_html(THIS_VERSION)
2888+
text += self.get_status_html(THIS_VERSION_DISPLAY)
28842889
text += "</div>\n"
28852890
text += "</body></html>\n"
28862891
return web.Response(content_type="text/html", text=text)

coverage/deploy

100644100755
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
GIT_HASH=$(git -C .. rev-parse --short HEAD)
2+
if [ -n "$(git -C .. status --porcelain)" ]; then
3+
GIT_HASH="${GIT_HASH}-dirty"
4+
fi
5+
echo "$GIT_HASH" > /Volumes/addon_configs/6adb4f0d_predbat/git_version.txt
16
cp ../apps/predbat/*.py /Volumes/addon_configs/6adb4f0d_predbat
27
cp ../apps/predbat/*.proto /Volumes/addon_configs/6adb4f0d_predbat
38
cp ../apps/predbat/*.so /Volumes/addon_configs/6adb4f0d_predbat

0 commit comments

Comments
 (0)