Updated Release of the Abodit State Machine
I published a new version of the Abodit State Machine to Nuget this evening. You can find it here.
One breaking change in this version is that the state machine is now specified using three Type parameters instead of two:
public class OccupancyStateMachine :
StateMachine<OccupancyStateMachine, Event, BuildingArea>
The third type parameter, TContext, is a context object that can be passed in with every event occurrence or tick. This means that you don’t need to store any extraneous data in the state machine itself and can keep it as a pure representation of the state of the system.
In the example above I have an OccupancyStateMachine and the context is a BuildingArea. Each call to EventHappens now takes the event that happened and a BuildingArea object.
When you define your state machine you will need to include 4 parameters in each lambda expression.
Here, for example, is the current state machine for a BuildingArea in my home automation. It uses a hierarchy of states with two base states: Not Occupied and Occupied. It has timers for activity within a room or for occupancy within rooms that are contained by a floor. Note how it also exposes an IObservable<State> so that other objects can subscribe to state machine changes. I didn’t want to take the Rx dependency in the state machine class itself but you can see how easy it is to hook it up.
Of interest also is the way I represent occupancy as three distinct states, the extra one ‘Asleep’ represents a room that is not-occupied in the sense that there is no motion there now but there was at some point during the evening before.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Abodit.StateMachine;
using log4net;
using Abodit.Units;
using AboditUnits.Units;
using System.Reactive.Subjects;
using System.Reactive.Linq;
namespace Abodit
{
/// <summary>
/// An Occupancy State machine handles not occupied, occupied, asleep
/// </summary>
[Serializable]
public class OccupancyStateMachine : StateMachine<OccupancyStateMachine, Event, BuildingArea>
{
private readonly Subject<State> watch = new Subject<State>();
public IObservable<State> Watch { get { return watch.AsObservable(); } }
public override void OnStateChanging(StateMachine<OccupancyStateMachine, Event, BuildingArea>.State newState, BuildingArea context)
{
watch.OnNext(newState);
}
public static readonly State Starting = AddState("Starting");
public static readonly State NotOccupied = AddState("Not occupied",
(m, e, s, c) => {
m.CancelScheduledEvent(eTick); // Stop the clock
m.IsTimerRunning = false;
m.IsRecentlyOccupied = false;
m.IsHeavilyOccupied = false;
m.After(new TimeSpan(hours:0, minutes:5, seconds:0), e5MinutesSinceOccupied);
m.After(new TimeSpan(hours:24, minutes:0, seconds:0), e24hoursSinceOccupied);
m.After(new TimeSpan(hours:48, minutes:0, seconds:0), e48hoursSinceOccupied);
},
(m, e, s, c) => { });
public static readonly State NotOccupiedIn5Minutes = AddState("Not occupied in over 5 minutes",
(m, e, s, c) => { },
(m, e, s, c) => { }, NotOccupied);
public static readonly State NotOccupiedInOver24Hours = AddState("Not occupied in over 24 hours",
(m, e, s, c) => { },
(m, e, s, c) => { }, NotOccupiedIn5Minutes);
public static readonly State NotOccupiedInOver48Hours = AddState("Not occupied in over 48 hours",
(m, e, s, c) => { },
(m, e, s, c) => { }, NotOccupiedInOver24Hours);
public static readonly State NotOccupiedInOver1Week = AddState("Not occupied in over 1 week",
(m, e, s, c) => { },
(m, e, s, c) => { }, NotOccupiedInOver48Hours);
public static readonly State Asleep = AddState("Asleep",
(m, e, s, c) =>
{
// Set a timer going for morning
var now = TimeProvider.Current.Now.LocalDateTime;
var morning = now.Hour < 8 ? now.AddHours(-now.Hour + 8) : now.AddHours(24 - now.Hour + 8);
m.At(morning.ToUniversalTime(), eMorning);
},
(m, e, s, c) => { },
parent:NotOccupied);
public static readonly State Occupied = AddState("Occupied",
(m, e, s, c) =>
{
m.IsRecentlyOccupied = true;
// Add a timer that runs while we are occupied
m.Every(new TimeSpan(hours:0, minutes:0, seconds:10), eTick);
// And set a timer going to mark 5 minutes since occupied
m.After(new TimeSpan(hours:0, minutes:5, seconds:0), e5MinutesAfterBecomingOccupied);
m.CancelScheduledEvent(e5MinutesSinceOccupied);
m.CancelScheduledEvent(e24hoursSinceOccupied);
m.CancelScheduledEvent(e48hoursSinceOccupied);
},
(m, e, s, c) => { });
public static readonly State HeavilyOccupied = AddState("Heavily occupied",
(m, e, s, c) => { },
(m, e, s, c) => { },
parent:Occupied);
private static readonly Event eStart = new Event("Starts");
private static readonly Event eUserActivity = new Event("User activity");
private static readonly Event eTick = new Event("Tick");
private static readonly Event eTimeout = new Event("Timeout");
private static readonly Event eMorning = new Event("Morning");
private static readonly Event e5MinutesAfterBecomingOccupied = new Event("5 minutes after becoming occupied");
private static readonly Event e5MinutesSinceOccupied = new Event("5 minutes since occupied");
private static readonly Event e24hoursSinceOccupied = new Event("24 hours since occupied");
private static readonly Event e48hoursSinceOccupied = new Event("48 hours since occupied");
private static readonly Event eAllChildrenNotOccupied = new Event("No child occupied");
private static readonly Event eAtLeastOneChildOccupied = new Event("At least one child occupied");
private double decliningActivity = 0.0; // Up 1000 every UserInput, down x0.9 every n seconds
private const int ActivityPerUserInput = 1000;
private const double rateOfDecline = 0.92;
public bool IsTimerRunning { get; set; }
public bool IsRecentlyOccupied { get; set; }
public bool IsHeavilyOccupied { get; set; }
static OccupancyStateMachine()
{
// On startup we transition immediately to starting
// but we want an event call to do this so we aren't doing any work
// in the constructor, and so the initialization only happens when it's
// a true 'cold start' not a 'warm start' from some database state
Starting
.When(eStart, (m, s, e, c) => { return NotOccupied; });
// Note: This is a hierarchical state machine so NotOccupied includes Asleep
NotOccupied
.When(eAtLeastOneChildOccupied, (m, s, e, c) =>
{
return Occupied;
})
.When(e5MinutesSinceOccupied, (m, s, e, c) =>
{
// Could signal something??
return s;
})
.When(e24hoursSinceOccupied, (m, s, e, c) =>
{
// Could signal something??
return s;
})
.When(e48hoursSinceOccupied, (m, s, e, c) =>
{
// Could signal something??
return s;
})
.When(eUserActivity, (m, s, e, c) =>
{
m.After(c.OccupancyTimeout, eTimeout); // start a new timeout
m.IsTimerRunning = true;
return Occupied;
});
// Asleep is a substate of not occupied so no need for more logic on becoming occupied ...
Asleep
.When(eMorning, (m, s, e, c) =>
{
// Eliminate Asleep if appropriate
return NotOccupied;
});
// Occupied includes recently occupied and heavily occupied ...
Occupied
.When(e5MinutesAfterBecomingOccupied, (m, s, e, c) =>
{
m.IsRecentlyOccupied = false;
return s;
})
.When(eUserActivity, (m, s, e, c) =>
{
// Accumulate activity ...
m.decliningActivity += ActivityPerUserInput;
m.CancelScheduledEvent(eTimeout); // cancel the old timeout
m.After(c.OccupancyTimeout, eTimeout); // start a new timeout
m.IsTimerRunning = true;
if (m.decliningActivity > 20 * ActivityPerUserInput)
return HeavilyOccupied;
else
return s;
})
.When(eAllChildrenNotOccupied, (m, s, e, c) =>
{
if (m.IsTimerRunning)
{
// If the timer is running ... wait until it runs out
return s;
}
else
{
DateTime nowLocal = TimeProvider.Current.Now.LocalDateTime;
if (nowLocal.Hour > 17)
return Asleep;
else
return NotOccupied;
}
})
.When(eTick, (m, s, e, c) =>
{
m.decliningActivity *= rateOfDecline;
return s;
})
.When(eTimeout, (m, s, e, c) =>
{
DateTime nowLocal = TimeProvider.Current.Now.LocalDateTime;
if (nowLocal.Hour > 17)
return Asleep;
else
return NotOccupied;
});
HeavilyOccupied.When(eTick, (m, s, e, c) =>
{
// Same code as Occupied but this one will override if we are in HeavilyOccupied mode
m.decliningActivity *= rateOfDecline;
// Fall back to just occupied when ...
if (m.decliningActivity < 0.2 * ActivityPerUserInput)
return Occupied;
else
return s;
});
}
public OccupancyStateMachine()
: base(Starting)
{
}
public OccupancyStateMachine(State initialState)
: base(initialState)
{
}
public override void Start()
{
this.EventHappens(eStart, null);
}
public void UserActivity(BuildingArea ba)
{
this.EventHappens(eUserActivity, ba);
}
public void AllChildrenNotOccupied(BuildingArea ba)
{
this.EventHappens(eAllChildrenNotOccupied, ba);
}
public void AtLeastOneChildOccupied(BuildingArea ba)
{
this.EventHappens(eAtLeastOneChildOccupied, ba);
}
}
}
Comments are closed.