mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-08-31 19:08:29 +00:00
Compare commits
11 commits
main
...
v2.40.0-fe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
690db6beb8 | ||
|
|
3e9f32c42e | ||
|
|
2bc3e32301 | ||
|
|
528373aa61 | ||
|
|
eaaf5ecce5 | ||
|
|
5a30461eaf | ||
|
|
0ec6de3d66 | ||
|
|
35b05f2631 | ||
|
|
4e8514c6cc | ||
|
|
23ffc83dea | ||
|
|
f7a44df69f |
19 changed files with 564 additions and 146 deletions
32
README.md
32
README.md
|
|
@ -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).
|
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
|
## Support
|
||||||
|
|
||||||
* Check out our [Discord Server](https://discord.gg/rWyeRH3K)
|
* Check out our [Discord Server](https://discord.gg/rWyeRH3K)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>2.42.1-local</Version>
|
<Version>2.36.6-local</Version>
|
||||||
<InformationalVersion>$(Version)</InformationalVersion>
|
<InformationalVersion>$(Version)</InformationalVersion>
|
||||||
<Authors>PepperDash Technology</Authors>
|
<Authors>PepperDash Technology</Authors>
|
||||||
<Company>PepperDash Technology</Company>
|
<Company>PepperDash Technology</Company>
|
||||||
|
|
|
||||||
|
|
@ -151,8 +151,6 @@ namespace PepperDash.Core
|
||||||
// Thread-safety lock for state changes
|
// Thread-safety lock for state changes
|
||||||
private readonly object _stateLock = new object();
|
private readonly object _stateLock = new object();
|
||||||
|
|
||||||
private volatile bool _isProgramStopping;
|
|
||||||
|
|
||||||
private bool disconnectLogged = false;
|
private bool disconnectLogged = false;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -209,11 +207,13 @@ namespace PepperDash.Core
|
||||||
{
|
{
|
||||||
if (programEventType == eProgramStatusEventType.Stopping)
|
if (programEventType == eProgramStatusEventType.Stopping)
|
||||||
{
|
{
|
||||||
_isProgramStopping = true;
|
if (client != null)
|
||||||
|
{
|
||||||
this.LogDebug("Program stopping. Closing connection");
|
this.LogDebug("Program stopping. Closing connection");
|
||||||
Disconnect();
|
Disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Connect method
|
/// Connect method
|
||||||
|
|
@ -228,12 +228,6 @@ namespace PepperDash.Core
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isProgramStopping)
|
|
||||||
{
|
|
||||||
this.LogDebug("Skipping connect because program is stopping");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ConnectEnabled = true;
|
ConnectEnabled = true;
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
@ -293,7 +287,13 @@ namespace PepperDash.Core
|
||||||
}
|
}
|
||||||
catch (SshConnectionException e)
|
catch (SshConnectionException e)
|
||||||
{
|
{
|
||||||
var ie = e.InnerException; // The details are inside, when present - remote can close the connection with no inner exception at all
|
var ie = e.InnerException; // The details are inside!!
|
||||||
|
|
||||||
|
if (ie is SocketException)
|
||||||
|
{
|
||||||
|
this.LogError("CONNECTION failure: Cannot reach host");
|
||||||
|
this.LogVerbose(ie, "Exception details: ");
|
||||||
|
}
|
||||||
|
|
||||||
if (ie is System.Net.Sockets.SocketException socketException)
|
if (ie is System.Net.Sockets.SocketException socketException)
|
||||||
{
|
{
|
||||||
|
|
@ -301,20 +301,20 @@ namespace PepperDash.Core
|
||||||
Hostname, Port);
|
Hostname, Port);
|
||||||
this.LogVerbose(socketException, "SocketException details: ");
|
this.LogVerbose(socketException, "SocketException details: ");
|
||||||
}
|
}
|
||||||
else if (ie is SshAuthenticationException)
|
if (ie is SshAuthenticationException)
|
||||||
{
|
{
|
||||||
this.LogError("Authentication failure for username {userName}", Username);
|
this.LogError("Authentication failure for username {userName}", Username);
|
||||||
this.LogVerbose(ie, "AuthenticationException details: ");
|
this.LogVerbose(ie, "AuthenticationException details: ");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
this.LogError("Error on connect: {error}", ie?.Message ?? e.Message);
|
this.LogError("Error on connect: {error}", ie.Message);
|
||||||
this.LogVerbose(ie ?? e, "Exception details: ");
|
this.LogVerbose(ie, "Exception details: ");
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectLogged = true;
|
disconnectLogged = true;
|
||||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
if (AutoReconnect)
|
||||||
{
|
{
|
||||||
this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs);
|
this.LogDebug("Checking autoreconnect: {autoReconnect}, {autoReconnectInterval}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||||
StartReconnectTimer();
|
StartReconnectTimer();
|
||||||
|
|
@ -326,7 +326,7 @@ namespace PepperDash.Core
|
||||||
|
|
||||||
disconnectLogged = true;
|
disconnectLogged = true;
|
||||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
if (AutoReconnect)
|
||||||
{
|
{
|
||||||
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||||
StartReconnectTimer();
|
StartReconnectTimer();
|
||||||
|
|
@ -338,7 +338,7 @@ namespace PepperDash.Core
|
||||||
this.LogVerbose(e, "Exception details: ");
|
this.LogVerbose(e, "Exception details: ");
|
||||||
disconnectLogged = true;
|
disconnectLogged = true;
|
||||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
if (AutoReconnect)
|
||||||
{
|
{
|
||||||
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||||
StartReconnectTimer();
|
StartReconnectTimer();
|
||||||
|
|
@ -473,7 +473,7 @@ namespace PepperDash.Core
|
||||||
{
|
{
|
||||||
connectLock.Release();
|
connectLock.Release();
|
||||||
}
|
}
|
||||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
if (AutoReconnect && ConnectEnabled)
|
||||||
{
|
{
|
||||||
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
this.LogDebug("Checking autoreconnect: {0}, {1}ms", AutoReconnect, AutoReconnectIntervalMs);
|
||||||
StartReconnectTimer();
|
StartReconnectTimer();
|
||||||
|
|
@ -516,11 +516,8 @@ namespace PepperDash.Core
|
||||||
this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim());
|
this.LogError("ObjectDisposedException sending '{message}'. Restarting connection...", text.Trim());
|
||||||
|
|
||||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
|
||||||
{
|
|
||||||
StartReconnectTimer();
|
StartReconnectTimer();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
this.LogException(ex, "Exception sending text: '{message}'", text);
|
this.LogException(ex, "Exception sending text: '{message}'", text);
|
||||||
|
|
@ -552,11 +549,8 @@ namespace PepperDash.Core
|
||||||
this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes));
|
this.LogException(ex, "ObjectDisposedException sending {message}", ComTextHelper.GetEscapedText(bytes));
|
||||||
|
|
||||||
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
KillClient(SocketStatus.SOCKET_STATUS_CONNECT_FAILED);
|
||||||
if (AutoReconnect && ConnectEnabled && !_isProgramStopping)
|
|
||||||
{
|
|
||||||
StartReconnectTimer();
|
StartReconnectTimer();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
this.LogException(ex, "Exception sending {message}", ComTextHelper.GetEscapedText(bytes));
|
this.LogException(ex, "Exception sending {message}", ComTextHelper.GetEscapedText(bytes));
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" />
|
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" />
|
||||||
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.274" />
|
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.90" />
|
||||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||||
<PackageReference Include="Serilog.Expressions" Version="4.0.0" />
|
<PackageReference Include="Serilog.Expressions" Version="4.0.0" />
|
||||||
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />
|
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
using System;
|
using System;
|
||||||
|
using PepperDash.Core;
|
||||||
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
namespace PepperDash.Essentials.Core.DeviceTypeInterfaces
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ namespace PepperDash.Essentials.Core
|
||||||
{
|
{
|
||||||
if (IsInAutoMode)
|
if (IsInAutoMode)
|
||||||
{
|
{
|
||||||
return _partitionSensor.PartitionPresentFeedback.BoolValue;
|
return _partitionSensor?.PartitionPresentFeedback?.BoolValue ?? _partitionPresent;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _partitionPresent;
|
return _partitionPresent;
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
<DocumentationFile>bin\$(Configuration)\PepperDash_Essentials_Core.xml</DocumentationFile>
|
<DocumentationFile>bin\$(Configuration)\PepperDash_Essentials_Core.xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.274" />
|
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="Crestron\CrestronGenericBaseDevice.cs.orig" />
|
<None Include="Crestron\CrestronGenericBaseDevice.cs.orig" />
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using Serilog.Events;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
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
|
/// 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
|
/// 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>
|
/// 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;
|
private EssentialsRoomCombinerPropertiesConfig _propertiesConfig;
|
||||||
|
|
||||||
|
|
@ -75,10 +76,29 @@ 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 List<IKeyed> _operationStatusProviderDevices = new List<IKeyed>();
|
||||||
|
|
||||||
|
private string _pendingCompletionOperationId;
|
||||||
|
|
||||||
|
private string _pendingCompletionScenarioKey;
|
||||||
|
|
||||||
|
private CombinationOperationStatus _combinationOperation = new CombinationOperationStatus
|
||||||
|
{
|
||||||
|
State = CombinationOperationState.Idle
|
||||||
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="EssentialsRoomCombiner"/> class, which manages room combination
|
/// Initializes a new instance of the <see cref="EssentialsRoomCombiner"/> class, which manages room combination
|
||||||
/// scenarios and partition states.
|
/// scenarios and partition states.
|
||||||
|
|
@ -105,6 +125,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
|
||||||
|
|
@ -133,6 +159,8 @@ namespace PepperDash.Essentials.Core
|
||||||
// connected and initialized
|
// connected and initialized
|
||||||
DeviceManager.AllDevicesInitialized += (o, a) =>
|
DeviceManager.AllDevicesInitialized += (o, a) =>
|
||||||
{
|
{
|
||||||
|
InitializeOperationStatusProviders();
|
||||||
|
|
||||||
if (IsInAutoMode)
|
if (IsInAutoMode)
|
||||||
{
|
{
|
||||||
DetermineRoomCombinationScenario();
|
DetermineRoomCombinationScenario();
|
||||||
|
|
@ -256,13 +284,19 @@ namespace PepperDash.Essentials.Core
|
||||||
|
|
||||||
private async Task ChangeScenario(IRoomCombinationScenario newScenario)
|
private async Task ChangeScenario(IRoomCombinationScenario newScenario)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|
||||||
if (newScenario == _currentScenario)
|
if (newScenario == _currentScenario)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var operationId = SetCombinationOperationStatus(
|
||||||
|
CombinationOperationState.InProgress,
|
||||||
|
newScenario != null ? newScenario.Key : null,
|
||||||
|
null,
|
||||||
|
true);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
// Deactivate the old scenario first
|
// Deactivate the old scenario first
|
||||||
if (_currentScenario != null)
|
if (_currentScenario != null)
|
||||||
{
|
{
|
||||||
|
|
@ -281,7 +315,20 @@ namespace PepperDash.Essentials.Core
|
||||||
|
|
||||||
RoomCombinationScenarioChanged?.Invoke(this, new EventArgs());
|
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
|
#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>
|
/// changes. Subscribers can use this event to update their logic or UI based on the new scenario.</remarks>
|
||||||
public event EventHandler<EventArgs> RoomCombinationScenarioChanged;
|
public event EventHandler<EventArgs> RoomCombinationScenarioChanged;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Occurs when the room combination operation status changes.
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<EventArgs> CombinationOperationStatusChanged;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the current room combination scenario.
|
/// Gets the current room combination scenario.
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Gets or sets the IsInAutoModeFeedback
|
/// Gets or sets the IsInAutoModeFeedback
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -455,6 +521,299 @@ namespace PepperDash.Essentials.Core
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#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>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ namespace PepperDash.Essentials.Core
|
||||||
{
|
{
|
||||||
/// <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 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")]
|
||||||
|
|
@ -54,6 +54,13 @@ namespace PepperDash.Essentials.Core
|
||||||
/// </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>
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,14 @@ namespace PepperDash.Essentials.Core
|
||||||
/// </summary>
|
/// </summary>
|
||||||
event EventHandler<EventArgs> RoomCombinationScenarioChanged;
|
event EventHandler<EventArgs> RoomCombinationScenarioChanged;
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The current room combination scenario
|
/// The current room combination scenario
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty("currentScenario")]
|
[JsonProperty("currentScenario")]
|
||||||
IRoomCombinationScenario CurrentScenario { get; }
|
IRoomCombinationScenario CurrentScenario { get; }
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// When true, indicates the current mode is auto mode
|
/// When true, indicates the current mode is auto mode
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -85,6 +87,56 @@ namespace PepperDash.Essentials.Core
|
||||||
void SetRoomCombinationScenario(string scenarioKey);
|
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>
|
/// <summary>
|
||||||
/// Represents a scenario for combining rooms, including activation, deactivation, and associated state.
|
/// Represents a scenario for combining rooms, including activation, deactivation, and associated state.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -368,7 +368,6 @@ namespace PepperDash.Essentials.Core
|
||||||
|
|
||||||
var key = args[1];
|
var key = args[1];
|
||||||
|
|
||||||
provider.SetSecret(key, "");
|
|
||||||
response = provider.SetSecret(key, "")
|
response = provider.SetSecret(key, "")
|
||||||
? $"Secret successfully deleted for {provider.Key}:{key}"
|
? $"Secret successfully deleted for {provider.Key}:{key}"
|
||||||
: $"Unable to delete secret for {provider.Key}:{key}";
|
: $"Unable to delete secret for {provider.Key}:{key}";
|
||||||
|
|
|
||||||
|
|
@ -190,6 +190,7 @@ namespace PepperDash.Essentials.Core.Web.RequestHandlers
|
||||||
if (Debug.WebsocketSink.HasActiveConnections)
|
if (Debug.WebsocketSink.HasActiveConnections)
|
||||||
{
|
{
|
||||||
Debug.LogMessage(LogEventLevel.Debug, "Debug websocket has active connections; keeping port forward");
|
Debug.LogMessage(LogEventLevel.Debug, "Debug websocket has active connections; keeping port forward");
|
||||||
|
StartPortForwardTimeout(port, csIp);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,6 @@
|
||||||
<ProjectReference Include="..\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj" />
|
<ProjectReference Include="..\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.274" />
|
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -21,6 +21,8 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||||
{
|
{
|
||||||
private readonly IEssentialsRoomCombiner _roomCombiner;
|
private readonly IEssentialsRoomCombiner _roomCombiner;
|
||||||
|
|
||||||
|
private readonly IEssentialsRoomCombinerWithOperationStatus _roomCombinerWithOperationStatus;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="IEssentialsRoomCombinerMessenger"/> class, which facilitates
|
/// Initializes a new instance of the <see cref="IEssentialsRoomCombinerMessenger"/> class, which facilitates
|
||||||
/// messaging for an <see cref="IEssentialsRoomCombiner"/> instance.
|
/// messaging for an <see cref="IEssentialsRoomCombiner"/> instance.
|
||||||
|
|
@ -35,6 +37,7 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||||
: base(key, messagePath, roomCombiner as IKeyName)
|
: base(key, messagePath, roomCombiner as IKeyName)
|
||||||
{
|
{
|
||||||
_roomCombiner = roomCombiner;
|
_roomCombiner = roomCombiner;
|
||||||
|
_roomCombinerWithOperationStatus = roomCombiner as IEssentialsRoomCombinerWithOperationStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -98,6 +101,19 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||||
SendFullStatus();
|
SendFullStatus();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (_roomCombinerWithOperationStatus != null)
|
||||||
|
{
|
||||||
|
_roomCombinerWithOperationStatus.CombinationOperationStatusChanged += (sender, args) =>
|
||||||
|
{
|
||||||
|
var message = new
|
||||||
|
{
|
||||||
|
combinationOperation = _roomCombinerWithOperationStatus.CombinationOperation
|
||||||
|
};
|
||||||
|
|
||||||
|
PostStatusMessage(JToken.FromObject(message));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
_roomCombiner.IsInAutoModeFeedback.OutputChange += (sender, args) =>
|
_roomCombiner.IsInAutoModeFeedback.OutputChange += (sender, args) =>
|
||||||
{
|
{
|
||||||
var message = new
|
var message = new
|
||||||
|
|
@ -138,6 +154,7 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||||
DisableAutoMode = _roomCombiner.DisableAutoMode,
|
DisableAutoMode = _roomCombiner.DisableAutoMode,
|
||||||
IsInAutoMode = _roomCombiner.IsInAutoMode,
|
IsInAutoMode = _roomCombiner.IsInAutoMode,
|
||||||
CurrentScenario = _roomCombiner.CurrentScenario,
|
CurrentScenario = _roomCombiner.CurrentScenario,
|
||||||
|
CombinationOperation = _roomCombinerWithOperationStatus != null ? _roomCombinerWithOperationStatus.CombinationOperation : null,
|
||||||
Rooms = rooms,
|
Rooms = rooms,
|
||||||
RoomCombinationScenarios = _roomCombiner.RoomCombinationScenarios,
|
RoomCombinationScenarios = _roomCombiner.RoomCombinationScenarios,
|
||||||
Partitions = _roomCombiner.Partitions
|
Partitions = _roomCombiner.Partitions
|
||||||
|
|
@ -194,6 +211,12 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||||
[JsonProperty("currentScenario", NullValueHandling = NullValueHandling.Ignore)]
|
[JsonProperty("currentScenario", NullValueHandling = NullValueHandling.Ignore)]
|
||||||
public IRoomCombinationScenario CurrentScenario { get; set; }
|
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>
|
/// <summary>
|
||||||
/// Gets or sets the collection of rooms associated with the entity.
|
/// Gets or sets the collection of rooms associated with the entity.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@
|
||||||
<Compile Remove="Messengers\SIMPLVtcMessenger.cs" />
|
<Compile Remove="Messengers\SIMPLVtcMessenger.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.274" />
|
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />
|
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@
|
||||||
<Compile Remove="RoomBridges\SourceDeviceMapDictionary.cs" />
|
<Compile Remove="RoomBridges\SourceDeviceMapDictionary.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.274" />
|
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
||||||
<PackageReference Include="WebSocketSharp-netstandard" Version="1.0.1" />
|
<PackageReference Include="WebSocketSharp-netstandard" Version="1.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -25,28 +25,11 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
/// Mobile Control touchpanel controller that provides app control, Zoom integration,
|
/// Mobile Control touchpanel controller that provides app control, Zoom integration,
|
||||||
/// and mobile control functionality for Crestron touchpanels.
|
/// and mobile control functionality for Crestron touchpanels.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class MobileControlTouchpanelController : TouchpanelBase, IHasFeedback, ITswAppControl, ITswZoomControl, IDeviceInfoProvider, IMobileControlCrestronTouchpanelController, ITheme, ICommunicationMonitor
|
public class MobileControlTouchpanelController : TouchpanelBase, IHasFeedback, ITswAppControl, ITswZoomControl, IDeviceInfoProvider, IMobileControlCrestronTouchpanelController, ITheme
|
||||||
{
|
{
|
||||||
private readonly MobileControlTouchpanelProperties localConfig;
|
private readonly MobileControlTouchpanelProperties localConfig;
|
||||||
private IMobileControlRoomMessenger _bridge;
|
private IMobileControlRoomMessenger _bridge;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the CommunicationMonitor tracking the panel's online/offline state
|
|
||||||
/// </summary>
|
|
||||||
public StatusMonitorBase CommunicationMonitor { get; private set; }
|
|
||||||
|
|
||||||
private sealed class NullCommunicationMonitor : StatusMonitorBase
|
|
||||||
{
|
|
||||||
public NullCommunicationMonitor(IKeyed parent) : base(parent, 120000, 300000)
|
|
||||||
{
|
|
||||||
Status = MonitorStatus.InError;
|
|
||||||
Message = "Panel is not initialized";
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Start() { }
|
|
||||||
public override void Stop() { }
|
|
||||||
}
|
|
||||||
|
|
||||||
private string _appUrl;
|
private string _appUrl;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -145,11 +128,6 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
{
|
{
|
||||||
localConfig = config;
|
localConfig = config;
|
||||||
|
|
||||||
if (panel != null)
|
|
||||||
{
|
|
||||||
CommunicationMonitor = new CrestronGenericBaseCommunicationMonitor(this, panel, 120000, 300000);
|
|
||||||
}
|
|
||||||
|
|
||||||
AddPostActivationAction(SubscribeForMobileControlUpdates);
|
AddPostActivationAction(SubscribeForMobileControlUpdates);
|
||||||
|
|
||||||
ThemeFeedback = new StringFeedback($"{Key}-theme", () => Theme);
|
ThemeFeedback = new StringFeedback($"{Key}-theme", () => Theme);
|
||||||
|
|
@ -388,8 +366,6 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override bool CustomActivate()
|
public override bool CustomActivate()
|
||||||
{
|
{
|
||||||
CommunicationMonitor?.Start();
|
|
||||||
|
|
||||||
var appMessenger = new ITswAppControlMessenger($"appControlMessenger-{Key}", $"/device/{Key}", this);
|
var appMessenger = new ITswAppControlMessenger($"appControlMessenger-{Key}", $"/device/{Key}", this);
|
||||||
|
|
||||||
var zoomMessenger = new ITswZoomControlMessenger($"zoomControlMessenger-{Key}", $"/device/{Key}", this);
|
var zoomMessenger = new ITswZoomControlMessenger($"zoomControlMessenger-{Key}", $"/device/{Key}", this);
|
||||||
|
|
@ -417,17 +393,6 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
return base.CustomActivate();
|
return base.CustomActivate();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Stops the CommunicationMonitor on deactivation.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>True if deactivation was successful; otherwise, false.</returns>
|
|
||||||
public override bool Deactivate()
|
|
||||||
{
|
|
||||||
CommunicationMonitor?.Stop();
|
|
||||||
|
|
||||||
return base.Deactivate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles device extender signal changes for system reserved signals.
|
/// Handles device extender signal changes for system reserved signals.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -554,8 +519,15 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
return false;
|
return false;
|
||||||
}) ? csIpAddress.ToString() : processorIp;
|
}) ? csIpAddress.ToString() : processorIp;
|
||||||
|
|
||||||
// replace the host but preserve whatever scheme (http/https) is already present in the URL
|
var match = Regex.Match(url, @"^http://([^:/]+):\d+/mc/app\?token=.+$");
|
||||||
var updatedUrl = Regex.Replace(url, @"^(https?)://[^:/]+", $"$1://{ip}");
|
if (match.Success)
|
||||||
|
{
|
||||||
|
string ipa = match.Groups[1].Value;
|
||||||
|
// ip will be "192.168.1.100"
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace ipa with ip but leave the rest of the string intact
|
||||||
|
var updatedUrl = Regex.Replace(url, @"^http://[^:/]+", $"http://{ip}");
|
||||||
|
|
||||||
this.LogVerbose("Updated URL: {updatedUrl}", updatedUrl);
|
this.LogVerbose("Updated URL: {updatedUrl}", updatedUrl);
|
||||||
|
|
||||||
|
|
@ -768,7 +740,7 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MobileControlTouchpanelControllerFactory()
|
public MobileControlTouchpanelControllerFactory()
|
||||||
{
|
{
|
||||||
TypeNames = new List<string>() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mctsw1080", "mcts1080", "mcxpanel", "mcdge1000" };
|
TypeNames = new List<string>() { "mccrestronapp", "mctsw550", "mctsw750", "mctsw1050", "mctsw560", "mctsw760", "mctsw1060", "mctsw570", "mctsw770", "mcts770", "mctsw1070", "mcts1070", "mcxpanel", "mcdge1000" };
|
||||||
MinimumEssentialsFrameworkVersion = "2.0.0";
|
MinimumEssentialsFrameworkVersion = "2.0.0";
|
||||||
|
|
||||||
factories = new Dictionary<string, Func<uint, CrestronControlSystem, string, BasicTriListWithSmartObject>>
|
factories = new Dictionary<string, Func<uint, CrestronControlSystem, string, BasicTriListWithSmartObject>>
|
||||||
|
|
@ -793,8 +765,6 @@ namespace PepperDash.Essentials.Touchpanel
|
||||||
{"ts770", (id, controlSystem, projectName) => new Ts770(id, controlSystem)},
|
{"ts770", (id, controlSystem, projectName) => new Ts770(id, controlSystem)},
|
||||||
{"tsw1070", (id, controlSystem, projectName) => new Tsw1070(id, controlSystem)},
|
{"tsw1070", (id, controlSystem, projectName) => new Tsw1070(id, controlSystem)},
|
||||||
{"ts1070", (id, controlSystem, projectName) => new Ts1070(id, controlSystem)},
|
{"ts1070", (id, controlSystem, projectName) => new Ts1070(id, controlSystem)},
|
||||||
{"tsw1080", (id, controlSystem, projectName) => new Tsw1080(id, controlSystem)},
|
|
||||||
{"ts1080", (id, controlSystem, projectName) => new Ts1080(id, controlSystem)},
|
|
||||||
{"dge1000", (id, controlSystem, projectName) => new Dge1000(id, controlSystem)}
|
{"dge1000", (id, controlSystem, projectName) => new Dge1000(id, controlSystem)}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,16 +127,6 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public int Port { get; private set; }
|
public int Port { get; private set; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the HTTP scheme to use for generated URLs, based on whether the direct server is configured as secure
|
|
||||||
/// </summary>
|
|
||||||
private string HttpScheme => _parent.Config.DirectServer.Secure ? "https" : "http";
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the WebSocket scheme to use for generated URLs, based on whether the direct server is configured as secure
|
|
||||||
/// </summary>
|
|
||||||
private string WsScheme => _parent.Config.DirectServer.Secure ? "wss" : "ws";
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the user app URL prefix
|
/// Gets the user app URL prefix
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -144,8 +134,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
return string.Format("{0}://{1}:{2}{3}?token=",
|
return string.Format("http://{0}:{1}{2}?token=",
|
||||||
HttpScheme,
|
|
||||||
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
|
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
|
||||||
Port,
|
Port,
|
||||||
_userAppBaseHref);
|
_userAppBaseHref);
|
||||||
|
|
@ -284,7 +273,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
{
|
{
|
||||||
base.Initialize();
|
base.Initialize();
|
||||||
|
|
||||||
_server = new HttpServer(Port, _parent.Config.DirectServer.Secure);
|
_server = new HttpServer(Port, false);
|
||||||
|
|
||||||
_server.OnGet += Server_OnGet;
|
_server.OnGet += Server_OnGet;
|
||||||
|
|
||||||
|
|
@ -302,7 +291,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
{
|
{
|
||||||
ClientCertificateRequired = false,
|
ClientCertificateRequired = false,
|
||||||
CheckCertificateRevocation = false,
|
CheckCertificateRevocation = false,
|
||||||
EnabledSslProtocols = SslProtocols.Tls12
|
EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -414,11 +403,11 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
ip = csIpAddress.ToString();
|
ip = csIpAddress.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
var appUrl = $"{HttpScheme}://{ip}:{Port}/mc/app?token={touchpanel.Key}";
|
var appUrl = $"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}";
|
||||||
|
|
||||||
this.LogVerbose("Sending URL {appUrl} to touchpanel {touchpanelKey}", appUrl, touchpanel.Touchpanel.Key);
|
this.LogVerbose("Sending URL {appUrl} to touchpanel {touchpanelKey}", appUrl, touchpanel.Touchpanel.Key);
|
||||||
|
|
||||||
touchpanel.Touchpanel.SetAppUrl(appUrl);
|
touchpanel.Touchpanel.SetAppUrl($"http://{ip}:{_parent.Config.DirectServer.Port}/mc/app?token={touchpanel.Key}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -498,7 +487,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
{
|
{
|
||||||
var config = new MobileControlApplicationConfig
|
var config = new MobileControlApplicationConfig
|
||||||
{
|
{
|
||||||
ApiPath = string.Format("{0}://{1}:{2}/mc/api", HttpScheme, processorIp, Port),
|
ApiPath = string.Format("http://{0}:{1}/mc/api", processorIp, _parent.Config.DirectServer.Port),
|
||||||
GatewayAppPath = "",
|
GatewayAppPath = "",
|
||||||
LogoPath = _parent.Config.ApplicationConfig?.LogoPath ?? "logo/logo.png",
|
LogoPath = _parent.Config.ApplicationConfig?.LogoPath ?? "logo/logo.png",
|
||||||
EnableDev = _parent.Config.ApplicationConfig?.EnableDev ?? false,
|
EnableDev = _parent.Config.ApplicationConfig?.EnableDev ?? false,
|
||||||
|
|
@ -1109,7 +1098,6 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
res.StatusCode = 200;
|
res.StatusCode = 200;
|
||||||
res.Close();
|
res.Close();
|
||||||
|
|
||||||
// remote log collector has no dedicated secure flag; keep it on http regardless of DirectServer.Secure
|
|
||||||
var logRequest = new HttpRequestMessage(HttpMethod.Post, $"http://{_parent.Config.DirectServer.Logging.Host}:{_parent.Config.DirectServer.Logging.Port}/logs")
|
var logRequest = new HttpRequestMessage(HttpMethod.Post, $"http://{_parent.Config.DirectServer.Logging.Host}:{_parent.Config.DirectServer.Logging.Port}/logs")
|
||||||
{
|
{
|
||||||
Content = new StringContent(body, Encoding.UTF8, "application/json"),
|
Content = new StringContent(body, Encoding.UTF8, "application/json"),
|
||||||
|
|
@ -1162,11 +1150,6 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
var qp = req.QueryString;
|
var qp = req.QueryString;
|
||||||
var token = qp["token"];
|
var token = qp["token"];
|
||||||
|
|
||||||
// Each join mints a single-use clientId; the panel webview must never replay a cached
|
|
||||||
// response, or it reconnects forever with an already-consumed id (1008 loop).
|
|
||||||
res.Headers.Add("Cache-Control", "no-store");
|
|
||||||
res.Headers.Add("Pragma", "no-cache");
|
|
||||||
|
|
||||||
this.LogVerbose("Join Room Request with token: {token}", token);
|
this.LogVerbose("Join Room Request with token: {token}", token);
|
||||||
|
|
||||||
byte[] body;
|
byte[] body;
|
||||||
|
|
@ -1230,7 +1213,8 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
this.LogVerbose("Assigning ClientId: {clientId} for token: {token} at {timestamp}", clientId, token, now);
|
this.LogVerbose("Assigning ClientId: {clientId} for token: {token} at {timestamp}", clientId, token, now);
|
||||||
|
|
||||||
// Construct WebSocket URL with clientId query parameter
|
// Construct WebSocket URL with clientId query parameter
|
||||||
var wsUrl = $"{WsScheme}://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{Port}{_wsPath}{token}?clientId={clientId}";
|
var wsProtocol = "ws";
|
||||||
|
var wsUrl = $"{wsProtocol}://{CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0)}:{Port}{_wsPath}{token}?clientId={clientId}";
|
||||||
|
|
||||||
// Construct the response object
|
// Construct the response object
|
||||||
JoinResponse jRes = new JoinResponse
|
JoinResponse jRes = new JoinResponse
|
||||||
|
|
@ -1242,8 +1226,7 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
Config = _parent.GetConfigWithPluginVersion(),
|
Config = _parent.GetConfigWithPluginVersion(),
|
||||||
CodeExpires = new DateTime().AddYears(1),
|
CodeExpires = new DateTime().AddYears(1),
|
||||||
UserCode = bridge.UserCode,
|
UserCode = bridge.UserCode,
|
||||||
UserAppUrl = string.Format("{0}://{1}:{2}/mc/app",
|
UserAppUrl = string.Format("http://{0}:{1}/mc/app",
|
||||||
HttpScheme,
|
|
||||||
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
|
CrestronEthernetHelper.GetEthernetParameter(CrestronEthernetHelper.ETHERNET_PARAMETER_TO_GET.GET_CURRENT_IP_ADDRESS, 0),
|
||||||
Port),
|
Port),
|
||||||
WebSocketUrl = wsUrl,
|
WebSocketUrl = wsUrl,
|
||||||
|
|
@ -1268,8 +1251,6 @@ namespace PepperDash.Essentials.WebSocketServer
|
||||||
{
|
{
|
||||||
res.StatusCode = 200;
|
res.StatusCode = 200;
|
||||||
res.ContentType = "application/json";
|
res.ContentType = "application/json";
|
||||||
res.Headers.Add("Cache-Control", "no-store");
|
|
||||||
res.Headers.Add("Pragma", "no-cache");
|
|
||||||
var version = new Version() { ServerVersion = _parent.GetConfigWithPluginVersion().RuntimeInfo.PluginVersion };
|
var version = new Version() { ServerVersion = _parent.GetConfigWithPluginVersion().RuntimeInfo.PluginVersion };
|
||||||
var message = JsonConvert.SerializeObject(version);
|
var message = JsonConvert.SerializeObject(version);
|
||||||
this.LogVerbose("{message}", message);
|
this.LogVerbose("{message}", message);
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.274" />
|
<PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.90" />
|
||||||
<PackageReference Include="System.IO.Compression" Version="4.0.0" />
|
<PackageReference Include="System.IO.Compression" Version="4.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue