01 — Time grid¶
Concept: every series lives on a TimeGrid: a half-open range [start, end) held in UTC and cut into equal steps that divide an hour (5, 10, 15, 20, 30 or 60 minutes).
You will learn: how to build a grid, why it is held in UTC, how daylight-saving days behave, and why invalid grids are refused instead of rounded.
from datetime import UTC, datetime, timedelta
from zoneinfo import ZoneInfo
from myenergy.domain.errors import GridError
from myenergy.domain.time import TimeGrid
PARIS = ZoneInfo("Europe/Paris")
day = TimeGrid(
start=datetime(2026, 1, 1, tzinfo=PARIS),
end=datetime(2026, 1, 2, tzinfo=PARIS),
step=timedelta(minutes=15),
)
len(day), day.step_hours, day.start
(96, 0.25, datetime.datetime(2025, 12, 31, 23, 0, tzinfo=datetime.timezone.utc))
The start is given on the Paris wall clock but stored in UTC: midnight in Paris is 23:00 UTC in winter. The grid keeps its timezone only for wall-clock questions such as is this an off-peak hour?. A step is labelled by its start, and local() shows it on the wall clock.
[day.local(i).strftime("%H:%M") for i in range(4)]
['00:00', '00:15', '00:30', '00:45']
Daylight-saving days¶
Because the grid is a UTC range, the autumn change (25 October 2026) needs no special case: the local day simply has 25 hours, and 02:00 appears twice on the wall clock.
autumn = TimeGrid(
start=datetime(2026, 10, 25, tzinfo=PARIS),
end=datetime(2026, 10, 26, tzinfo=PARIS),
step=timedelta(hours=1),
)
len(autumn), [autumn.local(i).strftime("%H:%M %Z") for i in range(4)]
(25, ['00:00 CEST', '01:00 CEST', '02:00 CEST', '02:00 CET'])
Nothing is rounded silently¶
A step that does not divide an hour, a start that is not on a step boundary, or a range that is not a whole number of steps is refused with the reason.
for bad in ({"step": timedelta(minutes=7)}, {"start": datetime(2026, 1, 1, 0, 5, tzinfo=UTC)}):
arguments = {
"start": datetime(2026, 1, 1, tzinfo=UTC),
"end": datetime(2026, 1, 2, tzinfo=UTC),
"step": timedelta(minutes=15),
}
try:
TimeGrid(**(arguments | bad))
except GridError as error:
print(error)
step 0:07:00 is not supported; use one of 5, 10, 15, 20, 30, 60 minutes start 2026-01-01T00:05:00+00:00 is not aligned on the 0:15:00 step
Moving around a grid¶
index() finds the step that starts at a moment, sub() takes part of a grid, and with_step() re-cuts the same range.
morning = day.sub(datetime(2026, 1, 1, 6, tzinfo=PARIS), datetime(2026, 1, 1, 12, tzinfo=PARIS))
hourly = day.with_step(timedelta(hours=1))
day.index(datetime(2026, 1, 1, 6, tzinfo=PARIS)), len(morning), len(hourly)
(24, 24, 24)
Next: 02 — Time series and resampling puts values on a grid and moves them between grids without losing a single kWh.