diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/PepperDash_Essentials_Core.csproj b/essentials-framework/Essentials Core/PepperDashEssentialsBase/PepperDash_Essentials_Core.csproj
index 8e688858..81f3774a 100644
--- a/essentials-framework/Essentials Core/PepperDashEssentialsBase/PepperDash_Essentials_Core.csproj
+++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/PepperDash_Essentials_Core.csproj
@@ -327,6 +327,7 @@
+
@@ -337,6 +338,7 @@
+
diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Timers/RetriggerableTimer.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Timers/RetriggerableTimer.cs
new file mode 100644
index 00000000..b31c9da0
--- /dev/null
+++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Timers/RetriggerableTimer.cs
@@ -0,0 +1,175 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Crestron.SimplSharp;
+
+using PepperDash.Core;
+using PepperDash.Essentials.Core;
+using PepperDash.Essentials.Core.Config;
+
+using Newtonsoft.Json;
+
+
+namespace PepperDash.Essentials.Core.Timers
+{
+ ///
+ /// A device that runs a retriggerable timer and can execute actions specified in config
+ ///
+ [Description("A retriggerable timer device")]
+ public class RetriggerableTimer : EssentialsDevice
+ {
+ private RetriggerableTimerPropertiesConfig _propertiesConfig;
+
+ private CTimer _timer;
+ private long _timerIntervalMs;
+
+ public RetriggerableTimer(string key, DeviceConfig config)
+ : base(key, config.Name)
+ {
+ var props = config.Properties.ToObject();
+ _propertiesConfig = props;
+
+ if (_propertiesConfig != null)
+ {
+ _timerIntervalMs = _propertiesConfig.TimerIntervalMs;
+ }
+ }
+
+ public override bool CustomActivate()
+ {
+ if (_propertiesConfig.StartTimerOnActivation)
+ {
+ StartTimer();
+ }
+
+ return base.CustomActivate();
+ }
+
+ private void CleanUpTimer()
+ {
+ if (_timer != null)
+ {
+ _timer.Stop();
+ _timer.Dispose();
+ }
+
+ _timer = null;
+ }
+
+ public void StartTimer()
+ {
+ CleanUpTimer();
+
+ _timer = new CTimer(TimerElapsedCallback, GetActionFromConfig(eRetriggerableTimerEvents.Elapsed), 0, _timerIntervalMs);
+ }
+
+ public void StopTimer()
+ {
+ _timer.Stop();
+
+ ExecuteAction(GetActionFromConfig(eRetriggerableTimerEvents.Stopped));
+ }
+
+ private DeviceActionWrapper GetActionFromConfig(eRetriggerableTimerEvents eventType)
+ {
+ var action = _propertiesConfig.Events[eRetriggerableTimerEvents.Elapsed];
+
+ if (action != null)
+ return action;
+ else return null;
+ }
+
+ ///
+ /// Executes the Elapsed action from confing when the timer elapses
+ ///
+ ///
+ private void TimerElapsedCallback(object action)
+ {
+ Debug.Console(1, this, Debug.ErrorLogLevel.Notice, "Timer Elapsed. Executing Action");
+
+ if (action == null)
+ {
+ Debug.Console(1, this, "Timer elapsed but unable to execute action. Action is null.");
+ return;
+ }
+
+ var devAction = action as DeviceActionWrapper;
+ if (devAction != null)
+ ExecuteAction(devAction);
+ else
+ {
+ Debug.Console(2, this, "Unable to cast action as DeviceActionWrapper. Cannot Execute");
+ }
+
+ }
+
+ private void ExecuteAction(DeviceActionWrapper action)
+ {
+ if (action == null)
+ return;
+
+ try
+ {
+ DeviceJsonApi.DoDeviceAction(action);
+ }
+ catch (Exception e)
+ {
+ Debug.Console(2, this, "Error Executing Action: {0}", e);
+ }
+ //finally // Not sure this is needed
+ //{
+ // _Timer.Reset(0, _TimerIntervalMs);
+ //}
+ }
+ }
+
+ ///
+ /// Configuration Properties for RetriggerableTimer
+ ///
+ public class RetriggerableTimerPropertiesConfig
+ {
+ [JsonProperty("startTimerOnActivation")]
+ public bool StartTimerOnActivation { get; set; }
+
+ [JsonProperty("timerIntervalMs")]
+ public long TimerIntervalMs { get; set; }
+
+ [JsonProperty("events")]
+ public Dictionary Events { get; set; }
+
+ public RetriggerableTimerPropertiesConfig()
+ {
+ Events = new Dictionary();
+ }
+ }
+
+ ///
+ /// The set of values describing events on the timer
+ ///
+ public enum eRetriggerableTimerEvents
+ {
+ Elapsed,
+ Stopped,
+ }
+
+ ///
+ /// Factory class
+ ///
+ public class RetriggerableTimerFactory : EssentialsDeviceFactory
+ {
+ public RetriggerableTimerFactory()
+ {
+ TypeNames = new List() { "retriggerabletimer" };
+ }
+
+ public override EssentialsDevice BuildDevice(DeviceConfig dc)
+ {
+ Debug.Console(1, "Factory Attempting to create new RetriggerableTimer Device");
+
+ return new RetriggerableTimer(dc.Key, dc);
+ }
+ }
+
+
+}
\ No newline at end of file
diff --git a/essentials-framework/Essentials Core/PepperDashEssentialsBase/Utilities/ActionSequence.cs b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Utilities/ActionSequence.cs
new file mode 100644
index 00000000..c62b6e47
--- /dev/null
+++ b/essentials-framework/Essentials Core/PepperDashEssentialsBase/Utilities/ActionSequence.cs
@@ -0,0 +1,153 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Crestron.SimplSharp;
+using Crestron.SimplSharpPro.CrestronThread;
+
+using PepperDash.Core;
+using PepperDash.Essentials.Core;
+using PepperDash.Essentials.Core.Config;
+
+using Newtonsoft.Json;
+
+namespace PepperDash.Essentials.Core.Utilities
+{
+ ///
+ /// A device that executes a sequence of actions with optional delays between actions
+ ///
+ [Description("A device that exectues a sequence of actions with optional delays between actions")]
+ public class ActionSequence : EssentialsDevice
+ {
+ private ActionSequencePropertiesConfig _propertiesConfig;
+
+ private CrestronQueue _actionQueue;
+
+ private Thread _worker;
+
+ private bool _allowActionsToExecute;
+
+ public ActionSequence(string key, DeviceConfig config)
+ : base(key, config.Name)
+ {
+ var props = config.Properties.ToObject();
+ _propertiesConfig = props;
+
+ if (_propertiesConfig != null)
+ {
+ if (_propertiesConfig.ActionSequence.Count > 0)
+ {
+ _actionQueue = new CrestronQueue(_propertiesConfig.ActionSequence.Count);
+ }
+ }
+ }
+
+ ///
+ /// Starts executing the sequenced actions
+ ///
+ public void StartSequence()
+ {
+ Debug.Console(1, this, "Starting Action Sequence");
+ _allowActionsToExecute = true;
+ AddActionsToQueue();
+ _worker = new Thread(ProcessActions, null, Thread.eThreadStartOptions.Running);
+ }
+
+ ///
+ /// Stops executing the sequenced actions
+ ///
+ public void StopSequence()
+ {
+ Debug.Console(1, this, "Stopping Action Sequence");
+ _allowActionsToExecute = false;
+ _worker.Abort();
+ }
+
+ ///
+ /// Populates the queue from the configuration information
+ ///
+ private void AddActionsToQueue()
+ {
+ Debug.Console(1, this, "Adding {0} actions to queue", _propertiesConfig.ActionSequence.Count);
+
+ for (int i = 0; i < _propertiesConfig.ActionSequence.Count; i++)
+ {
+ _actionQueue.Enqueue(_propertiesConfig.ActionSequence[i]);
+ }
+ }
+
+ private object ProcessActions(object obj)
+ {
+ while (_allowActionsToExecute && _actionQueue.Count > 0)
+ {
+ SequencedDeviceActionWrapper action = null;
+
+ action = _actionQueue.Dequeue();
+ if (action == null)
+ break;
+
+ // Delay before executing
+ if (action.DelayMs > 0)
+ Thread.Sleep(action.DelayMs);
+
+ ExecuteAction(action);
+ }
+
+ return null;
+ }
+
+ private void ExecuteAction(DeviceActionWrapper action)
+ {
+ if (action == null)
+ return;
+
+ try
+ {
+ DeviceJsonApi.DoDeviceAction(action);
+ }
+ catch (Exception e)
+ {
+ Debug.Console(2, this, "Error Executing Action: {0}", e);
+ }
+ }
+ }
+
+ ///
+ /// Configuration Properties for ActionSequence
+ ///
+ public class ActionSequencePropertiesConfig
+ {
+ [JsonProperty("actionSequence")]
+ public List ActionSequence { get; set; }
+
+ public ActionSequencePropertiesConfig()
+ {
+ ActionSequence = new List();
+ }
+ }
+
+ public class SequencedDeviceActionWrapper : DeviceActionWrapper
+ {
+ [JsonProperty("delayMs")]
+ public int DelayMs { get; set; }
+ }
+
+ ///
+ /// Factory class
+ ///
+ public class ActionSequenceFactory : EssentialsDeviceFactory
+ {
+ public ActionSequenceFactory()
+ {
+ TypeNames = new List() { "actionsequence" };
+ }
+
+ public override EssentialsDevice BuildDevice(DeviceConfig dc)
+ {
+ Debug.Console(1, "Factory Attempting to create new ActionSequence Device");
+
+ return new ActionSequence(dc.Key, dc);
+ }
+ }
+
+}
\ No newline at end of file