diff --git a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs
index eeba5772..cfbf5785 100644
--- a/src/PepperDash.Core/Logging/DebugWebsocketSink.cs
+++ b/src/PepperDash.Core/Logging/DebugWebsocketSink.cs
@@ -72,6 +72,20 @@ namespace PepperDash.Core
///
public bool IsRunning { get => _httpsServer?.IsListening ?? false; }
+ ///
+ /// Gets a value indicating whether there are active WebSocket connections.
+ ///
+ public bool HasActiveConnections
+ {
+ get
+ {
+ if (_httpsServer == null || !_httpsServer.IsListening) return false;
+ var service = _httpsServer.WebSocketServices[_path];
+ if (service == null) return false;
+ return service.Sessions.Count > 0;
+ }
+ }
+
private readonly ITextFormatter _textFormatter;
@@ -217,6 +231,8 @@ namespace PepperDash.Core
{
Debug.LogInformation("Starting Websocket Server on port: {0}", port);
+
+
Start(port, CertPath, _certificatePassword);
}
diff --git a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessengerWithSubscriptions.cs b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessengerWithSubscriptions.cs
index 887f1789..8603d5b6 100644
--- a/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessengerWithSubscriptions.cs
+++ b/src/PepperDash.Essentials.Core/DeviceTypeInterfaces/IMobileControlMessengerWithSubscriptions.cs
@@ -1,10 +1,12 @@
+using System;
using PepperDash.Core;
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
{
///
- /// Defines the contract for IMobileControlMessenger
+ /// Obsolete: messengers are subscription based by default; use IMobileControlMessenger instead.
///
+ [Obsolete("This interface is obsolete and will be removed in a future version. All messengers are now subscription based.")]
public interface IMobileControlMessengerWithSubscriptions : IMobileControlMessenger
{
///
diff --git a/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombiner.cs b/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombiner.cs
index a45adb27..7ca54c7a 100644
--- a/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombiner.cs
+++ b/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombiner.cs
@@ -5,6 +5,7 @@ using Serilog.Events;
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
@@ -17,7 +18,7 @@ namespace PepperDash.Essentials.Core
/// combinations based on partition states and predefined scenarios. It supports both automatic and manual modes
/// for managing room combinations. In automatic mode, the device determines the current room combination scenario
/// based on partition sensor states. In manual mode, scenarios can be set explicitly by the user.
- public class EssentialsRoomCombiner : EssentialsDevice, IEssentialsRoomCombiner
+ public class EssentialsRoomCombiner : EssentialsDevice, IEssentialsRoomCombinerWithOperationStatus
{
private EssentialsRoomCombinerPropertiesConfig _propertiesConfig;
@@ -75,10 +76,29 @@ namespace PepperDash.Essentials.Core
private CTimer _scenarioChangeDebounceTimer;
+ private CTimer _combinationOperationTimeoutTimer;
+
private int _scenarioChangeDebounceTimeSeconds = 10; // default to 10s
+ private const int DefaultCombinationOperationTimeoutSeconds = 300;
+
+ private int _combinationOperationTimeoutSeconds = DefaultCombinationOperationTimeoutSeconds;
+
private Mutex _scenarioChange = new Mutex();
+ private readonly object _combinationOperationLock = new object();
+
+ private readonly List _operationStatusProviderDevices = new List();
+
+ private string _pendingCompletionOperationId;
+
+ private string _pendingCompletionScenarioKey;
+
+ private CombinationOperationStatus _combinationOperation = new CombinationOperationStatus
+ {
+ State = CombinationOperationState.Idle
+ };
+
///
/// Initializes a new instance of the class, which manages room combination
/// scenarios and partition states.
@@ -105,6 +125,12 @@ namespace PepperDash.Essentials.Core
_scenarioChangeDebounceTimeSeconds = _propertiesConfig.ScenarioChangeDebounceTimeSeconds;
}
+ if (_propertiesConfig.CombinationOperationTimeoutSeconds.HasValue
+ && _propertiesConfig.CombinationOperationTimeoutSeconds.Value > 0)
+ {
+ _combinationOperationTimeoutSeconds = _propertiesConfig.CombinationOperationTimeoutSeconds.Value;
+ }
+
IsInAutoModeFeedback = new BoolFeedback(() => _isInAutoMode);
// default to auto mode
@@ -133,6 +159,8 @@ namespace PepperDash.Essentials.Core
// connected and initialized
DeviceManager.AllDevicesInitialized += (o, a) =>
{
+ InitializeOperationStatusProviders();
+
if (IsInAutoMode)
{
DetermineRoomCombinationScenario();
@@ -256,13 +284,19 @@ namespace PepperDash.Essentials.Core
private async Task ChangeScenario(IRoomCombinationScenario newScenario)
{
-
+ if (newScenario == _currentScenario)
+ {
+ return;
+ }
- if (newScenario == _currentScenario)
- {
- return;
- }
+ var operationId = SetCombinationOperationStatus(
+ CombinationOperationState.InProgress,
+ newScenario != null ? newScenario.Key : null,
+ null,
+ true);
+ try
+ {
// Deactivate the old scenario first
if (_currentScenario != null)
{
@@ -281,7 +315,20 @@ namespace PepperDash.Essentials.Core
RoomCombinationScenarioChanged?.Invoke(this, new EventArgs());
-
+ TryCompleteCombinationOperationIfReady(
+ operationId,
+ _currentScenario != null ? _currentScenario.Key : null);
+ }
+ catch (Exception ex)
+ {
+ this.LogException(ex, "Error changing room combination scenario");
+
+ TrySetCombinationOperationTerminalStatus(
+ operationId,
+ CombinationOperationState.Failed,
+ newScenario != null ? newScenario.Key : null,
+ "Combination operation failed");
+ }
}
#region IEssentialsRoomCombiner Members
@@ -293,6 +340,11 @@ namespace PepperDash.Essentials.Core
/// changes. Subscribers can use this event to update their logic or UI based on the new scenario.
public event EventHandler RoomCombinationScenarioChanged;
+ ///
+ /// Occurs when the room combination operation status changes.
+ ///
+ public event EventHandler CombinationOperationStatusChanged;
+
///
/// Gets the current room combination scenario.
///
@@ -304,6 +356,20 @@ namespace PepperDash.Essentials.Core
}
}
+ ///
+ /// Gets the current room combination operation status.
+ ///
+ public CombinationOperationStatus CombinationOperation
+ {
+ get
+ {
+ lock (_combinationOperationLock)
+ {
+ return CloneCombinationOperationStatus(_combinationOperation);
+ }
+ }
+ }
+
///
/// Gets or sets the IsInAutoModeFeedback
///
@@ -455,6 +521,299 @@ namespace PepperDash.Essentials.Core
}
#endregion
+
+ private string SetCombinationOperationStatus(
+ CombinationOperationState state,
+ string scenarioKey,
+ string message,
+ bool resetStartedUtc)
+ {
+ string operationId;
+
+ lock (_combinationOperationLock)
+ {
+ if (resetStartedUtc)
+ {
+ _combinationOperation = new CombinationOperationStatus
+ {
+ OperationId = Guid.NewGuid().ToString(),
+ ScenarioKey = scenarioKey,
+ StartedUtc = DateTime.UtcNow.ToString("o"),
+ State = state,
+ Message = message
+ };
+ }
+ else
+ {
+ _combinationOperation.ScenarioKey = scenarioKey ?? _combinationOperation.ScenarioKey;
+ _combinationOperation.State = state;
+ _combinationOperation.Message = message;
+ }
+
+ if (state == CombinationOperationState.InProgress)
+ {
+ _pendingCompletionOperationId = null;
+ _pendingCompletionScenarioKey = null;
+ }
+
+ operationId = _combinationOperation.OperationId;
+ }
+
+ if (state == CombinationOperationState.InProgress)
+ {
+ StartCombinationOperationTimeout(operationId);
+ }
+ else if (state == CombinationOperationState.Completed
+ || state == CombinationOperationState.Failed
+ || state == CombinationOperationState.TimedOut
+ || state == CombinationOperationState.Idle)
+ {
+ StopCombinationOperationTimeout();
+ }
+
+ CombinationOperationStatusChanged?.Invoke(this, EventArgs.Empty);
+
+ return operationId;
+ }
+
+ private void TrySetCombinationOperationTerminalStatus(
+ string operationId,
+ CombinationOperationState state,
+ string scenarioKey,
+ string message)
+ {
+ var statusUpdated = false;
+
+ lock (_combinationOperationLock)
+ {
+ if (_combinationOperation == null
+ || !string.Equals(_combinationOperation.OperationId, operationId, StringComparison.Ordinal)
+ || _combinationOperation.State != CombinationOperationState.InProgress)
+ {
+ return;
+ }
+
+ _combinationOperation.ScenarioKey = scenarioKey ?? _combinationOperation.ScenarioKey;
+ _combinationOperation.State = state;
+ _combinationOperation.Message = message;
+ _pendingCompletionOperationId = null;
+ _pendingCompletionScenarioKey = null;
+ statusUpdated = true;
+ }
+
+ if (!statusUpdated)
+ {
+ return;
+ }
+
+ StopCombinationOperationTimeout();
+ CombinationOperationStatusChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ private void StartCombinationOperationTimeout(string operationId)
+ {
+ StopCombinationOperationTimeout();
+
+ if (_combinationOperationTimeoutSeconds <= 0 || string.IsNullOrEmpty(operationId))
+ {
+ return;
+ }
+
+ _combinationOperationTimeoutTimer = new CTimer(
+ _ => HandleCombinationOperationTimeout(operationId),
+ _combinationOperationTimeoutSeconds * 1000);
+ }
+
+ private void StopCombinationOperationTimeout()
+ {
+ if (_combinationOperationTimeoutTimer == null)
+ {
+ return;
+ }
+
+ _combinationOperationTimeoutTimer.Dispose();
+ _combinationOperationTimeoutTimer = null;
+ }
+
+ private void HandleCombinationOperationTimeout(string operationId)
+ {
+ TrySetCombinationOperationTerminalStatus(
+ operationId,
+ CombinationOperationState.TimedOut,
+ null,
+ "Combination operation timed out");
+ }
+
+ private void InitializeOperationStatusProviders()
+ {
+ _operationStatusProviderDevices.Clear();
+
+ foreach (var device in DeviceManager.AllDevices)
+ {
+ if (IsOperationStatusProviderDevice(device))
+ {
+ _operationStatusProviderDevices.Add(device);
+ SubscribeToOperationStatusProviderChanged(device);
+ }
+ }
+
+ this.LogDebug("Room combiner {combinerKey} found {providerCount} post-combination status provider(s)", Key, _operationStatusProviderDevices.Count);
+ }
+
+ private static bool IsOperationStatusProviderDevice(object device)
+ {
+ if (device == null)
+ {
+ return false;
+ }
+
+ var type = device.GetType();
+
+ var roomCombinerKeyProperty = type.GetProperty("RoomCombinerKey", BindingFlags.Instance | BindingFlags.Public);
+ var scenarioReconciledProperty = type.GetProperty("ScenarioReconciled", BindingFlags.Instance | BindingFlags.Public);
+ var scenarioReconciledScenarioKeyProperty = type.GetProperty("ScenarioReconciledScenarioKey", BindingFlags.Instance | BindingFlags.Public);
+
+ return roomCombinerKeyProperty != null
+ && roomCombinerKeyProperty.PropertyType == typeof(string)
+ && roomCombinerKeyProperty.CanRead
+ && scenarioReconciledProperty != null
+ && scenarioReconciledProperty.PropertyType == typeof(bool)
+ && scenarioReconciledProperty.CanRead
+ && scenarioReconciledScenarioKeyProperty != null
+ && scenarioReconciledScenarioKeyProperty.PropertyType == typeof(string)
+ && scenarioReconciledScenarioKeyProperty.CanRead;
+ }
+
+ private void SubscribeToOperationStatusProviderChanged(object device)
+ {
+ var eventInfo = device.GetType().GetEvent("ScenarioReconciledChanged", BindingFlags.Instance | BindingFlags.Public);
+ if (eventInfo == null)
+ {
+ return;
+ }
+
+ if (eventInfo.EventHandlerType != typeof(EventHandler))
+ {
+ this.LogDebug("Room combiner {combinerKey} skipping provider event subscription for {providerType}: unsupported event type {eventType}", Key, device.GetType().Name, eventInfo.EventHandlerType);
+ return;
+ }
+
+ eventInfo.AddEventHandler(device, new EventHandler(OperationStatusProvider_ScenarioReconciledChanged));
+ }
+
+ private void OperationStatusProvider_ScenarioReconciledChanged(object sender, EventArgs e)
+ {
+ TryCompletePendingCombinationOperation();
+ }
+
+ private void TryCompletePendingCombinationOperation()
+ {
+ string operationId;
+ string scenarioKey;
+
+ lock (_combinationOperationLock)
+ {
+ operationId = _pendingCompletionOperationId;
+ scenarioKey = _pendingCompletionScenarioKey;
+ }
+
+ if (string.IsNullOrEmpty(operationId))
+ {
+ return;
+ }
+
+ TryCompleteCombinationOperationIfReady(operationId, scenarioKey);
+ }
+
+ private void TryCompleteCombinationOperationIfReady(string operationId, string scenarioKey)
+ {
+ if (string.IsNullOrEmpty(operationId))
+ {
+ return;
+ }
+
+ if (!AreOperationStatusProvidersSatisfied(scenarioKey))
+ {
+ lock (_combinationOperationLock)
+ {
+ if (_combinationOperation != null
+ && string.Equals(_combinationOperation.OperationId, operationId, StringComparison.Ordinal)
+ && _combinationOperation.State == CombinationOperationState.InProgress)
+ {
+ _pendingCompletionOperationId = operationId;
+ _pendingCompletionScenarioKey = scenarioKey;
+ }
+ }
+
+ return;
+ }
+
+ TrySetCombinationOperationTerminalStatus(
+ operationId,
+ CombinationOperationState.Completed,
+ scenarioKey,
+ null);
+ }
+
+ private bool AreOperationStatusProvidersSatisfied(string scenarioKey)
+ {
+ var matchingProviders = _operationStatusProviderDevices
+ .Where(d => string.Equals(GetStringPropertyValue(d, "RoomCombinerKey"), Key, StringComparison.OrdinalIgnoreCase))
+ .ToList();
+
+ if (!matchingProviders.Any())
+ {
+ return true;
+ }
+
+ foreach (var provider in matchingProviders)
+ {
+ var providerScenarioReconciled = GetBoolPropertyValue(provider, "ScenarioReconciled");
+ var providerScenarioKey = GetStringPropertyValue(provider, "ScenarioReconciledScenarioKey");
+
+ if (!providerScenarioReconciled
+ || !string.Equals(providerScenarioKey, scenarioKey, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static string GetStringPropertyValue(object target, string propertyName)
+ {
+ var propertyInfo = target.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public);
+ return propertyInfo != null ? propertyInfo.GetValue(target, null) as string : null;
+ }
+
+ private static bool GetBoolPropertyValue(object target, string propertyName)
+ {
+ var propertyInfo = target.GetType().GetProperty(propertyName, BindingFlags.Instance | BindingFlags.Public);
+ if (propertyInfo == null || propertyInfo.PropertyType != typeof(bool))
+ {
+ return false;
+ }
+
+ return (bool)propertyInfo.GetValue(target, null);
+ }
+
+ private static CombinationOperationStatus CloneCombinationOperationStatus(CombinationOperationStatus status)
+ {
+ if (status == null)
+ {
+ return null;
+ }
+
+ return new CombinationOperationStatus
+ {
+ OperationId = status.OperationId,
+ ScenarioKey = status.ScenarioKey,
+ StartedUtc = status.StartedUtc,
+ State = status.State,
+ Message = status.Message
+ };
+ }
}
///
diff --git a/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombinerPropertiesConfig.cs b/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombinerPropertiesConfig.cs
index 868f3992..13287c72 100644
--- a/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombinerPropertiesConfig.cs
+++ b/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombinerPropertiesConfig.cs
@@ -11,10 +11,10 @@ namespace PepperDash.Essentials.Core
///
public class EssentialsRoomCombinerPropertiesConfig
{
- ///
- /// Gets or sets a value indicating whether the system operates in automatic mode.
- /// Some systems don't have partitions sensors, and show shouldn't allow auto mode to be turned on. When this is true in the configuration,
- /// auto mode won't be allowed to be turned on.
+ ///
+ /// Gets or sets a value indicating whether the system operates in automatic mode.
+ /// Some systems don't have partitions sensors, and show shouldn't allow auto mode to be turned on. When this is true in the configuration,
+ /// auto mode won't be allowed to be turned on.
///
[JsonProperty("disableAutoMode")]
public bool DisableAutoMode { get; set; }
@@ -49,11 +49,18 @@ namespace PepperDash.Essentials.Core
[JsonProperty("defaultScenarioKey")]
public string defaultScenarioKey { get; set; }
- ///
- /// Gets or sets the debounce time, in seconds, for scenario changes.
+ ///
+ /// Gets or sets the debounce time, in seconds, for scenario changes.
///
[JsonProperty("scenarioChangeDebounceTimeSeconds")]
public int ScenarioChangeDebounceTimeSeconds { get; set; }
+
+ ///
+ /// Gets or sets the timeout, in seconds, for room combination operations.
+ /// When null or less than or equal to zero, the default timeout is used.
+ ///
+ [JsonProperty("combinationOperationTimeoutSeconds", NullValueHandling = NullValueHandling.Ignore)]
+ public int? CombinationOperationTimeoutSeconds { get; set; }
}
///
@@ -61,14 +68,14 @@ namespace PepperDash.Essentials.Core
///
public class PartitionConfig : IKeyName
{
- ///
- /// Gets or sets the unique key associated with the object.
+ ///
+ /// Gets or sets the unique key associated with the object.
///
[JsonProperty("key")]
public string Key { get; set; }
- ///
- /// Gets or sets the name associated with the object.
+ ///
+ /// Gets or sets the name associated with the object.
///
[JsonProperty("name")]
public string Name { get; set; }
@@ -91,26 +98,26 @@ namespace PepperDash.Essentials.Core
///
public class RoomCombinationScenarioConfig : IKeyName
{
- ///
- /// Gets or sets the key associated with the object.
+ ///
+ /// Gets or sets the key associated with the object.
///
[JsonProperty("key")]
public string Key { get; set; }
- ///
- /// Gets or sets the name associated with the object.
+ ///
+ /// Gets or sets the name associated with the object.
///
[JsonProperty("name")]
public string Name { get; set; }
- ///
- /// Gets or sets a value indicating whether to hide this scenario in the UI.
+ ///
+ /// Gets or sets a value indicating whether to hide this scenario in the UI.
///
[JsonProperty("hideInUi", NullValueHandling = NullValueHandling.Ignore)]
- public bool HideInUi { get; set; }
-
- ///
- /// Gets or sets the collection of partition states.
+ public bool HideInUi { get; set; }
+
+ ///
+ /// Gets or sets the collection of partition states.
///
[JsonProperty("partitionStates")]
public List PartitionStates { get; set; }
@@ -121,14 +128,14 @@ namespace PepperDash.Essentials.Core
[JsonProperty("uiMap")]
public Dictionary UiMap { get; set; }
- ///
- /// Gets or sets the list of actions to be performed during device activation.
+ ///
+ /// Gets or sets the list of actions to be performed during device activation.
///
[JsonProperty("activationActions")]
public List ActivationActions { get; set; }
- ///
- /// Gets or sets the list of actions to be performed when a device is deactivated.
+ ///
+ /// Gets or sets the list of actions to be performed when a device is deactivated.
///
[JsonProperty("deactivationActions")]
public List DeactivationActions { get; set; }
@@ -139,14 +146,14 @@ namespace PepperDash.Essentials.Core
///
public class PartitionState
{
- ///
- /// Gets or sets the partition key used to group and organize data within a storage system.
+ ///
+ /// Gets or sets the partition key used to group and organize data within a storage system.
///
[JsonProperty("partitionKey")]
public string PartitionKey { get; set; }
- ///
- /// Gets or sets a value indicating whether a partition is currently present.
+ ///
+ /// Gets or sets a value indicating whether a partition is currently present.
///
[JsonProperty("partitionSensedState")]
public bool PartitionPresent { get; set; }
diff --git a/src/PepperDash.Essentials.Core/Room/Combining/IEssentialsRoomCombiner.cs b/src/PepperDash.Essentials.Core/Room/Combining/IEssentialsRoomCombiner.cs
index 34d0a0a0..641993ea 100644
--- a/src/PepperDash.Essentials.Core/Room/Combining/IEssentialsRoomCombiner.cs
+++ b/src/PepperDash.Essentials.Core/Room/Combining/IEssentialsRoomCombiner.cs
@@ -16,12 +16,14 @@ namespace PepperDash.Essentials.Core
///
event EventHandler RoomCombinationScenarioChanged;
+
///
/// The current room combination scenario
///
[JsonProperty("currentScenario")]
IRoomCombinationScenario CurrentScenario { get; }
+
///
/// When true, indicates the current mode is auto mode
///
@@ -85,6 +87,56 @@ namespace PepperDash.Essentials.Core
void SetRoomCombinationScenario(string scenarioKey);
}
+ ///
+ /// Optional extension for room combiners that provide operation lifecycle status.
+ ///
+ public interface IEssentialsRoomCombinerWithOperationStatus : IEssentialsRoomCombiner
+ {
+ ///
+ /// Indicates that the room combination operation status has changed.
+ ///
+ event EventHandler CombinationOperationStatusChanged;
+
+ ///
+ /// Gets the current room combination operation status.
+ ///
+ [JsonProperty("combinationOperation")]
+ CombinationOperationStatus CombinationOperation { get; }
+ }
+
+ ///
+ /// Defines lifecycle states for a room combination operation.
+ ///
+ public enum CombinationOperationState
+ {
+ Idle,
+ InProgress,
+ Completed,
+ Failed,
+ TimedOut
+ }
+
+ ///
+ /// Represents room combination operation status details.
+ ///
+ public class CombinationOperationStatus
+ {
+ [JsonProperty("operationId", NullValueHandling = NullValueHandling.Ignore)]
+ public string OperationId { get; set; }
+
+ [JsonProperty("scenarioKey", NullValueHandling = NullValueHandling.Ignore)]
+ public string ScenarioKey { get; set; }
+
+ [JsonProperty("startedUtc", NullValueHandling = NullValueHandling.Ignore)]
+ public string StartedUtc { get; set; }
+
+ [JsonProperty("state")]
+ public CombinationOperationState State { get; set; }
+
+ [JsonProperty("message", NullValueHandling = NullValueHandling.Ignore)]
+ public string Message { get; set; }
+ }
+
///
/// Represents a scenario for combining rooms, including activation, deactivation, and associated state.
///
diff --git a/src/PepperDash.Essentials.Core/Secrets/SecretsManager.cs b/src/PepperDash.Essentials.Core/Secrets/SecretsManager.cs
index 382f5d55..ac4e80d8 100644
--- a/src/PepperDash.Essentials.Core/Secrets/SecretsManager.cs
+++ b/src/PepperDash.Essentials.Core/Secrets/SecretsManager.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using Crestron.SimplSharp;
using PepperDash.Core;
+using PepperDash.Core.Logging;
using Serilog.Events;
namespace PepperDash.Essentials.Core
@@ -296,19 +297,14 @@ namespace PepperDash.Essentials.Core
{
var secretPresent = provider.TestSecret(key);
- Debug.LogMessage(LogEventLevel.Verbose, provider, "SecretsProvider {0} {1} contain a secret entry for {2}", provider.Key, secretPresent ? "does" : "does not", key);
+ provider.LogVerbose("SecretsProvider {0} {1} contain a secret entry for {2}", provider.Key, secretPresent ? "does" : "does not", key);
if (!secretPresent)
return
- String.Format(
- "Unable to update secret for {0}:{1} - Please use the 'SetSecret' command to modify it");
+ $"Unable to update secret for {provider.Key}:{key} - Please use the 'SetSecret' command to modify it";
var response = provider.SetSecret(key, secret)
- ? String.Format(
- "Secret successfully set for {0}:{1}",
- provider.Key, key)
- : String.Format(
- "Unable to set secret for {0}:{1}",
- provider.Key, key);
+ ? $"Secret successfully set for {provider.Key}:{key}"
+ : $"Unable to set secret for {provider.Key}:{key}";
return response;
}
@@ -316,19 +312,14 @@ namespace PepperDash.Essentials.Core
{
var secretPresent = provider.TestSecret(key);
- Debug.LogMessage(LogEventLevel.Verbose, provider, "SecretsProvider {0} {1} contain a secret entry for {2}", provider.Key, secretPresent ? "does" : "does not", key);
+ provider.LogVerbose("SecretsProvider {0} {1} contain a secret entry for {2}", provider.Key, secretPresent ? "does" : "does not", key);
if (secretPresent)
return
- String.Format(
- "Unable to set secret for {0}:{1} - Please use the 'UpdateSecret' command to modify it");
+ $"Unable to set secret for {provider.Key}:{key} - Please use the 'UpdateSecret' command to modify it";
var response = provider.SetSecret(key, secret)
- ? String.Format(
- "Secret successfully set for {0}:{1}",
- provider.Key, key)
- : String.Format(
- "Unable to set secret for {0}:{1}",
- provider.Key, key);
+ ? $"Secret successfully set for {provider.Key}:{key}"
+ : $"Unable to set secret for {provider.Key}:{key}";
return response;
}
@@ -377,15 +368,10 @@ namespace PepperDash.Essentials.Core
var key = args[1];
-
provider.SetSecret(key, "");
response = provider.SetSecret(key, "")
- ? String.Format(
- "Secret successfully deleted for {0}:{1}",
- provider.Key, key)
- : String.Format(
- "Unable to delete secret for {0}:{1}",
- provider.Key, key);
+ ? $"Secret successfully deleted for {provider.Key}:{key}"
+ : $"Unable to delete secret for {provider.Key}:{key}";
CrestronConsole.ConsoleCommandResponse(response);
return;
diff --git a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs
index 56c983ae..59c662dc 100644
--- a/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs
+++ b/src/PepperDash.Essentials.Core/Web/RequestHandlers/DebugSessionRequestHandler.cs
@@ -17,7 +17,10 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
/// Represents a DebugSessionRequestHandler
///
public class DebugSessionRequestHandler : WebApiBaseRequestHandler
- {
+ {
+ private CTimer _portForwardTimeoutTimer;
+ private readonly object _timerLock = new object();
+
///
/// Constructor
///
@@ -48,6 +51,7 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0);
var port = 0;
+ string csIp = null;
if (!Debug.WebsocketSink.IsRunning)
{
@@ -57,15 +61,18 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
// Start the WS Server
Debug.WebsocketSink.StartServerAndSetPort(port);
Debug.SetWebSocketMinimumDebugLevel(Serilog.Events.LogEventLevel.Verbose);
+ }
- // Attempt to forward the port to the CS LAN
- try
+ // Attempt to get the CS LAN IP and forward the port
+ try
+ {
+ var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(
+ EthernetAdapterType.EthernetCSAdapter);
+ csIp = CrestronEthernetHelper.GetEthernetParameter(
+ CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId);
+
+ if (port > 0)
{
- var csAdapterId = CrestronEthernetHelper.GetAdapterdIdForSpecifiedAdapterType(
- EthernetAdapterType.EthernetCSAdapter);
- var csIp = CrestronEthernetHelper.GetEthernetParameter(
- CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId);
-
var result = CrestronEthernetHelper.AddPortForwarding(
(ushort)port, (ushort)port, csIp,
CrestronEthernetHelper.ePortMapTransport.TCP);
@@ -77,26 +84,29 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
else
{
Debug.LogMessage(LogEventLevel.Information, "Port {0} forwarded to CS LAN for debug websocket", port);
+ StartPortForwardTimeout(port, csIp);
}
}
- catch (ArgumentException)
- {
- Debug.LogMessage(LogEventLevel.Debug, "This processor does not have a CS LAN adapter; skipping port forwarding");
- }
- catch (Exception ex)
- {
- Debug.LogMessage(LogEventLevel.Warning, "Error automatically forwarding debug websocket port to CS LAN: {0}", ex.Message);
- }
+ }
+ catch (ArgumentException)
+ {
+ Debug.LogMessage(LogEventLevel.Debug, "This processor does not have a CS LAN adapter; skipping port forwarding");
+ }
+ catch (Exception ex)
+ {
+ Debug.LogMessage(LogEventLevel.Warning, "Error automatically forwarding debug websocket port to CS LAN: {0}", ex.Message);
}
var url = Debug.WebsocketSink.Url;
- object data = new
+ var data = new
{
- url = Debug.WebsocketSink.Url
+ url = Debug.WebsocketSink.Url,
+ fallbackUrl = csIp != null ? url.Replace(csIp, ip) : null
};
Debug.LogMessage(LogEventLevel.Information, "Debug Session URL: {0}", url);
+ Debug.LogMessage(LogEventLevel.Information, "Fallback Debug Session URL: {0}", data.fallbackUrl);
// Return the port number with the full url of the WS Server
var res = JsonConvert.SerializeObject(data);
@@ -120,6 +130,8 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
///
protected override void HandlePost(HttpCwsContext context)
{
+ CancelPortForwardTimeout();
+
var port = Debug.WebsocketSink.Port;
Debug.WebsocketSink.StopServer();
@@ -132,17 +144,24 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
var csIp = CrestronEthernetHelper.GetEthernetParameter(
CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, csAdapterId);
- var result = CrestronEthernetHelper.RemovePortForwarding(
- (ushort)port, (ushort)port, csIp,
- CrestronEthernetHelper.ePortMapTransport.TCP);
-
- if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr)
+ if (port <= 0)
{
- Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding for debug websocket: {0}", result);
+ Debug.LogMessage(LogEventLevel.Debug, "Debug websocket port is not set; skipping port forwarding removal");
}
else
{
- Debug.LogMessage(LogEventLevel.Information, "Port forwarding for port {0} removed", port);
+ var result = CrestronEthernetHelper.RemovePortForwarding(
+ (ushort)port, (ushort)port, csIp,
+ CrestronEthernetHelper.ePortMapTransport.TCP);
+
+ if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr)
+ {
+ Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding for debug websocket: {0}", result);
+ }
+ else
+ {
+ Debug.LogMessage(LogEventLevel.Information, "Port forwarding for port {0} removed", port);
+ }
}
}
catch (ArgumentException)
@@ -161,5 +180,55 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
Debug.LogMessage(LogEventLevel.Information, "Websocket Debug Session Stopped");
}
+ private void StartPortForwardTimeout(int port, string csIp)
+ {
+ lock (_timerLock)
+ {
+ _portForwardTimeoutTimer?.Dispose();
+ _portForwardTimeoutTimer = new CTimer(_ =>
+ {
+ if (Debug.WebsocketSink.HasActiveConnections)
+ {
+ Debug.LogMessage(LogEventLevel.Debug, "Debug websocket has active connections; keeping port forward");
+ return;
+ }
+
+ Debug.LogMessage(LogEventLevel.Information, "No debug websocket connection within 30 seconds; removing port forward for port {0}", port);
+
+ try
+ {
+ var result = CrestronEthernetHelper.RemovePortForwarding(
+ (ushort)port, (ushort)port, csIp,
+ CrestronEthernetHelper.ePortMapTransport.TCP);
+
+ if (result != CrestronEthernetHelper.PortForwardingUserPatRetCodes.NoErr)
+ {
+ Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding on timeout: {0}", result);
+ }
+ else
+ {
+ Debug.LogMessage(LogEventLevel.Information, "Port forwarding for port {0} removed due to timeout", port);
+ }
+ }
+ catch (Exception ex)
+ {
+ Debug.LogMessage(LogEventLevel.Warning, "Error removing port forwarding on timeout: {0}", ex.Message);
+ }
+ }, 30000);
+ }
+ }
+
+ ///
+ /// Cancels the port forward timeout timer if a session is being explicitly stopped.
+ ///
+ private void CancelPortForwardTimeout()
+ {
+ lock (_timerLock)
+ {
+ _portForwardTimeoutTimer?.Dispose();
+ _portForwardTimeoutTimer = null;
+ }
+ }
+
}
}
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs
index 966d8d77..65a279f2 100644
--- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/IEssentialsRoomCombinerMessenger.cs
@@ -21,6 +21,8 @@ namespace PepperDash.Essentials.AppServer.Messengers
{
private readonly IEssentialsRoomCombiner _roomCombiner;
+ private readonly IEssentialsRoomCombinerWithOperationStatus _roomCombinerWithOperationStatus;
+
///
/// Initializes a new instance of the class, which facilitates
/// messaging for an instance.
@@ -35,6 +37,7 @@ namespace PepperDash.Essentials.AppServer.Messengers
: base(key, messagePath, roomCombiner as IKeyName)
{
_roomCombiner = roomCombiner;
+ _roomCombinerWithOperationStatus = roomCombiner as IEssentialsRoomCombinerWithOperationStatus;
}
///
@@ -98,6 +101,19 @@ namespace PepperDash.Essentials.AppServer.Messengers
SendFullStatus();
};
+ if (_roomCombinerWithOperationStatus != null)
+ {
+ _roomCombinerWithOperationStatus.CombinationOperationStatusChanged += (sender, args) =>
+ {
+ var message = new
+ {
+ combinationOperation = _roomCombinerWithOperationStatus.CombinationOperation
+ };
+
+ PostStatusMessage(JToken.FromObject(message));
+ };
+ }
+
_roomCombiner.IsInAutoModeFeedback.OutputChange += (sender, args) =>
{
var message = new
@@ -138,6 +154,7 @@ namespace PepperDash.Essentials.AppServer.Messengers
DisableAutoMode = _roomCombiner.DisableAutoMode,
IsInAutoMode = _roomCombiner.IsInAutoMode,
CurrentScenario = _roomCombiner.CurrentScenario,
+ CombinationOperation = _roomCombinerWithOperationStatus != null ? _roomCombinerWithOperationStatus.CombinationOperation : null,
Rooms = rooms,
RoomCombinationScenarios = _roomCombiner.RoomCombinationScenarios,
Partitions = _roomCombiner.Partitions
@@ -194,6 +211,12 @@ namespace PepperDash.Essentials.AppServer.Messengers
[JsonProperty("currentScenario", NullValueHandling = NullValueHandling.Ignore)]
public IRoomCombinationScenario CurrentScenario { get; set; }
+ ///
+ /// Gets or sets the room combination operation status.
+ ///
+ [JsonProperty("combinationOperation", NullValueHandling = NullValueHandling.Ignore)]
+ public CombinationOperationStatus CombinationOperation { get; set; }
+
///
/// Gets or sets the collection of rooms associated with the entity.
///
diff --git a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs
index 3031f4ba..eb3afec3 100644
--- a/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs
+++ b/src/PepperDash.Essentials.MobileControl.Messengers/Messengers/MessengerBase.cs
@@ -264,6 +264,9 @@ namespace PepperDash.Essentials.AppServer.Messengers
message.Name = _device.Name;
+ message.MessageBasePath = MessagePath;
+
+
var token = JToken.FromObject(message);
PostStatusMessage(token, MessagePath, clientId);
diff --git a/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs b/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs
index ec7219a3..963e7fd5 100644
--- a/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs
+++ b/src/PepperDash.Essentials.MobileControl/MobileControlConfig.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
@@ -40,9 +41,10 @@ namespace PepperDash.Essentials
public bool EnableApiServer { get; set; } = true;
///
- /// Enable subscriptions for Messengers
+ /// Enables subscriptions for messengers
///
[JsonProperty("enableMessengerSubscriptions")]
+ [Obsolete("This property is obsolete and will be removed in a future version. All messengers are now subscription based.")]
public bool EnableMessengerSubscriptions { get; set; }
}
@@ -288,4 +290,4 @@ namespace PepperDash.Essentials
///
NEO
}
-}
\ No newline at end of file
+}