mirror of
https://github.com/PepperDash/Essentials.git
synced 2026-01-21 16:34:48 +00:00
Compare commits
41 Commits
v2.20.1-st
...
update-cre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef73b09d08 | ||
|
|
48610d915f | ||
|
|
81df2738de | ||
|
|
08fbec416f | ||
|
|
7594b22096 | ||
|
|
d1babf6b9b | ||
|
|
2187c9fb0d | ||
|
|
a5e6059160 | ||
|
|
9ef4aedcce | ||
|
|
f7c7160bf0 | ||
|
|
dbf5740841 | ||
|
|
c07e099a79 | ||
|
|
06cb508f3a | ||
|
|
e93b5b34cc | ||
|
|
4f5d4ef87a | ||
|
|
636da8cc8c | ||
|
|
5de1e2d7bb | ||
|
|
03bbb84894 | ||
|
|
d17394cdd7 | ||
|
|
8467afde38 | ||
|
|
5c016fb4b8 | ||
|
|
2fbc32947c | ||
|
|
c06b57a5f9 | ||
|
|
6d64fffc50 | ||
|
|
0c4ebdaf1d | ||
|
|
2c49fb9321 | ||
|
|
c55de61da9 | ||
|
|
42444ede0a | ||
|
|
3d50f5f5ac | ||
|
|
11d62aebe1 | ||
|
|
edc10a9c2a | ||
|
|
9be5823956 | ||
|
|
35371dde22 | ||
|
|
d3ceb4d7e7 | ||
|
|
a782b57100 | ||
|
|
1360de599f | ||
|
|
fd95f5fed1 | ||
|
|
ea983a6f37 | ||
|
|
937a64f996 | ||
|
|
54328536a8 | ||
|
|
591f12f086 |
@@ -5,7 +5,7 @@ using System.Text.RegularExpressions;
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
///
|
||||
/// Helper class for formatting communication text and byte data for debugging purposes.
|
||||
/// </summary>
|
||||
public class ComTextHelper
|
||||
{
|
||||
|
||||
@@ -287,7 +287,7 @@ namespace PepperDash.Core
|
||||
}
|
||||
catch (SshConnectionException e)
|
||||
{
|
||||
var ie = e.InnerException; // The details are inside!!
|
||||
var ie = e.InnerException; // The details are inside!!
|
||||
|
||||
if (ie is SocketException)
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using System;
|
||||
namespace PepperDash.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// The available settings for stream debugging response types
|
||||
/// The available settings for stream debugging data format types
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum eStreamDebuggingDataTypeSettings
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.90" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.Library" Version="2.21.171" />
|
||||
<PackageReference Include="Serilog" Version="3.1.1" />
|
||||
<PackageReference Include="Serilog.Expressions" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.Formatting.Compact" Version="2.0.0" />
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using Crestron.SimplSharp;
|
||||
using Crestron.SimplSharpPro;
|
||||
using Crestron.SimplSharpPro.GeneralIO;
|
||||
using PepperDash.Core;
|
||||
using PepperDash.Core.Logging;
|
||||
using Serilog.Events;
|
||||
@@ -85,61 +87,29 @@ namespace PepperDash.Essentials.Core
|
||||
{
|
||||
if (Port == null)
|
||||
{
|
||||
Debug.LogMessage(LogEventLevel.Information, this, "Configured com Port for this device does not exist.");
|
||||
this.LogInformation($"Configured {Port.Parent.GetType().Name}-comport-{Port.ID} for {Key} does not exist.");
|
||||
return;
|
||||
}
|
||||
// TODO [ ] - Remove commented out code once verified working
|
||||
//if (Port.Parent is CrestronControlSystem || Port.Parent is CenIoCom102)
|
||||
if (Port.Parent is GenericBase genericDevice && genericDevice.Registerable)
|
||||
|
||||
|
||||
if (Port.Parent is CrestronControlSystem || Port.Parent is CenIoCom102)
|
||||
{
|
||||
//this.LogInformation($"INFO: Attempting to register {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID}");
|
||||
var result = genericDevice.Register();
|
||||
var result = Port.Register();
|
||||
if (result != eDeviceRegistrationUnRegistrationResponse.Success)
|
||||
{
|
||||
this.LogError($"ERROR: Cannot register {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {result})");
|
||||
return; // false
|
||||
this.LogError($"Cannot register {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {result})");
|
||||
return;
|
||||
}
|
||||
this.LogInformation($"Successfully registered {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {result})");
|
||||
}
|
||||
|
||||
var specResult = Port.SetComPortSpec(Spec);
|
||||
if (specResult != 0)
|
||||
{
|
||||
this.LogError($"ERROR: Cannot set comspec for {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {specResult})");
|
||||
this.LogError($"Cannot set comspec for {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {specResult})");
|
||||
return;
|
||||
}
|
||||
//this.LogInformation($"INFO: Successfully set comspec for {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {specResult})");
|
||||
|
||||
|
||||
// TODO [ ] - Remove debug logging once verified working
|
||||
// if (Port.Parent is CenIoCom102)
|
||||
// {
|
||||
// Port.PropertyChanged += (s, e) =>
|
||||
// {
|
||||
// this.LogInformation($@"RegisterAndConfigureComPort: PropertyChanged Fired >>
|
||||
// comPort-'{Port.ID}',
|
||||
// Property Changed-'{e.Property}',
|
||||
// Value Changed-'{e.Value}',
|
||||
// deviceName-'{Port.DeviceName}',
|
||||
// parentDevice-'{Port.ParentDevice}',
|
||||
// parent-`{Port.Parent}`,
|
||||
// online-`{Port.IsOnline}`,
|
||||
// present-`{Port.Present}`,
|
||||
// supportedBaudRates-'{Port.SupportedBaudRates}'");
|
||||
// };
|
||||
// Port.ExtendedInformationChanged += (s, e) =>
|
||||
// {
|
||||
|
||||
// this.LogInformation($@"RegisterAndConfigureComPort: ExtendedInformationChanged Fired >>
|
||||
// comPort-'{Port.ID}',
|
||||
// {e.Protocol},
|
||||
// {e.BaudRate},
|
||||
// {e.Parity},
|
||||
// {e.DataBits},
|
||||
// {e.StopBits},
|
||||
// HW Handshake-'{e.HardwareHandshakeSetting}',
|
||||
// SW Handshake-'{e.SoftwareHandshakeSetting}'");
|
||||
// };
|
||||
// }
|
||||
this.LogInformation($"Successfully set comspec for {Key} using {Port.Parent.GetType().Name}-comport-{Port.ID} (result == {specResult})");
|
||||
|
||||
Port.SerialDataReceived += Port_SerialDataReceived;
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ using PepperDash.Core;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Config
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads the ConfigObject from the file
|
||||
/// </summary>
|
||||
public class EssentialsConfig : BasicConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads the ConfigObject from the file
|
||||
/// </summary>
|
||||
public class EssentialsConfig : BasicConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the SystemUrl
|
||||
/// </summary>
|
||||
@@ -32,24 +32,33 @@ namespace PepperDash.Essentials.Core.Config
|
||||
/// Gets the SystemUuid extracted from the SystemUrl
|
||||
/// </summary>
|
||||
[JsonProperty("systemUuid")]
|
||||
public string SystemUuid
|
||||
public string SystemUuid
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(SystemUrl))
|
||||
return "missing url";
|
||||
string uuid;
|
||||
|
||||
if (SystemUrl.Contains("#"))
|
||||
{
|
||||
var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/#.*");
|
||||
string uuid = result.Groups[1].Value;
|
||||
return uuid;
|
||||
} else
|
||||
{
|
||||
var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/.*");
|
||||
string uuid = result.Groups[1].Value;
|
||||
return uuid;
|
||||
if (string.IsNullOrEmpty(SystemUrl))
|
||||
{
|
||||
uuid = "missing url";
|
||||
}
|
||||
else if (SystemUrl.Contains("#"))
|
||||
{
|
||||
var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/#.*");
|
||||
uuid = result.Groups[1].Value;
|
||||
}
|
||||
else if (SystemUrl.Contains("detail"))
|
||||
{
|
||||
var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/detail\/(.*)\/.*");
|
||||
uuid = result.Groups[1].Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = Regex.Match(SystemUrl, @"https?:\/\/.*\/systems\/(.*)\/.*");
|
||||
uuid = result.Groups[1].Value;
|
||||
}
|
||||
|
||||
return uuid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,24 +66,33 @@ namespace PepperDash.Essentials.Core.Config
|
||||
/// Gets the TemplateUuid extracted from the TemplateUrl
|
||||
/// </summary>
|
||||
[JsonProperty("templateUuid")]
|
||||
public string TemplateUuid
|
||||
public string TemplateUuid
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(TemplateUrl))
|
||||
return "missing template url";
|
||||
string uuid;
|
||||
|
||||
if (TemplateUrl.Contains("#"))
|
||||
{
|
||||
var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/templates\/(.*)\/#.*");
|
||||
string uuid = result.Groups[1].Value;
|
||||
return uuid;
|
||||
} else
|
||||
{
|
||||
var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/system-templates\/(.*)\/system-template-versions\/(.*)\/.*");
|
||||
string uuid = result.Groups[2].Value;
|
||||
return uuid;
|
||||
if (string.IsNullOrEmpty(TemplateUrl))
|
||||
{
|
||||
uuid = "missing template url";
|
||||
}
|
||||
else if (TemplateUrl.Contains("#"))
|
||||
{
|
||||
var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/templates\/(.*)\/#.*");
|
||||
uuid = result.Groups[1].Value;
|
||||
}
|
||||
else if (TemplateUrl.Contains("detail"))
|
||||
{
|
||||
var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/system-templates\/detail\/(.*)\/system-template-versions\/detail\/(.*)\/.*");
|
||||
uuid = result.Groups[2].Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
var result = Regex.Match(TemplateUrl, @"https?:\/\/.*\/system-templates\/(.*)\/system-template-versions\/(.*)\/.*");
|
||||
uuid = result.Groups[2].Value;
|
||||
}
|
||||
|
||||
return uuid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +115,7 @@ namespace PepperDash.Essentials.Core.Config
|
||||
{
|
||||
Rooms = new List<DeviceConfig>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents version data for Essentials and its packages
|
||||
@@ -147,7 +165,7 @@ namespace PepperDash.Essentials.Core.Config
|
||||
/// Represents a SystemTemplateConfigs
|
||||
/// </summary>
|
||||
public class SystemTemplateConfigs
|
||||
{
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the System
|
||||
/// </summary>
|
||||
@@ -157,5 +175,5 @@ namespace PepperDash.Essentials.Core.Config
|
||||
/// Gets or sets the Template
|
||||
/// </summary>
|
||||
public EssentialsConfig Template { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -77,9 +77,6 @@ namespace PepperDash.Essentials.Core
|
||||
/// A name that will override the source's name on the UI
|
||||
/// </summary>
|
||||
[JsonProperty("name")]
|
||||
/// <summary>
|
||||
/// Gets or sets the Name
|
||||
/// </summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -14,13 +14,14 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Timers;
|
||||
|
||||
namespace PepperDash.Essentials.Core.Fusion
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a EssentialsHuddleSpaceFusionSystemControllerBase
|
||||
/// </summary>
|
||||
public class IEssentialsRoomFusionController : EssentialsDevice, IOccupancyStatusProvider, IFusionHelpRequest
|
||||
public class IEssentialsRoomFusionController : EssentialsDevice, IOccupancyStatusProvider, IFusionHelpRequest, IHasFeedback
|
||||
{
|
||||
private IEssentialsRoomFusionControllerPropertiesConfig _config;
|
||||
|
||||
@@ -87,15 +88,17 @@ namespace PepperDash.Essentials.Core.Fusion
|
||||
/// <inheritdoc />
|
||||
public StringFeedback HelpRequestStatusFeedback { get; private set; }
|
||||
|
||||
private Timer _helpRequestTimeoutTimer;
|
||||
|
||||
#region System Info Sigs
|
||||
/// <summary>
|
||||
/// Gets the DefaultHelpRequestTimeoutMs
|
||||
/// </summary>
|
||||
public int HelpRequestTimeoutMs => _config.HelpRequestTimeoutMs;
|
||||
|
||||
//StringSigData SystemName;
|
||||
//StringSigData Model;
|
||||
//StringSigData SerialNumber;
|
||||
//StringSigData Uptime;
|
||||
|
||||
#endregion
|
||||
/// <summary>
|
||||
/// Gets whether to use a timer for help requests
|
||||
/// </summary>
|
||||
public bool UseHelpRequestTimer => _config.UseTimeoutForHelpRequests;
|
||||
|
||||
#region Processor Info Sigs
|
||||
|
||||
@@ -240,6 +243,19 @@ namespace PepperDash.Essentials.Core.Fusion
|
||||
|
||||
this.LogDebug("Occupancy setup complete");
|
||||
|
||||
HelpRequestResponseFeedback = new StringFeedback("HelpRequestResponse", () => FusionRoom.Help.OutputSig.StringValue);
|
||||
|
||||
HelpRequestSentFeedback = new BoolFeedback("HelpRequestSent", () => _helpRequestSent);
|
||||
HelpRequestStatusFeedback = new StringFeedback("HelpRequestStatus", () => _helpRequestStatus.ToString());
|
||||
|
||||
Feedbacks.Add(HelpRequestResponseFeedback);
|
||||
Feedbacks.Add(HelpRequestSentFeedback);
|
||||
Feedbacks.Add(HelpRequestStatusFeedback);
|
||||
if (RoomOccupancyRemoteStringFeedback != null)
|
||||
Feedbacks.Add(RoomOccupancyRemoteStringFeedback);
|
||||
if (RoomIsOccupiedFeedback != null)
|
||||
Feedbacks.Add(RoomIsOccupiedFeedback);
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -303,10 +319,6 @@ namespace PepperDash.Essentials.Core.Fusion
|
||||
|
||||
FusionRVI.GenerateFileForAllFusionDevices();
|
||||
|
||||
HelpRequestResponseFeedback = new StringFeedback("HelpRequestResponse", () => FusionRoom.Help.OutputSig.StringValue);
|
||||
|
||||
HelpRequestSentFeedback = new BoolFeedback("HelpRequestSent", () => _helpRequestSent);
|
||||
HelpRequestStatusFeedback = new StringFeedback("HelpRequestStatus", () => _helpRequestStatus.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -339,6 +351,11 @@ namespace PepperDash.Essentials.Core.Fusion
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public FeedbackCollection<Feedback> Feedbacks { get; private set; } = new FeedbackCollection<Feedback>();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// ScheduleChange event
|
||||
/// </summary>
|
||||
@@ -1772,7 +1789,7 @@ namespace PepperDash.Essentials.Core.Fusion
|
||||
{
|
||||
if (args.EventId == FusionEventIds.HelpMessageReceivedEventId)
|
||||
{
|
||||
this.LogInformation( "Help message received from Fusion for room '{0}'",
|
||||
this.LogInformation("Help message received from Fusion for room '{0}'",
|
||||
Room.Name);
|
||||
|
||||
this.LogDebug("Help message content: {0}", FusionRoom.Help.OutputSig.StringValue);
|
||||
@@ -1791,7 +1808,7 @@ namespace PepperDash.Essentials.Core.Fusion
|
||||
break;
|
||||
case "Please call the helpdesk.":
|
||||
// this.LogInformation("Please call the helpdesk.");
|
||||
// _helpRequestStatus = eFusionHelpResponse.CallHelpDesk;
|
||||
_helpRequestStatus = eFusionHelpResponse.CallHelpDesk;
|
||||
break;
|
||||
case "Please wait, I will reschedule your meeting to a different room.":
|
||||
// this.LogInformation("Please wait, I will reschedule your meeting to a different room.",
|
||||
@@ -1818,13 +1835,21 @@ namespace PepperDash.Essentials.Core.Fusion
|
||||
_helpRequestStatus = eFusionHelpResponse.None;
|
||||
}
|
||||
|
||||
if(_helpRequestStatus == eFusionHelpResponse.None)
|
||||
if (_helpRequestStatus == eFusionHelpResponse.None)
|
||||
{
|
||||
_helpRequestSent = false;
|
||||
HelpRequestSentFeedback.FireUpdate();
|
||||
}
|
||||
|
||||
|
||||
HelpRequestStatusFeedback.FireUpdate();
|
||||
|
||||
if (_helpRequestTimeoutTimer != null)
|
||||
{
|
||||
_helpRequestTimeoutTimer.Stop();
|
||||
_helpRequestTimeoutTimer.Elapsed -= OnTimedEvent;
|
||||
_helpRequestTimeoutTimer.Dispose();
|
||||
_helpRequestTimeoutTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1895,10 +1920,34 @@ namespace PepperDash.Essentials.Core.Fusion
|
||||
_helpRequestSent = true;
|
||||
HelpRequestSentFeedback.FireUpdate();
|
||||
|
||||
if (UseHelpRequestTimer)
|
||||
{
|
||||
if (_helpRequestTimeoutTimer == null)
|
||||
{
|
||||
_helpRequestTimeoutTimer = new Timer(HelpRequestTimeoutMs);
|
||||
_helpRequestTimeoutTimer.AutoReset = false;
|
||||
_helpRequestTimeoutTimer.Enabled = true;
|
||||
|
||||
_helpRequestTimeoutTimer.Elapsed += OnTimedEvent;
|
||||
}
|
||||
|
||||
_helpRequestTimeoutTimer.Interval = HelpRequestTimeoutMs;
|
||||
_helpRequestTimeoutTimer.Start();
|
||||
|
||||
this.LogDebug("Help request timeout timer started for room '{0}' with timeout of {1} ms.",
|
||||
Room.Name, HelpRequestTimeoutMs);
|
||||
}
|
||||
|
||||
_helpRequestStatus = eFusionHelpResponse.HelpRequested;
|
||||
HelpRequestStatusFeedback.FireUpdate();
|
||||
}
|
||||
|
||||
private void OnTimedEvent(object source, ElapsedEventArgs e)
|
||||
{
|
||||
this.LogInformation("Help request timeout reached for room '{0}'. Cancelling help request.", Room.Name);
|
||||
CancelHelpRequest();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void CancelHelpRequest()
|
||||
{
|
||||
@@ -1909,7 +1958,16 @@ namespace PepperDash.Essentials.Core.Fusion
|
||||
HelpRequestSentFeedback.FireUpdate();
|
||||
_helpRequestStatus = eFusionHelpResponse.None;
|
||||
HelpRequestStatusFeedback.FireUpdate();
|
||||
Debug.LogMessage(LogEventLevel.Information, this, "Help request cancelled in Fusion for room '{0}'", Room.Name);
|
||||
Debug.LogMessage(LogEventLevel.Information, this, "Help request cancelled for room '{0}'", Room.Name);
|
||||
}
|
||||
|
||||
if (_helpRequestTimeoutTimer != null)
|
||||
{
|
||||
_helpRequestTimeoutTimer.Stop();
|
||||
_helpRequestTimeoutTimer.Elapsed -= OnTimedEvent;
|
||||
_helpRequestTimeoutTimer.Dispose();
|
||||
_helpRequestTimeoutTimer = null;
|
||||
this.LogDebug("Help request timeout timer stopped for room '{0}'.", Room.Name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,4 +56,16 @@ public class IEssentialsRoomFusionControllerPropertiesConfig
|
||||
/// </summary>
|
||||
[JsonProperty("use24HourTimeFormat")]
|
||||
public bool Use24HourTimeFormat { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to use a timeout for help requests
|
||||
/// </summary>
|
||||
[JsonProperty("useTimeoutForHelpRequests")]
|
||||
public bool UseTimeoutForHelpRequests { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timeout duration for help requests in milliseconds
|
||||
/// </summary>
|
||||
[JsonProperty("helpRequestTimeoutMs")]
|
||||
public int HelpRequestTimeoutMs { get; set; } = 30000;
|
||||
}
|
||||
@@ -25,7 +25,7 @@
|
||||
<DocumentationFile>bin\$(Configuration)\PepperDash_Essentials_Core.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.171" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="Crestron\CrestronGenericBaseDevice.cs.orig" />
|
||||
|
||||
@@ -105,12 +105,21 @@ namespace PepperDash.Essentials.Room.Config
|
||||
/// </summary>
|
||||
public class EssentialsRoomPropertiesConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Addresses
|
||||
/// </summary>
|
||||
[JsonProperty("addresses")]
|
||||
public EssentialsRoomAddressPropertiesConfig Addresses { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Description
|
||||
/// </summary>
|
||||
[JsonProperty("description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Emergency
|
||||
/// </summary>
|
||||
[JsonProperty("emergency")]
|
||||
public EssentialsRoomEmergencyConfig Emergency { get; set; }
|
||||
|
||||
@@ -226,11 +235,11 @@ namespace PepperDash.Essentials.Room.Config
|
||||
/// Indicates if this room represents a combination of other rooms
|
||||
/// </summary>
|
||||
[JsonProperty("isRoomCombinationScenario")]
|
||||
/// <summary>
|
||||
/// Gets or sets the IsRoomCombinationScenario
|
||||
/// </summary>
|
||||
public bool IsRoomCombinationScenario { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
public EssentialsRoomPropertiesConfig()
|
||||
{
|
||||
LogoLight = new EssentialsLogoPropertiesConfig();
|
||||
@@ -243,10 +252,10 @@ namespace PepperDash.Essentials.Room.Config
|
||||
/// </summary>
|
||||
public class EssentialsRoomUiBehaviorConfig
|
||||
{
|
||||
[JsonProperty("disableActivityButtonsWhileWarmingCooling")]
|
||||
/// <summary>
|
||||
/// Gets or sets the DisableActivityButtonsWhileWarmingCooling
|
||||
/// </summary>
|
||||
[JsonProperty("disableActivityButtonsWhileWarmingCooling")]
|
||||
public bool DisableActivityButtonsWhileWarmingCooling { get; set; }
|
||||
}
|
||||
|
||||
@@ -255,74 +264,86 @@ namespace PepperDash.Essentials.Room.Config
|
||||
/// </summary>
|
||||
public class EssentialsAvRoomPropertiesConfig : EssentialsRoomPropertiesConfig
|
||||
{
|
||||
[JsonProperty("defaultAudioKey")]
|
||||
/// <summary>
|
||||
/// Gets or sets the DefaultAudioKey
|
||||
/// </summary>
|
||||
[JsonProperty("defaultAudioKey")]
|
||||
public string DefaultAudioKey { get; set; }
|
||||
[JsonProperty("sourceListKey")]
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the DefaultOnDspPresetKey
|
||||
/// </summary>
|
||||
[JsonProperty("defaultOnDspPresetKey")]
|
||||
public string DefaultOnDspPresetKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the DefaultOffDspPresetKey
|
||||
/// </summary>
|
||||
[JsonProperty("defaultOffDspPresetKey")]
|
||||
public string DefaultOffDspPresetKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the SourceListKey
|
||||
/// </summary>
|
||||
/// </summary>
|
||||
[JsonProperty("sourceListKey")]
|
||||
public string SourceListKey { get; set; }
|
||||
[JsonProperty("destinationListKey")]
|
||||
/// <summary>
|
||||
/// Gets or sets the DestinationListKey
|
||||
/// </summary>
|
||||
[JsonProperty("destinationListKey")]
|
||||
public string DestinationListKey { get; set; }
|
||||
[JsonProperty("audioControlPointListKey")]
|
||||
/// <summary>
|
||||
/// Gets or sets the AudioControlPointListKey
|
||||
/// </summary>
|
||||
[JsonProperty("audioControlPointListKey")]
|
||||
public string AudioControlPointListKey { get; set; }
|
||||
[JsonProperty("cameraListKey")]
|
||||
/// <summary>
|
||||
/// Gets or sets the CameraListKey
|
||||
/// </summary>
|
||||
[JsonProperty("cameraListKey")]
|
||||
public string CameraListKey { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("defaultSourceItem")]
|
||||
/// <summary>
|
||||
/// Gets or sets the DefaultSourceItem
|
||||
/// </summary>
|
||||
[JsonProperty("defaultSourceItem")]
|
||||
public string DefaultSourceItem { get; set; }
|
||||
/// <summary>
|
||||
/// Indicates if the room supports advanced sharing
|
||||
/// </summary>
|
||||
[JsonProperty("supportsAdvancedSharing")]
|
||||
/// <summary>
|
||||
/// Gets or sets the SupportsAdvancedSharing
|
||||
/// </summary>
|
||||
public bool SupportsAdvancedSharing { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates if non-tech users can change the share mode
|
||||
/// </summary>
|
||||
[JsonProperty("userCanChangeShareMode")]
|
||||
/// <summary>
|
||||
/// Gets or sets the UserCanChangeShareMode
|
||||
/// </summary>
|
||||
public bool UserCanChangeShareMode { get; set; }
|
||||
|
||||
|
||||
[JsonProperty("matrixRoutingKey", NullValueHandling = NullValueHandling.Ignore)]
|
||||
/// <summary>
|
||||
/// Gets or sets the MatrixRoutingKey
|
||||
/// </summary>
|
||||
[JsonProperty("matrixRoutingKey", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string MatrixRoutingKey { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a EssentialsConferenceRoomPropertiesConfig
|
||||
/// </summary>
|
||||
public class EssentialsConferenceRoomPropertiesConfig : EssentialsAvRoomPropertiesConfig
|
||||
{
|
||||
[JsonProperty("videoCodecKey")]
|
||||
/// <summary>
|
||||
/// Gets or sets the VideoCodecKey
|
||||
/// </summary>
|
||||
[JsonProperty("videoCodecKey")]
|
||||
public string VideoCodecKey { get; set; }
|
||||
[JsonProperty("audioCodecKey")]
|
||||
/// <summary>
|
||||
/// Gets or sets the AudioCodecKey
|
||||
/// </summary>
|
||||
[JsonProperty("audioCodecKey")]
|
||||
public string AudioCodecKey { get; set; }
|
||||
|
||||
}
|
||||
@@ -337,12 +358,15 @@ namespace PepperDash.Essentials.Room.Config
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
[JsonProperty("deviceKeys")]
|
||||
/// <summary>
|
||||
/// Gets or sets the DeviceKeys
|
||||
/// </summary>
|
||||
[JsonProperty("deviceKeys")]
|
||||
public List<string> DeviceKeys { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
public EssentialsEnvironmentPropertiesConfig()
|
||||
{
|
||||
DeviceKeys = new List<string>();
|
||||
@@ -355,6 +379,9 @@ namespace PepperDash.Essentials.Room.Config
|
||||
/// </summary>
|
||||
public class EssentialsRoomFusionConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the the IpId as a UInt16
|
||||
/// </summary>
|
||||
public uint IpIdInt
|
||||
{
|
||||
get
|
||||
@@ -371,16 +398,16 @@ namespace PepperDash.Essentials.Room.Config
|
||||
}
|
||||
}
|
||||
|
||||
[JsonProperty("ipId")]
|
||||
/// <summary>
|
||||
/// Gets or sets the IpId
|
||||
/// </summary>
|
||||
[JsonProperty("ipId")]
|
||||
public string IpId { get; set; }
|
||||
|
||||
[JsonProperty("joinMapKey")]
|
||||
/// <summary>
|
||||
/// Gets or sets the JoinMapKey
|
||||
/// </summary>
|
||||
[JsonProperty("joinMapKey")]
|
||||
public string JoinMapKey { get; set; }
|
||||
|
||||
}
|
||||
@@ -390,16 +417,16 @@ namespace PepperDash.Essentials.Room.Config
|
||||
/// </summary>
|
||||
public class EssentialsRoomMicrophonePrivacyConfig
|
||||
{
|
||||
[JsonProperty("deviceKey")]
|
||||
/// <summary>
|
||||
/// Gets or sets the DeviceKey
|
||||
/// </summary>
|
||||
[JsonProperty("deviceKey")]
|
||||
public string DeviceKey { get; set; }
|
||||
|
||||
[JsonProperty("behaviour")]
|
||||
/// <summary>
|
||||
/// Gets or sets the Behaviour
|
||||
/// </summary>
|
||||
[JsonProperty("behaviour")]
|
||||
public string Behaviour { get; set; }
|
||||
}
|
||||
|
||||
@@ -408,12 +435,15 @@ namespace PepperDash.Essentials.Room.Config
|
||||
/// </summary>
|
||||
public class EssentialsHelpPropertiesConfig
|
||||
{
|
||||
[JsonProperty("message")]
|
||||
/// <summary>
|
||||
/// Gets or sets the Message
|
||||
/// </summary>
|
||||
[JsonProperty("message")]
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ShowCallButton
|
||||
/// </summary>
|
||||
[JsonProperty("showCallButton")]
|
||||
public bool ShowCallButton { get; set; }
|
||||
|
||||
@@ -421,11 +451,11 @@ namespace PepperDash.Essentials.Room.Config
|
||||
/// Defaults to "Call Help Desk"
|
||||
/// </summary>
|
||||
[JsonProperty("callButtonText")]
|
||||
/// <summary>
|
||||
/// Gets or sets the CallButtonText
|
||||
/// </summary>
|
||||
public string CallButtonText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructor
|
||||
/// </summary>
|
||||
public EssentialsHelpPropertiesConfig()
|
||||
{
|
||||
CallButtonText = "Call Help Desk";
|
||||
@@ -437,22 +467,28 @@ namespace PepperDash.Essentials.Room.Config
|
||||
/// </summary>
|
||||
public class EssentialsOneButtonMeetingPropertiesConfig
|
||||
{
|
||||
[JsonProperty("enable")]
|
||||
/// <summary>
|
||||
/// Gets or sets the Enable
|
||||
/// </summary>
|
||||
[JsonProperty("enable")]
|
||||
public bool Enable { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a EssentialsRoomAddressPropertiesConfig
|
||||
/// </summary>
|
||||
public class EssentialsRoomAddressPropertiesConfig
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the PhoneNumber
|
||||
/// </summary>
|
||||
[JsonProperty("phoneNumber")]
|
||||
public string PhoneNumber { get; set; }
|
||||
|
||||
[JsonProperty("sipAddress")]
|
||||
/// <summary>
|
||||
/// Gets or sets the SipAddress
|
||||
/// </summary>
|
||||
[JsonProperty("sipAddress")]
|
||||
public string SipAddress { get; set; }
|
||||
}
|
||||
|
||||
@@ -462,14 +498,18 @@ namespace PepperDash.Essentials.Room.Config
|
||||
/// </summary>
|
||||
public class EssentialsLogoPropertiesConfig
|
||||
{
|
||||
[JsonProperty("type")]
|
||||
/// <summary>
|
||||
/// Gets or sets the Type
|
||||
/// </summary>
|
||||
[JsonProperty("type")]
|
||||
public string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the Url
|
||||
/// </summary>
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// GetLogoUrlLight method
|
||||
/// </summary>
|
||||
@@ -502,22 +542,28 @@ namespace PepperDash.Essentials.Room.Config
|
||||
/// </summary>
|
||||
public class EssentialsRoomOccSensorConfig
|
||||
{
|
||||
[JsonProperty("deviceKey")]
|
||||
/// <summary>
|
||||
/// Gets or sets the DeviceKey
|
||||
/// </summary>
|
||||
[JsonProperty("deviceKey")]
|
||||
public string DeviceKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the TimeoutMinutes
|
||||
/// </summary>
|
||||
[JsonProperty("timeoutMinutes")]
|
||||
public int TimeoutMinutes { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents a EssentialsRoomTechConfig
|
||||
/// </summary>
|
||||
public class EssentialsRoomTechConfig
|
||||
{
|
||||
[JsonProperty("password")]
|
||||
/// <summary>
|
||||
/// Gets or sets the Password
|
||||
/// </summary>
|
||||
[JsonProperty("password")]
|
||||
public string Password { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ namespace PepperDash.Essentials.Core.Routing
|
||||
/// <param name="inputPort">The currently selected input port on the destination device.</param>
|
||||
private void UpdateDestination(IRoutingSinkWithSwitching destination, RoutingInputPort inputPort)
|
||||
{
|
||||
// Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Updating destination {destination} with inputPort {inputPort}", this,destination?.Key, inputPort?.Key);
|
||||
Debug.LogMessage(Serilog.Events.LogEventLevel.Verbose, "Updating destination {destination} with inputPort {inputPort}", this, destination?.Key, inputPort?.Key);
|
||||
|
||||
if(inputPort == null)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using PepperDash.Core;
|
||||
using PepperDash.Core.Logging;
|
||||
using PepperDash.Essentials.Core;
|
||||
using PepperDash.Essentials.Core.Config;
|
||||
using Serilog.Events;
|
||||
@@ -9,7 +10,7 @@ namespace PepperDash.Essentials.Devices.Common.Generic
|
||||
/// <summary>
|
||||
/// Represents a GenericSink
|
||||
/// </summary>
|
||||
public class GenericSink : EssentialsDevice, IRoutingSinkWithInputPort
|
||||
public class GenericSink : EssentialsDevice, IRoutingSinkWithSwitchingWithInputPort
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the GenericSink class
|
||||
@@ -20,7 +21,7 @@ namespace PepperDash.Essentials.Devices.Common.Generic
|
||||
{
|
||||
InputPorts = new RoutingPortCollection<RoutingInputPort>();
|
||||
|
||||
var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo, eRoutingPortConnectionType.Hdmi, null, this);
|
||||
var inputPort = new RoutingInputPort(RoutingPortNames.AnyVideoIn, eRoutingSignalType.AudioVideo | eRoutingSignalType.SecondaryAudio, eRoutingPortConnectionType.Hdmi, null, this);
|
||||
|
||||
InputPorts.Add(inputPort);
|
||||
}
|
||||
@@ -66,6 +67,15 @@ namespace PepperDash.Essentials.Devices.Common.Generic
|
||||
/// Event fired when the current source changes
|
||||
/// </summary>
|
||||
public event SourceInfoChangeHandler CurrentSourceChange;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event InputChangedEventHandler InputChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ExecuteSwitch(object inputSelector)
|
||||
{
|
||||
this.LogDebug("GenericSink Executing Switch to: {inputSelector}", inputSelector);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -29,6 +29,6 @@
|
||||
<ProjectReference Include="..\PepperDash.Essentials.Core\PepperDash.Essentials.Core.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.171" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -31,7 +31,12 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||
/// <remarks>
|
||||
/// Unsoliciited feedback from a device in a messenger will ONLY be sent to devices in this subscription list. When a client disconnects, it's ID will be removed from the collection.
|
||||
/// </remarks>
|
||||
protected HashSet<string> SubscriberIds = new HashSet<string>();
|
||||
private readonly HashSet<string> subscriberIds = new HashSet<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Lock object for thread-safe access to SubscriberIds
|
||||
/// </summary>
|
||||
private readonly object _subscriberLock = new object();
|
||||
|
||||
private readonly List<string> _deviceInterfaces;
|
||||
|
||||
@@ -189,18 +194,18 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||
{
|
||||
if (!enableMessengerSubscriptions)
|
||||
{
|
||||
this.LogWarning("Messenger subscriptions not enabled");
|
||||
return;
|
||||
}
|
||||
|
||||
if (SubscriberIds.Any(id => id == clientId))
|
||||
lock (_subscriberLock)
|
||||
{
|
||||
this.LogVerbose("Client {clientId} already subscribed", clientId);
|
||||
return;
|
||||
if (!subscriberIds.Add(clientId))
|
||||
{
|
||||
this.LogVerbose("Client {clientId} already subscribed", clientId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SubscriberIds.Add(clientId);
|
||||
|
||||
this.LogDebug("Client {clientId} subscribed", clientId);
|
||||
}
|
||||
|
||||
@@ -212,19 +217,26 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||
{
|
||||
if (!enableMessengerSubscriptions)
|
||||
{
|
||||
this.LogWarning("Messenger subscriptions not enabled");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!SubscriberIds.Any(i => i == clientId))
|
||||
bool wasSubscribed;
|
||||
lock (_subscriberLock)
|
||||
{
|
||||
wasSubscribed = subscriberIds.Contains(clientId);
|
||||
if (wasSubscribed)
|
||||
{
|
||||
subscriberIds.Remove(clientId);
|
||||
}
|
||||
}
|
||||
|
||||
if (!wasSubscribed)
|
||||
{
|
||||
this.LogVerbose("Client with ID {clientId} is not subscribed", clientId);
|
||||
return;
|
||||
}
|
||||
|
||||
SubscriberIds.RemoveWhere((i) => i == clientId);
|
||||
|
||||
this.LogInformation("Client with ID {clientId} unsubscribed", clientId);
|
||||
this.LogDebug("Client with ID {clientId} unsubscribed", clientId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -258,7 +270,8 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError(ex, "Exception posting status message for {messagePath} to {clientId}", MessagePath, clientId ?? "all clients");
|
||||
this.LogError("Exception posting status message for {messagePath} to {clientId}: {message}", MessagePath, clientId ?? "all clients", ex.Message);
|
||||
this.LogDebug(ex, "Stack trace: ");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +300,8 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
this.LogError(ex, "Exception posting status message for {type} to {clientId}", type, clientId ?? "all clients");
|
||||
this.LogError("Exception posting status message for {type} to {clientId}: {message}", type, clientId ?? "all clients", ex.Message);
|
||||
this.LogDebug(ex, "Stack trace: ");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +326,14 @@ namespace PepperDash.Essentials.AppServer.Messengers
|
||||
// If client is null or empty, this message is unsolicited feedback. Iterate through the subscriber list and send to all interested parties
|
||||
if (string.IsNullOrEmpty(clientId))
|
||||
{
|
||||
foreach (var client in SubscriberIds)
|
||||
// Create a snapshot of subscribers to avoid collection modification during iteration
|
||||
List<string> subscriberSnapshot;
|
||||
lock (_subscriberLock)
|
||||
{
|
||||
subscriberSnapshot = new List<string>(subscriberIds);
|
||||
}
|
||||
|
||||
foreach (var client in subscriberSnapshot)
|
||||
{
|
||||
AppServerController?.SendMessageObject(new MobileControlMessage { Type = !string.IsNullOrEmpty(type) ? type : MessagePath, ClientId = client, Content = content });
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<Compile Remove="Messengers\SIMPLVtcMessenger.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.171" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PepperDash.Core\PepperDash.Core.csproj" />
|
||||
|
||||
@@ -1312,6 +1312,11 @@ namespace PepperDash.Essentials
|
||||
/// <inheritdoc />
|
||||
public override void Initialize()
|
||||
{
|
||||
if (!Config.EnableMessengerSubscriptions)
|
||||
{
|
||||
this.LogWarning("Messenger subscriptions disabled. add \"enableMessengerSubscriptions\": true to config for {key} to enable.", Key);
|
||||
}
|
||||
|
||||
foreach (var messenger in _messengers)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<Compile Remove="RoomBridges\SourceDeviceMapDictionary.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.90" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.ProgramLibrary" Version="2.21.171" />
|
||||
<PackageReference Include="WebSocketSharp-netstandard" Version="1.0.1" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace PepperDash.Essentials.Touchpanel
|
||||
this.LogInformation("Setting theme to {theme}", theme.Value);
|
||||
_tpDevice.UpdateTheme(theme.Value);
|
||||
|
||||
PostStatusMessage(JToken.FromObject(new { theme = theme.Value }), id);
|
||||
PostStatusMessage(JToken.FromObject(new { theme = theme.Value }), clientId: id);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.90" />
|
||||
<PackageReference Include="Crestron.SimplSharp.SDK.Program" Version="2.21.171" />
|
||||
<PackageReference Include="System.IO.Compression" Version="4.0.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user