diff --git a/src/Atc.Kepware.Configuration/GlobalUsings.cs b/src/Atc.Kepware.Configuration/GlobalUsings.cs index a4fb33d..7048879 100644 --- a/src/Atc.Kepware.Configuration/GlobalUsings.cs +++ b/src/Atc.Kepware.Configuration/GlobalUsings.cs @@ -1,10 +1,13 @@ +global using System.Collections.Concurrent; global using System.ComponentModel.DataAnnotations; global using System.Diagnostics.CodeAnalysis; global using System.Net; global using System.Net.Http.Headers; global using System.Net.Mime; +global using System.Reflection; global using System.Text; global using System.Text.Json; +global using System.Text.Json.Nodes; global using System.Text.Json.Serialization; global using Atc.Data.Models; global using Atc.Helpers; diff --git a/src/Atc.Kepware.Configuration/Services/Connectivity/KepwareConfigurationClientConnectivity.cs b/src/Atc.Kepware.Configuration/Services/Connectivity/KepwareConfigurationClientConnectivity.cs index f5f6b92..586c0b0 100644 --- a/src/Atc.Kepware.Configuration/Services/Connectivity/KepwareConfigurationClientConnectivity.cs +++ b/src/Atc.Kepware.Configuration/Services/Connectivity/KepwareConfigurationClientConnectivity.cs @@ -9,6 +9,8 @@ namespace Atc.Kepware.Configuration.Services; [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "OK")] public sealed partial class KepwareConfigurationClient { + private static readonly ConcurrentDictionary DeviceTypeJsonMappingTypeLookup = []; + public async Task> IsChannelDefined( string channelName, CancellationToken cancellationToken) @@ -206,6 +208,30 @@ public async Task> IsTagGroupDefined( return response.Adapt?>>(); } + public async Task?>> GetDevicesByChannelName( + string channelName, + CancellationToken cancellationToken) + where TDevice : DeviceBase + { + ArgumentNullException.ThrowIfNull(channelName); + + if (!IsValidConnectivityName( + channelName, + deviceName: null, + tagGroupNameOrTagName: null, + tagGroupStructure: null, + out var errorMessage)) + { + return await Task.FromResult(HttpClientRequestResultFactory?>.CreateBadRequest(errorMessage!)); + } + + HttpClientRequestResult?> response = await Get>( + $"{EndpointPathTemplateConstants.Channels}/{channelName}/{EndpointPathTemplateConstants.Devices}", + cancellationToken); + + return ProcessGetDevicesByChannelNameResponse(response); + } + public async Task> GetTags( string channelName, string deviceName, @@ -472,6 +498,92 @@ public Task> DeleteTagGroup( cancellationToken); } + private HttpClientRequestResult?> ProcessGetDevicesByChannelNameResponse(HttpClientRequestResult?> response) + where TDevice : DeviceBase + { + // No data, return early, nothing to adapt + if (!response.HasData) + { + return new HttpClientRequestResult?>() + { + CommunicationSucceeded = response.CommunicationSucceeded, + StatusCode = response.StatusCode, + Message = response.Message, + Exception = response.Exception, + }; + } + + // No actual devices, return early, nothing to adapt + if (response.Data!.Count == 0) + { + return new HttpClientRequestResult?>([]) + { + CommunicationSucceeded = response.CommunicationSucceeded, + StatusCode = response.StatusCode, + Message = response.Message, + Exception = response.Exception, + }; + } + + if (!TryGetDeviceTypeJsonMappingType(out Type? jsonMappingType)) + { + return new HttpClientRequestResult?>() + { + CommunicationSucceeded = response.CommunicationSucceeded, + StatusCode = response.StatusCode, + Message = response.Message, + Exception = new NotSupportedException($"Could not find a JSON mapping type for {typeof(TDevice).Name}"), + }; + } + + // Deserialize from JSON and adapt to the desired type + IList deviceTypes = response + .Data + .Select(x => JsonSerializer.Deserialize(x.ToString(), jsonMappingType, jsonSerializerOptions)) + .Select(x => x.Adapt()) + .ToList(); + + return new HttpClientRequestResult?>(deviceTypes) + { + CommunicationSucceeded = response.CommunicationSucceeded, + StatusCode = response.StatusCode, + Message = response.Message, + }; + } + + private static bool TryGetDeviceTypeJsonMappingType([NotNullWhen(true)] out Type? jsonMappingType) + where TDevice : DeviceBase + { + jsonMappingType = DeviceTypeJsonMappingTypeLookup.GetOrAdd(typeof(TDevice), GetDeviceTypeJsonMappingType); + return jsonMappingType != null; + } + + /// + /// Each driver device type has a corresponding type in this assembly for mapping from JSON. Try finding + /// that type by looking for a shared interface with the device type, that derives from IDeviceBase. + /// + /// + /// The type that maps to the device type, or null if not found. + /// + private static Type? GetDeviceTypeJsonMappingType(Type deviceType) + { + // Get the implemented interfaces that derive from IDeviceBase, but are not IDeviceBase + ISet implementedInterfaces = deviceType + .GetInterfaces() + .Where(x + => typeof(IDeviceBase).IsAssignableFrom(x) + && x != typeof(IDeviceBase)) + .ToHashSet(); + + // Find the first type in this assembly that shares an interface + return typeof(KepwareConfigurationClient) + .Assembly + .GetTypes() + .FirstOrDefault(x => x + .GetInterfaces() + .Any(y => implementedInterfaces.Contains(y))); + } + private Task?>> GetTagsResultForPathTemplate( string pathTemplate, CancellationToken cancellationToken) diff --git a/src/Atc.Kepware.Configuration/Services/IKepwareConfigurationClientConnectivity.cs b/src/Atc.Kepware.Configuration/Services/IKepwareConfigurationClientConnectivity.cs index 53b191d..6cc05ec 100644 --- a/src/Atc.Kepware.Configuration/Services/IKepwareConfigurationClientConnectivity.cs +++ b/src/Atc.Kepware.Configuration/Services/IKepwareConfigurationClientConnectivity.cs @@ -90,6 +90,17 @@ Task> IsTagGroupDefined( string channelName, CancellationToken cancellationToken); + /// + /// Returns a list of all devices under the specified channel. + /// + /// The Channel Name. + /// The CancellationToken. + /// A driver specific implementation. + Task?>> GetDevicesByChannelName( + string channelName, + CancellationToken cancellationToken) + where TDevice : DeviceBase; + /// /// Returns the properties of the specified EuroMap63 device. ///