Skip to content

Commit 8ad3d7a

Browse files
author
=
committed
Add dashboard update indicator
1 parent 0207f5f commit 8ad3d7a

7 files changed

Lines changed: 126 additions & 7 deletions

File tree

‎apps/predbat/frontend.zip‎

493 Bytes
Binary file not shown.

‎apps/predbat/tests/test_web_if.py‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,14 @@ def _error_total(page):
229229
print("ERROR: /api/browse allowed directory traversal")
230230
failed = 1
231231

232-
my_predbat.set_state_wrapper("update.predbat_version", "on", attributes={"in_progress": True})
233-
if requests.get(base_url + "/api/status").json().get("updating") is not True:
232+
my_predbat.set_state_wrapper("update.predbat_version", "on", attributes={"in_progress": True, "latest_version": "v99.0.0"})
233+
update_status = requests.get(base_url + "/api/status").json()
234+
if update_status.get("updating") is not True:
234235
print("ERROR: /api/status did not report an in-progress Predbat update")
235236
failed = 1
237+
if update_status.get("update_available") is not True or update_status.get("latest_version") != "v99.0.0":
238+
print("ERROR: /api/status did not report the available Predbat update")
239+
failed = 1
236240
my_predbat.set_state_wrapper("update.predbat_version", "on", attributes={"in_progress": False})
237241

238242
print("Test apps.yaml editor API and generated schema")

‎apps/predbat/web.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3015,7 +3015,12 @@ async def html_api_get_status(self, request):
30153015

30163016
predbat_active, _ = self.get_ha_config("active", None)
30173017

3018-
updating = self.get_state_wrapper("update.predbat_version", attribute="in_progress", default=False) is True
3018+
update_entity = "update.predbat_version"
3019+
updating = self.get_state_wrapper(update_entity, attribute="in_progress", default=False) is True
3020+
update_available = self.get_state_wrapper(update_entity, default="off") == "on"
3021+
latest_version = self.get_state_wrapper(update_entity, attribute="latest_version", default=None)
3022+
if latest_version in (None, "", "unknown", "unavailable", "check HACS"):
3023+
latest_version = None
30193024

30203025
# Configuration health
30213026
config_errors = len(self.arg_errors)
@@ -3031,6 +3036,8 @@ async def html_api_get_status(self, request):
30313036
"last_updated": last_updated,
30323037
"last_started": last_started,
30333038
"version": THIS_VERSION_DISPLAY,
3039+
"latest_version": latest_version,
3040+
"update_available": update_available,
30343041
# Predbat configuration
30353042
"mode": mode,
30363043
"debug_enable": debug_enable,

‎frontend/src/App.tsx‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,14 @@ function App() {
108108
* consecutive failures before the user is told about it.
109109
*/
110110
const [controlError, setControlError] = useState<string | null>(null)
111+
const [updateRequested, setUpdateRequested] = useState(false)
112+
113+
useEffect(() => {
114+
if (!updateRequested) return
115+
116+
const timeout = window.setTimeout(() => setUpdateRequested(false), 15_000)
117+
return () => window.clearTimeout(timeout)
118+
}, [updateRequested])
111119

112120
/*
113121
* Consecutive failure count for each endpoint.
@@ -238,6 +246,7 @@ function App() {
238246
previousCalculatingRef.current = data.calculating
239247

240248
setStatusData(data)
249+
if (data.updating) setUpdateRequested(false)
241250

242251
/*
243252
* Predbat has just completed a calculation.
@@ -317,6 +326,36 @@ function App() {
317326
})
318327
}
319328

329+
async function installLatestUpdate() {
330+
setUpdateRequested(true)
331+
332+
try {
333+
const response = await fetch('./api/service', {
334+
method: 'POST',
335+
headers: { 'Content-Type': 'application/json' },
336+
body: JSON.stringify({
337+
service: 'update/install',
338+
data: { entity_id: 'update.predbat_version' }
339+
})
340+
})
341+
342+
if (!response.ok) throw new Error(`HTTP ${response.status}`)
343+
344+
const result = await response.json() as { result?: unknown } | null
345+
if (result?.result === 'error') throw new Error('Home Assistant rejected the update request')
346+
347+
setControlError(null)
348+
fetchStatus().catch(() => {
349+
// Polling will retry if Predbat restarts before returning status.
350+
})
351+
} catch (error) {
352+
console.error('Unable to start the Predbat update:', error)
353+
const detail = error instanceof Error ? error.message : 'Unknown error'
354+
setControlError(`Predbat could not start the update. ${detail}`)
355+
setUpdateRequested(false)
356+
}
357+
}
358+
320359
/*
321360
* Manual retry used by the error notice.
322361
*/
@@ -569,6 +608,9 @@ function App() {
569608
status={statusData.status}
570609
mode={statusData.mode}
571610
version={statusData.version}
611+
latestVersion={statusData.latest_version}
612+
updateAvailable={statusData.update_available}
613+
updating={updating || updateRequested}
572614
lastUpdated={statusData.last_updated}
573615
configOk={statusData.config_ok}
574616

@@ -583,6 +625,8 @@ function App() {
583625
onReadOnlyChange={(value) => updateControl('set_read_only', value)}
584626

585627
onDebugChange={(value) => updateControl('debug_enable', value)}
628+
629+
onUpdate={installLatestUpdate}
586630
/>
587631

588632
<PlanSummary plan={planData.plan} />

‎frontend/src/components/StatusCard.css‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,43 @@
2626
color: var(--color-text-muted);
2727
}
2828

29+
.status-card-version-row {
30+
display: flex;
31+
align-items: center;
32+
justify-content: flex-end;
33+
gap: 0.6rem;
34+
}
35+
36+
.status-card-update {
37+
display: inline-flex;
38+
align-items: center;
39+
gap: 0.3rem;
40+
min-height: 1.75rem;
41+
padding: 0.25rem 0.55rem;
42+
color: var(--color-success);
43+
background: color-mix(in srgb, var(--color-success) 12%, transparent);
44+
border: 1px solid color-mix(in srgb, var(--color-success) 45%, transparent);
45+
border-radius: 999px;
46+
font: inherit;
47+
font-size: 0.72rem;
48+
font-weight: 700;
49+
cursor: pointer;
50+
}
51+
52+
.status-card-update:hover:not(:disabled) {
53+
background: color-mix(in srgb, var(--color-success) 20%, transparent);
54+
}
55+
56+
.status-card-update:focus-visible {
57+
outline: 2px solid var(--color-primary);
58+
outline-offset: 2px;
59+
}
60+
61+
.status-card-update:disabled {
62+
opacity: 0.65;
63+
cursor: wait;
64+
}
65+
2966
.status-card-label,
3067
.status-card-detail > span {
3168
font-size: 0.72rem;

‎frontend/src/components/StatusCard.tsx‎

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
2+
import { faCircleArrowUp } from '@fortawesome/free-solid-svg-icons'
3+
14
import './StatusCard.css'
25

36
type StatusCardProps = {
47
status: string
58
mode: string
69
version: string
10+
latestVersion: string | null
11+
updateAvailable: boolean
12+
updating: boolean
713

814
lastUpdated: string | null
915

@@ -17,6 +23,7 @@ type StatusCardProps = {
1723
onActiveChange: (value: boolean) => void
1824
onReadOnlyChange: (value: boolean) => void
1925
onDebugChange: (value: boolean) => void
26+
onUpdate: () => void
2027
}
2128

2229
// Relative labels refresh when the parent supplies the next status poll.
@@ -67,6 +74,9 @@ function StatusCard({
6774
status,
6875
mode,
6976
version,
77+
latestVersion,
78+
updateAvailable,
79+
updating,
7080
lastUpdated,
7181
configOk,
7282
active,
@@ -75,7 +85,8 @@ function StatusCard({
7585
onModeChange,
7686
onActiveChange,
7787
onReadOnlyChange,
78-
onDebugChange
88+
onDebugChange,
89+
onUpdate
7990
}: StatusCardProps) {
8091
const statusHealth = getStatusHealth(status)
8192

@@ -85,9 +96,23 @@ function StatusCard({
8596
<div className="status-card-header">
8697
<h2>Predbat Status</h2>
8798

88-
<span className="status-card-version">
89-
Predbat {version}
90-
</span>
99+
<div className="status-card-version-row">
100+
<span className="status-card-version">Predbat {version}</span>
101+
102+
{updateAvailable && (
103+
<button
104+
type="button"
105+
className="status-card-update"
106+
disabled={updating}
107+
title={latestVersion ? `Install Predbat ${latestVersion}` : 'Install the latest Predbat update'}
108+
aria-label={latestVersion ? `Update Predbat to ${latestVersion}` : 'Update Predbat'}
109+
onClick={onUpdate}
110+
>
111+
<FontAwesomeIcon icon={faCircleArrowUp} aria-hidden="true" />
112+
{updating ? 'Updating' : 'Update'}
113+
</button>
114+
)}
115+
</div>
91116
</div>
92117

93118
{/* Current status */}

‎frontend/src/types/status.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export type PredbatStatus = {
1010
last_started: string | null
1111

1212
version: string
13+
latest_version: string | null
14+
update_available: boolean
1315

1416
mode: string
1517
debug_enable: boolean

0 commit comments

Comments
 (0)