Skip to content

Commit 339b44a

Browse files
Merge pull request #5355 from springfall2008/fix/entity-history-mid-window-change
fix(web): show the /entity history row that follows a mid-window change
2 parents a5d92a9 + 681343e commit 339b44a

2 files changed

Lines changed: 75 additions & 2 deletions

File tree

‎apps/predbat/tests/test_web_history_table.py‎

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ def run_web_history_table_tests(my_predbat):
146146
}
147147
filled_30, filled_5, sorted_ts_30, display_slots_5 = build_entity_history_table_data(selections, fetch)
148148

149-
expected_30 = {utc(10, 0), utc(23, 30)}
149+
# 10:30 is the row that first reports the 10:07 reading; 00:00 would be past the newest record
150+
expected_30 = {utc(10, 0), utc(10, 30), utc(23, 30)}
150151
if set(sorted_ts_30) != expected_30:
151152
print(f" ERROR: expected 30-min rows {expected_30}, got {set(sorted_ts_30)}")
152153
failed += 1
@@ -202,4 +203,62 @@ def run_web_history_table_tests(my_predbat):
202203
print(f" ERROR: expected the unusable records to be skipped, got rows {sorted_ts_30}")
203204
failed += 1
204205

206+
# -------------------------------------------------------------------------
207+
# predbat.status only records on a change, so a window nothing was recorded in gets no row and
208+
# the page folds it into "N unchanged slots hidden". A change part way through a window (23:10)
209+
# is therefore never reported by a row: 23:00 still reads Exporting, 23:30 to 04:00 are hidden as
210+
# "unchanged" beneath it, and the 04:30 row is the first to say Charging - flagged as a change
211+
# five hours late, in the very window the charge ended. The timeline chart showed it correctly.
212+
print("Test: a change part way through a window is reported by the next row, not hidden as unchanged")
213+
selections = [{"entity_id": "predbat.status", "attribute": None}]
214+
215+
def stamp(day, hour, minute):
216+
"""Build a UTC timestamp on 2026-10-<day>."""
217+
return datetime(2026, 10, day, hour, minute, 0, tzinfo=timezone.utc)
218+
219+
transitions = [
220+
(stamp(1, 22, 0), "Exporting"),
221+
(stamp(1, 23, 10), "Charging"),
222+
(stamp(2, 4, 35), "Demand"),
223+
(stamp(2, 13, 35), "Freeze exporting"),
224+
]
225+
records = [{"last_updated": when.strftime("%Y-%m-%dT%H:%M:%S%z"), "state": state} for when, state in transitions]
226+
filled_30, filled_5, sorted_ts_30, display_slots_5 = build_entity_history_table_data(selections, {"predbat.status": make_history(records)})
227+
228+
def actual_state(when):
229+
"""Return the state the entity really held at the given time."""
230+
held = "-"
231+
for changed_at, state in transitions:
232+
if changed_at <= when:
233+
held = state
234+
return held
235+
236+
row_2330 = stamp(1, 23, 30)
237+
if row_2330 not in sorted_ts_30:
238+
print(f" ERROR: the status became Charging at 23:10 so the 23:30 row must be shown, rows are {[ts.strftime('%d %H:%M') for ts in sorted_ts_30]}")
239+
failed += 1
240+
else:
241+
value, changed, prev_value = filled_30[0][row_2330]
242+
if value != "Charging" or not changed or prev_value != "Exporting":
243+
print(f" ERROR: expected the 23:30 row to flag the Exporting->Charging change, got value={value} changed={changed} prev={prev_value}")
244+
failed += 1
245+
246+
value, changed, prev_value = filled_30[0][stamp(2, 4, 30)]
247+
if value != "Charging" or changed:
248+
print(f" ERROR: Charging began at 23:10, so the 04:30 row should read 'Charging' without flagging a change, got value={value} changed={changed} prev={prev_value}")
249+
failed += 1
250+
251+
# The page presents every 30-min slot missing between two rows as unchanged from the older row
252+
misreported = []
253+
for newer, older in zip(sorted_ts_30, sorted_ts_30[1:]):
254+
shown = filled_30[0][older][0]
255+
slot = older + timedelta(minutes=30)
256+
while slot < newer:
257+
if actual_state(slot) != shown:
258+
misreported.append("{} was {} but is hidden as unchanged from {}".format(slot.strftime("%d %H:%M"), actual_state(slot), shown))
259+
slot += timedelta(minutes=30)
260+
if misreported:
261+
print(f" ERROR: {len(misreported)} hidden slots did not hold the value of the row beneath them, e.g. {misreported[0]}")
262+
failed += 1
263+
205264
return failed

‎apps/predbat/web.py‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ def build_entity_history_table_data(entity_selections, entity_data_fetch):
152152
"""
153153
entity_records = []
154154
all_timestamps_30min = set()
155+
following_timestamps_30min = set()
155156

156157
for selection in entity_selections:
157158
entity_id = selection["entity_id"]
@@ -184,13 +185,26 @@ def build_entity_history_table_data(entity_selections, entity_data_fetch):
184185
# A record makes the window it landed in a row, so activity is always on screen
185186
minutes = last_updated_stamp.hour * 60 + last_updated_stamp.minute
186187
rounded_minutes_30 = (minutes // 30) * 30
187-
all_timestamps_30min.add(last_updated_stamp.replace(minute=rounded_minutes_30 % 60, hour=rounded_minutes_30 // 60, second=0, microsecond=0))
188+
window_start = last_updated_stamp.replace(minute=rounded_minutes_30 % 60, hour=rounded_minutes_30 // 60, second=0, microsecond=0)
189+
all_timestamps_30min.add(window_start)
190+
191+
# A row reports the state at its own start, so a change part way through a window
192+
# first shows in the NEXT row. An entity that only records on a change leaves that
193+
# window empty, and without a row there the change was hidden as "unchanged" until
194+
# the next record - which was then flagged as the change, hours late.
195+
following_timestamps_30min.add(window_start + timedelta(minutes=30))
188196

189197
# str2time is only reliable for ordering once parsed - the raw strings mix UTC history with
190198
# the local-time "now" record get_history_with_now() appends
191199
records.sort(key=lambda record: record[0])
192200
entity_records.append(records)
193201

202+
# Never add a row beyond the newest record, that would be a row in the future
203+
newest_record = max((records[-1][0] for records in entity_records if records), default=None)
204+
for following in following_timestamps_30min:
205+
if following <= newest_record:
206+
all_timestamps_30min.add(following)
207+
194208
# Sort timestamps in reverse chronological order
195209
sorted_timestamps_30min = sorted(all_timestamps_30min, reverse=True)
196210

0 commit comments

Comments
 (0)