Compare commits

..

4 Commits

Author SHA1 Message Date
equinoy
c0971a4f8e Merge remote-tracking branch 'origin/screen-lift-controller-mute-logic' into feature/circuittype-property-versiport 2026-01-22 15:36:27 -06:00
equinoy
8bc6d4392b feat: add circuitType property to IOPortConfig and implement state inversion in digital input devices 2026-01-22 15:04:25 -06:00
Nick Genovese
f49901d3fa fix: Improve status messages in StatusMonitorCollection
Enhanced error and warning message generation to use monitor names when available, include counts, proper pluralization, and append "Offline" when issues are present. Avoided multiple enumerations by converting to lists. "Room Ok." is shown when no issues are detected.
2026-01-01 18:01:37 -06:00
Nick Genovese
7910b7931e feat: Add mute logic to ScreenLiftController
- adds a config value that mutes the display when the screen is in the up position
- screens will now mute/unmute based on their position if the config is set
2026-01-01 09:02:04 -06:00
16 changed files with 242 additions and 5463 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -15,23 +15,23 @@ namespace PepperDash.Core
/// Unique Key
/// </summary>
public string Key { get; protected set; }
/// <summary>
/// Gets or sets the Name
/// </summary>
/// <summary>
/// Gets or sets the Name
/// </summary>
public string Name { get; protected set; }
/// <summary>
///
/// </summary>
public bool Enabled { get; protected set; }
// /// <summary>
// /// A place to store reference to the original config object, if any. These values should
// /// NOT be used as properties on the device as they are all publicly-settable values.
// /// </summary>
/// <summary>
/// A place to store reference to the original config object, if any. These values should
/// NOT be used as properties on the device as they are all publicly-settable values.
/// </summary>
//public DeviceConfig Config { get; private set; }
// /// <summary>
// /// Helper method to check if Config exists
// /// </summary>
/// <summary>
/// Helper method to check if Config exists
/// </summary>
//public bool HasConfig { get { return Config != null; } }
List<Action> _PreActivationActions;

View File

@@ -41,9 +41,6 @@ namespace PepperDash.Core
CrestronConsole.PrintLine(message);
}
/// <summary>
/// Constructor for DebugConsoleSink
/// </summary>
public DebugConsoleSink(ITextFormatter formatProvider )
{
_textFormatter = formatProvider ?? new JsonFormatter();
@@ -51,9 +48,6 @@ namespace PepperDash.Core
}
/// <summary>
/// Provides extension methods for DebugConsoleSink
/// </summary>
public static class DebugConsoleSinkExtensions
{
/// <summary>

View File

@@ -27,9 +27,6 @@ namespace PepperDash.Core.Logging
CrestronLogger.WriteToLog(message, (uint)logEvent.Level);
}
/// <summary>
/// Constructor for DebugCrestronLoggerSink
/// </summary>
public DebugCrestronLoggerSink()
{
CrestronLogger.Initialize(1, LoggerModeEnum.RM);

View File

@@ -63,9 +63,6 @@ namespace PepperDash.Core.Logging
handler(message);
}
/// <summary>
/// Constructor for DebugErrorLogSink
/// </summary>
public DebugErrorLogSink(ITextFormatter formatter = null)
{
_formatter = formatter;

View File

@@ -4,9 +4,6 @@ using Log = PepperDash.Core.Debug;
namespace PepperDash.Core.Logging
{
/// <summary>
/// Provides extension methods for logging on IKeyed objects
/// </summary>
public static class DebugExtensions
{
/// <summary>

View File

@@ -32,9 +32,6 @@ namespace PepperDash.Core
private const string _certificateName = "selfCres";
private const string _certificatePassword = "cres12345";
/// <summary>
/// Gets the Port
/// </summary>
public int Port
{ get
{
@@ -44,9 +41,6 @@ namespace PepperDash.Core
}
}
/// <summary>
/// Gets the Url
/// </summary>
public string Url
{
get
@@ -64,9 +58,6 @@ namespace PepperDash.Core
private readonly ITextFormatter _textFormatter;
/// <summary>
/// Constructor for DebugWebsocketSink
/// </summary>
public DebugWebsocketSink(ITextFormatter formatProvider)
{
@@ -226,9 +217,6 @@ namespace PepperDash.Core
}
}
/// <summary>
/// Provides extension methods for DebugWebsocketSink
/// </summary>
public static class DebugWebsocketSinkExtensions
{
/// <summary>
@@ -249,9 +237,6 @@ namespace PepperDash.Core
{
private DateTime _connectionTime;
/// <summary>
/// Gets the ConnectedDuration
/// </summary>
public TimeSpan ConnectedDuration
{
get
@@ -267,17 +252,11 @@ namespace PepperDash.Core
}
}
/// <summary>
/// Constructor for DebugClient
/// </summary>
public DebugClient()
{
Debug.Console(0, "DebugClient Created");
}
/// <summary>
/// OnOpen method
/// </summary>
protected override void OnOpen()
{
base.OnOpen();
@@ -288,9 +267,6 @@ namespace PepperDash.Core
_connectionTime = DateTime.Now;
}
/// <summary>
/// OnMessage method
/// </summary>
protected override void OnMessage(MessageEventArgs e)
{
base.OnMessage(e);
@@ -298,9 +274,6 @@ namespace PepperDash.Core
Debug.Console(0, "WebSocket UiClient Message: {0}", e.Data);
}
/// <summary>
/// OnClose method
/// </summary>
protected override void OnClose(CloseEventArgs e)
{
base.OnClose(e);
@@ -309,9 +282,6 @@ namespace PepperDash.Core
}
/// <summary>
/// OnError method
/// </summary>
protected override void OnError(WebSocketSharp.ErrorEventArgs e)
{
base.OnError(e);

View File

@@ -5,16 +5,9 @@ using System.Threading.Tasks;
namespace PepperDash.Core.Web.RequestHandlers
{
/// <summary>
/// CWS Base Async Handler, implements IHttpCwsHandler
/// </summary>
public abstract class WebApiBaseRequestAsyncHandler:IHttpCwsHandler
{
private readonly Dictionary<string, Func<HttpCwsContext, Task>> _handlers;
/// <summary>
/// Indicates whether CORS is enabled
/// </summary>
protected readonly bool EnableCors;
/// <summary>

View File

@@ -10,10 +10,6 @@ namespace PepperDash.Core.Web.RequestHandlers
public abstract class WebApiBaseRequestHandler : IHttpCwsHandler
{
private readonly Dictionary<string, Action<HttpCwsContext>> _handlers;
/// <summary>
/// Indicates whether CORS is enabled
/// </summary>
protected readonly bool EnableCors;
/// <summary>

View File

@@ -45,9 +45,9 @@ namespace PepperDash.Core.Web
/// </summary>
public bool IsRegistered { get; private set; }
// /// <summary>
// /// Http request handler
// /// </summary>
/// <summary>
/// Http request handler
/// </summary>
//public IHttpCwsHandler HttpRequestHandler
//{
// get { return _server.HttpRequestHandler; }
@@ -58,9 +58,9 @@ namespace PepperDash.Core.Web
// }
//}
// /// <summary>
// /// Received request event handler
// /// </summary>
/// <summary>
/// Received request event handler
/// </summary>
//public event EventHandler<HttpCwsRequestEventArgs> ReceivedRequestEvent
//{
// add { _server.ReceivedRequestEvent += new HttpCwsRequestEventHandler(value); }

View File

@@ -21,6 +21,7 @@ namespace PepperDash.Essentials.Core.CrestronIO
public class GenericDigitalInputDevice : EssentialsBridgeableDevice, IDigitalInput, IHasFeedback
{
private DigitalInput inputPort;
private bool invertState;
/// <summary>
/// Gets or sets the InputStateFeedback
@@ -41,7 +42,10 @@ namespace PepperDash.Essentials.Core.CrestronIO
IOPortConfig config)
: base(key, name)
{
InputStateFeedback = new BoolFeedback("inputState", () => inputPort.State);
invertState = !string.IsNullOrEmpty(config.CircuitType) &&
config.CircuitType.Equals("NC", StringComparison.OrdinalIgnoreCase);
InputStateFeedback = new BoolFeedback("inputState", () => invertState ? !inputPort.State : inputPort.State);
AddPostActivationAction(() =>
{

View File

@@ -18,6 +18,7 @@ namespace PepperDash.Essentials.Core.CrestronIO
public class GenericVersiportDigitalInputDevice : EssentialsBridgeableDevice, IDigitalInput, IPartitionStateProvider, IHasFeedback
{
private Versiport inputPort;
private bool invertState;
/// <summary>
/// Gets or sets the InputStateFeedback
@@ -47,7 +48,10 @@ namespace PepperDash.Essentials.Core.CrestronIO
public GenericVersiportDigitalInputDevice(string key, string name, Func<IOPortConfig, Versiport> postActivationFunc, IOPortConfig config) :
base(key, name)
{
InputStateFeedback = new BoolFeedback("inputState", () => inputPort.DigitalIn);
invertState = !string.IsNullOrEmpty(config.CircuitType) &&
config.CircuitType.Equals("NC", StringComparison.OrdinalIgnoreCase);
InputStateFeedback = new BoolFeedback("inputState", () => invertState ? !inputPort.DigitalIn : inputPort.DigitalIn);
PartitionPresentFeedback = new BoolFeedback("partitionPresent", () => !inputPort.DigitalIn);
AddPostActivationAction(() =>

View File

@@ -37,5 +37,12 @@ namespace PepperDash.Essentials.Core.CrestronIO
/// </summary>
[JsonProperty("minimumChange")]
public int MinimumChange { get; set; }
/// <summary>
/// Gets or sets the circuit type: "NO" (Normally Open) or "NC" (Normally Closed)
/// If set to "NC", the input state will be inverted. Defaults to "NO" if not specified.
/// </summary>
[JsonProperty("circuitType")]
public string CircuitType { get; set; } = "NO";
}
}

View File

@@ -14,7 +14,7 @@ using PepperDash.Core;
namespace PepperDash.Essentials.Core
{
/// <summary>
///
///
/// </summary>
public class StatusMonitorCollection : IStatusMonitor
{
@@ -59,51 +59,61 @@ namespace PepperDash.Essentials.Core
void ProcessStatuses()
{
var InError = Monitors.Where(m => m.Status == MonitorStatus.InError);
var InWarning = Monitors.Where(m => m.Status == MonitorStatus.InWarning);
var IsOk = Monitors.Where(m => m.Status == MonitorStatus.IsOk);
var InError = Monitors.Where(m => m.Status == MonitorStatus.InError).ToList();
var InWarning = Monitors.Where(m => m.Status == MonitorStatus.InWarning).ToList();
var IsOk = Monitors.Where(m => m.Status == MonitorStatus.IsOk).ToList();
MonitorStatus initialStatus;
string prefix = "0:";
if (InError.Count() > 0)
if (InError.Any())
{
initialStatus = MonitorStatus.InError;
prefix = "3:";
}
else if (InWarning.Count() > 0)
else if (InWarning.Any())
{
initialStatus = MonitorStatus.InWarning;
prefix = "2:";
}
else if (IsOk.Count() > 0)
else if (IsOk.Any())
initialStatus = MonitorStatus.IsOk;
else
initialStatus = MonitorStatus.StatusUnknown;
// Build the error message string
if (InError.Count() > 0 || InWarning.Count() > 0)
{
StringBuilder sb = new StringBuilder(prefix);
if (InError.Count() > 0)
{
// Do string splits and joins
sb.Append(string.Format("{0} Errors:", InError.Count()));
foreach (var mon in InError)
sb.Append(string.Format("{0}, ", mon.Parent.Key));
}
if (InWarning.Count() > 0)
{
sb.Append(string.Format("{0} Warnings:", InWarning.Count()));
foreach (var mon in InWarning)
sb.Append(string.Format("{0}, ", mon.Parent.Key));
}
Message = sb.ToString();
}
else
{
Message = "Room Ok.";
}
if (InError.Any() || InWarning.Any())
{
var errorNames = InError
.Select(mon => mon.Parent is IKeyName keyName ? keyName.Name : mon.Parent.Key)
.ToList();
var warningNames = InWarning
.Select(mon => mon.Parent is IKeyName keyName ? keyName.Name : mon.Parent.Key)
.ToList();
var sb = new StringBuilder(prefix);
if (errorNames.Count > 0)
{
sb.Append($"{errorNames.Count} Error{(errorNames.Count > 1 ? "s" : "")}: ");
sb.Append(string.Join(", ", errorNames));
}
if (warningNames.Count > 0)
{
if (errorNames.Count > 0)
sb.Append("; ");
sb.Append($"{warningNames.Count} Warning{(warningNames.Count > 1 ? "s" : "")}: ");
sb.Append(string.Join(", ", warningNames));
}
sb.Append(" Offline");
Message = sb.ToString();
}
else
{
Message = "Room Ok.";
}
// Want to fire even if status doesn't change because the message may.
Status = initialStatus;

View File

@@ -19,7 +19,7 @@ namespace PepperDash.Essentials.Devices.Common.Shades
{
None,
Raise,
Lower
Lower,
}
/// <summary>
@@ -50,7 +50,8 @@ namespace PepperDash.Essentials.Devices.Common.Shades
get { return _isInUpPosition; }
set
{
if (value == _isInUpPosition) return;
if (value == _isInUpPosition)
return;
_isInUpPosition = value;
IsInUpPosition.FireUpdate();
PositionChanged?.Invoke(this, new EventArgs());
@@ -87,7 +88,11 @@ namespace PepperDash.Essentials.Devices.Common.Shades
/// <summary>
/// Constructor for ScreenLiftController
/// </summary>
public ScreenLiftController(string key, string name, ScreenLiftControllerConfigProperties config)
public ScreenLiftController(
string key,
string name,
ScreenLiftControllerConfigProperties config
)
: base(key, name)
{
Config = config;
@@ -105,27 +110,60 @@ namespace PepperDash.Essentials.Devices.Common.Shades
switch (Mode)
{
case eScreenLiftControlMode.momentary:
{
RaiseRelayConfig = Config.Relays["raise"];
LowerRelayConfig = Config.Relays["lower"];
break;
}
{
RaiseRelayConfig = Config.Relays["raise"];
LowerRelayConfig = Config.Relays["lower"];
break;
}
case eScreenLiftControlMode.latched:
{
LatchedRelayConfig = Config.Relays["latched"];
break;
}
{
LatchedRelayConfig = Config.Relays["latched"];
break;
}
}
IsInUpPosition.OutputChange += (sender, args) =>
{
this.LogDebug(
"ScreenLiftController '{name}' IsInUpPosition changed to {position}",
Name,
IsInUpPosition.BoolValue ? "Up" : "Down"
);
if (!Config.MuteOnScreenUp)
{
return;
}
if (args.BoolValue)
{
return;
}
if (DisplayDevice is IBasicVideoMuteWithFeedback videoMute)
{
this.LogInformation("Unmuting video because screen is down");
videoMute.VideoMuteOff();
}
};
IsInUpPosition.FireUpdate();
}
private void IsCoolingDownFeedback_OutputChange(object sender, FeedbackEventArgs e)
{
if (!DisplayDevice.IsCoolingDownFeedback.BoolValue && Type == eScreenLiftControlType.lift)
if (
!DisplayDevice.IsCoolingDownFeedback.BoolValue
&& Type == eScreenLiftControlType.lift
)
{
Raise();
return;
}
if (DisplayDevice.IsCoolingDownFeedback.BoolValue && Type == eScreenLiftControlType.screen)
if (
DisplayDevice.IsCoolingDownFeedback.BoolValue
&& Type == eScreenLiftControlType.screen
)
{
Raise();
return;
@@ -150,18 +188,18 @@ namespace PepperDash.Essentials.Devices.Common.Shades
switch (Mode)
{
case eScreenLiftControlMode.momentary:
{
this.LogDebug("Getting relays for {mode}", Mode);
RaiseRelay = GetSwitchedOutputFromDevice(RaiseRelayConfig.DeviceKey);
LowerRelay = GetSwitchedOutputFromDevice(LowerRelayConfig.DeviceKey);
break;
}
{
this.LogDebug("Getting relays for {mode}", Mode);
RaiseRelay = GetSwitchedOutputFromDevice(RaiseRelayConfig.DeviceKey);
LowerRelay = GetSwitchedOutputFromDevice(LowerRelayConfig.DeviceKey);
break;
}
case eScreenLiftControlMode.latched:
{
this.LogDebug("Getting relays for {mode}", Mode);
LatchedRelay = GetSwitchedOutputFromDevice(LatchedRelayConfig.DeviceKey);
break;
}
{
this.LogDebug("Getting relays for {mode}", Mode);
LatchedRelay = GetSwitchedOutputFromDevice(LatchedRelayConfig.DeviceKey);
break;
}
}
this.LogDebug("Getting display with key {displayKey}", DisplayDeviceKey);
@@ -172,7 +210,8 @@ namespace PepperDash.Essentials.Devices.Common.Shades
this.LogDebug("Subscribing to {displayKey} feedbacks", DisplayDeviceKey);
DisplayDevice.IsWarmingUpFeedback.OutputChange += IsWarmingUpFeedback_OutputChange;
DisplayDevice.IsCoolingDownFeedback.OutputChange += IsCoolingDownFeedback_OutputChange;
DisplayDevice.IsCoolingDownFeedback.OutputChange +=
IsCoolingDownFeedback_OutputChange;
}
return base.CustomActivate();
@@ -183,10 +222,17 @@ namespace PepperDash.Essentials.Devices.Common.Shades
/// </summary>
public void Raise()
{
if (RaiseRelay == null && LatchedRelay == null) return;
if (RaiseRelay == null && LatchedRelay == null)
return;
this.LogDebug("Raise called for {type}", Type);
if (Config.MuteOnScreenUp && DisplayDevice is IBasicVideoMuteWithFeedback videoMute)
{
this.LogInformation("Muting video because screen is going up");
videoMute.VideoMuteOn();
}
// If device is moving, bank the command
if (_isMoving)
{
@@ -200,33 +246,33 @@ namespace PepperDash.Essentials.Devices.Common.Shades
switch (Mode)
{
case eScreenLiftControlMode.momentary:
{
PulseOutput(RaiseRelay, RaiseRelayConfig.PulseTimeInMs);
{
PulseOutput(RaiseRelay, RaiseRelayConfig.PulseTimeInMs);
// Set moving flag and start timer if movement time is configured
if (RaiseRelayConfig.MoveTimeInMs > 0)
{
_isMoving = true;
_currentMovement = RequestedState.Raise;
if (_movementTimer.Enabled)
{
_movementTimer.Stop();
}
_movementTimer.Interval = RaiseRelayConfig.MoveTimeInMs;
_movementTimer.Start();
}
else
{
InUpPosition = true;
}
break;
}
case eScreenLiftControlMode.latched:
// Set moving flag and start timer if movement time is configured
if (RaiseRelayConfig.MoveTimeInMs > 0)
{
LatchedRelay.Off();
InUpPosition = true;
break;
_isMoving = true;
_currentMovement = RequestedState.Raise;
if (_movementTimer.Enabled)
{
_movementTimer.Stop();
}
_movementTimer.Interval = RaiseRelayConfig.MoveTimeInMs;
_movementTimer.Start();
}
else
{
InUpPosition = true;
}
break;
}
case eScreenLiftControlMode.latched:
{
LatchedRelay.Off();
InUpPosition = true;
break;
}
}
}
@@ -235,7 +281,8 @@ namespace PepperDash.Essentials.Devices.Common.Shades
/// </summary>
public void Lower()
{
if (LowerRelay == null && LatchedRelay == null) return;
if (LowerRelay == null && LatchedRelay == null)
return;
this.LogDebug("Lower called for {type}", Type);
@@ -252,33 +299,33 @@ namespace PepperDash.Essentials.Devices.Common.Shades
switch (Mode)
{
case eScreenLiftControlMode.momentary:
{
PulseOutput(LowerRelay, LowerRelayConfig.PulseTimeInMs);
{
PulseOutput(LowerRelay, LowerRelayConfig.PulseTimeInMs);
// Set moving flag and start timer if movement time is configured
if (LowerRelayConfig.MoveTimeInMs > 0)
{
_isMoving = true;
_currentMovement = RequestedState.Lower;
if (_movementTimer.Enabled)
{
_movementTimer.Stop();
}
_movementTimer.Interval = LowerRelayConfig.MoveTimeInMs;
_movementTimer.Start();
}
else
{
InUpPosition = false;
}
break;
}
case eScreenLiftControlMode.latched:
// Set moving flag and start timer if movement time is configured
if (LowerRelayConfig.MoveTimeInMs > 0)
{
LatchedRelay.On();
InUpPosition = false;
break;
_isMoving = true;
_currentMovement = RequestedState.Lower;
if (_movementTimer.Enabled)
{
_movementTimer.Stop();
}
_movementTimer.Interval = LowerRelayConfig.MoveTimeInMs;
_movementTimer.Start();
}
else
{
InUpPosition = false;
}
break;
}
case eScreenLiftControlMode.latched:
{
LatchedRelay.On();
InUpPosition = false;
break;
}
}
}
@@ -339,16 +386,13 @@ namespace PepperDash.Essentials.Devices.Common.Shades
{
output.On();
var timer = new Timer(pulseTime)
{
AutoReset = false
};
var timer = new Timer(pulseTime) { AutoReset = false };
timer.Elapsed += (sender, e) =>
{
output.Off();
timer.Dispose();
};
{
output.Off();
timer.Dispose();
};
timer.Start();
}
@@ -361,7 +405,10 @@ namespace PepperDash.Essentials.Devices.Common.Shades
}
else
{
this.LogWarning("Error: Unable to get relay device with key '{relayKey}'", relayKey);
this.LogWarning(
"Error: Unable to get relay device with key '{relayKey}'",
relayKey
);
return null;
}
}
@@ -375,11 +422,13 @@ namespace PepperDash.Essentials.Devices.Common.Shades
}
else
{
this.LogWarning("Error: Unable to get display device with key '{displayKey}'", displayKey);
this.LogWarning(
"Error: Unable to get display device with key '{displayKey}'",
displayKey
);
return null;
}
}
}
/// <summary>
@@ -387,7 +436,7 @@ namespace PepperDash.Essentials.Devices.Common.Shades
/// </summary>
public class ScreenLiftControllerFactory : EssentialsDeviceFactory<RelayControlledShade>
{
/// <summary>
/// <summary>
/// Constructor for ScreenLiftControllerFactory
/// </summary>
public ScreenLiftControllerFactory()
@@ -404,4 +453,4 @@ namespace PepperDash.Essentials.Devices.Common.Shades
return new ScreenLiftController(dc.Key, dc.Name, props);
}
}
}
}

View File

@@ -5,37 +5,41 @@ using PepperDash.Essentials.Core.DeviceTypeInterfaces;
namespace PepperDash.Essentials.Devices.Common.Shades
{
/// <summary>
/// Represents a ScreenLiftControllerConfigProperties
/// </summary>
public class ScreenLiftControllerConfigProperties
{
/// <summary>
/// Gets or sets the DisplayDeviceKey
/// Represents a ScreenLiftControllerConfigProperties
/// </summary>
[JsonProperty("displayDeviceKey")]
public string DisplayDeviceKey { get; set; }
public class ScreenLiftControllerConfigProperties
{
/// <summary>
/// Gets or sets the DisplayDeviceKey
/// </summary>
[JsonProperty("displayDeviceKey")]
public string DisplayDeviceKey { get; set; }
/// <summary>
/// Gets or sets the Type
/// </summary>
[JsonProperty("type")]
[JsonConverter(typeof(StringEnumConverter))]
public eScreenLiftControlType Type { get; set; }
/// <summary>
/// Gets or sets the Type
/// </summary>
[JsonProperty("type")]
[JsonConverter(typeof(StringEnumConverter))]
public eScreenLiftControlType Type { get; set; }
/// <summary>
/// Gets or sets the Mode
/// </summary>
[JsonProperty("mode")]
[JsonConverter(typeof(StringEnumConverter))]
public eScreenLiftControlMode Mode { get; set; }
/// <summary>
/// Gets or sets the Mode
/// </summary>
[JsonProperty("mode")]
[JsonConverter(typeof(StringEnumConverter))]
public eScreenLiftControlMode Mode { get; set; }
/// <summary>
/// Gets or sets the Relays
/// </summary>
[JsonProperty("relays")]
public Dictionary<string, ScreenLiftRelaysConfig> Relays { get; set; }
/// <summary>
/// Gets or sets the Relays
/// </summary>
[JsonProperty("relays")]
public Dictionary<string, ScreenLiftRelaysConfig> Relays { get; set; }
}
}
/// <summary>
/// Mutes the display when the screen is in the up position
/// </summary>
[JsonProperty("muteOnScreenUp")]
public bool MuteOnScreenUp { get; set; }
}
}