From 8e9dbb6f44faf570102cd1c7899219b2999a90cd Mon Sep 17 00:00:00 2001 From: equinoy Date: Tue, 7 Jul 2026 10:54:59 -0500 Subject: [PATCH] feat: add optional room combiner operation timeout setting --- .../Room/Combining/EssentialsRoomCombiner.cs | 112 ++++++++++++++++-- .../EssentialsRoomCombinerPropertiesConfig.cs | 63 +++++----- 2 files changed, 139 insertions(+), 36 deletions(-) diff --git a/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombiner.cs b/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombiner.cs index 937fffd8..ed164eb7 100644 --- a/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombiner.cs +++ b/src/PepperDash.Essentials.Core/Room/Combining/EssentialsRoomCombiner.cs @@ -75,8 +75,14 @@ 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(); @@ -112,6 +118,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 @@ -268,7 +280,7 @@ namespace PepperDash.Essentials.Core return; } - SetCombinationOperationStatus( + var operationId = SetCombinationOperationStatus( CombinationOperationState.InProgress, newScenario != null ? newScenario.Key : null, null, @@ -294,21 +306,21 @@ namespace PepperDash.Essentials.Core RoomCombinationScenarioChanged?.Invoke(this, new EventArgs()); - SetCombinationOperationStatus( + TrySetCombinationOperationTerminalStatus( + operationId, CombinationOperationState.Completed, _currentScenario != null ? _currentScenario.Key : null, - null, - false); + null); } catch (Exception ex) { this.LogException(ex, "Error changing room combination scenario"); - SetCombinationOperationStatus( + TrySetCombinationOperationTerminalStatus( + operationId, CombinationOperationState.Failed, newScenario != null ? newScenario.Key : null, - "Combination operation failed", - false); + "Combination operation failed"); } } @@ -503,12 +515,14 @@ namespace PepperDash.Essentials.Core #endregion - private void SetCombinationOperationStatus( + private string SetCombinationOperationStatus( CombinationOperationState state, string scenarioKey, string message, bool resetStartedUtc) { + string operationId; + lock (_combinationOperationLock) { if (resetStartedUtc) @@ -528,9 +542,91 @@ namespace PepperDash.Essentials.Core _combinationOperation.State = state; _combinationOperation.Message = message; } + + 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; + 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 static CombinationOperationStatus CloneCombinationOperationStatus(CombinationOperationStatus status) 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; }