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.

public class OccupancyStateMachine : StateMachine<OccupancyStateMachine, Event, BuildingArea> { private static readonly ILog log = LogManager.GetLogger("OccState"); [NonSerialized] private readonly Subject<State> watch = new Subject<State>(); public IObservable<State> Watch => watch.AsObservable(); public override void OnStateChanging(StateMachine<OccupancyStateMachine, Event, BuildingArea>.State newState, BuildingArea context) { watch.OnNext(newState); } [NonSerialized] private readonly Subject<Event> watchEvents = new Subject<Event>(); public IObservable<Event> WatchEvents => watchEvents.AsObservable(); public override void OnEventHappened(Event @event) { watchEvents.OnNext(@event); } //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 c.IsTimerRunning.Current = false; c.IsHeavilyOccupied.Current = false; m.After(new TimeSpan(0,30,0), eHalfHourPostOccupancy ); m.After(new TimeSpan(0,60,0), eOneHourPostOccupancy ); }, (m, e, s, c) => { }); 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.CancelScheduledEvent(eMorning); m.At(morning.ToUniversalTime(), eMorning); }, (m, e, s, c) => { }, parent:NotOccupied); public static readonly State Occupied = AddState("Occupied", (m, e, s, c) => { m.CancelScheduledEvent(eTimeout); m.After(c.OccupancyTimeout, eTimeout); // start a new timeout c.IsTimerRunning.Current = true; // Add a timer that runs while we are occupied m.CancelScheduledEvent(eTick); // remove any old eTick events m.Every(new TimeSpan(hours:0, minutes:0, seconds:10), eTick); // And kill any post occupancy timers m.CancelScheduledEvent(eHalfHourPostOccupancy); m.CancelScheduledEvent(eOneHourPostOccupancy); m.CancelScheduledEvent(eMorning); }, (m, e, s, c) => { }); /// <summary> /// Room is occupied and all doors into it are enclosed /// </summary> public static readonly State OccupiedAndEnclosed = AddState("Occupied and enclosed", (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 eSensorActivity = new Event("Sensor activity"); // maintains occupancy, does not set it private static readonly Event eTick = new Event("Tick"); // 10s tick while occupied private static readonly Event eDoorOpens = new Event("Door opens"); // At least one door opened private static readonly Event eDoorCloses = new Event("Door closes"); // All doors closed private static readonly Event eTimeout = new Event("Timeout"); private static readonly Event eMorning = new Event("Morning"); private static readonly Event eTimeoutHalfHourPostOccupancy = new Event("Timeout Half Hour"); private static readonly Event eTimeoutOneHourPostOccupancy = new Event("Timeout One Hour"); private static readonly Event eAllChildrenNotOccupied = new Event("No child occupied"); private static readonly Event eAtLeastOneChildOccupied = new Event("At least one child occupied"); // Public events that we expose public static readonly Event eHalfHourPostOccupancy = new Event("Half hour post occupancy"); public static readonly Event eOneHourPostOccupancy = new Event("One hour post occupancy"); static OccupancyStateMachine() { // Note: This is a hierarchical state machine so NotOccupied includes Asleep NotOccupied .When(eAtLeastOneChildOccupied, Occupied) .When(eDoorOpens, Occupied) .When(eUserActivity, (m, s, e, c) => { if (c.Enclosed.Current) return OccupiedAndEnclosed; else return Occupied; } ); // Asleep is a substate of not occupied so no need for more logic on becoming occupied ... Asleep .When(eMorning, NotOccupied); // Occupied includes recently occupied and heavily occupied ... Occupied .When(eUserActivity, (m, s, e, c) => { m.CancelScheduledEvent(eTimeout); // cancel the old timeout m.After(c.OccupancyTimeout, eTimeout); // start a new timeout c.IsTimerRunning.Current = true; if (c.Enclosed.Current) return OccupiedAndEnclosed; else return s; }) .When(eSensorActivity, (m, s, e, c) => { m.CancelScheduledEvent(eTimeout); // cancel the old timeout m.After(c.OccupancyTimeout, eTimeout); // start a new timeout c.IsTimerRunning.Current = true; return s; }) .When(eDoorCloses, (m, s, e, c) => { // Tricky, it closes but unless there is motion who knows what state we are in???? return s; }) .When(eAllChildrenNotOccupied, (m, s, e, c) => { if (c.IsTimerRunning.Current) { // If the timer is running ... wait until it runs out return s; } else { // recursion??? m.EventHappens(eTimeout, c); // otherwise ... if (c.Time.EveningTo6AM.On) return Asleep; else return NotOccupied; } }) .When(eTick, (m, s, e, c) => { return s; }) .When(eTimeout, (m, s, e, c) => { c.IsTimerRunning.Current = false; // No action if we have occupied children if (c.HasOccupiedChildren.Current) return s; // No timeout if occupied and enclosed ??? if (s == OccupiedAndEnclosed) { c.AddToLog("Kept occupied because enclosed"); m.After(c.OccupancyTimeout, eTimeout); // start a new timeout return s; } if (c.Time.EveningTo6AM.On) return Asleep; else return NotOccupied; }); OccupiedAndEnclosed .When(eDoorOpens, (m, s, e, c) => { return Occupied; }) .When(eTick, (m, s, e, c) => { return s; }); } public OccupancyStateMachine() : base(NotOccupied) { } public override void Start() { log.Debug("Start"); this.EventHappens(eStart, null); } public void UserActivity(BuildingArea buildingArea) { string was = this.CurrentState.ToString(); this.EventHappens(eUserActivity, buildingArea); log.Debug("User activity " + was + " -> " + this.CurrentState.ToString()); } public void DoorOpens(BuildingArea buildingArea) { this.EventHappens(eDoorOpens, buildingArea); } public void DoorCloses(BuildingArea buildingArea) { this.EventHappens(eDoorCloses, buildingArea); } public void SensorMaintainingActivity(BuildingArea buildingArea) { var was = this.CurrentState; this.EventHappens(eUserActivity, buildingArea); if (was != this.CurrentState) log.Debug($"Sensor maintaining occupancy state {was} -> {this.CurrentState}"); } public void AllChildrenNotOccupied(BuildingArea buildingArea) { var was = this.CurrentState; buildingArea.HasOccupiedChildren.Current = false; this.EventHappens(eAllChildrenNotOccupied, buildingArea); if (was != this.CurrentState) { log.Debug($"{buildingArea.Name} all children not occupied " + was + " -> " + this.CurrentState); } } public void AtLeastOneChildOccupied(BuildingArea buildingArea) { var was = this.CurrentState; buildingArea.HasOccupiedChildren.Current = true; this.EventHappens(eAtLeastOneChildOccupied, buildingArea); if (was != this.CurrentState) { log.Debug($"{buildingArea.Name} at least one child occupied " + was + " -> " + this.CurrentState); } } public override string ToString() { return "Occupancy state: " + this.CurrentState.Name; } } 


Wed Jul 11 2012 07:28:14 GMT-0700 (Pacific Daylight Time)


Next page: Dynamically building 'Or' Expressions in LINQ

Previous page: Building a better .NET State Machine


Disqus goes here