feat: add optional room combiner operation timeout setting

This commit is contained in:
equinoy 2026-07-07 10:54:59 -05:00
parent e623865c2a
commit 8e9dbb6f44
2 changed files with 139 additions and 36 deletions

View file

@ -75,8 +75,14 @@ namespace PepperDash.Essentials.Core
private CTimer _scenarioChangeDebounceTimer; private CTimer _scenarioChangeDebounceTimer;
private CTimer _combinationOperationTimeoutTimer;
private int _scenarioChangeDebounceTimeSeconds = 10; // default to 10s private int _scenarioChangeDebounceTimeSeconds = 10; // default to 10s
private const int DefaultCombinationOperationTimeoutSeconds = 300;
private int _combinationOperationTimeoutSeconds = DefaultCombinationOperationTimeoutSeconds;
private Mutex _scenarioChange = new Mutex(); private Mutex _scenarioChange = new Mutex();
private readonly object _combinationOperationLock = new object(); private readonly object _combinationOperationLock = new object();
@ -112,6 +118,12 @@ namespace PepperDash.Essentials.Core
_scenarioChangeDebounceTimeSeconds = _propertiesConfig.ScenarioChangeDebounceTimeSeconds; _scenarioChangeDebounceTimeSeconds = _propertiesConfig.ScenarioChangeDebounceTimeSeconds;
} }
if (_propertiesConfig.CombinationOperationTimeoutSeconds.HasValue
&& _propertiesConfig.CombinationOperationTimeoutSeconds.Value > 0)
{
_combinationOperationTimeoutSeconds = _propertiesConfig.CombinationOperationTimeoutSeconds.Value;
}
IsInAutoModeFeedback = new BoolFeedback(() => _isInAutoMode); IsInAutoModeFeedback = new BoolFeedback(() => _isInAutoMode);
// default to auto mode // default to auto mode
@ -268,7 +280,7 @@ namespace PepperDash.Essentials.Core
return; return;
} }
SetCombinationOperationStatus( var operationId = SetCombinationOperationStatus(
CombinationOperationState.InProgress, CombinationOperationState.InProgress,
newScenario != null ? newScenario.Key : null, newScenario != null ? newScenario.Key : null,
null, null,
@ -294,21 +306,21 @@ namespace PepperDash.Essentials.Core
RoomCombinationScenarioChanged?.Invoke(this, new EventArgs()); RoomCombinationScenarioChanged?.Invoke(this, new EventArgs());
SetCombinationOperationStatus( TrySetCombinationOperationTerminalStatus(
operationId,
CombinationOperationState.Completed, CombinationOperationState.Completed,
_currentScenario != null ? _currentScenario.Key : null, _currentScenario != null ? _currentScenario.Key : null,
null, null);
false);
} }
catch (Exception ex) catch (Exception ex)
{ {
this.LogException(ex, "Error changing room combination scenario"); this.LogException(ex, "Error changing room combination scenario");
SetCombinationOperationStatus( TrySetCombinationOperationTerminalStatus(
operationId,
CombinationOperationState.Failed, CombinationOperationState.Failed,
newScenario != null ? newScenario.Key : null, newScenario != null ? newScenario.Key : null,
"Combination operation failed", "Combination operation failed");
false);
} }
} }
@ -503,12 +515,14 @@ namespace PepperDash.Essentials.Core
#endregion #endregion
private void SetCombinationOperationStatus( private string SetCombinationOperationStatus(
CombinationOperationState state, CombinationOperationState state,
string scenarioKey, string scenarioKey,
string message, string message,
bool resetStartedUtc) bool resetStartedUtc)
{ {
string operationId;
lock (_combinationOperationLock) lock (_combinationOperationLock)
{ {
if (resetStartedUtc) if (resetStartedUtc)
@ -528,9 +542,91 @@ namespace PepperDash.Essentials.Core
_combinationOperation.State = state; _combinationOperation.State = state;
_combinationOperation.Message = message; _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); 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) private static CombinationOperationStatus CloneCombinationOperationStatus(CombinationOperationStatus status)

View file

@ -11,10 +11,10 @@ namespace PepperDash.Essentials.Core
/// </summary> /// </summary>
public class EssentialsRoomCombinerPropertiesConfig public class EssentialsRoomCombinerPropertiesConfig
{ {
/// <summary> /// <summary>
/// Gets or sets a value indicating whether the system operates in automatic mode. /// 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 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.</remarks> /// auto mode won't be allowed to be turned on.</remarks>
/// </summary> /// </summary>
[JsonProperty("disableAutoMode")] [JsonProperty("disableAutoMode")]
public bool DisableAutoMode { get; set; } public bool DisableAutoMode { get; set; }
@ -49,11 +49,18 @@ namespace PepperDash.Essentials.Core
[JsonProperty("defaultScenarioKey")] [JsonProperty("defaultScenarioKey")]
public string defaultScenarioKey { get; set; } public string defaultScenarioKey { get; set; }
/// <summary> /// <summary>
/// Gets or sets the debounce time, in seconds, for scenario changes. /// Gets or sets the debounce time, in seconds, for scenario changes.
/// </summary> /// </summary>
[JsonProperty("scenarioChangeDebounceTimeSeconds")] [JsonProperty("scenarioChangeDebounceTimeSeconds")]
public int ScenarioChangeDebounceTimeSeconds { get; set; } 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> /// <summary>
@ -61,14 +68,14 @@ namespace PepperDash.Essentials.Core
/// </summary> /// </summary>
public class PartitionConfig : IKeyName public class PartitionConfig : IKeyName
{ {
/// <summary> /// <summary>
/// Gets or sets the unique key associated with the object. /// Gets or sets the unique key associated with the object.
/// </summary> /// </summary>
[JsonProperty("key")] [JsonProperty("key")]
public string Key { get; set; } public string Key { get; set; }
/// <summary> /// <summary>
/// Gets or sets the name associated with the object. /// Gets or sets the name associated with the object.
/// </summary> /// </summary>
[JsonProperty("name")] [JsonProperty("name")]
public string Name { get; set; } public string Name { get; set; }
@ -91,26 +98,26 @@ namespace PepperDash.Essentials.Core
/// </summary> /// </summary>
public class RoomCombinationScenarioConfig : IKeyName public class RoomCombinationScenarioConfig : IKeyName
{ {
/// <summary> /// <summary>
/// Gets or sets the key associated with the object. /// Gets or sets the key associated with the object.
/// </summary> /// </summary>
[JsonProperty("key")] [JsonProperty("key")]
public string Key { get; set; } public string Key { get; set; }
/// <summary> /// <summary>
/// Gets or sets the name associated with the object. /// Gets or sets the name associated with the object.
/// </summary> /// </summary>
[JsonProperty("name")] [JsonProperty("name")]
public string Name { get; set; } public string Name { get; set; }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
[JsonProperty("hideInUi", NullValueHandling = NullValueHandling.Ignore)] [JsonProperty("hideInUi", NullValueHandling = NullValueHandling.Ignore)]
public bool HideInUi { get; set; } public bool HideInUi { get; set; }
/// <summary> /// <summary>
/// Gets or sets the collection of partition states. /// Gets or sets the collection of partition states.
/// </summary> /// </summary>
[JsonProperty("partitionStates")] [JsonProperty("partitionStates")]
public List<PartitionState> PartitionStates { get; set; } public List<PartitionState> PartitionStates { get; set; }
@ -121,14 +128,14 @@ namespace PepperDash.Essentials.Core
[JsonProperty("uiMap")] [JsonProperty("uiMap")]
public Dictionary<string, string> UiMap { get; set; } public Dictionary<string, string> UiMap { get; set; }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
[JsonProperty("activationActions")] [JsonProperty("activationActions")]
public List<DeviceActionWrapper> ActivationActions { get; set; } public List<DeviceActionWrapper> ActivationActions { get; set; }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
[JsonProperty("deactivationActions")] [JsonProperty("deactivationActions")]
public List<DeviceActionWrapper> DeactivationActions { get; set; } public List<DeviceActionWrapper> DeactivationActions { get; set; }
@ -139,14 +146,14 @@ namespace PepperDash.Essentials.Core
/// </summary> /// </summary>
public class PartitionState public class PartitionState
{ {
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
[JsonProperty("partitionKey")] [JsonProperty("partitionKey")]
public string PartitionKey { get; set; } public string PartitionKey { get; set; }
/// <summary> /// <summary>
/// Gets or sets a value indicating whether a partition is currently present. /// Gets or sets a value indicating whether a partition is currently present.
/// </summary> /// </summary>
[JsonProperty("partitionSensedState")] [JsonProperty("partitionSensedState")]
public bool PartitionPresent { get; set; } public bool PartitionPresent { get; set; }