Skip to content

Commit fe2d94e

Browse files
committed
Demo code for EV charging blog post
1 parent 5d08cd1 commit fe2d94e

File tree

5 files changed

+183
-0
lines changed

5 files changed

+183
-0
lines changed

EvChargerTiming/ChargingSchedule.cs

+47
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2022 Jon Skeet. All rights reserved.
2+
// Use of this source code is governed by the Apache License 2.0,
3+
// as found in the LICENSE.txt file.
4+
5+
using NodaTime;
6+
7+
namespace EvChargerTiming;
8+
9+
/// <summary>
10+
/// The rules for a single day within a charging schedule.
11+
/// <see cref="Start"/> is inclusive; <see cref="End"/> is exclusive, and we assume
12+
/// that Start is less than or equal to End (validated elsewhere). In a real system
13+
/// we'd want to be able to handle "charge to the end of the day" (and there's no LocalTime
14+
/// representation of "midnight at the end of the day"), and potentially be able to have
15+
/// "End less than Start" options for charging between (say) 11pm and 2am.
16+
///
17+
/// All of that logic could be added here, and it wouldn't affect the design in terms of
18+
/// time zone conversions (which is the point of this sample code).
19+
/// </summary>
20+
public record ChargingScheduleDay(IsoDayOfWeek DayOfWeek, LocalTime Start, LocalTime End)
21+
{
22+
public bool Contains(LocalTime now) =>
23+
Start <= now && now < End;
24+
}
25+
26+
/// <summary>
27+
/// A schedule for the EV charger, expressed as one <see cref="ChargingScheduleDay"/> per day of the week.
28+
/// </summary>
29+
public class ChargingSchedule
30+
{
31+
private readonly List<ChargingScheduleDay> days;
32+
33+
public ChargingSchedule(List<ChargingScheduleDay> days)
34+
{
35+
// TODO: Validation that there's exactly one entry per day-of-the-week, etc.
36+
this.days = days;
37+
}
38+
39+
/// <summary>
40+
/// Given the local date and time, should the charger be on or off?
41+
/// </summary>
42+
public bool IsChargingEnabled(LocalDateTime dateTime)
43+
{
44+
var day = days.Single(candidate => candidate.DayOfWeek == dateTime.DayOfWeek);
45+
return day.Contains(dateTime.TimeOfDay);
46+
}
47+
}

EvChargerTiming/EvCharger.cs

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2022 Jon Skeet. All rights reserved.
2+
// Use of this source code is governed by the Apache License 2.0,
3+
// as found in the LICENSE.txt file.
4+
5+
namespace EvChargerTiming;
6+
7+
public class EvCharger
8+
{
9+
/// <summary>
10+
/// Whether or not the charger is currently "on" in terms of
11+
/// being able to supply power. Somewhat orthogonally, the real
12+
/// system would be able to report whether a car was actually plugged
13+
/// in or not, how much current was being drawn etc.
14+
/// </summary>
15+
public bool On { get; private set; }
16+
17+
/// <summary>
18+
/// Method to change to the given state.
19+
/// (This could be a property setter, or two different methods...
20+
/// all kinds of different design options here which aren't relevant
21+
/// to the point of this sample code.)
22+
/// </summary>
23+
public void ChangeState(bool on)
24+
{
25+
// Probably some logging here...
26+
On = on;
27+
}
28+
}
+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Copyright 2022 Jon Skeet. All rights reserved.
2+
// Use of this source code is governed by the Apache License 2.0,
3+
// as found in the LICENSE.txt file.using NodaTime;
4+
5+
using Microsoft.Extensions.Logging;
6+
using NodaTime;
7+
using NodaTime.Text;
8+
9+
namespace EvChargerTiming;
10+
11+
public class EvChargerController
12+
{
13+
// Note: Noda Time exposes a ZonedClock which could be used for this,
14+
// which just composes a clock and a zone, but I've separated them out for
15+
// clarity.
16+
private readonly DateTimeZone zone;
17+
private readonly IClock clock;
18+
private readonly ChargingSchedule schedule;
19+
private readonly EvCharger charger;
20+
private readonly ILogger logger;
21+
22+
public EvChargerController(EvCharger charger, ChargingSchedule schedule, DateTimeZone zone, IClock clock, ILogger logger)
23+
{
24+
this.charger = charger;
25+
this.schedule = schedule;
26+
this.zone = zone;
27+
this.clock = clock;
28+
this.logger = logger;
29+
}
30+
31+
/// <summary>
32+
/// Infinite loop which just checks periodically whether the charger should be on or not,
33+
/// based on the schedule.
34+
/// </summary>
35+
/// <param name="pollingInterval">The interval at which to check whether or not the charger should be on.</param>
36+
public void MainLoop(TimeSpan pollingInterval)
37+
{
38+
// In a real system we'd want ways of shutting down, updating the schedule,
39+
// changing the target time zone, updating the time zone database etc.
40+
while (true)
41+
{
42+
Instant now = clock.GetCurrentInstant();
43+
44+
// Note: converting an instant into a local date/time is always unambiguous. (Every
45+
// instant maps to exactly one local date/time.) Conversions in the opposite
46+
// direction may be ambiguous or invalid.
47+
ZonedDateTime nowInTimeZone = now.InZone(zone);
48+
49+
bool shouldBeOn = schedule.IsChargingEnabled(nowInTimeZone.LocalDateTime);
50+
if (charger.On != shouldBeOn)
51+
{
52+
logger.LogInformation("At {now} ({local} local), changing state to {state}",
53+
InstantPattern.ExtendedIso.Format(now),
54+
ZonedDateTimePattern.GeneralFormatOnlyIso.Format(nowInTimeZone),
55+
shouldBeOn);
56+
57+
charger.ChangeState(shouldBeOn);
58+
}
59+
60+
// We *could* predict when we'll next need to turn the charger on.
61+
// However, that's significantly more fiddly than just checking periodically.
62+
// A check of something "once per minute" will take very little power,
63+
// and be much simpler than trying to predict how long to sleep for.
64+
// The reality of EV charging is that we don't need to be massively accurate here;
65+
// sleeping for several seconds or even a few minutes between checks should be fine.
66+
Thread.Sleep(pollingInterval);
67+
}
68+
}
69+
}
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.2" />
11+
<PackageReference Include="NodaTime" Version="3.1.5" />
12+
</ItemGroup>
13+
14+
</Project>

EvChargerTiming/EvChargerTiming.sln

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio Version 17
4+
VisualStudioVersion = 17.3.32901.215
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EvChargerTiming", "EvChargerTiming.csproj", "{C385414B-617D-47B6-B9D3-1112FAAB481B}"
7+
EndProject
8+
Global
9+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
10+
Debug|Any CPU = Debug|Any CPU
11+
Release|Any CPU = Release|Any CPU
12+
EndGlobalSection
13+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
14+
{C385414B-617D-47B6-B9D3-1112FAAB481B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15+
{C385414B-617D-47B6-B9D3-1112FAAB481B}.Debug|Any CPU.Build.0 = Debug|Any CPU
16+
{C385414B-617D-47B6-B9D3-1112FAAB481B}.Release|Any CPU.ActiveCfg = Release|Any CPU
17+
{C385414B-617D-47B6-B9D3-1112FAAB481B}.Release|Any CPU.Build.0 = Release|Any CPU
18+
EndGlobalSection
19+
GlobalSection(SolutionProperties) = preSolution
20+
HideSolutionNode = FALSE
21+
EndGlobalSection
22+
GlobalSection(ExtensibilityGlobals) = postSolution
23+
SolutionGuid = {98CBD966-E924-48DA-803C-C810C011D6BD}
24+
EndGlobalSection
25+
EndGlobal

0 commit comments

Comments
 (0)