Scheduling
Many applications require some sort of scheduled or periodic tasks to be executed, such as switching off lights at a specific time.
There are several ways to do this in .NET that may or may not be suitable for use in NetDaemon apps. NetDaemon has an extension that supports scheduling based on the standard System.Reactive.Concurrency.IScheduler
interface. It is recommended to use this extension for scheduling tasks as it will automatically cancel them when an app is stopped, and it adds support for Cron expressions.
Setup
If you use the NetDaemon project template you will already have the scheduler available and you can skip this setup.
To set up the scheduler manually you should:
- Include the NetDaemon.Extensions.Scheduling nuget package
Install-Package NetDaemon.Extensions.Scheduling
- Make sure to call
.AddNetDaemonScheduler()
on theServiceCollection
in the hosts Program.cs
.ConfigureServices((_, services) =>
services
.AddAppsFromAssembly(Assembly.GetExecutingAssembly())
.AddNetDaemonStateManager()
.AddNetDaemonScheduler()
Injecting the scheduler
You can get an instance of the IScheduler
interface bye simply injecting it into your apps constructor:
using System.Reactive.Concurrency.Scheduler;
[NetDaemonApp]
class MyApp(IScheduler scheduler)
{ }
The scheduler you will receive is based on the System.Reactive.Concurrency.DefaultScheduler.Instance
. This scheduler is however wrapped with additional behavior that will make sure that any tasks you schedule on this scheduler will be cancelled when the application is stopped. It will also log exceptions from scheduled tasks to the configured ILogger
.
IScheduler.Now always returns UTC. Use Now.LocalDateTime
to get the current local time.
Using the Scheduler
The System.Reactive.Concurrency.Scheduler
namespace provides several extension methods for IScheduler
that allow you to schedule tasks at a specific time, after a specific TimeSpan
, or periodically. You can use these framework provided methods directly on the scheduler you received via the constructor and they will be scheduled using the cancellation and logging behavior.
For example, to turn off the lights in my living in 2 minutes from now I can use:
scheduler.Schedule(TimeSpan.FromMinutes(2), () => entities.Light.Living.TurnOff());
Scheduling periodic jobs using the default scheduling methods have some limitations:
- Setting up more advanced schedules can be complicated.
- Daylight savings time can cause problems when running jobs at a specific time of a day.
- When a job throws an exception it will be logged, but subsequent jobs of this schedule will not be executed.
As a more convenient way to schedule periodic tasks, the NetDaemon Scheduling Extensions provides an extension method ScheduleCron()
. It can be used like this:
public CronSchedulingApp(IHaContext ha, IScheduler scheduler)
{
var entities = new Entities(ha);
scheduler.ScheduleCron("45 23 * * *", () => entities.Light.Living.TurnOff());
}
Which will turn off the living room light at 23:45 each day.
The first argument of this method is a Cron expression that describes the pattern of the schedule. These expressions can meet a large variety of scheduling demands. This Cron expression will be evaluated using the local timezone that is setup for your environment.
The ScheduleCron()
extension method uses Cronos to parse your Cron expression. See its docs for the exact specification.
Unit testing scheduling apps
Timing in some apps can be pretty complicated. It can therefore be useful to create unit tests for your schedules. For this purpose there is a special version of the IScheduler
interface implemented by Microsoft.Reactive.Testing.TestScheduler
in the Microsoft.Reactive.Testing
nuget package.
This scheduler allows you to do time traveling in unit tests:
[Fact]
public void TestCron()
{
var haContextMoq = new Mock<IHaContext>();
var testScheduler = new Microsoft.Reactive.Testing.TestScheduler();
testScheduler.AdvanceTo(new DateTime(2020, 2, 1, 23, 44, 0).ToUniversalTime().Ticks);
var app = new CronSchedulingApp(haContextMoq.Object, testScheduler);
haContextMoq.VerifyNoOtherCalls();
testScheduler.AdvanceBy(TimeSpan.FromMinutes(1).Ticks);
haContextMoq.Verify(h => h.CallService("light", "turn_off",
It.Is<ServiceTarget>(s => s.EntityIds!.Single() == "light.living"),
It.IsAny<LightTurnOffParameters>()));
}
The TestScheduler
is initially setup to 23:44
local time, just before the lights are supposed to be turned off. The test code then creates an instance of our CronSchedulingApp
and passes it a mock of the IHaContext
and the TestScheduler
. Initially no calls should be made on the IHaContext
. But when the TestScheduler
advances 1 minute we expect the app to call CallService
on the IHaContext
to turn off the lights in the living room.