diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IProjectorScreenLiftControl.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IProjectorScreenLiftControl.cs
new file mode 100644
index 00000000..26895114
--- /dev/null
+++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IProjectorScreenLiftControl.cs
@@ -0,0 +1,27 @@
+using Crestron.SimplSharpPro.DeviceSupport;
+using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+
+namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
+{
+ ///
+ /// Defines a class that has warm up and cool down
+ ///
+ public interface IProjectorScreenLiftControl
+ {
+ void Raise();
+ void Lower();
+ BoolFeedback IsInUpPosition { get; }
+ bool InUpPosition { get; }
+ event EventHandler PositionChanged;
+ string DisplayDeviceKey { get; }
+ eScreenLiftControlType Type { get; } // screen/lift
+ }
+
+ public enum eScreenLiftControlType
+ {
+ lift,
+ screen
+ }
+}
\ No newline at end of file
diff --git a/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs
new file mode 100644
index 00000000..1bb7f503
--- /dev/null
+++ b/src/PepperDash.Essentials.Devices.Common/Displays/ScreenLiftController.cs
@@ -0,0 +1,266 @@
+using System;
+using System.Collections.Generic;
+using Crestron.SimplSharp;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using PepperDash.Core;
+using PepperDash.Essentials.Core;
+using PepperDash.Essentials.Core.Config;
+using PepperDash.Essentials.Core.CrestronIO;
+using PepperDash.Essentials.Core.DeviceTypeInterfaces;
+using PepperDash.Essentials.Devices.Common;
+using Serilog.Events;
+
+namespace PepperDash.Essentials.Devices.Common.Shades
+{
+ ///
+ /// Controls a single shade using three relays
+ ///
+ public class ScreenLiftController : EssentialsDevice, IProjectorScreenLiftControl
+ {
+ readonly ScreenLiftControllerConfigProperties Config;
+ readonly ScreenLiftRelaysConfig RaiseRelayConfig;
+ readonly ScreenLiftRelaysConfig LowerRelayConfig;
+ readonly ScreenLiftRelaysConfig LatchedRelayConfig;
+
+ Displays.DisplayBase DisplayDevice;
+ ISwitchedOutput RaiseRelay;
+ ISwitchedOutput LowerRelay;
+ ISwitchedOutput LatchedRelay;
+
+ public bool InUpPosition
+ {
+ get { return _isInUpPosition; }
+ set
+ {
+ if (value == _isInUpPosition) return;
+ _isInUpPosition = value;
+ IsInUpPosition.FireUpdate();
+ PositionChanged?.Invoke(this, new EventArgs());
+ }
+ }
+
+ private bool _isInUpPosition { get; set; }
+ public eScreenLiftControlType Type { get; private set; }
+ public eScreenLiftControlMode Mode { get; private set; }
+
+ public string DisplayDeviceKey { get; private set; }
+ public BoolFeedback IsInUpPosition { get; private set; }
+
+ public event EventHandler PositionChanged;
+
+ public ScreenLiftController(string key, string name, ScreenLiftControllerConfigProperties config)
+ : base(key, name)
+ {
+ Config = config;
+ DisplayDeviceKey = Config.DisplayDeviceKey;
+ Mode = Config.Mode;
+ Type = Config.Type;
+
+ IsInUpPosition = new BoolFeedback(() => _isInUpPosition);
+
+ switch (Mode)
+ {
+ case eScreenLiftControlMode.momentary:
+ {
+ RaiseRelayConfig = Config.Relays["raise"];
+ LowerRelayConfig = Config.Relays["lower"];
+ break;
+ }
+ case eScreenLiftControlMode.latched:
+ {
+ LatchedRelayConfig = Config.Relays["latched"];
+ break;
+ }
+ }
+ }
+
+ private void IsCoolingDownFeedback_OutputChange(object sender, FeedbackEventArgs e)
+ {
+ if (!DisplayDevice.IsCoolingDownFeedback.BoolValue && Type == eScreenLiftControlType.lift)
+ {
+ Raise();
+ return;
+ }
+ if (DisplayDevice.IsCoolingDownFeedback.BoolValue && Type == eScreenLiftControlType.screen)
+ {
+ Raise();
+ return;
+ }
+ }
+
+ private void IsWarmingUpFeedback_OutputChange(object sender, FeedbackEventArgs e)
+ {
+ if (DisplayDevice.IsWarmingUpFeedback.BoolValue)
+ {
+ Lower();
+ }
+ }
+
+ public override bool CustomActivate()
+ {
+ //Create ISwitchedOutput objects based on props
+ switch (Mode)
+ {
+ case eScreenLiftControlMode.momentary:
+ {
+ Debug.LogMessage(LogEventLevel.Debug, this, $"Getting relays for {Mode}");
+ RaiseRelay = GetSwitchedOutputFromDevice(RaiseRelayConfig.DeviceKey);
+ LowerRelay = GetSwitchedOutputFromDevice(LowerRelayConfig.DeviceKey);
+ break;
+ }
+ case eScreenLiftControlMode.latched:
+ {
+ Debug.LogMessage(LogEventLevel.Debug, this, $"Getting relays for {Mode}");
+ LatchedRelay = GetSwitchedOutputFromDevice(LatchedRelayConfig.DeviceKey);
+ break;
+ }
+ }
+
+ Debug.LogMessage(LogEventLevel.Debug, this, $"Getting display with key {DisplayDeviceKey}");
+ DisplayDevice = GetDisplayBaseFromDevice(DisplayDeviceKey);
+
+ if (DisplayDevice != null)
+ {
+ Debug.LogMessage(LogEventLevel.Debug, this, $"Subscribing to {DisplayDeviceKey} feedbacks");
+
+ DisplayDevice.IsWarmingUpFeedback.OutputChange += IsWarmingUpFeedback_OutputChange;
+ DisplayDevice.IsCoolingDownFeedback.OutputChange += IsCoolingDownFeedback_OutputChange;
+ }
+
+ return base.CustomActivate();
+ }
+
+ public void Raise()
+ {
+ if (RaiseRelay == null && LatchedRelay == null) return;
+
+ Debug.LogMessage(LogEventLevel.Debug, this, $"Raising {Type}");
+
+ switch (Mode)
+ {
+ case eScreenLiftControlMode.momentary:
+ {
+ PulseOutput(RaiseRelay, RaiseRelayConfig.PulseTimeInMs);
+ break;
+ }
+ case eScreenLiftControlMode.latched:
+ {
+ LatchedRelay.Off();
+ break;
+ }
+ }
+ InUpPosition = true;
+ }
+
+ public void Lower()
+ {
+ if (LowerRelay == null && LatchedRelay == null) return;
+
+ Debug.LogMessage(LogEventLevel.Debug, this, $"Lowering {Type}");
+
+ switch (Mode)
+ {
+ case eScreenLiftControlMode.momentary:
+ {
+ PulseOutput(LowerRelay, LowerRelayConfig.PulseTimeInMs);
+ break;
+ }
+ case eScreenLiftControlMode.latched:
+ {
+ LatchedRelay.On();
+ break;
+ }
+ }
+ InUpPosition = false;
+ }
+
+ void PulseOutput(ISwitchedOutput output, int pulseTime)
+ {
+ output.On();
+ CTimer pulseTimer = new CTimer(new CTimerCallbackFunction((o) => output.Off()), pulseTime);
+ }
+
+ ///
+ /// Attempts to get the port on teh specified device from config
+ ///
+ ///
+ ///
+ ISwitchedOutput GetSwitchedOutputFromDevice(string relayKey)
+ {
+ var portDevice = DeviceManager.GetDeviceForKey(relayKey);
+ if (portDevice != null)
+ {
+ return (portDevice as ISwitchedOutput);
+ }
+ else
+ {
+ Debug.LogMessage(LogEventLevel.Debug, this, "Error: Unable to get relay device with key '{0}'", relayKey);
+ return null;
+ }
+ }
+
+ Displays.DisplayBase GetDisplayBaseFromDevice(string displayKey)
+ {
+ var displayDevice = DeviceManager.GetDeviceForKey(displayKey);
+ if (displayDevice != null)
+ {
+ return displayDevice as Displays.DisplayBase;
+ }
+ else
+ {
+ Debug.LogMessage(LogEventLevel.Debug, this, "Error: Unable to get display device with key '{0}'", displayKey);
+ return null;
+ }
+ }
+
+ }
+
+ public class ScreenLiftControllerConfigProperties
+ {
+ [JsonProperty("displayDeviceKey")]
+ public string DisplayDeviceKey { get; set; }
+
+ [JsonProperty("type")]
+ [JsonConverter(typeof(StringEnumConverter))]
+ public eScreenLiftControlType Type { get; set; }
+
+ [JsonProperty("mode")]
+ [JsonConverter(typeof(StringEnumConverter))]
+ public eScreenLiftControlMode Mode { get; set; }
+
+ [JsonProperty("relays")]
+ public Dictionary Relays { get; set; }
+
+ }
+ public class ScreenLiftRelaysConfig
+ {
+ [JsonProperty("deviceKey")]
+ public string DeviceKey { get; set; }
+
+ [JsonProperty("pulseTimeInMs")]
+ public int PulseTimeInMs { get; set; }
+ }
+
+ public class ScreenLiftControllerFactory : EssentialsDeviceFactory
+ {
+ public ScreenLiftControllerFactory()
+ {
+ TypeNames = new List() { "screenliftcontroller" };
+ }
+
+ public override EssentialsDevice BuildDevice(DeviceConfig dc)
+ {
+ Debug.LogMessage(LogEventLevel.Debug, "Factory Attempting to create new Generic Comm Device");
+ var props = Newtonsoft.Json.JsonConvert.DeserializeObject(dc.Properties.ToString());
+
+ return new ScreenLiftController(dc.Key, dc.Name, props);
+ }
+ }
+
+ public enum eScreenLiftControlMode
+ {
+ momentary,
+ latched
+ }
+}
\ No newline at end of file