Handling times for an EV charger
This morning (October 30th 2022), the clocks went back in the UK - the time that would have been 2am fell back to 1am. This is just the regular fall back" transition - there's nothing special about it.
As it happens, I'd driven my electric car for quite a long journey yesterday, so I had it plugged in to charge overnight... and that's where things get interesting.
My electricity tariff is called Octopus Go, which is designed for electric vehicle owners. Any electricity I use between 12:30am and 4:30am is significantly cheaper than at other times. I use a PodPoint charger, which allows me to control when the car will charge via an app. For each day of the week, there's a start time and an end time - the charger turns on at the start time, and off at the end time. (If the car isn't plugged in while it's on", that's fine. Likewise if the car finishes charging, it will stop drawing power.) Unsurprisingly, I have my schedule set for 12:30am to 4:30am every day. (If I know I need more charge than 4 hours provides, I tweak the schedule and then set it back.) The app looks like this:
Normally, that schedule will get me 4 hours of charging. But this morning was special due to the clocks going back... and I didn't know what would happen. If the charger handled the schedule as if the local time is between 12:30am and 4:30am, then the charger should be on" then it should charge for 5 hours:
- It would charge for 1 1/2 hours between 12:30am and 2am
- Local time would fall back to 1am
- It would charge for 3 1/2 hours between 1am (the second occurrence of 1am!) and 4:30am
Assuming that happened, what rate would Octopus charge me for these 5 hours? The same logic should mean the whole charging period is on the cheap tariff... but would something go wrong?
I was geekily excited by all this and tweeted as much:
What actually happened?Exciting experiment tonight! I have my electric car charger set to charge between 00:30 and 04:30, as that's when I get cheap electricity. The clocks go back (2:00 to 1:00) tonight. So: a) will I get 5 hours of charging? b) will all of it be at the cheap rate? Enquiring minds etc
The car definitely charged for 5 hours. The PodPoint app shows each charging session, as shown in the screenshot below. (The session only ends when I remove the cable from the car, but the charging duration is measured separately.)
The price there is only what PodPoint thinks I'll be charged. Octopus makes data available the day after, but I'll be checking three things when they do:
- How today is represented in the CSV file you can download from them
- How today is represented in the web graphs of usage
- How much the electricity actually cost me
(I'm fairly convinced it will all be cheap, but it'll be good to check.)
What should the code for an EV charger look like?I had various responses to my tweet, including at least a few people informing me that the industry standard approach to time zone handling is to convert everything to UTC internally and only convert to local time for display purposes. Those responses are the reason for this blog post... because in my view, that's absolutely the wrong way to treat this situation.
If you haven't previously read my post on why storing UTC is not a silver bullet you may wish to do so, and my objections this time aren't entirely unrelated, but it's not quite the same thing. In particular, the problems with using a conversion to UTC have nothing to do with time zone rules changing in the future.
Let's consider the information we have here:
- The charging schedule is expressed in local start/end times on a per-day-of-week basis, e.g. Monday: 00:30 to 04:30". Note that there are no dates here; just days of the week and local times.
- The charger needs to know the current local date and time. Typically (but not necessarily) that will mean:
- The charger knows the current instant in time (i.e. it has a system clock)
- The charger knows the target" time zone for which the schedule should be applied (e.g. Europe/London)
- The charger knows the rules for that time zone
My immediate question to the proposal of the charger should convert everything to UTC" is to ask what that even means, given the information above. Knowing that the time zone is Europe/London, how does one convert a schedule entry of Monday: 00:30 to 04:30" to UTC? A conversion to UTC is normally for a local date and time in a particular time zone to an instant in time. Here we don't have a local date and time; we have a day of the week and a time. In Europe/London, Monday" will sometimes have a UTC offset of +00, and sometimes have an offset of +01. (And Sunday" can vary over the course of the day - as it would today, starting off with a UTC offset of +01 and ending with a UTC offset of +00.)
The next question would involve dealing with ambiguity and skipped times. Suppose my schedule for Sunday was Start=01:15, End=01:45. Assuming the conversion code was pinned to a specific date, how would those be converted into UTC today, when each of those times occurs twice? What about on March 27th 2022, when those times didn't occur at all due to a spring forward" from 1am to 2am?
Finally, I would ask where the requirement to convert to UTC came from. Is this thinking through the requirements, or just applying received wisdom of always convert everything to UTC"?
Slightly generalizing my earlier statement, I would probably write the requirement as:
The charger status (on or off) is determined by the charging schedule, applied to the current local date and time. The charger should be on" if the current local time is between the start and end time in the schedule for the current local day-of-week.
That doesn't require any conversion to UTC. It doesn't even require that the system is aware of the current instant in time at all - it only needs to know the current local date and time, because that's the context in which the requirements are expressed.
So how do we know when to turn the charger on or off? If we cared about turning on and off at exactly the right time, we'd probably want to work out the duration between now and the next change - and that probably would involve conversions to UTC. But that's unecessary. The way I'd write this would be to just have an infinite loop, checking whether the charger should be on or not, then sleeping for a bit. (That could be sleeping for 1 second, 10 seconds, a minute or even 5 minutes.)
I've created somewhat pseudo-code" (it's valid C#, it compiles, and would work - but there's no application hooked up to use the library) for this in my GitHub demo repo, but the most important aspects are discussed below. I should note that there are no tests, and it isn't designed to handle:
- Changes to the time zone database
- Changes to the target time zone
- Changes to the schedule
- Shutdown requests
- Handling schedules where Start is later than End (e.g. to have a schedule of 11pm to 2am")
- Handling an end time of midnight" in a schedule
None of these would be hard to handle (and the first three would be much harder to handle in any system that started from a position of convert everything to UTC) but would be distractions from the main business of how should the conversions work".
The main loop is in EvChargerController, which is reproduced in its entirety below (other than comments; see the full code for the comments):
using Microsoft.Extensions.Logging;using NodaTime;using NodaTime.Text;namespace EvChargerTiming;public class EvChargerController{ private readonly DateTimeZone zone; private readonly IClock clock; private readonly ChargingSchedule schedule; private readonly EvCharger charger; private readonly ILogger logger; public EvChargerController(EvCharger charger, ChargingSchedule schedule, DateTimeZone zone, IClock clock, ILogger logger) { this.charger = charger; this.schedule = schedule; this.zone = zone; this.clock = clock; this.logger = logger; } public void MainLoop(TimeSpan pollingInterval) { while (true) { Instant now = clock.GetCurrentInstant(); ZonedDateTime nowInTimeZone = now.InZone(zone); bool shouldBeOn = schedule.IsChargingEnabled(nowInTimeZone.LocalDateTime); if (charger.On != shouldBeOn) { logger.LogInformation("At {now} ({local} local), changing state to {state}", InstantPattern.ExtendedIso.Format(now), ZonedDateTimePattern.GeneralFormatOnlyIso.Format(nowInTimeZone), shouldBeOn); charger.ChangeState(shouldBeOn); } Thread.Sleep(pollingInterval); } }}
The only conversion involved is from the current instant in time to the local time in the target time zone. That's much easier than converting from a local time into an instant, because there's no scope for ambiguity or skipped values. The result of the conversion is used immediately rather than stored, which means we don't need to worry about what data going stale if the time zone rules change.
I do use the instant when logging - in reality, I'd expect the logging infrastructure to log the instant at which the log entry is created, but I thought I'd demonstrate that it's potentially useful to specify the instant and the result of the conversion. (As it happens, ZonedDateTimePattern.GeneralFormatOnlyIso includes the the UTC offset anyway, so the instant could be inferred from that, but hey.)
The ChargingSchedule type used by EvChargerController is even simpler. Again, I've cut the comments out - the full code has comments.
using NodaTime;namespace EvChargerTiming;public record ChargingScheduleDay(IsoDayOfWeek DayOfWeek, LocalTime Start, LocalTime End){ public bool Contains(LocalTime now) => Start <= now && now < End;}public class ChargingSchedule{ private readonly List<ChargingScheduleDay> days; public ChargingSchedule(List<ChargingScheduleDay> days) { this.days = days; } public bool IsChargingEnabled(LocalDateTime dateTime) { var day = days.Single(candidate => candidate.DayOfWeek == dateTime.DayOfWeek); return day.Contains(dateTime.TimeOfDay); }}
The key part here is the signature of the sole method within ChargingSchedule:
public bool IsChargingEnabled(LocalDateTime dateTime)
From the perspective of turning the charger on and off, all we need to know is whether or not it should be on at a particular local date and time - which maps precisely onto the requirements.
Everything else derives from that requirement - and as you can see, the implementation is really trivial. There are basically three lines of real code", and they're very easily testable.
ConclusionWhen working with a date/time challenge, the first response should be I need specific and clear requirements" rather than we should use UTC". Let the requirements drive the code. In this particular case, all the data is inherently local", and we never want to store any instants in time, so the conventional wisdom of converting to UTC really doesn't help.
I'd also note that it's a lot easier to spot that only the local date/time is relevant when using Noda Time than it would have been with the .NET built-in types - a signature of IsChargingEnabled(DateTime dateTime) would have needed more careful documentation to explain its intention.
Finally, remember that conversions from an instant in time to a local date/time are generally simpler than the other way round, as they're always unambiguous. The solution above never needs to convert in the other direction, so we never need to make any decisions of how to handle ambiguous or skipped values.
None of this is intended to imply that you should never use UTC. When storing current/past timestamps (rather than user data) I'd almost always use UTC. But user data itself is rarely expressed in UTC, and sometimes (as here) we never need to do a conversion to UTC in order to process the data - if you don't need to convert it, why would you do so?