@@ -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
0 commit comments