Compare commits

...

23 commits

Author SHA1 Message Date
equinoy
58c7f9bb2f Merge branch 'feature/room-combination-change-in-progress-lockout' of https://github.com/PepperDash/Essentials into feature/room-combination-change-in-progress-lockout 2026-07-15 14:44:44 -05:00
equinoy
a4f6c10e0c fix: potential fix for pull request finding 2026-07-15 14:44:27 -05:00
equinoy
c77181714b
Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-15 14:37:28 -05:00
equinoy
2d453ba8b5
Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-15 14:37:15 -05:00
equinoy
40693f8430
Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-15 14:36:58 -05:00
equinoy
1ffa010905
Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-15 14:36:44 -05:00
equinoy
a638ffa252
Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-15 14:36:09 -05:00
equinoy
ae01c59b97 docs(readme): document room combiner operation lifecycle and combinationOperationTimeoutSeconds default behavior 2026-07-15 09:52:07 -05:00
equinoy
707195b8c5 fix: guard PartitionPresent getter against null sensor feedback
EssentialsPartitionController.PartitionPresent threw a NullReferenceException in Auto mode when the partition sensor's PartitionPresentFeedback was not yet initialized, breaking combiner fullStatus serialization to MC clients. Fall back to the last-known _partitionPresent value instead of dereferencing a null sensor feedback.
2026-07-14 19:48:32 -05:00
equinoy
b499097448 feat: gate combiner completion on provider reconciliation 2026-07-07 12:18:11 -05:00
equinoy
8e9dbb6f44 feat: add optional room combiner operation timeout setting 2026-07-07 12:18:11 -05:00
equinoy
e623865c2a feat: add optional room combiner operation status lifecycle 2026-07-07 12:18:11 -05:00
Andrew Welker
f85343d8fd fix: string formatting for console responses was incorrect in some cases and causing exceptions 2026-07-07 12:18:11 -05:00
anthropic-code-agent[bot]
fb424ed5ad refactor: marked mobile control subscription items as obsolete
Co-authored-by: ndorin <18535240+ndorin@users.noreply.github.com>
2026-07-07 12:18:11 -05:00
Neil Dorin
9eacc508c2 Clarify summary for EnableMessengerSubscriptions property
Updated the summary comment for EnableMessengerSubscriptions property to clarify its purpose.
2026-07-07 12:18:11 -05:00
copilot-swe-agent[bot]
13a4f8101e Update XML summary for EnableMessengerSubscriptions to reflect obsolete status 2026-07-07 12:18:10 -05:00
Neil Dorin
493cd91daa Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-07 12:18:10 -05:00
anthropic-code-agent[bot]
8a9c8b5535 Mark IMobileControlMessengerWithSubscriptions and EnableMessengerSubscriptions as obsolete
All messengers are now subscription based in v3.x, making these
constructs no longer necessary.

Closes #1435

Agent-Logs-Url: https://github.com/PepperDash/Essentials/sessions/bda64c9c-5343-412b-801f-5e60816bc38d

Co-authored-by: ndorin <18535240+ndorin@users.noreply.github.com>
2026-07-07 12:18:10 -05:00
anthropic-code-agent[bot]
0896327dfe Initial plan 2026-07-07 12:18:10 -05:00
Neil Dorin
ee971c4ce1 fix: add port forward timeout handling in DebugSessionRequestHandler 2026-07-07 12:18:10 -05:00
Neil Dorin
7ff4f29c11 fix: improve CS LAN IP handling and update fallback debug session URL in DebugSessionRequestHandler 2026-07-07 12:18:09 -05:00
Neil Dorin
e4cd3617a1 fix: add csIp handling and update debug session URL in DebugSessionRequestHandler 2026-07-07 12:18:09 -05:00
Neil Dorin
6f2231a8d4 Potential fix for pull request finding
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-07-07 12:18:09 -05:00
14 changed files with 645 additions and 106 deletions

View file

@ -54,6 +54,38 @@ Utilization of Essentials Framework falls into the following categories:
For detailed documentation, see the [Wiki](https://github.com/PepperDash/EssentialsFramework/wiki).
## Room Combiner Operation Lifecycle
Essentials room combiner now exposes an operation lifecycle that clients can use to show progress and terminal status during scenario changes.
Lifecycle states:
- Idle
- InProgress
- Completed
- Failed
- TimedOut
The lifecycle is exposed on room combiners that implement the operation-status extension and is included in app server combiner messages under the combinationOperation payload.
Configuration:
- combinationOperationTimeoutSeconds (optional): timeout in seconds for an in-progress operation.
- Default when omitted: 300 seconds.
- Values less than or equal to 0 are treated as invalid and fall back to the default.
Example room combiner properties:
```json
{
"defaultToManualMode": false,
"defaultScenarioKey": "divided",
"combinationOperationTimeoutSeconds": 30,
"roomKeys": ["roomA", "roomB"]
}
```
Operational note:
- When an operation reaches a terminal state (Completed, Failed, TimedOut), the operation timeout timer is stopped.
## Support
* Check out our [Discord Server](https://discord.gg/rWyeRH3K)

View file

@ -72,6 +72,20 @@ namespace PepperDash.Core
/// </summary>
public bool IsRunning { get => _httpsServer?.IsListening ?? false; }
/// <summary>
/// Gets a value indicating whether there are active WebSocket connections.
/// </summary>
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;
@ -218,6 +232,8 @@ namespace PepperDash.Core
Debug.LogInformation("Starting Websocket Server on port: {0}", port);
Start(port, CertPath, _certificatePassword);
}

View file

@ -1,5 +1,5 @@
using System;
using PepperDash.Core;
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
{
/// <summary>

View file

@ -1,10 +1,12 @@
using System;
using PepperDash.Core;
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
{
/// <summary>
/// Defines the contract for IMobileControlMessenger
/// Obsolete: messengers are subscription based by default; use IMobileControlMessenger instead.
/// </summary>
[Obsolete("This interface is obsolete and will be removed in a future version. All messengers are now subscription based.")]
public interface IMobileControlMessengerWithSubscriptions : IMobileControlMessenger
{
/// <summary>

View file

@ -30,7 +30,7 @@ namespace PepperDash.Essentials.Core
{
if (IsInAutoMode)
{
return _partitionSensor.PartitionPresentFeedback.BoolValue;
return _partitionSensor?.PartitionPresentFeedback?.BoolValue ?? _partitionPresent;
}
return _partitionPresent;

View file

@ -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.</remarks>
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<IKeyed> _operationStatusProviderDevices = new List<IKeyed>();
private string _pendingCompletionOperationId;
private string _pendingCompletionScenarioKey;
private CombinationOperationStatus _combinationOperation = new CombinationOperationStatus
{
State = CombinationOperationState.Idle
};
/// <summary>
/// Initializes a new instance of the <see cref="EssentialsRoomCombiner"/> 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;
}
var operationId = SetCombinationOperationStatus(
CombinationOperationState.InProgress,
newScenario != null ? newScenario.Key : null,
null,
true);
if (newScenario == _currentScenario)
{
return;
}
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.</remarks>
public event EventHandler<EventArgs> RoomCombinationScenarioChanged;
/// <summary>
/// Occurs when the room combination operation status changes.
/// </summary>
public event EventHandler<EventArgs> CombinationOperationStatusChanged;
/// <summary>
/// Gets the current room combination scenario.
/// </summary>
@ -304,6 +356,20 @@ namespace PepperDash.Essentials.Core
}
}
/// <summary>
/// Gets the current room combination operation status.
/// </summary>
public CombinationOperationStatus CombinationOperation
{
get
{
lock (_combinationOperationLock)
{
return CloneCombinationOperationStatus(_combinationOperation);
}
}
}
/// <summary>
/// Gets or sets the IsInAutoModeFeedback
/// </summary>
@ -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<EventArgs>))
{
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<EventArgs>(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
};
}
}
/// <summary>

View file

@ -13,7 +13,7 @@ namespace PepperDash.Essentials.Core
{
/// <summary>
/// Gets or sets a value indicating whether the system operates in automatic mode.
/// <remarks>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,
/// <remarks>Some systems don't have partitions sensors, and 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.</remarks>
/// </summary>
[JsonProperty("disableAutoMode")]
@ -54,6 +54,13 @@ namespace PepperDash.Essentials.Core
/// </summary>
[JsonProperty("scenarioChangeDebounceTimeSeconds")]
public int ScenarioChangeDebounceTimeSeconds { get; set; }
/// <summary>
/// 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.
/// </summary>
[JsonProperty("combinationOperationTimeoutSeconds", NullValueHandling = NullValueHandling.Ignore)]
public int? CombinationOperationTimeoutSeconds { get; set; }
}
/// <summary>

View file

@ -16,12 +16,14 @@ namespace PepperDash.Essentials.Core
/// </summary>
event EventHandler<EventArgs> RoomCombinationScenarioChanged;
/// <summary>
/// The current room combination scenario
/// </summary>
[JsonProperty("currentScenario")]
IRoomCombinationScenario CurrentScenario { get; }
/// <summary>
/// When true, indicates the current mode is auto mode
/// </summary>
@ -85,6 +87,56 @@ namespace PepperDash.Essentials.Core
void SetRoomCombinationScenario(string scenarioKey);
}
/// <summary>
/// Optional extension for room combiners that provide operation lifecycle status.
/// </summary>
public interface IEssentialsRoomCombinerWithOperationStatus : IEssentialsRoomCombiner
{
/// <summary>
/// Indicates that the room combination operation status has changed.
/// </summary>
event EventHandler<EventArgs> CombinationOperationStatusChanged;
/// <summary>
/// Gets the current room combination operation status.
/// </summary>
[JsonProperty("combinationOperation")]
CombinationOperationStatus CombinationOperation { get; }
}
/// <summary>
/// Defines lifecycle states for a room combination operation.
/// </summary>
public enum CombinationOperationState
{
Idle,
InProgress,
Completed,
Failed,
TimedOut
}
/// <summary>
/// Represents room combination operation status details.
/// </summary>
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; }
}
/// <summary>
/// Represents a scenario for combining rooms, including activation, deactivation, and associated state.
/// </summary>

View file

@ -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,9 @@ 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;

View file

@ -18,6 +18,9 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
/// </summary>
public class DebugSessionRequestHandler : WebApiBaseRequestHandler
{
private CTimer _portForwardTimeoutTimer;
private readonly object _timerLock = new object();
/// <summary>
/// Constructor
/// </summary>
@ -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
/// <param name="context"></param>
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,56 @@ 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");
StartPortForwardTimeout(port, csIp);
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);
}
}
/// <summary>
/// Cancels the port forward timeout timer if a session is being explicitly stopped.
/// </summary>
private void CancelPortForwardTimeout()
{
lock (_timerLock)
{
_portForwardTimeoutTimer?.Dispose();
_portForwardTimeoutTimer = null;
}
}
}
}

View file

@ -52,20 +52,8 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
var device = DeviceManager.GetDeviceForKey(deviceObj.ToString()) as IHasFeedback;
if (device == null)
{
context.Response.StatusCode = 200;
context.Response.StatusDescription = "OK";
context.Response.ContentType = "application/json";
context.Response.ContentEncoding = System.Text.Encoding.UTF8;
var resp = new
{
BoolValues = Array.Empty<object>(),
IntValues = Array.Empty<object>(),
SerialValues = Array.Empty<object>()
};
var respJs = JsonConvert.SerializeObject(resp, Formatting.Indented);
context.Response.Write(respJs, false);
context.Response.StatusCode = 404;
context.Response.StatusDescription = "Not Found";
context.Response.End();
return;

View file

@ -21,6 +21,8 @@ namespace PepperDash.Essentials.AppServer.Messengers
{
private readonly IEssentialsRoomCombiner _roomCombiner;
private readonly IEssentialsRoomCombinerWithOperationStatus _roomCombinerWithOperationStatus;
/// <summary>
/// Initializes a new instance of the <see cref="IEssentialsRoomCombinerMessenger"/> class, which facilitates
/// messaging for an <see cref="IEssentialsRoomCombiner"/> instance.
@ -35,6 +37,7 @@ namespace PepperDash.Essentials.AppServer.Messengers
: base(key, messagePath, roomCombiner as IKeyName)
{
_roomCombiner = roomCombiner;
_roomCombinerWithOperationStatus = roomCombiner as IEssentialsRoomCombinerWithOperationStatus;
}
/// <summary>
@ -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; }
/// <summary>
/// Gets or sets the room combination operation status.
/// </summary>
[JsonProperty("combinationOperation", NullValueHandling = NullValueHandling.Ignore)]
public CombinationOperationStatus CombinationOperation { get; set; }
/// <summary>
/// Gets or sets the collection of rooms associated with the entity.
/// </summary>

View file

@ -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);

View file

@ -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;
/// <summary>
/// Enable subscriptions for Messengers
/// Enables subscriptions for messengers
/// </summary>
[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; }
}