From bf59eb9c1fe8fd7bcbcb8183f44892459e6e8946 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Fri, 6 Mar 2026 17:16:54 -0500 Subject: [PATCH 01/38] feat(api): cache ProductInfo for client when requested --- .../ClientHandler/ProjectApiHandler.cs | 19 +++++++++++- Kepware.Api/KepwareApiClient.cs | 30 ++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/Kepware.Api/ClientHandler/ProjectApiHandler.cs b/Kepware.Api/ClientHandler/ProjectApiHandler.cs index cd3d028..54bb2e5 100644 --- a/Kepware.Api/ClientHandler/ProjectApiHandler.cs +++ b/Kepware.Api/ClientHandler/ProjectApiHandler.cs @@ -230,6 +230,23 @@ public async Task LoadProject(bool blnLoadFullProject = false, Cancella { Stopwatch stopwatch = Stopwatch.StartNew(); + var project = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + + if (project == null) + { + m_logger.LogWarning("Failed to load project"); + project = new Project(); + } + + // If not loading full project, return with just project properties. + if (!blnLoadFullProject) + { + m_logger.LogInformation("Loaded project properties in {ElapsedMilliseconds} ms", stopwatch.ElapsedMilliseconds); + return project; + + } + + var productInfo = await m_kepwareApiClient.GetProductInfoAsync(cancellationToken).ConfigureAwait(false); if (blnLoadFullProject && productInfo?.SupportsJsonProjectLoadService == true) @@ -280,7 +297,7 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false } else { - var project = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + project = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(cancellationToken: cancellationToken).ConfigureAwait(false); if (project == null) { diff --git a/Kepware.Api/KepwareApiClient.cs b/Kepware.Api/KepwareApiClient.cs index 0887994..8d606c1 100644 --- a/Kepware.Api/KepwareApiClient.cs +++ b/Kepware.Api/KepwareApiClient.cs @@ -30,6 +30,7 @@ public partial class KepwareApiClient : IKepwareDefaultValueProvider /// The value for an unknown client or hostname. /// public const string UNKNOWN = "Unknown"; + private const string ENDPOINT_STATUS = "/config/v1/status"; private const string ENDPOINT_DOC = "/config/v1/doc"; private const string ENDPOINT_ABOUT = "/config/v1/about"; @@ -40,6 +41,7 @@ public partial class KepwareApiClient : IKepwareDefaultValueProvider private bool? m_isConnected = null; private bool? m_hasValidCredentials = null; + private ProductInfo? m_productInfo = null; /// /// Gets the name of the client instance. @@ -51,6 +53,26 @@ public partial class KepwareApiClient : IKepwareDefaultValueProvider /// public string ClientHostName => m_httpClient.BaseAddress?.Host ?? UNKNOWN; + /// + /// Gets the product information of the connected Kepware server, which includes + /// product name and version information. This information is retrieved during + /// and and cached for future use. + /// If neither method has not been performed yet, it will attempt to retrieve the product information + /// on demand. If the product information cannot be retrieved, it will return null. + /// + public ProductInfo? ProductInfo + { + get + { + if (m_productInfo == null) + { + // Try to get the product info if we don't have it yet, this can happen if the connection test has not been called yet + var _ = GetProductInfoAsync().ConfigureAwait(false).GetAwaiter().GetResult(); + } + return m_productInfo; + } + } + /// /// Gets the client options for the Kepware server connection. /// @@ -206,9 +228,10 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false /// /// Gets the product information from the Kepware server which includes product name and version information. /// Uses the /config/v1/about endpoint + /// Will update the client's product info property, which can be used in other calls to avoid calling the API multiple times for the same information. /// /// The cancellation token. - /// A task that represents the asynchronous operation. The task result contains the product information. + /// A task that represents the asynchronous operation. The task result contains the product information. public async Task GetProductInfoAsync(CancellationToken cancellationToken = default) { try @@ -218,6 +241,9 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false { var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); var prodInfo = JsonSerializer.Deserialize(content, KepJsonContext.Default.ProductInfo); + + // Set Product Info for the client if we have a valid response, so we can use it in other calls without needing to call the API again + m_productInfo = prodInfo; return prodInfo; } @@ -235,6 +261,8 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false m_logger.LogWarning(jsonEx, "Failed to parse ProductInfo from {BaseAddress}", m_httpClient.BaseAddress); } + // If we cannot get the product info, we set it to null and return null + m_productInfo = null; return null; } From bf78c054b30060dcc108b295430ac9faeef4beaf Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Fri, 6 Mar 2026 17:17:52 -0500 Subject: [PATCH 02/38] test(api): Added unit and integration testing for caching ProductInfo --- Kepware.Api.Test/ApiClient/GetProductInfo.cs | 23 +++++++++++++++++++ .../ApiClient/GetProductInfo.cs | 9 ++++++++ Kepware.Api.TestIntg/appsettings.json | 4 ++-- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/Kepware.Api.Test/ApiClient/GetProductInfo.cs b/Kepware.Api.Test/ApiClient/GetProductInfo.cs index 92089b2..0cb6368 100644 --- a/Kepware.Api.Test/ApiClient/GetProductInfo.cs +++ b/Kepware.Api.Test/ApiClient/GetProductInfo.cs @@ -32,6 +32,16 @@ public async Task GetProductInfoAsync_ShouldReturnProductInfo_WhenApiRespondsSuc Assert.Equal(240, result.ProductVersionBuild); Assert.Equal(0, result.ProductVersionPatch); + // Also verify that the ProductInfo property on the client is populated correctly + Assert.NotNull(_kepwareApiClient.ProductInfo); + Assert.Equal("012", _kepwareApiClient.ProductInfo.ProductId); + Assert.Equal("KEPServerEX", _kepwareApiClient.ProductInfo.ProductName); + Assert.Equal("V6.17.240.0", _kepwareApiClient.ProductInfo.ProductVersion); + Assert.Equal(6, _kepwareApiClient.ProductInfo.ProductVersionMajor); + Assert.Equal(17, _kepwareApiClient.ProductInfo.ProductVersionMinor); + Assert.Equal(240, _kepwareApiClient.ProductInfo.ProductVersionBuild); + Assert.Equal(0, _kepwareApiClient.ProductInfo.ProductVersionPatch); + } #region GetProductInfoAsync - SupportsJsonProjectLoadService @@ -57,6 +67,10 @@ public async Task GetProductInfoAsync_ShouldReturnCorrect_SupportsJsonProjectLoa // Assert Assert.NotNull(result); Assert.Equal(expectedResult, result.SupportsJsonProjectLoadService); + + // Also verify that the ProductInfo property on the client is populated correctly + Assert.NotNull(_kepwareApiClient.ProductInfo); + Assert.Equal(expectedResult, _kepwareApiClient.ProductInfo.SupportsJsonProjectLoadService); } #endregion @@ -99,6 +113,9 @@ public async Task GetProductInfoAsync_ShouldReturnNull_WhenApiReturnsError() // Assert Assert.Null(result); + + // ProductInfo property should also be null on error + Assert.Null(_kepwareApiClient.ProductInfo); } [Fact] @@ -114,6 +131,9 @@ public async Task GetProductInfoAsync_ShouldReturnNull_WhenApiReturnsInvalidJson // Assert Assert.Null(result); + + // ProductInfo property should also be null on error + Assert.Null(_kepwareApiClient.ProductInfo); } [Fact] @@ -128,6 +148,9 @@ public async Task GetProductInfoAsync_ShouldReturnNull_OnHttpRequestException() // Assert Assert.Null(result); + + // ProductInfo property should also be null on error + Assert.Null(_kepwareApiClient.ProductInfo); } #endregion } diff --git a/Kepware.Api.TestIntg/ApiClient/GetProductInfo.cs b/Kepware.Api.TestIntg/ApiClient/GetProductInfo.cs index d609c6a..d35505d 100644 --- a/Kepware.Api.TestIntg/ApiClient/GetProductInfo.cs +++ b/Kepware.Api.TestIntg/ApiClient/GetProductInfo.cs @@ -30,6 +30,15 @@ public async Task GetProductInfoAsync_ShouldReturnProductInfo_WhenApiRespondsSuc Assert.Equal(_productInfo.ProductVersionBuild, result.ProductVersionBuild); Assert.Equal(_productInfo.ProductVersionPatch, result.ProductVersionPatch); + // Also verify that the ProductInfo property on the client is updated + Assert.NotNull(_kepwareApiClient.ProductInfo); + Assert.Equal(_productInfo.ProductId, _kepwareApiClient.ProductInfo.ProductId); + Assert.Equal(_productInfo.ProductName, _kepwareApiClient.ProductInfo.ProductName); + Assert.Equal(_productInfo.ProductVersion, _kepwareApiClient.ProductInfo.ProductVersion); + Assert.Equal(_productInfo.ProductVersionMajor, _kepwareApiClient.ProductInfo.ProductVersionMajor); + Assert.Equal(_productInfo.ProductVersionMinor, _kepwareApiClient.ProductInfo.ProductVersionMinor); + Assert.Equal(_productInfo.ProductVersionBuild, _kepwareApiClient.ProductInfo.ProductVersionBuild); + Assert.Equal(_productInfo.ProductVersionPatch, _kepwareApiClient.ProductInfo.ProductVersionPatch); } } diff --git a/Kepware.Api.TestIntg/appsettings.json b/Kepware.Api.TestIntg/appsettings.json index 14128aa..2126b20 100644 --- a/Kepware.Api.TestIntg/appsettings.json +++ b/Kepware.Api.TestIntg/appsettings.json @@ -3,9 +3,9 @@ "IntegrationTest": true, "TestServer": { "Host": "https://localhost", - "Port": 57513, + "Port": 57512, "UserName": "Administrator", - "Password": "Kepware400400400" + "Password": "" } } } \ No newline at end of file From 9d5fd71fd4009c30030fd3d9f90f4beb5278badd Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Sat, 7 Mar 2026 16:09:18 -0500 Subject: [PATCH 03/38] feat(api): added generic methods to clear connection and other caches --- .../ClientHandler/GenericApiHandler.cs | 16 +++- Kepware.Api/KepwareApiClient.cs | 77 ++++++++++++------- 2 files changed, 63 insertions(+), 30 deletions(-) diff --git a/Kepware.Api/ClientHandler/GenericApiHandler.cs b/Kepware.Api/ClientHandler/GenericApiHandler.cs index 2eada65..5793852 100644 --- a/Kepware.Api/ClientHandler/GenericApiHandler.cs +++ b/Kepware.Api/ClientHandler/GenericApiHandler.cs @@ -788,7 +788,7 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner } #endregion - #region private methods + #region private / internal methods #region deserialize protected Task DeserializeJsonAsync(HttpResponseMessage httpResponse, CancellationToken cancellationToken = default) @@ -825,6 +825,20 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner } #endregion + /// + /// Clears any internal caches (supported drivers, supported channels/devices). + /// Called when the underlying connection is lost so subsequent calls re-fetch data. + /// + internal void InvalidateCaches() + { + // drop cached drivers so next call re-loads from /doc endpoint + m_cachedSupportedDrivers = null; + + // clear cached channel/device property dictionaries + m_cachedSupportedChannels.Clear(); + m_cachedSupportedDevices.Clear(); + } + #endregion } diff --git a/Kepware.Api/KepwareApiClient.cs b/Kepware.Api/KepwareApiClient.cs index 8d606c1..fe59e48 100644 --- a/Kepware.Api/KepwareApiClient.cs +++ b/Kepware.Api/KepwareApiClient.cs @@ -55,23 +55,11 @@ public partial class KepwareApiClient : IKepwareDefaultValueProvider /// /// Gets the product information of the connected Kepware server, which includes - /// product name and version information. This information is retrieved during + /// product name and version information. This caches the value during /// and and cached for future use. - /// If neither method has not been performed yet, it will attempt to retrieve the product information - /// on demand. If the product information cannot be retrieved, it will return null. + /// It will return null if there is no cached value. /// - public ProductInfo? ProductInfo - { - get - { - if (m_productInfo == null) - { - // Try to get the product info if we don't have it yet, this can happen if the connection test has not been called yet - var _ = GetProductInfoAsync().ConfigureAwait(false).GetAwaiter().GetResult(); - } - return m_productInfo; - } - } + public ProductInfo? ProductInfo => m_productInfo; /// /// Gets the client options for the Kepware server connection. @@ -163,7 +151,7 @@ public async Task TestConnectionAsync(CancellationToken cancellationToken if (!response.IsSuccessStatusCode) { m_logger.LogWarning("Failed to connect to {ClientName}-client at {BaseAddress}, Reason: {ReasonPhrase}", ClientName, m_httpClient.BaseAddress, response.ReasonPhrase); - m_isConnected = null; // set connection state to null if we cannot connect + ClearConnectionState(); // set connection state to null if we cannot connect return false; // connection failed } @@ -177,7 +165,7 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false if (status?.FirstOrDefault()?.Healthy == false) { m_logger.LogWarning("Failed to connect to {ClientName}-client at {BaseAddress}, Reason: {String}", ClientName, m_httpClient.BaseAddress, "Server Status Check Failed"); - m_isConnected = null; // set connection state to null if we cannot connect + ClearConnectionState(); // set connection state to null if we cannot connect return false; // connection failed } @@ -189,12 +177,12 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false // Inital connection attempt or a reconnection due to failure, // we need to check the product info and credentials - var prodInfo = await GetProductInfoAsync(cancellationToken).ConfigureAwait(false); + _ = await GetProductInfoAsync(cancellationToken).ConfigureAwait(false); // If we cannot get the product info, we assume the connection is not healthy - if (prodInfo == null) + if (m_productInfo == null) { - m_isConnected = null; // set connection state to null if we cannot get product info + ClearConnectionState(); // set connection state to null if we cannot get product info return false; } @@ -204,12 +192,12 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false // If we do not have valid credentials, we assume the connection is not healthy if (m_hasValidCredentials != true) { - m_isConnected = null; // set connection state to null if we cannot connect or credentials are invalid + ClearConnectionState(); // set connection state to null if we cannot connect or credentials are invalid m_logger.LogWarning("Connection to {ClientName}-client at {BaseAddress} failed because credentials are invalid", ClientName, m_httpClient.BaseAddress); return false; } - m_logger.LogInformation("Successfully connected to {ClientName}-client: {ProductName} {ProductVersion} on {BaseAddress}", ClientName, prodInfo?.ProductName, prodInfo?.ProductVersion, m_httpClient.BaseAddress); + m_logger.LogInformation("Successfully connected to {ClientName}-client: {ProductName} {ProductVersion} on {BaseAddress}", ClientName, m_productInfo?.ProductName, m_productInfo?.ProductVersion, m_httpClient.BaseAddress); m_isConnected = true; // set connection state to true if we have a valid product info and credentials return m_isConnected.Value; // return true if we have a valid connection @@ -227,25 +215,29 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false /// /// Gets the product information from the Kepware server which includes product name and version information. - /// Uses the /config/v1/about endpoint /// Will update the client's product info property, which can be used in other calls to avoid calling the API multiple times for the same information. + /// Uses the /config/v1/about endpoint /// /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the product information. public async Task GetProductInfoAsync(CancellationToken cancellationToken = default) { + if (m_productInfo != null) + { + // return cached product info if we have it + return m_productInfo; + } + try { var response = await m_httpClient.GetAsync(ENDPOINT_ABOUT, cancellationToken).ConfigureAwait(false); if (response.IsSuccessStatusCode) { var content = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - var prodInfo = JsonSerializer.Deserialize(content, KepJsonContext.Default.ProductInfo); // Set Product Info for the client if we have a valid response, so we can use it in other calls without needing to call the API again - m_productInfo = prodInfo; - - return prodInfo; + m_productInfo = JsonSerializer.Deserialize(content, KepJsonContext.Default.ProductInfo); + return m_productInfo; } else { @@ -321,14 +313,41 @@ async Task> IKepwareDefaultValueProvider } #endregion - #region internal + #region Private / internal helper methods + /// + /// Clears all client-level connection state and optionally handler caches. + /// Call this whenever the connection should be considered lost or stale. + /// + /// If true also clears cached credential validation state. + private void ClearConnectionState(bool clearCredentials = true) + { + // Clear derived product info and connection flags + m_productInfo = null; + m_isConnected = null; + + // Optionally clear credential status so next TestConnection re-evaluates + if (clearCredentials) + m_hasValidCredentials = null; + + // Invalidate caches on handlers that keep them + try + { + // GenericConfig may implement an InvalidateCaches method (see suggestion below) + (GenericConfig as ClientHandler.GenericApiHandler)?.InvalidateCaches(); + } + catch + { + // swallow - defensive: don't throw from a state-clear helper + } + } + /// /// Invoked by Handler, when they receice a http request exception /// /// internal void OnHttpRequestException(HttpRequestException httpEx) { - m_isConnected = null; + ClearConnectionState(false); } #endregion } From a1ace8eed6fdb82f0994e34f9dceea281ec83093 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Fri, 13 Mar 2026 17:55:47 -0400 Subject: [PATCH 04/38] chore(test): Update file name --- .../ApiClient/{GetProductInfo.cs => GetProductInfoTests.cs} | 0 .../ApiClient/{TestConnection.cs => TestConnectionTests.cs} | 0 .../ApiClient/{GetProductInfo.cs => GetProductInfoTests.cs} | 0 .../ApiClient/{LoadEntity.cs => LoadEntityTests.cs} | 0 .../ApiClient/{TestConnection.cs => TestConnectionTests.cs} | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename Kepware.Api.Test/ApiClient/{GetProductInfo.cs => GetProductInfoTests.cs} (100%) rename Kepware.Api.Test/ApiClient/{TestConnection.cs => TestConnectionTests.cs} (100%) rename Kepware.Api.TestIntg/ApiClient/{GetProductInfo.cs => GetProductInfoTests.cs} (100%) rename Kepware.Api.TestIntg/ApiClient/{LoadEntity.cs => LoadEntityTests.cs} (100%) rename Kepware.Api.TestIntg/ApiClient/{TestConnection.cs => TestConnectionTests.cs} (100%) diff --git a/Kepware.Api.Test/ApiClient/GetProductInfo.cs b/Kepware.Api.Test/ApiClient/GetProductInfoTests.cs similarity index 100% rename from Kepware.Api.Test/ApiClient/GetProductInfo.cs rename to Kepware.Api.Test/ApiClient/GetProductInfoTests.cs diff --git a/Kepware.Api.Test/ApiClient/TestConnection.cs b/Kepware.Api.Test/ApiClient/TestConnectionTests.cs similarity index 100% rename from Kepware.Api.Test/ApiClient/TestConnection.cs rename to Kepware.Api.Test/ApiClient/TestConnectionTests.cs diff --git a/Kepware.Api.TestIntg/ApiClient/GetProductInfo.cs b/Kepware.Api.TestIntg/ApiClient/GetProductInfoTests.cs similarity index 100% rename from Kepware.Api.TestIntg/ApiClient/GetProductInfo.cs rename to Kepware.Api.TestIntg/ApiClient/GetProductInfoTests.cs diff --git a/Kepware.Api.TestIntg/ApiClient/LoadEntity.cs b/Kepware.Api.TestIntg/ApiClient/LoadEntityTests.cs similarity index 100% rename from Kepware.Api.TestIntg/ApiClient/LoadEntity.cs rename to Kepware.Api.TestIntg/ApiClient/LoadEntityTests.cs diff --git a/Kepware.Api.TestIntg/ApiClient/TestConnection.cs b/Kepware.Api.TestIntg/ApiClient/TestConnectionTests.cs similarity index 100% rename from Kepware.Api.TestIntg/ApiClient/TestConnection.cs rename to Kepware.Api.TestIntg/ApiClient/TestConnectionTests.cs From 2d7b193cbbb26f25336edcfab7515177b04d727c Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Sat, 14 Mar 2026 17:47:47 -0400 Subject: [PATCH 05/38] feat(api): Query Parameter support for Load Methods and Serialize handling --- .../ApiClient/GenericHandleTests.cs | 116 ++++++++++++++ .../ApiClient/LoadEntityTests.cs | 43 +++++- Kepware.Api/ClientHandler/AdminApiHandler.cs | 24 +-- .../ClientHandler/GenericApiHandler.cs | 145 +++++++++++++++--- 4 files changed, 295 insertions(+), 33 deletions(-) create mode 100644 Kepware.Api.Test/ApiClient/GenericHandleTests.cs diff --git a/Kepware.Api.Test/ApiClient/GenericHandleTests.cs b/Kepware.Api.Test/ApiClient/GenericHandleTests.cs new file mode 100644 index 0000000..ac0c0be --- /dev/null +++ b/Kepware.Api.Test/ApiClient/GenericHandleTests.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Reflection; +using System.Threading.Tasks; +using Kepware.Api.Model; +using Kepware.Api.Test.ApiClient; +using Microsoft.Extensions.Logging; +using Moq; +using Moq.Contrib.HttpClient; +using Xunit; + +namespace Kepware.Api.Test.ApiClient +{ + public class GenericHandler : TestApiClientBase + { + [Fact] + public void AppendQueryString_PrivateMethod_EncodesAndSkipsNullsAndAppendsCorrectly() + { + // Arrange + var method = typeof(Kepware.Api.ClientHandler.GenericApiHandler) + .GetMethod("AppendQueryString", BindingFlags.NonPublic | BindingFlags.Static); + Assert.NotNull(method); + + var query = new[] + { + new KeyValuePair("a", "b"), + new KeyValuePair("space", "x y"), + new KeyValuePair("skip", null) // should be skipped + }; + + // Act + var result1 = (string)method!.Invoke(null, new object[] { "https://api/config", query })!; + var result2 = (string)method!.Invoke(null, new object[] { "https://api/config?existing=1", query })!; + + // Assert + Assert.Equal("https://api/config?a=b&space=x%20y", result1); + Assert.Equal("https://api/config?existing=1&a=b&space=x%20y", result2); + } + + [Fact] + public async Task LoadCollectionAsync_AppendsQueryAndReturnsCollection() + { + // Arrange + var channelsJson = """ + [ + { + "PROJECT_ID": 676550906, + "common.ALLTYPES_NAME": "Channel1", + "common.ALLTYPES_DESCRIPTION": "Example Simulator Channel", + "servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator" + }, + { + "PROJECT_ID": 676550906, + "common.ALLTYPES_NAME": "Data Type Examples", + "common.ALLTYPES_DESCRIPTION": "Example Simulator Channel", + "servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator" + } + ] + """; + + var query = new[] + { + new KeyValuePair("status", "active"), + new KeyValuePair("name", "John Doe"), + new KeyValuePair("skip", null) + }; + + // Expect encoded space in "John Doe" and null entry skipped + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels?status=active&name=John%20Doe") + .ReturnsResponse(channelsJson, "application/json"); + + // Act + var result = await _kepwareApiClient.GenericConfig.LoadCollectionAsync((string?)null, query); + + // Assert + Assert.NotNull(result); + Assert.Equal(2, result.Count); + Assert.Contains(result, c => c.Name == "Channel1"); + Assert.Contains(result, c => c.Name == "Data Type Examples"); + } + + [Fact] + public async Task LoadEntityAsync_AppendsQueryAndReturnsEntity() + { + // Arrange + var channelJson = """ + { + "PROJECT_ID": 676550906, + "common.ALLTYPES_NAME": "Channel1", + "common.ALLTYPES_DESCRIPTION": "Example Simulator Channel", + "servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator" + } + """; + + var query = new[] + { + new KeyValuePair("status", "active"), + new KeyValuePair("name", "John Doe"), + new KeyValuePair("skip", null) + }; + + // Expect encoded space in "John Doe" and null entry skipped + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Channel1?status=active&name=John%20Doe") + .ReturnsResponse(channelJson, "application/json"); + + // Act + var result = await _kepwareApiClient.GenericConfig.LoadEntityAsync("Channel1", query); + + // Assert + Assert.NotNull(result); + Assert.Equal("Channel1", result.Name); + Assert.Equal("Example Simulator Channel", result.Description); + } + } +} diff --git a/Kepware.Api.TestIntg/ApiClient/LoadEntityTests.cs b/Kepware.Api.TestIntg/ApiClient/LoadEntityTests.cs index f1ebfc3..c688852 100644 --- a/Kepware.Api.TestIntg/ApiClient/LoadEntityTests.cs +++ b/Kepware.Api.TestIntg/ApiClient/LoadEntityTests.cs @@ -276,7 +276,7 @@ public async Task LoadEntityAsync_ShouldReturnTagCollection_WhenApiRespondsSucce [Fact] public async Task LoadEntityAsync_ShouldReturnTagGroupCollectionInTagGroup_WhenApiRespondsSuccessfully() { - // TODO: Currently this test fails due to issue in EndpointResolver. + // TODO: Clean up test. Fix was made and test is successful. // Arrange try { @@ -335,5 +335,46 @@ public async Task LoadEntityAsync_ShouldReturnTagCollectionFromTagGroup_WhenApiR await DeleteAllChannelsAsync(); } #endregion + + #region LoadEntityAsync - Return Channel Collection with all children - Serialize query + [SkippableFact] + public async Task LoadEntityAsync_ShouldReturnChannelAndChildren_WhenApiRespondsSuccessfully() + { + // Skip the test if the serialize feature is not supported by server version. + Skip.If(!_productInfo.SupportsJsonProjectLoadService, "Test only applicable for versions that support JsonProjectLoad"); + + // Arrange + var channel = await AddTestChannel(); + var device = await AddTestDevice(channel); + var tagGroup = await AddTestTagGroup(device); + var tagGroup2 = await AddTestTagGroup(tagGroup, "TagGroup2"); + + var query = new[] + { + new KeyValuePair("content", "serialize"), + }; + + // Act + var result = await _kepwareApiClient.GenericConfig.LoadEntityAsync(channel.Name, query); + + // Assert + Assert.NotNull(result); + Assert.NotNull(result.Devices); + Assert.Contains(result.Devices, g => g.Name == device.Name); + + var foundDevice = result.Devices.Find(d => d.Name == device.Name); + Assert.NotNull(foundDevice); + Assert.NotNull(foundDevice.TagGroups); + Assert.Contains(foundDevice.TagGroups, tg => tg.Name == tagGroup.Name); + + var foundTagGroup = foundDevice.TagGroups.Find(tg => tg.Name == tagGroup.Name); + Assert.NotNull(foundTagGroup); + Assert.NotNull(foundTagGroup.TagGroups); + Assert.Contains(foundTagGroup.TagGroups, tg => tg.Name == tagGroup2.Name); + + // Cleanup + await DeleteAllChannelsAsync(); + } + #endregion } } diff --git a/Kepware.Api/ClientHandler/AdminApiHandler.cs b/Kepware.Api/ClientHandler/AdminApiHandler.cs index 6173cfc..eea1215 100644 --- a/Kepware.Api/ClientHandler/AdminApiHandler.cs +++ b/Kepware.Api/ClientHandler/AdminApiHandler.cs @@ -42,7 +42,7 @@ public AdminApiHandler(KepwareApiClient kepwareApiClient, ILoggerThe current or null if retrieval fails. public Task GetAdminSettingsAsync(CancellationToken cancellationToken = default) { - return m_kepwareApiClient.GenericConfig.LoadEntityAsync(name: null, cancellationToken); + return m_kepwareApiClient.GenericConfig.LoadEntityAsync(name: null, cancellationToken: cancellationToken); } /// @@ -117,7 +117,7 @@ public async Task SetAdminSettingsAsync(AdminSettings settings, Cancellati /// The configuration, or null if not found. public Task GetUaEndpointAsync(string name, CancellationToken cancellationToken = default) { - return m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken); + return m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken: cancellationToken); } /// @@ -135,7 +135,7 @@ public async Task CreateOrUpdateUaEndpointAsync(UaEndpoint endpoint, Cance try { var endpointUrl = EndpointResolver.ResolveEndpoint([endpoint.Name]); - var currentEndpoint = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync(endpointUrl, cancellationToken); + var currentEndpoint = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync(endpointUrl, cancellationToken: cancellationToken); if (currentEndpoint == null) { @@ -162,7 +162,7 @@ public async Task CreateOrUpdateUaEndpointAsync(UaEndpoint endpoint, Cance /// A token that can be used to request cancellation of the operation. /// True if the endpoint was successfully deleted; otherwise, false. public Task DeleteUaEndpointAsync(string name, CancellationToken cancellationToken = default) - => m_kepwareApiClient.GenericConfig.DeleteItemAsync(name, cancellationToken); + => m_kepwareApiClient.GenericConfig.DeleteItemAsync(name, cancellationToken: cancellationToken); #endregion @@ -186,7 +186,7 @@ public Task DeleteUaEndpointAsync(string name, CancellationToken cancellat /// The configuration, or null if not found. public Task GetServerUserGroupAsync(string name, CancellationToken cancellationToken = default) { - return m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken); + return m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken: cancellationToken); } /// @@ -204,7 +204,7 @@ public async Task CreateOrUpdateServerUserGroupAsync(ServerUserGroup userG try { var endpointUrl = EndpointResolver.ResolveEndpoint([userGroup.Name]); - var currentGroup = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync(endpointUrl, cancellationToken); + var currentGroup = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync(endpointUrl, cancellationToken: cancellationToken); if (currentGroup == null) { @@ -231,7 +231,7 @@ public async Task CreateOrUpdateServerUserGroupAsync(ServerUserGroup userG /// A token that can be used to request cancellation of the operation. /// True if the group was successfully deleted; otherwise, false. public Task DeleteServerUserGroupAsync(string name, CancellationToken cancellationToken = default) - => m_kepwareApiClient.GenericConfig.DeleteItemAsync(name, cancellationToken); + => m_kepwareApiClient.GenericConfig.DeleteItemAsync(name, cancellationToken: cancellationToken); #endregion @@ -254,7 +254,7 @@ public Task DeleteServerUserGroupAsync(string name, CancellationToken canc /// The configuration, or null if not found. public Task GetServerUserAsync(string name, CancellationToken cancellationToken = default) { - return m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken); + return m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken: cancellationToken); } /// @@ -272,7 +272,7 @@ public async Task CreateOrUpdateServerUserAsync(ServerUser user, Cancellat try { var endpointUrl = EndpointResolver.ResolveEndpoint([user.Name]); - var currentUser = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync(endpointUrl, cancellationToken); + var currentUser = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync(endpointUrl, cancellationToken: cancellationToken); if (currentUser == null) { @@ -309,7 +309,7 @@ public async Task CreateOrUpdateServerUserAsync(ServerUser user, Cancellat /// A token that can be used to request cancellation of the operation. /// True if the user was successfully deleted; otherwise, false. public Task DeleteServerUserAsync(string name, CancellationToken cancellationToken = default) - => m_kepwareApiClient.GenericConfig.DeleteItemAsync(name, cancellationToken); + => m_kepwareApiClient.GenericConfig.DeleteItemAsync(name, cancellationToken: cancellationToken); #endregion @@ -345,7 +345,7 @@ public Task DeleteServerUserAsync(string name, CancellationToken cancellat public Task GetProjectPermissionAsync(string serverUserGroupName, ProjectPermissionName projectPermissionName, CancellationToken cancellationToken = default) { var endpoint = EndpointResolver.ResolveEndpoint([serverUserGroupName, projectPermissionName]); - return m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync(endpoint, cancellationToken); + return m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync(endpoint, cancellationToken: cancellationToken); } /// @@ -371,7 +371,7 @@ public async Task UpdateProjectPermissionAsync(string serverUserGroupName, try { var endpointUrl = EndpointResolver.ResolveEndpoint([serverUserGroupName, projectPermission.Name]); - var existingPermission = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync(endpointUrl, cancellationToken); + var existingPermission = await m_kepwareApiClient.GenericConfig.LoadEntityByEndpointAsync(endpointUrl, cancellationToken: cancellationToken); if (existingPermission == null) { diff --git a/Kepware.Api/ClientHandler/GenericApiHandler.cs b/Kepware.Api/ClientHandler/GenericApiHandler.cs index 5793852..bf510c6 100644 --- a/Kepware.Api/ClientHandler/GenericApiHandler.cs +++ b/Kepware.Api/ClientHandler/GenericApiHandler.cs @@ -159,7 +159,7 @@ public async Task UpdateItemsAsync(List<(K item, K? oldItem)> item foreach (var pair in items) { var endpoint = $"{collectionEndpoint}/{Uri.EscapeDataString(pair.oldItem!.Name)}"; - var currentEntity = await LoadEntityByEndpointAsync(endpoint, cancellationToken).ConfigureAwait(false); + var currentEntity = await LoadEntityByEndpointAsync(endpoint, cancellationToken: cancellationToken).ConfigureAwait(false); if (currentEntity == null) { m_logger.LogError("Failed to load {TypeName} from {Endpoint}", typeof(K).Name, endpoint); @@ -478,13 +478,16 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner /// /// The type of the entity to load. /// The name of the entity to load. If null, loads the default entity. + /// Optional query parameters to append to the request URI. /// A token that can be used to request cancellation of the operation. /// The loaded entity of type or null if not found. - public Task LoadEntityAsync(string? name = default, CancellationToken cancellationToken = default) + public Task LoadEntityAsync(string? name = default, IEnumerable>? query = null, CancellationToken cancellationToken = default) where T : BaseEntity, new() { var endpoint = EndpointResolver.ResolveEndpoint(string.IsNullOrEmpty(name) ? [] : [name]); - return LoadEntityByEndpointAsync(endpoint, cancellationToken); + endpoint = AppendQueryString(endpoint, query); + var serializedRequest = query != null && query.Any(kv => kv.Key.Equals("content", StringComparison.OrdinalIgnoreCase) && kv.Value == "serialize"); + return LoadEntityByEndpointAsync(endpoint, serialized: serializedRequest, cancellationToken: cancellationToken); } /// @@ -492,13 +495,16 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner /// /// The type of the entity to load. /// The owner of the entity. + /// Optional query parameters to append to the request URI. /// A token that can be used to request cancellation of the operation. /// The loaded entity of type or null if not found. - public Task LoadEntityAsync(IEnumerable owner, CancellationToken cancellationToken = default) + public Task LoadEntityAsync(IEnumerable owner, IEnumerable>? query = null, CancellationToken cancellationToken = default) where T : BaseEntity, new() { var endpoint = EndpointResolver.ResolveEndpoint(owner); - return LoadEntityByEndpointAsync(endpoint, cancellationToken); + endpoint = AppendQueryString(endpoint, query); + var serializedRequest = query != null && query.Any(kv => kv.Key.Equals("content", StringComparison.OrdinalIgnoreCase) && kv.Value == "serialize"); + return LoadEntityByEndpointAsync(endpoint, serialized: serializedRequest, cancellationToken: cancellationToken); } /// @@ -507,14 +513,18 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner /// The type of the entity to load. /// The name of the entity to load. /// The owner of the entity. + /// Optional query parameters to append to the request URI. /// A token that can be used to request cancellation of the operation. /// The loaded entity of type or null if not found. - public async Task LoadEntityAsync(string name, NamedEntity owner, CancellationToken cancellationToken = default) + public async Task LoadEntityAsync(string name, NamedEntity owner, IEnumerable>? query = null, CancellationToken cancellationToken = default) where T : BaseEntity, new() { var endpoint = EndpointResolver.ResolveEndpoint(owner, name); + endpoint = AppendQueryString(endpoint, query); - var entity = await LoadEntityByEndpointAsync(endpoint, cancellationToken); + var serializedRequest = query != null && query.Any(kv => kv.Key.Equals("content", StringComparison.OrdinalIgnoreCase) && kv.Value == "serialize"); + + var entity = await LoadEntityByEndpointAsync(endpoint, serialized: serializedRequest, cancellationToken: cancellationToken); if (entity is IHaveOwner ownable) { @@ -523,7 +533,7 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner return entity; } - protected internal async Task LoadEntityByEndpointAsync(string endpoint, CancellationToken cancellationToken = default) + protected internal async Task LoadEntityByEndpointAsync(string endpoint, bool serialized = false, CancellationToken cancellationToken = default) where T : BaseEntity, new() { try @@ -536,8 +546,16 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner m_logger.LogWarning("Failed to load {TypeName} from {Endpoint}: {ReasonPhrase}", typeof(T).Name, endpoint, response.ReasonPhrase); return default; } + var entity = default(T); - var entity = await DeserializeJsonAsync(response, cancellationToken).ConfigureAwait(false); + if (serialized) + { + entity = await DeserializeJsonLoadSerializedAsync(response, cancellationToken).ConfigureAwait(false); + } + else + { + entity = await DeserializeJsonAsync(response, cancellationToken).ConfigureAwait(false); + } return entity; } @@ -558,22 +576,24 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner /// /// The type of the entity collection to load. /// The owner of the entity collection. + /// Optional query parameters to append to the request URI. /// A token that can be used to request cancellation of the operation. /// The loaded collection of entities of type or null if not found. - public Task LoadCollectionAsync(string? owner = default, CancellationToken cancellationToken = default) + public Task LoadCollectionAsync(string? owner = default, IEnumerable>? query = null, CancellationToken cancellationToken = default) where T : EntityCollection, new() - => LoadCollectionAsync(owner, cancellationToken); + => LoadCollectionAsync(owner, query, cancellationToken); /// /// Loads a collection of entities of type asynchronously by its owner from the Kepware server. /// /// The type of the entity collection to load. /// The owner of the entity collection. + /// Optional query parameters to append to the request URI. /// A token that can be used to request cancellation of the operation. /// The loaded collection of entities of type or null if not found. - public Task LoadCollectionAsync(NamedEntity owner, CancellationToken cancellationToken = default) + public Task LoadCollectionAsync(NamedEntity owner, IEnumerable>? query = null, CancellationToken cancellationToken = default) where T : EntityCollection, new() - => LoadCollectionAsync(owner, cancellationToken); + => LoadCollectionAsync(owner, query, cancellationToken); /// /// Loads a collection of entities of type asynchronously by its owner from the Kepware server. @@ -581,12 +601,13 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner /// The type of the entity collection to load. /// The type of the entities in the collection. /// The owner of the entity collection. + /// Optional query parameters to append to the request URI. /// A token that can be used to request cancellation of the operation. /// The loaded collection of entities of type or null if not found. - public Task LoadCollectionAsync(string? owner = default, CancellationToken cancellationToken = default) + public Task LoadCollectionAsync(string? owner = default, IEnumerable>? query = null, CancellationToken cancellationToken = default) where T : EntityCollection, new() where K : BaseEntity, new() - => LoadCollectionAsync(string.IsNullOrEmpty(owner) ? [] : [owner], cancellationToken); + => LoadCollectionAsync(string.IsNullOrEmpty(owner) ? [] : [owner], query, cancellationToken); /// /// Loads a collection of entities of type asynchronously by its owner from the Kepware server. @@ -594,13 +615,14 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner /// The type of the entity collection to load. /// The type of the entities in the collection. /// The owner of the entity collection. + /// Optional query parameters to append to the request URI. /// A token that can be used to request cancellation of the operation. /// The loaded collection of entities of type or null if not found. - public async Task LoadCollectionAsync(NamedEntity owner, CancellationToken cancellationToken = default) + public async Task LoadCollectionAsync(NamedEntity owner, IEnumerable>? query = null, CancellationToken cancellationToken = default) where T : EntityCollection, new() where K : BaseEntity, new() { - var collection = await LoadCollectionByEndpointAsync(EndpointResolver.ResolveEndpoint(owner), cancellationToken); + var collection = await LoadCollectionByEndpointAsync(AppendQueryString(EndpointResolver.ResolveEndpoint(owner), query), cancellationToken); if (collection != null) { collection.Owner = owner; @@ -618,12 +640,13 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner /// The type of the entity collection to load. /// The type of the entities in the collection. /// The owner of the entity collection. + /// Optional query parameters to append to the request URI. /// A token that can be used to request cancellation of the operation. /// The loaded collection of entities of type or null if not found. - public Task LoadCollectionAsync(IEnumerable owner, CancellationToken cancellationToken = default) + public Task LoadCollectionAsync(IEnumerable owner, IEnumerable>? query = null, CancellationToken cancellationToken = default) where T : EntityCollection, new() where K : BaseEntity, new() - => LoadCollectionByEndpointAsync(EndpointResolver.ResolveEndpoint(owner), cancellationToken); + => LoadCollectionByEndpointAsync(AppendQueryString(EndpointResolver.ResolveEndpoint(owner), query), cancellationToken); protected internal async Task LoadCollectionByEndpointAsync(string endpoint, CancellationToken cancellationToken = default) where T : EntityCollection, new() @@ -823,6 +846,57 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner return null; } } + + /// + /// Special deserialization method for responses from endpoints with `?content=serialize` that wrap the actual object in an additional + /// layer with the object name as the property name, e.g. `{ "Channel": { ... } }`. This is required to properly handle dynamic properties + /// on channels/devices/etc that conform to a different model from JSON type info for the base entity. This would cause + /// deserialization to fail if we tried to deserialize directly to the target type. + /// + /// + /// + /// + /// + protected Task DeserializeJsonLoadSerializedAsync(HttpResponseMessage httpResponse, CancellationToken cancellationToken = default) + where K : BaseEntity, new() => DeserializeJsonLoadSerializedAsync(httpResponse, KepJsonContext.GetJsonTypeInfo(), cancellationToken); + + /// + /// Special deserialization method for responses from endpoints with `?content=serialize` that wrap the actual object in an additional + /// layer with the object name as the property name, e.g. `{ "Channel": { ... } }`. This is required to properly handle dynamic properties + /// on channels/devices/etc that conform to a different model from JSON type info for the base entity. This would cause + /// deserialization to fail if we tried to deserialize directly to the target type. + /// + /// + /// + /// + /// + /// + protected async Task DeserializeJsonLoadSerializedAsync(HttpResponseMessage httpResponse, JsonTypeInfo jsonTypeInfo, CancellationToken cancellationToken = default) + where K : BaseEntity, new() + { + try + { + using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken); + var wrapper = await JsonSerializer.DeserializeAsync>( + stream, KepJsonContext.Default.DictionaryStringJsonElement, cancellationToken).ConfigureAwait(false) + ?? throw new JsonException("Response was not a JSON object."); + + var first = wrapper.Values.FirstOrDefault(); + if (first.ValueKind != JsonValueKind.Object) + throw new JsonException("Expected the first property to be a JSON object."); + + return first.Deserialize(jsonTypeInfo: KepJsonContext.GetJsonTypeInfo()) + ?? throw new JsonException("Failed to deserialize channel object."); + //return await JsonSerializer.DeserializeAsync(stream, jsonTypeInfo, cancellationToken); + } + catch (JsonException ex) + { + m_logger.LogError(ex, "JSON Deserialization failed"); + return default; + } + } + + #endregion /// @@ -841,5 +915,36 @@ internal void InvalidateCaches() #endregion + /// + /// Append query parameters to an endpoint string. Encodes keys and values with Uri.EscapeDataString. + /// Null or empty values are skipped. If `endpoint` already contains a query, parameters are appended with &. + /// + private static string AppendQueryString(string endpoint, IEnumerable>? query) + { + if (query == null) + return endpoint; + + var sb = new StringBuilder(); + foreach (var kv in query) + { + if (kv.Key == null) + continue; + // Skip parameters with null values to match typical REST filter behavior. + if (kv.Value is null) + continue; + + if (sb.Length > 0) + sb.Append('&'); + + sb.Append(Uri.EscapeDataString(kv.Key)); + sb.Append('='); + sb.Append(Uri.EscapeDataString(kv.Value)); + } + + if (sb.Length == 0) + return endpoint; + + return endpoint + (endpoint.Contains('?') ? "&" : "?") + sb.ToString(); + } } -} +} \ No newline at end of file From d022f5544ad9fa765bd00a60d7c9e9dbd4988f32 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Mon, 16 Mar 2026 14:25:16 -0400 Subject: [PATCH 06/38] feat(api): Enhanced LoadProject to use content=serialze option in an optimized recursion --- .../ApiClient/ProjectLoadTests.cs | 6 +- .../ApiClient/ProjectLoadTests.cs | 97 ++++++- .../ApiClient/_TestIntgApiClientBase.cs | 8 +- .../ClientHandler/ProjectApiHandler.cs | 271 ++++++++++++++---- .../Model/Project/Channel.Properties.cs | 5 + .../Model/Project/Device.Properties.cs | 25 ++ Kepware.Api/Model/Project/Project.cs | 5 + 7 files changed, 349 insertions(+), 68 deletions(-) create mode 100644 Kepware.Api/Model/Project/Device.Properties.cs diff --git a/Kepware.Api.Test/ApiClient/ProjectLoadTests.cs b/Kepware.Api.Test/ApiClient/ProjectLoadTests.cs index af3ed55..171618f 100644 --- a/Kepware.Api.Test/ApiClient/ProjectLoadTests.cs +++ b/Kepware.Api.Test/ApiClient/ProjectLoadTests.cs @@ -98,7 +98,7 @@ public async Task LoadProject_ShouldLoadCorrectly_BasedOnProductSupport( await ConfigureToServeEndpoints(); } - var project = await _kepwareApiClient.Project.LoadProject(true); + var project = await _kepwareApiClient.Project.LoadProjectAsync(true); project.IsLoadedByProjectLoadService.ShouldBe(supportsJsonLoad); @@ -182,7 +182,7 @@ public async Task LoadProject_NotFull_ShouldLoadCorrectly_BasedOnProductSupport( await ConfigureToServeEndpoints(); - var project = await _kepwareApiClient.Project.LoadProject(blnLoadFullProject: false); + var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: false); project.ShouldNotBeNull(); project.Channels.ShouldBeNull("Channels list should be null."); @@ -201,7 +201,7 @@ public async Task LoadProject_ShouldReturnEmptyProject_WhenHttpRequestFails() .ThrowsAsync(new HttpRequestException()); // Act - var project = await _kepwareApiClient.Project.LoadProject(true); + var project = await _kepwareApiClient.Project.LoadProjectAsync(true); // Assert project.ShouldNotBeNull(); diff --git a/Kepware.Api.TestIntg/ApiClient/ProjectLoadTests.cs b/Kepware.Api.TestIntg/ApiClient/ProjectLoadTests.cs index 6127e3f..bfde274 100644 --- a/Kepware.Api.TestIntg/ApiClient/ProjectLoadTests.cs +++ b/Kepware.Api.TestIntg/ApiClient/ProjectLoadTests.cs @@ -110,6 +110,99 @@ private static void CompareTagGroupsRecursive(DeviceTagGroupCollection? expected } } + [Fact] + public async Task LoadProject_Full_ShouldLoadCorrectly_BasedOnProductSupport() + { + // Arrange + var channel = await AddTestChannel(); + var device = await AddTestDevice(channel); + var tags = await AddSimulatorTestTags(device); + var tagGroup = await AddTestTagGroup(device); + var tagGroup2 = await AddTestTagGroup(tagGroup, "TagGroup2"); + + var channel2 = await AddTestChannel("Channel2"); + var device2 = await AddTestDevice(channel2); + var tags2 = await AddSimulatorTestTags(device2); + var tagGroup_2 = await AddTestTagGroup(device2); + var tagGroup2_2 = await AddTestTagGroup(tagGroup_2, "TagGroup2"); + + var pro = new Project(); + + // Act + var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: true); + + // Assert + Assert.NotNull(project); + Assert.NotNull(project.Channels); + Assert.Contains(project.Channels, c => c.Name == channel.Name); + + var foundChannel = project.Channels.Find(c => c.Name == channel.Name); + Assert.NotNull(foundChannel); + Assert.NotNull(foundChannel.Devices); + Assert.Contains(foundChannel.Devices, d => d.Name == device.Name); + + var foundDevice = foundChannel.Devices.Find(d => d.Name == device.Name); + Assert.NotNull(foundDevice); + Assert.NotNull(foundDevice.Tags); + Assert.Equal(tags.Count, foundDevice.Tags.Count); + Assert.NotNull(foundDevice.TagGroups); + Assert.Contains(foundDevice.TagGroups, tg => tg.Name == tagGroup.Name); + + var foundTagGroup = foundDevice.TagGroups.Find(tg => tg.Name == tagGroup.Name); + Assert.NotNull(foundTagGroup); + Assert.NotNull(foundTagGroup.TagGroups); + Assert.Contains(foundTagGroup.TagGroups, tg => tg.Name == tagGroup2.Name); + + + // Clean up + await DeleteAllChannelsAsync(); + } + + [Fact] + public async Task LoadProject_Full_LargeProject_ShouldLoadCorrectly_BasedOnProductSupport() + { + // Arrange + var channel = await AddTestChannel(); + var device = await AddTestDevice(channel); + var tags = await AddSimulatorTestTags(device, count: 10000); + var tagGroup = await AddTestTagGroup(device); + var tagGroup2 = await AddTestTagGroup(tagGroup, "TagGroup2"); + + var channel2 = await AddTestChannel("Channel2"); + var device2 = await AddTestDevice(channel2); + var tags2 = await AddSimulatorTestTags(device2); + var tagGroup_2 = await AddTestTagGroup(device2); + var tagGroup2_2 = await AddTestTagGroup(tagGroup_2, "TagGroup2"); + + // Act + var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: true); + + // Assert + Assert.NotNull(project); + Assert.NotNull(project.Channels); + Assert.Contains(project.Channels, c => c.Name == channel.Name); + + var foundChannel = project.Channels.Find(c => c.Name == channel.Name); + Assert.NotNull(foundChannel); + Assert.NotNull(foundChannel.Devices); + Assert.Contains(foundChannel.Devices, d => d.Name == device.Name); + + var foundDevice = foundChannel.Devices.Find(d => d.Name == device.Name); + Assert.NotNull(foundDevice); + Assert.NotNull(foundDevice.Tags); + Assert.Equal(tags.Count, foundDevice.Tags.Count); + Assert.NotNull(foundDevice.TagGroups); + Assert.Contains(foundDevice.TagGroups, tg => tg.Name == tagGroup.Name); + + var foundTagGroup = foundDevice.TagGroups.Find(tg => tg.Name == tagGroup.Name); + Assert.NotNull(foundTagGroup); + Assert.NotNull(foundTagGroup.TagGroups); + Assert.Contains(foundTagGroup.TagGroups, tg => tg.Name == tagGroup2.Name); + + + // Clean up + await DeleteAllChannelsAsync(); + } [Fact] public async Task LoadProject_NotFull_ShouldLoadCorrectly_BasedOnProductSupport() { @@ -119,7 +212,7 @@ public async Task LoadProject_NotFull_ShouldLoadCorrectly_BasedOnProductSupport( var tags = await AddSimulatorTestTags(device); // Act - var project = await _kepwareApiClient.Project.LoadProject(blnLoadFullProject: false); + var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: false); // Assert project.ShouldNotBeNull(); @@ -139,7 +232,7 @@ public async Task LoadProject_ShouldReturnEmptyProject_WhenHttpRequestFails() { // Act - var project = await _badCredKepwareApiClient.Project.LoadProject(true); + var project = await _badCredKepwareApiClient.Project.LoadProjectAsync(true); // Assert project.ShouldNotBeNull(); diff --git a/Kepware.Api.TestIntg/ApiClient/_TestIntgApiClientBase.cs b/Kepware.Api.TestIntg/ApiClient/_TestIntgApiClientBase.cs index fbf1a9d..23beace 100644 --- a/Kepware.Api.TestIntg/ApiClient/_TestIntgApiClientBase.cs +++ b/Kepware.Api.TestIntg/ApiClient/_TestIntgApiClientBase.cs @@ -153,25 +153,25 @@ protected async Task AddSimulatorTestTag(DeviceTagGroup owner, string name protected List CreateSimulatorTestTags(string name = "Tag", string address = "K000", int count = 2) { - return Enumerable.Range(1, count) + return Enumerable.Range(0, count-1) .Select(i => CreateTestTag(name: $"{name}{i}", address: $"{address}{i}")) .ToList(); } protected async Task> AddSimulatorTestTags(Device owner, string name = "Tag", string address = "K000", int count = 2) { - var tagsList = CreateSimulatorTestTags(name, address); + var tagsList = CreateSimulatorTestTags(name, address, count); foreach (var tag in tagsList) { tag.Owner = owner; } - await _kepwareApiClient.GenericConfig.InsertItemsAsync(tagsList, owner: owner); + await _kepwareApiClient.GenericConfig.InsertItemsAsync(tagsList, pageSize: count ,owner: owner); return tagsList; } protected async Task> AddSimulatorTestTags(DeviceTagGroup owner, string name = "Tag1", string address = "K000", int count = 2) { - var tagsList = CreateSimulatorTestTags(name, address); + var tagsList = CreateSimulatorTestTags(name, address, count); foreach (var tag in tagsList) { tag.Owner = owner; diff --git a/Kepware.Api/ClientHandler/ProjectApiHandler.cs b/Kepware.Api/ClientHandler/ProjectApiHandler.cs index 54bb2e5..be89a90 100644 --- a/Kepware.Api/ClientHandler/ProjectApiHandler.cs +++ b/Kepware.Api/ClientHandler/ProjectApiHandler.cs @@ -61,7 +61,7 @@ public ProjectApiHandler(KepwareApiClient kepwareApiClient, ChannelApiHandler ch /// A task that represents the asynchronous operation. The task result contains a tuple with the counts of inserts, updates, and deletes. public async Task<(int inserts, int updates, int deletes)> CompareAndApply(Project sourceProject, CancellationToken cancellationToken = default) { - var projectFromApi = await LoadProject(blnLoadFullProject: true, cancellationToken: cancellationToken); + var projectFromApi = await LoadProjectAsync(blnLoadFullProject: true, cancellationToken: cancellationToken); await projectFromApi.Cleanup(m_kepwareApiClient, true, cancellationToken).ConfigureAwait(false); return await CompareAndApply(sourceProject, projectFromApi, cancellationToken).ConfigureAwait(false); } @@ -217,6 +217,21 @@ public async Task SetProjectPropertiesAsync(Project project, CancellationT #endregion #region LoadProject + + /// + /// Does the same as but is marked as obsolete. + /// + /// Indicates whether to load the full project. + /// The cancellation token. + /// A task that represents the asynchronous operation. The task result contains the loaded . + /// Deprecated in v1.1.0; will be removed in future release + [Obsolete("Use LoadProjectAsync() instead. This will be removed in future release", false)] + [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] + public async Task LoadProject(bool blnLoadFullProject = false, CancellationToken cancellationToken = default) + { + return await LoadProjectAsync(blnLoadFullProject, cancellationToken).ConfigureAwait(false); + } + /// /// Loads the project from the Kepware server. If is true, it loads the full project, otherwise only /// the project properties will be returned. @@ -224,9 +239,9 @@ public async Task SetProjectPropertiesAsync(Project project, CancellationT /// Indicates whether to load the full project. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the loaded . - /// NOTE: When loading a full project, the project will be loaded via the JsonProjectLoad service if supported by the server. (Kepware Server 6.17+ - /// and Thingworx Kepware Server 1.10+) Otherwise, the project will be loaded by recursively loading all objects in the project. - public async Task LoadProject(bool blnLoadFullProject = false, CancellationToken cancellationToken = default) + /// NOTE: When loading a full project, the project will be loaded either via the JsonProjectLoad service, an "optimized" + /// recursion that uses the content=serialize query or a basic recurion through project tree. + public async Task LoadProjectAsync(bool blnLoadFullProject = false, CancellationToken cancellationToken = default) { Stopwatch stopwatch = Stopwatch.StartNew(); @@ -249,41 +264,59 @@ public async Task LoadProject(bool blnLoadFullProject = false, Cancella var productInfo = await m_kepwareApiClient.GetProductInfoAsync(cancellationToken).ConfigureAwait(false); - if (blnLoadFullProject && productInfo?.SupportsJsonProjectLoadService == true) + if (productInfo?.SupportsJsonProjectLoadService == true) { try { - var response = await m_kepwareApiClient.HttpClient.GetAsync(ENDPONT_FULL_PROJECT, cancellationToken).ConfigureAwait(false); - if (response.IsSuccessStatusCode) + // Optimized recursive loading approach that uses the content=serialize query parameter. + // This approach significantly reduces the number of API calls when loading projects with a large number of tags and prevents + // timeout errors with large projects. + + // TODO: change threshold to configurable option + // Currently hardcoded to 100000 tags as the threshold to use content=serialize based loading or full recursive loading. + var tagLimit = 100000; + if (int.TryParse(project.GetDynamicProperty(Properties.ProjectSettings.TagsDefined), out int count) && count > tagLimit) { - var prjRoot = await JsonSerializer.DeserializeAsync( - await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), - KepJsonContext.Default.JsonProjectRoot, cancellationToken).ConfigureAwait(false); + m_logger.LogInformation("Project has greater than {TagLimit} tags defined. Loading project via optimized recursion...", tagLimit); + + project = await LoadProjectOptimizedRecurisveAsync(project, tagLimit, cancellationToken).ConfigureAwait(false); + + if (!project.IsEmpty) + { + SetOwnersFullProject(project); + m_logger.LogInformation("Loaded project via optimized recursion in {ElapsedMilliseconds} ms", stopwatch.ElapsedMilliseconds); + } + return project; + } - if (prjRoot?.Project != null) + // If project has less than tagLimit number of tags, load full project via JsonProjectLoad service. + else + { + m_logger.LogInformation("Project has less than {TagLimit} tags defined. Loading project via JsonProjectLoad Service...", tagLimit); + var response = await m_kepwareApiClient.HttpClient.GetAsync(ENDPONT_FULL_PROJECT, cancellationToken).ConfigureAwait(false); + if (response.IsSuccessStatusCode) { - prjRoot.Project.IsLoadedByProjectLoadService = true; + var prjRoot = await JsonSerializer.DeserializeAsync( + await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), + KepJsonContext.Default.JsonProjectRoot, cancellationToken).ConfigureAwait(false); - if (prjRoot.Project.Channels != null) - foreach (var channel in prjRoot.Project.Channels) - { - if (channel.Devices != null) - foreach (var device in channel.Devices) - { - device.Owner = channel; - - if (device.Tags != null) - foreach (var tag in device.Tags) - tag.Owner = device; - - if (device.TagGroups != null) - SetOwnerRecursive(device.TagGroups, device); - } - } + // Set the Owner property for all loaded entities. + if (prjRoot?.Project != null) + { + prjRoot.Project.IsLoadedByProjectLoadService = true; + + SetOwnersFullProject(prjRoot.Project); - m_logger.LogInformation("Loaded project via JsonProjectLoad Service in {ElapsedMilliseconds} ms", stopwatch.ElapsedMilliseconds); - return prjRoot.Project; + m_logger.LogInformation("Loaded project via JsonProjectLoad Service in {ElapsedMilliseconds} ms", stopwatch.ElapsedMilliseconds); + project = prjRoot.Project; + } + else + { + m_logger.LogWarning("Failed to deserialize project loaded via JsonProjectLoad Service"); + project = new Project(); + } } + return project; } } catch (HttpRequestException httpEx) @@ -297,28 +330,92 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false } else { - project = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + project = await LoadProjectRecursiveAsync(project, cancellationToken).ConfigureAwait(false); - if (project == null) + if (!project.IsEmpty) { - m_logger.LogWarning("Failed to load project"); - project = new Project(); + m_logger.LogInformation("Loaded project in via non-optimized recursion {ElapsedMilliseconds} ms", stopwatch.ElapsedMilliseconds); + } + + return project; + } + } + + private static void SetOwnersFullProject(Project project) + { + if (project.Channels != null) + foreach (var channel in project.Channels) + { + if (channel.Devices != null) + foreach (var device in channel.Devices) + { + device.Owner = channel; + + if (device.Tags != null) + foreach (var tag in device.Tags) + tag.Owner = device; + + if (device.TagGroups != null) + SetOwnerRecursive(device.TagGroups, device); + } } - else if (blnLoadFullProject) + } + + private async Task LoadProjectOptimizedRecurisveAsync(Project project, int tagLimit, CancellationToken cancellationToken = default) + { + project.Channels = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(cancellationToken: cancellationToken); + if (project.Channels != null) + { + int totalChannelCount = project.Channels.Count; + int loadedChannelCount = 0; + await Task.WhenAll(project.Channels.Select(async (channel, c_index) => { - project.Channels = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(cancellationToken: cancellationToken).ConfigureAwait(false); - if (project.Channels != null) + if (channel.GetDynamicProperty(Properties.Channel.StaticTagCount) < tagLimit) { - int totalChannelCount = project.Channels.Count; - int loadedChannelCount = 0; - await Task.WhenAll(project.Channels.Select(async channel => + var query = new[] + { + new KeyValuePair("content", "serialize") + }; + var loadedChannel = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(channel.Name, query, cancellationToken: cancellationToken); + if (loadedChannel != null) { - channel.Devices = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(channel, cancellationToken).ConfigureAwait(false); + project.Channels[c_index] = loadedChannel; + } + else + { + // Failed to load channel, log warning and end without incrementing completion. + m_logger.LogWarning("Failed to load {ChannelName}", channel.Name); + return; + } + } + else + { + channel.Devices = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(channel, cancellationToken: cancellationToken).ConfigureAwait(false); - if (channel.Devices != null) + if (channel.Devices != null) + { + await Task.WhenAll(channel.Devices.Select(async (device, d_index) => { - await Task.WhenAll(channel.Devices.Select(async device => + if (device.GetDynamicProperty(Properties.Device.StaticTagCount) < tagLimit) + { + var query = new[] + { + new KeyValuePair("content", "serialize") + }; + var loadedDevice = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(device.Name, channel, query, cancellationToken: cancellationToken).ConfigureAwait(false); + if (loadedDevice != null) + { + project.Channels[c_index].Devices![d_index] = loadedDevice; + } + else + { + // Failed to load device, log warning and end without incrementing completion. + m_logger.LogWarning("Failed to load {DeviceName} in channel {ChannelName}", device.Name, channel.Name); + return; + } + } + else { device.Tags = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(device, cancellationToken: cancellationToken).ConfigureAwait(false); device.TagGroups = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(device, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -327,27 +424,83 @@ await Task.WhenAll(channel.Devices.Select(async device => { await LoadTagGroupsRecursiveAsync(m_kepwareApiClient, device.TagGroups, cancellationToken: cancellationToken).ConfigureAwait(false); } - })); - } - // Log information, loaded channel x of y - loadedChannelCount++; - if (totalChannelCount == 1) - { - m_logger.LogInformation("Loaded channel {ChannelName}", channel.Name); - } - else + } + + })); + } + + } + // Log information, loaded channel x of y + loadedChannelCount++; + if (totalChannelCount == 1) + { + m_logger.LogInformation("Loaded channel {ChannelName}", channel.Name); + } + else + { + m_logger.LogInformation("Loaded channel {ChannelName} {LoadedChannelCount} of {TotalChannelCount}", channel.Name, loadedChannelCount, totalChannelCount); + } + + })); + + // If loaded channel count doesn't match total channel count, log warning that some channels may have failed to load. + // Return empty project to avoid returning a partially loaded project which may cause issues for consumers of the API. + if (loadedChannelCount != totalChannelCount) + { + m_logger.LogWarning("Only loaded {LoadedChannelCount} of {TotalChannelCount} channels. Some channels may have fail to load.", loadedChannelCount, totalChannelCount); + project = new Project(); + } + } + + return project; + } + + private async Task LoadProjectRecursiveAsync(Project project, CancellationToken cancellationToken = default) + { + project.Channels = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + + if (project.Channels != null) + { + int totalChannelCount = project.Channels.Count; + int loadedChannelCount = 0; + await Task.WhenAll(project.Channels.Select(async channel => + { + channel.Devices = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(channel, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (channel.Devices != null) + { + await Task.WhenAll(channel.Devices.Select(async device => + { + device.Tags = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(device, cancellationToken: cancellationToken).ConfigureAwait(false); + device.TagGroups = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(device, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (device.TagGroups != null) { - m_logger.LogInformation("Loaded channel {ChannelName} {LoadedChannelCount} of {TotalChannelCount}", channel.Name, loadedChannelCount, totalChannelCount); + await LoadTagGroupsRecursiveAsync(m_kepwareApiClient, device.TagGroups, cancellationToken: cancellationToken).ConfigureAwait(false); } - })); } + // Log information, loaded channel x of y + loadedChannelCount++; + if (totalChannelCount == 1) + { + m_logger.LogInformation("Loaded channel {ChannelName}", channel.Name); + } + else + { + m_logger.LogInformation("Loaded channel {ChannelName} {LoadedChannelCount} of {TotalChannelCount}", channel.Name, loadedChannelCount, totalChannelCount); + } - m_logger.LogInformation("Loaded project in {ElapsedMilliseconds} ms", stopwatch.ElapsedMilliseconds); + })); + // If loaded channel count doesn't match total channel count, log warning that some channels may have failed to load. + // Return empty project to avoid returning a partially loaded project which may cause issues for consumers of the API. + if (loadedChannelCount != totalChannelCount) + { + m_logger.LogWarning("Only loaded {LoadedChannelCount} of {TotalChannelCount} channels. Some channels may have fail to load.", loadedChannelCount, totalChannelCount); + project = new Project(); } - - return project; } + return project; } #endregion @@ -429,8 +582,8 @@ internal static async Task LoadTagGroupsRecursiveAsync(KepwareApiClient apiClien foreach (var tagGroup in tagGroups) { // Load the Tag Groups and Tags of the current Tag Group - tagGroup.TagGroups = await apiClient.GenericConfig.LoadCollectionAsync(tagGroup, cancellationToken).ConfigureAwait(false); - tagGroup.Tags = await apiClient.GenericConfig.LoadCollectionAsync(tagGroup, cancellationToken).ConfigureAwait(false); + tagGroup.TagGroups = await apiClient.GenericConfig.LoadCollectionAsync(tagGroup, cancellationToken: cancellationToken).ConfigureAwait(false); + tagGroup.Tags = await apiClient.GenericConfig.LoadCollectionAsync(tagGroup, cancellationToken: cancellationToken).ConfigureAwait(false); // Recursively load the Tag Groups and Tags of the child Tag Groups if (tagGroup.TagGroups != null && tagGroup.TagGroups.Count > 0) diff --git a/Kepware.Api/Model/Project/Channel.Properties.cs b/Kepware.Api/Model/Project/Channel.Properties.cs index a9a0208..2b0e131 100644 --- a/Kepware.Api/Model/Project/Channel.Properties.cs +++ b/Kepware.Api/Model/Project/Channel.Properties.cs @@ -45,6 +45,11 @@ public static class Channel /// public const string DiagnosticsCapture = "servermain.CHANNEL_DIAGNOSTICS_CAPTURE"; + /// + /// Value of the static tag count for the channel. + /// + public const string StaticTagCount = "servermain.CHANNEL_STATIC_TAG_COUNT"; + } } } \ No newline at end of file diff --git a/Kepware.Api/Model/Project/Device.Properties.cs b/Kepware.Api/Model/Project/Device.Properties.cs new file mode 100644 index 0000000..006c64f --- /dev/null +++ b/Kepware.Api/Model/Project/Device.Properties.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Kepware.Api.Model +{ + public partial class Properties + { + public static class Device + { + /// + /// The driver used by this channel. + /// + public const string DeviceDriver = "servermain.MULTIPLE_TYPES_DEVICE_DRIVER"; + + /// + /// Value of the static tag count for the channel. + /// + public const string StaticTagCount = "servermain.DEVICE_STATIC_TAG_COUNT"; + + } + } +} diff --git a/Kepware.Api/Model/Project/Project.cs b/Kepware.Api/Model/Project/Project.cs index bfbcceb..f441603 100644 --- a/Kepware.Api/Model/Project/Project.cs +++ b/Kepware.Api/Model/Project/Project.cs @@ -24,6 +24,11 @@ public class Project : DefaultEntity /// public bool IsLoadedByProjectLoadService { get; internal set; } = false; + /// + /// If this is true, it indicates that this is an empty project object that was instantiated without data from the server. + /// + public bool IsEmpty => Channels == null && DynamicProperties.Count == 0; + /// /// Initializes a new instance of the class. /// From e27b4fcf58f180f70cbd0e8680bc21e595bc1455 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Mon, 16 Mar 2026 18:31:43 -0400 Subject: [PATCH 07/38] feat(api): Added ProjectLoadTagLimit as a configurable property for controlling optimized recursion load behavior. --- .../ApiClient/ProjectLoadTests.cs | 68 +- .../ApiClient/_TestApiClientBase.cs | 48 +- Kepware.Api.Test/Kepware.Api.Test.csproj | 2 +- .../projectLoadSerializeData/Channel1.json | 52 + .../dataTypeExamples.16bitDevice.json | 926 ++++++++++++++++++ .../projectProperties.json | 80 ++ .../simulationExamples.json | 250 +++++ Kepware.Api.Test/_data/simdemo_en-us.json | 6 +- .../ApiClient/ProjectLoadTests.cs | 54 + .../ApiClient/_TestIntgApiClientBase.cs | 2 +- .../ClientHandler/ProjectApiHandler.cs | 161 +-- Kepware.Api/KepwareApiClientOptions.cs | 11 + 12 files changed, 1582 insertions(+), 78 deletions(-) create mode 100644 Kepware.Api.Test/_data/projectLoadSerializeData/Channel1.json create mode 100644 Kepware.Api.Test/_data/projectLoadSerializeData/dataTypeExamples.16bitDevice.json create mode 100644 Kepware.Api.Test/_data/projectLoadSerializeData/projectProperties.json create mode 100644 Kepware.Api.Test/_data/projectLoadSerializeData/simulationExamples.json diff --git a/Kepware.Api.Test/ApiClient/ProjectLoadTests.cs b/Kepware.Api.Test/ApiClient/ProjectLoadTests.cs index 171618f..6554f41 100644 --- a/Kepware.Api.Test/ApiClient/ProjectLoadTests.cs +++ b/Kepware.Api.Test/ApiClient/ProjectLoadTests.cs @@ -15,6 +15,7 @@ using Kepware.Api.Test.ApiClient; using Kepware.Api.Util; using Shouldly; +using Xunit.Sdk; namespace Kepware.Api.Test.ApiClient { @@ -98,7 +99,7 @@ public async Task LoadProject_ShouldLoadCorrectly_BasedOnProductSupport( await ConfigureToServeEndpoints(); } - var project = await _kepwareApiClient.Project.LoadProjectAsync(true); + var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: true); project.IsLoadedByProjectLoadService.ShouldBe(supportsJsonLoad); @@ -140,6 +141,71 @@ public async Task LoadProject_ShouldLoadCorrectly_BasedOnProductSupport( } } + [Theory] + [InlineData("KEPServerEX", "12", 6, 17, true)] + [InlineData("ThingWorxKepwareServer", "12", 6, 17, true)] + [InlineData("ThingWorxKepwareEdge", "13", 1, 10, true)] + [InlineData("Kepware Edge", "13", 1, 0, true)] + public async Task LoadProject_ShouldLoadCorrectly_Serialize_BasedOnProductSupport( + string productName, string productId, int majorVersion, int minorVersion, bool supportsJsonLoad) + { + ConfigureConnectedClient(productName, productId, majorVersion, minorVersion); + + if (supportsJsonLoad) + { + await ConfigureToServeEndpoints(); + } + else + { + // Skip this test case at runtime because it expects the server to serve a full JSON project. + throw SkipException.ForSkip($"Product {productName} v{majorVersion}.{minorVersion} (id={productId}) does not support JSON project load. Skipping full-project test case."); + } + + var tagLimitOverride = 100; // Set a high tag limit to ensure all tags are loaded for comparison + + var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: true, projectLoadTagLimit: tagLimitOverride); + + // Optimized recursion is done for this test, which will result in false. + project.IsLoadedByProjectLoadService.ShouldBeFalse(); + + project.ShouldNotBeNull(); + project.Channels.ShouldNotBeEmpty("Channels list should not be empty."); + + var testProject = await LoadJsonTestDataAsync(); + var compareResult = EntityCompare.Compare(testProject?.Project?.Channels, project?.Channels); + + compareResult.ShouldNotBeNull(); + compareResult.UnchangedItems.ShouldNotBeEmpty("All channels should be unchanged."); + compareResult.ChangedItems.ShouldBeEmpty("No channels should be changed."); + compareResult.ItemsOnlyInLeft.ShouldBeEmpty("No channels should exist only in the test data."); + compareResult.ItemsOnlyInRight.ShouldBeEmpty("No channels should exist only in the loaded project."); + + foreach (var (ExpectedChannel, LoadedChannel) in testProject?.Project?.Channels?.Zip(project?.Channels ?? []) ?? []) + { + var deviceCompareResult = EntityCompare.Compare(ExpectedChannel.Devices, LoadedChannel.Devices); + deviceCompareResult.ShouldNotBeNull(); + deviceCompareResult.UnchangedItems.ShouldNotBeEmpty($"All devices in channel {ExpectedChannel.Name} should be unchanged."); + deviceCompareResult.ChangedItems.ShouldBeEmpty($"No devices in channel {ExpectedChannel.Name} should be changed."); + deviceCompareResult.ItemsOnlyInLeft.ShouldBeEmpty($"No devices should exist only in the test data for channel {ExpectedChannel.Name}."); + deviceCompareResult.ItemsOnlyInRight.ShouldBeEmpty($"No devices should exist only in the loaded project for channel {ExpectedChannel.Name}."); + + foreach (var (ExpectedDevice, LoadedDevice) in ExpectedChannel.Devices?.Zip(LoadedChannel.Devices ?? []) ?? []) + { + if (ExpectedDevice.Tags?.Count > 0 || LoadedDevice.Tags?.Count > 0) + { + var tagCompareResult = EntityCompare.Compare(ExpectedDevice.Tags, LoadedDevice.Tags); + tagCompareResult.ShouldNotBeNull(); + tagCompareResult.UnchangedItems.ShouldNotBeEmpty($"All tags in device {ExpectedDevice.Name} should be unchanged."); + tagCompareResult.ChangedItems.ShouldBeEmpty($"No tags in device {ExpectedDevice.Name} should be changed."); + tagCompareResult.ItemsOnlyInLeft.ShouldBeEmpty($"No tags should exist only in the test data for device {ExpectedDevice.Name}."); + tagCompareResult.ItemsOnlyInRight.ShouldBeEmpty($"No tags should exist only in the loaded project for device {ExpectedDevice.Name}."); + } + + CompareTagGroupsRecursive(ExpectedDevice.TagGroups, LoadedDevice.TagGroups, ExpectedDevice.Name); + } + } + } + private static void CompareTagGroupsRecursive(DeviceTagGroupCollection? expected, DeviceTagGroupCollection? actual, string parentName) { if ((expected?.Count ?? 0) == 0 && (actual?.Count ?? 0) == 0) diff --git a/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs b/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs index eb3133e..725a6b1 100644 --- a/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs +++ b/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs @@ -146,7 +146,21 @@ protected async Task ConfigureToServeEndpoints(string filePath = "_data/simdemo_ { var projectData = await LoadJsonTestDataAsync(filePath); - var channels = projectData.Project?.Channels?.Select(c => new Channel { Name = c.Name, Description = c.Description, DynamicProperties = c.DynamicProperties }).ToList() ?? []; + + var channels = projectData.Project?.Channels? + .Select(c => + { + var ch = new Channel { Name = c.Name, Description = c.Description, DynamicProperties = c.DynamicProperties }; + int staticCount = c.Name switch + { + "Channel1" => 2, + "Simulation Examples" => 24, + "Data Type Examples" => 216, + _ => 0 + }; + ch.SetDynamicProperty(Properties.Channel.StaticTagCount, staticCount); + return ch; + }).ToList() ?? new List(); // Serve project details _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project") @@ -163,7 +177,21 @@ protected async Task ConfigureToServeEndpoints(string filePath = "_data/simdemo_ if (channel.Devices != null) { - var devices = channel.Devices.Select(d => new Device { Name = d.Name, Description = d.Description, DynamicProperties = d.DynamicProperties }).ToList(); + var devices = channel.Devices + .Select(d => + { + var dev = new Device { Name = d.Name, Description = d.Description, DynamicProperties = d.DynamicProperties }; + int staticCount = d.Name switch + { + "Device1" => 2, + "Functions" => 24, + "16 Bit Device" => 98, + "8 Bit Device" => 118, + _ => 0 + }; + dev.SetDynamicProperty(Properties.Device.StaticTagCount, staticCount); + return dev; + }).ToList() ?? new List(); _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + $"/config/v1/project/channels/{channel.Name}/devices") .ReturnsResponse(JsonSerializer.Serialize(devices), "application/json"); @@ -181,6 +209,22 @@ protected async Task ConfigureToServeEndpoints(string filePath = "_data/simdemo_ } } } + + // Additional endpoints for content=serialize mocking + var projectPropertiesString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/projectProperties.json"); + var channel1String = await File.ReadAllTextAsync("_data/projectLoadSerializeData/channel1.json"); + var sixteenBitDeviceString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/dataTypeExamples.16BitDevice.json"); + var simExamplesChannelString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/simulationExamples.json"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project") + .ReturnsResponse(projectPropertiesString, "application/json"); + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Channel1?content=serialize") + .ReturnsResponse(channel1String, "application/json"); + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/16 Bit Device?content=serialize") + .ReturnsResponse(sixteenBitDeviceString, "application/json"); + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Simulation Examples?content=serialize") + .ReturnsResponse(simExamplesChannelString, "application/json"); + } private void ConfigureToServeEndpointsTagGroupsRecursive(string endpoint, IEnumerable tagGroups) { diff --git a/Kepware.Api.Test/Kepware.Api.Test.csproj b/Kepware.Api.Test/Kepware.Api.Test.csproj index a9e2f77..e120c72 100644 --- a/Kepware.Api.Test/Kepware.Api.Test.csproj +++ b/Kepware.Api.Test/Kepware.Api.Test.csproj @@ -34,7 +34,7 @@ - + PreserveNewest diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/Channel1.json b/Kepware.Api.Test/_data/projectLoadSerializeData/Channel1.json new file mode 100644 index 0000000..8ece78c --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/Channel1.json @@ -0,0 +1,52 @@ +{ + "channels": { + "common.ALLTYPES_NAME": "Channel1", + "common.ALLTYPES_DESCRIPTION": "Example Simulator Channel", + "servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator", + "servermain.CHANNEL_DIAGNOSTICS_CAPTURE": false, + "servermain.CHANNEL_UNIQUE_ID": 1704486747, + "servermain.CHANNEL_WRITE_OPTIMIZATIONS_METHOD": 2, + "servermain.CHANNEL_WRITE_OPTIMIZATIONS_DUTY_CYCLE": 10, + "servermain.CHANNEL_NON_NORMALIZED_FLOATING_POINT_HANDLING": 0, + "simulator.CHANNEL_ITEM_PERSISTENCE": false, + "simulator.CHANNEL_ITEM_PERSISTENCE_DATA_FILE": "C:\\ProgramData\\PTC\\Kepware Server\\V7\\Simulator\\Channel1.dat", + "devices": [ + { + "common.ALLTYPES_NAME": "Device1", + "common.ALLTYPES_DESCRIPTION": "Example Simulator Device", + "servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator", + "servermain.DEVICE_MODEL": 0, + "servermain.DEVICE_UNIQUE_ID": 1808204482, + "servermain.DEVICE_ID_FORMAT": 1, + "servermain.DEVICE_ID_STRING": "1", + "servermain.DEVICE_ID_HEXADECIMAL": 1, + "servermain.DEVICE_ID_DECIMAL": 1, + "servermain.DEVICE_ID_OCTAL": 1, + "servermain.DEVICE_DATA_COLLECTION": true, + "servermain.DEVICE_SCAN_MODE": 0, + "servermain.DEVICE_SCAN_MODE_RATE_MS": 1000, + "servermain.DEVICE_SCAN_MODE_PROVIDE_INITIAL_UPDATES_FROM_CACHE": false, + "tags": [ + { + "common.ALLTYPES_NAME": "Tag1", + "common.ALLTYPES_DESCRIPTION": "Ramping Read/Write tag used to verify client connection", + "servermain.TAG_ADDRESS": "R0001", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Tag2", + "common.ALLTYPES_DESCRIPTION": "Constant Read/Write tag used to verify client connection", + "servermain.TAG_ADDRESS": "K0001", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/dataTypeExamples.16bitDevice.json b/Kepware.Api.Test/_data/projectLoadSerializeData/dataTypeExamples.16bitDevice.json new file mode 100644 index 0000000..6b491dc --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/dataTypeExamples.16bitDevice.json @@ -0,0 +1,926 @@ +{ + "devices": { + "common.ALLTYPES_NAME": "16 Bit Device", + "common.ALLTYPES_DESCRIPTION": "Example Simulator Device", + "servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator", + "servermain.DEVICE_MODEL": 0, + "servermain.DEVICE_UNIQUE_ID": 18472794, + "servermain.DEVICE_ID_FORMAT": 1, + "servermain.DEVICE_ID_STRING": "3", + "servermain.DEVICE_ID_HEXADECIMAL": 3, + "servermain.DEVICE_ID_DECIMAL": 3, + "servermain.DEVICE_ID_OCTAL": 3, + "servermain.DEVICE_DATA_COLLECTION": true, + "servermain.DEVICE_SCAN_MODE": 0, + "servermain.DEVICE_SCAN_MODE_RATE_MS": 1000, + "servermain.DEVICE_SCAN_MODE_PROVIDE_INITIAL_UPDATES_FROM_CACHE": false, + "tag_groups": [ + { + "common.ALLTYPES_NAME": "B Registers", + "common.ALLTYPES_DESCRIPTION": "Boolean registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0001", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0002", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0003", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0004", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "BooleanArray", + "common.ALLTYPES_DESCRIPTION": "Array of 4 Boolean Registers", + "servermain.TAG_ADDRESS": "B0010 [4]", + "servermain.TAG_DATA_TYPE": 21, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + }, + { + "common.ALLTYPES_NAME": "K Registers", + "common.ALLTYPES_DESCRIPTION": "Constant Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + }, + { + "common.ALLTYPES_NAME": "R Registers", + "common.ALLTYPES_DESCRIPTION": "Ramping Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "R0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "R0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "R0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "R1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "R0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "R1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "R0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "R0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + }, + { + "common.ALLTYPES_NAME": "S Registers", + "common.ALLTYPES_DESCRIPTION": "String Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "String1", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0001", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String2", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0002", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String3", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0003", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String4", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0004", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "StringArray[4]", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string array", + "servermain.TAG_ADDRESS": "S0010 [4]", + "servermain.TAG_DATA_TYPE": 20, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/projectProperties.json b/Kepware.Api.Test/_data/projectLoadSerializeData/projectProperties.json new file mode 100644 index 0000000..27a7a92 --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/projectProperties.json @@ -0,0 +1,80 @@ +{ + "PROJECT_ID": 960924075, + "common.ALLTYPES_DESCRIPTION": "Example project utilizing Simulator Driver.", + "servermain.PROJECT_TITLE": "Simulation Driver Demo", + "servermain.PROJECT_TAGS_DEFINED": "246", + "opcdaserver.PROJECT_OPC_DA_1_ENABLED": true, + "opcdaserver.PROJECT_OPC_DA_2_ENABLED": true, + "opcdaserver.PROJECT_OPC_DA_3_ENABLED": true, + "opcdaserver.PROJECT_OPC_SHOW_HINTS_ON_BROWSE": false, + "opcdaserver.PROJECT_OPC_SHOW_TAG_PROPERTIES_ON_BROWSE": false, + "opcdaserver.PROJECT_OPC_SHUTDOWN_WAIT_SEC": 15, + "opcdaserver.PROJECT_OPC_SYNC_REQUEST_WAIT_SEC": 15, + "opcdaserver.PROJECT_OPC_ENABLE_DIAGS": false, + "opcdaserver.PROJECT_OPC_MAX_CONNECTIONS": 512, + "opcdaserver.PROJECT_OPC_MAX_TAG_GROUPS": 2000, + "opcdaserver.PROJECT_OPC_REJECT_UNSUPPORTED_LANG_ID": true, + "opcdaserver.PROJECT_OPC_IGNORE_DEADBAND_ON_CACHE": false, + "opcdaserver.PROJECT_OPC_IGNORE_BROWSE_FILTER": false, + "opcdaserver.PROJECT_OPC_205A_DATA_TYPE_SUPPORT": true, + "opcdaserver.PROJECT_OPC_SYNC_READ_ERROR_ON_BAD_QUALITY": false, + "opcdaserver.PROJECT_OPC_RETURN_INITIAL_UPDATES_IN_SINGLE_CALLBACK": false, + "opcdaserver.PROJECT_OPC_RESPECT_CLIENT_LANG_ID": true, + "opcdaserver.PROJECT_OPC_COMPLIANT_DATA_CHANGE": true, + "opcdaserver.PROJECT_OPC_IGNORE_GROUP_UPDATE_RATE": false, + "wwtoolkitinterface.ENABLED": false, + "wwtoolkitinterface.SERVICE_NAME": "server_runtime", + "wwtoolkitinterface.CLIENT_UPDATE_INTERVAL_MS": 100, + "ddeserver.ENABLE": false, + "ddeserver.SERVICE_NAME": "ptcdde", + "ddeserver.ADVANCED_DDE": true, + "ddeserver.XLTABLE": true, + "ddeserver.CF_TEXT": true, + "ddeserver.CLIENT_UPDATE_INTERVAL_MS": 100, + "ddeserver.REQUEST_TIMEOUT_SEC": 15, + "uaserverinterface.PROJECT_OPC_UA_ENABLE": true, + "uaserverinterface.PROJECT_OPC_UA_DIAGNOSTICS": false, + "uaserverinterface.PROJECT_OPC_UA_ANONYMOUS_LOGIN": false, + "uaserverinterface.PROJECT_OPC_UA_MAX_CONNECTIONS": 128, + "uaserverinterface.PROJECT_OPC_UA_MIN_SESSION_TIMEOUT_SEC": 15, + "uaserverinterface.PROJECT_OPC_UA_MAX_SESSION_TIMEOUT_SEC": 60, + "uaserverinterface.PROJECT_OPC_UA_TAG_CACHE_TIMEOUT_SEC": 5, + "uaserverinterface.PROJECT_OPC_UA_BROWSE_TAG_PROPERTIES": false, + "uaserverinterface.PROJECT_OPC_UA_BROWSE_ADDRESS_HINTS": false, + "uaserverinterface.PROJECT_OPC_UA_MAX_DATA_QUEUE_SIZE": 2, + "uaserverinterface.PROJECT_OPC_UA_MAX_RETRANSMIT_QUEUE_SIZE": 10, + "uaserverinterface.PROJECT_OPC_UA_MAX_NOTIFICATION_PER_PUBLISH": 65536, + "aeserverinterface.ENABLE_AE_SERVER": false, + "aeserverinterface.ENABLE_SIMPLE_EVENTS": true, + "aeserverinterface.MAX_SUBSCRIPTION_BUFFER_SIZE": 100, + "aeserverinterface.MIN_SUBSCRIPTION_BUFFER_TIME_MS": 1000, + "aeserverinterface.MIN_KEEP_ALIVE_TIME_MS": 1000, + "hdaserver.ENABLE": false, + "hdaserver.ENABLE_DIAGNOSTICS": false, + "thingworxinterface.ENABLED": false, + "thingworxinterface.HOSTNAME": "localhost", + "thingworxinterface.PORT": 443, + "thingworxinterface.RESOURCE": "/Thingworx/WS", + "thingworxinterface.APPKEY": "", + "thingworxinterface.ALLOW_SELF_SIGNED_CERTIFICATE": false, + "thingworxinterface.TRUST_ALL_CERTIFICATES": false, + "thingworxinterface.DISABLE_ENCRYPTION": false, + "thingworxinterface.MAX_THING_COUNT": 500, + "thingworxinterface.THING_NAME": "Kepware Server", + "thingworxinterface.PUBLISH_FLOOR_MSEC": 1000, + "thingworxinterface.LOGGING_ENABLED": false, + "thingworxinterface.LOG_LEVEL": 3, + "thingworxinterface.VERBOSE": false, + "thingworxinterface.STORE_AND_FORWARD_ENABLED": false, + "thingworxinterface.STORAGE_PATH": "C:\\ProgramData\\PTC\\Kepware Server\\V7", + "thingworxinterface.DATASTORE_MAXSIZE": 2048, + "thingworxinterface.FORWARD_MODE": 0, + "thingworxinterface.DATASTORE_ID": 421728385, + "thingworxinterface.DELAY_BETWEEN_PUBLISHES": 0, + "thingworxinterface.MAX_UPDATES_PER_PUBLISH": 25000, + "thingworxinterface.PROXY_ENABLED": false, + "thingworxinterface.PROXY_HOST": "localhost", + "thingworxinterface.PROXY_PORT": 3128, + "thingworxinterface.PROXY_USERNAME": "", + "thingworxinterface.PROXY_PASSWORD": "" +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/simulationExamples.json b/Kepware.Api.Test/_data/projectLoadSerializeData/simulationExamples.json new file mode 100644 index 0000000..9729550 --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/simulationExamples.json @@ -0,0 +1,250 @@ +{ + "channels": { + "common.ALLTYPES_NAME": "Simulation Examples", + "common.ALLTYPES_DESCRIPTION": "Example Simulator Channel", + "servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator", + "servermain.CHANNEL_DIAGNOSTICS_CAPTURE": false, + "servermain.CHANNEL_UNIQUE_ID": 2691320731, + "servermain.CHANNEL_WRITE_OPTIMIZATIONS_METHOD": 2, + "servermain.CHANNEL_WRITE_OPTIMIZATIONS_DUTY_CYCLE": 10, + "servermain.CHANNEL_NON_NORMALIZED_FLOATING_POINT_HANDLING": 0, + "simulator.CHANNEL_ITEM_PERSISTENCE": false, + "simulator.CHANNEL_ITEM_PERSISTENCE_DATA_FILE": "C:\\ProgramData\\PTC\\Kepware Server\\V7\\Simulator\\Simulation Examples.dat", + "devices": [ + { + "common.ALLTYPES_NAME": "Functions", + "common.ALLTYPES_DESCRIPTION": "Example Simulator Device", + "servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator", + "servermain.DEVICE_MODEL": 0, + "servermain.DEVICE_UNIQUE_ID": 2266623120, + "servermain.DEVICE_ID_FORMAT": 1, + "servermain.DEVICE_ID_STRING": "4", + "servermain.DEVICE_ID_HEXADECIMAL": 4, + "servermain.DEVICE_ID_DECIMAL": 4, + "servermain.DEVICE_ID_OCTAL": 4, + "servermain.DEVICE_DATA_COLLECTION": true, + "servermain.DEVICE_SCAN_MODE": 0, + "servermain.DEVICE_SCAN_MODE_RATE_MS": 1000, + "servermain.DEVICE_SCAN_MODE_PROVIDE_INITIAL_UPDATES_FROM_CACHE": false, + "tags": [ + { + "common.ALLTYPES_NAME": "Ramp1", + "common.ALLTYPES_DESCRIPTION": "Value increments by 4 from 35 to 100 every 120 ms", + "servermain.TAG_ADDRESS": "RAMP (120, 35, 100, 4)", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Ramp2", + "common.ALLTYPES_DESCRIPTION": "Value decrements by 0.25 from 200.50 to 150.75 every 300 ms", + "servermain.TAG_ADDRESS": "RAMP (300, 150.750000, 200.500000, -0.250000)", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Ramp3", + "common.ALLTYPES_DESCRIPTION": "Value increments by 1 from 0 to 1000 every 250 ms", + "servermain.TAG_ADDRESS": "RAMP (250, 0, 1000, 1)", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Ramp4", + "common.ALLTYPES_DESCRIPTION": "Value decrements by 5 from 1000 to -1000 every 2000 ms", + "servermain.TAG_ADDRESS": "RAMP (2000, -1000, 1000, -5)", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Ramp5", + "common.ALLTYPES_DESCRIPTION": "Value decrements by 500 from 1000000 to -1000000 every 250 ms", + "servermain.TAG_ADDRESS": "RAMP (250, -1000000, 1000000, -500)", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Ramp6", + "common.ALLTYPES_DESCRIPTION": "Value increments by 1250 from 0 to 1 billion every 1000 ms", + "servermain.TAG_ADDRESS": "RAMP (1000, 0, 1000000000, 1250)", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Ramp7", + "common.ALLTYPES_DESCRIPTION": "Value decrements by 1 billion to -1 billion every 1000 ms", + "servermain.TAG_ADDRESS": "RAMP (1000, -1000000000, 1000000000, -5555)", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Ramp8", + "common.ALLTYPES_DESCRIPTION": "Value decrements by 0.25 from 200.50 to 150.75 every 300 ms", + "servermain.TAG_ADDRESS": "RAMP (1000, 150.750000, 200.500000, -0.250000)", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Random1", + "common.ALLTYPES_DESCRIPTION": "Random values from -20 to 75 that change every 30 ms", + "servermain.TAG_ADDRESS": "RANDOM (30, -20, 75)", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Random2", + "common.ALLTYPES_DESCRIPTION": "Random values from 0 to 1000 that change every 100 ms", + "servermain.TAG_ADDRESS": "RANDOM (100, 0, 1000)", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Random3", + "common.ALLTYPES_DESCRIPTION": "Random values from -1000 to 0 that change every 100 ms", + "servermain.TAG_ADDRESS": "RANDOM (100, -1000, 0)", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Random4", + "common.ALLTYPES_DESCRIPTION": "Random values from -999 to 999 that change every 1000 ms", + "servermain.TAG_ADDRESS": "RANDOM (1000, -999, 999)", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Random5", + "common.ALLTYPES_DESCRIPTION": "Random values from -1000000000 to 1000000000 that change every 100 ms", + "servermain.TAG_ADDRESS": "RANDOM (100, -1000000000, 1000000000)", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Random6", + "common.ALLTYPES_DESCRIPTION": "Random values from -1000000 to 1000000 that change every 1000 ms", + "servermain.TAG_ADDRESS": "RANDOM (1000, -1000000, 1000000)", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Random7", + "common.ALLTYPES_DESCRIPTION": "Random values from 0 to 1000000000 that change every 1000 ms", + "servermain.TAG_ADDRESS": "RANDOM (1000, 0, 1000000000)", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Random8", + "common.ALLTYPES_DESCRIPTION": "Random values from 0 to 1000000 that change every 100 ms", + "servermain.TAG_ADDRESS": "RANDOM (100, 0, 1000000)", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Sine1", + "common.ALLTYPES_DESCRIPTION": "Sine values between -40 and 40 at 0.05 Hz with 0 phase shift", + "servermain.TAG_ADDRESS": "SINE (10, -40.000000, 40.000000, 0.050000, 0)", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Sine2", + "common.ALLTYPES_DESCRIPTION": "Sine values between -40 and 40 at 0.05 Hz with 180 phase shift", + "servermain.TAG_ADDRESS": "SINE (10, -40.000000, 40.000000, 0.050000, 180)", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Sine3", + "common.ALLTYPES_DESCRIPTION": "Sine values between -40 and 40 at 0.1 Hz with 0 phase shift", + "servermain.TAG_ADDRESS": "SINE (10, -40.000000, 40.000000, 0.100000, 0)", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Sine4", + "common.ALLTYPES_DESCRIPTION": "Sine values between -40 and 40 at 0.1 Hz with 360 phase shift", + "servermain.TAG_ADDRESS": "SINE (10, -40.000000, 40.000000, 0.100000, 360)", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "User1", + "common.ALLTYPES_DESCRIPTION": "Sequential string values that change every 1000 ms", + "servermain.TAG_ADDRESS": "USER (1000,Hello,world!,This,is,a,test.)", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "User2", + "common.ALLTYPES_DESCRIPTION": "Sequential float values that change every 250 ms", + "servermain.TAG_ADDRESS": "USER (250,15.16,23.42,4.8)", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "User3", + "common.ALLTYPES_DESCRIPTION": "Sequential Boolean values that change every 200 ms", + "servermain.TAG_ADDRESS": "USER (200,1,0,0,1,0,0,1,0,0,0,1,0,1,0,1,0,0,0,1,0,1,0,1,0,0,0)", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "User4", + "common.ALLTYPES_DESCRIPTION": "A comma is a delimiter unless it is preceded with a backslash", + "servermain.TAG_ADDRESS": "USER (1500,To display a comma\\, place,a backslash in front of it.)", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 0, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/simdemo_en-us.json b/Kepware.Api.Test/_data/simdemo_en-us.json index ce4243c..e93f6ce 100644 --- a/Kepware.Api.Test/_data/simdemo_en-us.json +++ b/Kepware.Api.Test/_data/simdemo_en-us.json @@ -13,7 +13,7 @@ "servermain.CHANNEL_WRITE_OPTIMIZATIONS_DUTY_CYCLE": 10, "servermain.CHANNEL_NON_NORMALIZED_FLOATING_POINT_HANDLING": 0, "simulator.CHANNEL_ITEM_PERSISTENCE": false, - "simulator.CHANNEL_ITEM_PERSISTENCE_DATA_FILE": "C:\\ProgramData\\Kepware\\KEPServerEX\\V6\\Simulator\\Channel1.dat", + "simulator.CHANNEL_ITEM_PERSISTENCE_DATA_FILE": "C:\\ProgramData\\PTC\\Kepware Server\\V7\\Simulator\\Channel1.dat", "devices": [ { "common.ALLTYPES_NAME": "Device1", @@ -63,7 +63,7 @@ "servermain.CHANNEL_WRITE_OPTIMIZATIONS_DUTY_CYCLE": 10, "servermain.CHANNEL_NON_NORMALIZED_FLOATING_POINT_HANDLING": 0, "simulator.CHANNEL_ITEM_PERSISTENCE": false, - "simulator.CHANNEL_ITEM_PERSISTENCE_DATA_FILE": "C:\\ProgramData\\Kepware\\KEPServerEX\\V6\\Simulator\\Data Type Examples.dat", + "simulator.CHANNEL_ITEM_PERSISTENCE_DATA_FILE": "C:\\ProgramData\\PTC\\Kepware Server\\V7\\Simulator\\Data Type Examples.dat", "devices": [ { "common.ALLTYPES_NAME": "16 Bit Device", @@ -2105,7 +2105,7 @@ "servermain.CHANNEL_WRITE_OPTIMIZATIONS_DUTY_CYCLE": 10, "servermain.CHANNEL_NON_NORMALIZED_FLOATING_POINT_HANDLING": 0, "simulator.CHANNEL_ITEM_PERSISTENCE": false, - "simulator.CHANNEL_ITEM_PERSISTENCE_DATA_FILE": "C:\\ProgramData\\Kepware\\KEPServerEX\\V6\\Simulator\\Simulation Examples.dat", + "simulator.CHANNEL_ITEM_PERSISTENCE_DATA_FILE": "C:\\ProgramData\\PTC\\Kepware Server\\V7\\Simulator\\Simulation Examples.dat", "devices": [ { "common.ALLTYPES_NAME": "Functions", diff --git a/Kepware.Api.TestIntg/ApiClient/ProjectLoadTests.cs b/Kepware.Api.TestIntg/ApiClient/ProjectLoadTests.cs index bfde274..bc01688 100644 --- a/Kepware.Api.TestIntg/ApiClient/ProjectLoadTests.cs +++ b/Kepware.Api.TestIntg/ApiClient/ProjectLoadTests.cs @@ -203,6 +203,60 @@ public async Task LoadProject_Full_LargeProject_ShouldLoadCorrectly_BasedOnProdu // Clean up await DeleteAllChannelsAsync(); } + + + [Fact] + public async Task LoadProject_Full_OverrideForOptimizedRecursion_ShouldLoadCorrectly_BasedOnProductSupport() + { + // Arrange + var channel = await AddTestChannel(); + var device = await AddTestDevice(channel); + var tags = await AddSimulatorTestTags(device, count: 200); + var tagGroup = await AddTestTagGroup(device); + var tagGroup2 = await AddTestTagGroup(tagGroup, "TagGroup2"); + var tagsTagGroup2 = await AddSimulatorTestTags(tagGroup2, count: 10); + + var channel2 = await AddTestChannel("Channel2"); + var device2 = await AddTestDevice(channel2); + var tags2 = await AddSimulatorTestTags(device2); + var tagGroup_2 = await AddTestTagGroup(device2); + var tagGroup2_2 = await AddTestTagGroup(tagGroup_2, "TagGroup2"); + + // Act + var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: true, projectLoadTagLimit: 100); + + // Assert + Assert.NotNull(project); + Assert.NotNull(project.Channels); + Assert.Contains(project.Channels, c => c.Name == channel.Name); + + var foundChannel = project.Channels.Find(c => c.Name == channel.Name); + Assert.NotNull(foundChannel); + Assert.NotNull(foundChannel.Devices); + Assert.Contains(foundChannel.Devices, d => d.Name == device.Name); + + var foundDevice = foundChannel.Devices.Find(d => d.Name == device.Name); + Assert.NotNull(foundDevice); + Assert.NotNull(foundDevice.Tags); + Assert.Equal(tags.Count, foundDevice.Tags.Count); + Assert.NotNull(foundDevice.TagGroups); + Assert.Contains(foundDevice.TagGroups, tg => tg.Name == tagGroup.Name); + + var foundTagGroup = foundDevice.TagGroups.Find(tg => tg.Name == tagGroup.Name); + Assert.NotNull(foundTagGroup); + Assert.NotNull(foundTagGroup.TagGroups); + Assert.Contains(foundTagGroup.TagGroups, tg => tg.Name == tagGroup2.Name); + + var foundTagGroup2 = foundTagGroup.TagGroups.Find(tg => tg.Name == tagGroup2.Name); + Assert.NotNull(foundTagGroup2); + Assert.NotNull(foundTagGroup2.Tags); + Assert.Equal(tagsTagGroup2.Count, foundTagGroup2.Tags.Count); + + + // Clean up + await DeleteAllChannelsAsync(); + } + [Fact] public async Task LoadProject_NotFull_ShouldLoadCorrectly_BasedOnProductSupport() { diff --git a/Kepware.Api.TestIntg/ApiClient/_TestIntgApiClientBase.cs b/Kepware.Api.TestIntg/ApiClient/_TestIntgApiClientBase.cs index 23beace..1f16211 100644 --- a/Kepware.Api.TestIntg/ApiClient/_TestIntgApiClientBase.cs +++ b/Kepware.Api.TestIntg/ApiClient/_TestIntgApiClientBase.cs @@ -153,7 +153,7 @@ protected async Task AddSimulatorTestTag(DeviceTagGroup owner, string name protected List CreateSimulatorTestTags(string name = "Tag", string address = "K000", int count = 2) { - return Enumerable.Range(0, count-1) + return Enumerable.Range(0, count) .Select(i => CreateTestTag(name: $"{name}{i}", address: $"{address}{i}")) .ToList(); } diff --git a/Kepware.Api/ClientHandler/ProjectApiHandler.cs b/Kepware.Api/ClientHandler/ProjectApiHandler.cs index be89a90..c0d41c7 100644 --- a/Kepware.Api/ClientHandler/ProjectApiHandler.cs +++ b/Kepware.Api/ClientHandler/ProjectApiHandler.cs @@ -219,7 +219,7 @@ public async Task SetProjectPropertiesAsync(Project project, CancellationT #region LoadProject /// - /// Does the same as but is marked as obsolete. + /// Does the same as but is marked as obsolete. /// /// Indicates whether to load the full project. /// The cancellation token. @@ -229,7 +229,7 @@ public async Task SetProjectPropertiesAsync(Project project, CancellationT [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public async Task LoadProject(bool blnLoadFullProject = false, CancellationToken cancellationToken = default) { - return await LoadProjectAsync(blnLoadFullProject, cancellationToken).ConfigureAwait(false); + return await LoadProjectAsync(blnLoadFullProject, m_kepwareApiClient.ClientOptions.ProjectLoadTagLimit ,cancellationToken).ConfigureAwait(false); } /// @@ -237,11 +237,15 @@ public async Task LoadProject(bool blnLoadFullProject = false, Cancella /// the project properties will be returned. /// /// Indicates whether to load the full project. + /// The tag count threshold to determine whether to use optimized content=serialize + /// loading or basic recursive loading when loading the full project. This is only applicable for projects loaded with + /// the full project load option and when the JsonProjectLoad service is supported by the server. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains the loaded . /// NOTE: When loading a full project, the project will be loaded either via the JsonProjectLoad service, an "optimized" - /// recursion that uses the content=serialize query or a basic recurion through project tree. - public async Task LoadProjectAsync(bool blnLoadFullProject = false, CancellationToken cancellationToken = default) + /// recursion that uses the content=serialize query or a basic recurion through project tree. Putting a value for + /// will override the value set in during initial client creation. + public async Task LoadProjectAsync(bool blnLoadFullProject = false, int projectLoadTagLimit = 0, CancellationToken cancellationToken = default) { Stopwatch stopwatch = Stopwatch.StartNew(); @@ -266,20 +270,23 @@ public async Task LoadProjectAsync(bool blnLoadFullProject = false, Can if (productInfo?.SupportsJsonProjectLoadService == true) { + // Set by as the threshold to use content=serialize based loading or full recursive loading. Default + // set by class. + if (projectLoadTagLimit <= 0) + projectLoadTagLimit = m_kepwareApiClient.ClientOptions.ProjectLoadTagLimit; + try { + // Optimized recursive loading approach that uses the content=serialize query parameter. // This approach significantly reduces the number of API calls when loading projects with a large number of tags and prevents // timeout errors with large projects. - - // TODO: change threshold to configurable option - // Currently hardcoded to 100000 tags as the threshold to use content=serialize based loading or full recursive loading. - var tagLimit = 100000; - if (int.TryParse(project.GetDynamicProperty(Properties.ProjectSettings.TagsDefined), out int count) && count > tagLimit) + + if (int.TryParse(project.GetDynamicProperty(Properties.ProjectSettings.TagsDefined), out int count) && count > projectLoadTagLimit) { - m_logger.LogInformation("Project has greater than {TagLimit} tags defined. Loading project via optimized recursion...", tagLimit); + m_logger.LogInformation("Project has greater than {TagLimit} tags defined. Loading project via optimized recursion...", projectLoadTagLimit); - project = await LoadProjectOptimizedRecurisveAsync(project, tagLimit, cancellationToken).ConfigureAwait(false); + project = await LoadProjectOptimizedRecurisveAsync(project, projectLoadTagLimit, cancellationToken).ConfigureAwait(false); if (!project.IsEmpty) { @@ -292,7 +299,7 @@ public async Task LoadProjectAsync(bool blnLoadFullProject = false, Can // If project has less than tagLimit number of tags, load full project via JsonProjectLoad service. else { - m_logger.LogInformation("Project has less than {TagLimit} tags defined. Loading project via JsonProjectLoad Service...", tagLimit); + m_logger.LogInformation("Project has less than {TagLimit} tags defined. Loading project via JsonProjectLoad Service...", projectLoadTagLimit); var response = await m_kepwareApiClient.HttpClient.GetAsync(ENDPONT_FULL_PROJECT, cancellationToken).ConfigureAwait(false); if (response.IsSuccessStatusCode) { @@ -368,80 +375,94 @@ private async Task LoadProjectOptimizedRecurisveAsync(Project project, { int totalChannelCount = project.Channels.Count; int loadedChannelCount = 0; - await Task.WhenAll(project.Channels.Select(async (channel, c_index) => - { - if (channel.GetDynamicProperty(Properties.Channel.StaticTagCount) < tagLimit) + // Create a list of tasks by iterating indices to avoid modifying the collection while it's being enumerated. + var channelTasks = new List(); + for (int c_index = 0; c_index < project.Channels.Count; c_index++) + { + int channelIndex = c_index; + var channel = project.Channels[channelIndex]; + channelTasks.Add(Task.Run(async () => { - var query = new[] - { - new KeyValuePair("content", "serialize") - }; - var loadedChannel = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(channel.Name, query, cancellationToken: cancellationToken); - if (loadedChannel != null) + if (channel.GetDynamicProperty(Properties.Channel.StaticTagCount) < tagLimit) { - project.Channels[c_index] = loadedChannel; + var query = new[] + { + new KeyValuePair("content", "serialize") + }; + var loadedChannel = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(channel.Name, query, cancellationToken: cancellationToken).ConfigureAwait(false); + if (loadedChannel != null) + { + project.Channels[channelIndex] = loadedChannel; + } + else + { + // Failed to load channel, log warning and end without incrementing completion. + m_logger.LogWarning("Failed to load {ChannelName}", channel.Name); + return; + } } else { - // Failed to load channel, log warning and end without incrementing completion. - m_logger.LogWarning("Failed to load {ChannelName}", channel.Name); - return; - } - } - else - { - channel.Devices = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(channel, cancellationToken: cancellationToken).ConfigureAwait(false); + channel.Devices = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(channel, cancellationToken: cancellationToken).ConfigureAwait(false); - if (channel.Devices != null) - { - await Task.WhenAll(channel.Devices.Select(async (device, d_index) => - { - if (device.GetDynamicProperty(Properties.Device.StaticTagCount) < tagLimit) + if (channel.Devices != null) { - var query = new[] + var deviceTasks = new List(); + for (int d_index = 0; d_index < channel.Devices.Count; d_index++) { + int deviceIndex = d_index; + var device = channel.Devices[deviceIndex]; + deviceTasks.Add(Task.Run(async () => + { + if (device.GetDynamicProperty(Properties.Device.StaticTagCount) < tagLimit) + { + var query = new[] + { new KeyValuePair("content", "serialize") }; - var loadedDevice = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(device.Name, channel, query, cancellationToken: cancellationToken).ConfigureAwait(false); - if (loadedDevice != null) - { - project.Channels[c_index].Devices![d_index] = loadedDevice; - } - else - { - // Failed to load device, log warning and end without incrementing completion. - m_logger.LogWarning("Failed to load {DeviceName} in channel {ChannelName}", device.Name, channel.Name); - return; + var loadedDevice = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(device.Name, channel, query, cancellationToken: cancellationToken).ConfigureAwait(false); + if (loadedDevice != null) + { + project.Channels[channelIndex].Devices![deviceIndex] = loadedDevice; + } + else + { + // Failed to load device, log warning and end without incrementing completion. + m_logger.LogWarning("Failed to load {DeviceName} in channel {ChannelName}", device.Name, channel.Name); + return; + } + } + else + { + device.Tags = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(device, cancellationToken: cancellationToken).ConfigureAwait(false); + device.TagGroups = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(device, cancellationToken: cancellationToken).ConfigureAwait(false); + + if (device.TagGroups != null) + { + await LoadTagGroupsRecursiveAsync(m_kepwareApiClient, device.TagGroups, cancellationToken: cancellationToken).ConfigureAwait(false); + } + } + })); } + await Task.WhenAll(deviceTasks).ConfigureAwait(false); } - else - { - device.Tags = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(device, cancellationToken: cancellationToken).ConfigureAwait(false); - device.TagGroups = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(device, cancellationToken: cancellationToken).ConfigureAwait(false); - - if (device.TagGroups != null) - { - await LoadTagGroupsRecursiveAsync(m_kepwareApiClient, device.TagGroups, cancellationToken: cancellationToken).ConfigureAwait(false); - } - } - - })); } - } - // Log information, loaded channel x of y - loadedChannelCount++; - if (totalChannelCount == 1) - { - m_logger.LogInformation("Loaded channel {ChannelName}", channel.Name); - } - else - { - m_logger.LogInformation("Loaded channel {ChannelName} {LoadedChannelCount} of {TotalChannelCount}", channel.Name, loadedChannelCount, totalChannelCount); - } + // Log information, loaded channel x of y + System.Threading.Interlocked.Increment(ref loadedChannelCount); + if (totalChannelCount == 1) + { + m_logger.LogInformation("Loaded channel {ChannelName}", channel.Name); + } + else + { + m_logger.LogInformation("Loaded channel {ChannelName} {LoadedChannelCount} of {TotalChannelCount}", channel.Name, loadedChannelCount, totalChannelCount); + } + })); + } - })); + await Task.WhenAll(channelTasks).ConfigureAwait(false); // If loaded channel count doesn't match total channel count, log warning that some channels may have failed to load. // Return empty project to avoid returning a partially loaded project which may cause issues for consumers of the API. diff --git a/Kepware.Api/KepwareApiClientOptions.cs b/Kepware.Api/KepwareApiClientOptions.cs index 7cd951d..26c7622 100644 --- a/Kepware.Api/KepwareApiClientOptions.cs +++ b/Kepware.Api/KepwareApiClientOptions.cs @@ -60,5 +60,16 @@ public class KepwareApiClientOptions /// Gets or sets an optional tag object for additional configuration metadata. /// public object? Tag { get; init; } + + /// + /// Gets the maximum number of tags that can be loaded in a single action when using ProjectLoad methods. + /// This limit is crucial for managing performance and resource utilization when working with large projects, + /// as it helps to prevent excessive memory usage and potential timeouts that may occur when attempting to + /// load an excessively large number of tags at once. + /// + /// This property is initialized to a default value of 100,000. It is important to + /// consider this limit when working with projects that may contain a large number of tags, as exceeding this + /// limit may result in performance degradation or incomplete data loading. + public int ProjectLoadTagLimit { get; init; } = 100000; } } From 7dd31afa3e1b4578518735fc4647ac208cff2d52 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Mon, 16 Mar 2026 18:32:12 -0400 Subject: [PATCH 08/38] feat(api): added DeviceDriver to device model --- Kepware.Api/Model/Project/Device.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Kepware.Api/Model/Project/Device.cs b/Kepware.Api/Model/Project/Device.cs index e0015aa..9dc3944 100644 --- a/Kepware.Api/Model/Project/Device.cs +++ b/Kepware.Api/Model/Project/Device.cs @@ -70,6 +70,12 @@ public Device(string name, string channelName) [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public DeviceTagGroupCollection? TagGroups { get; set; } + /// + /// Gets the driver used by this device. + /// + [YamlIgnore, JsonIgnore] + public string? DeviceDriver => GetDynamicProperty(Properties.Device.DeviceDriver); + /// /// Gets the unique ID key for the device. /// From b19ee4cf80ef45f500cd3cdc0e9957a6c3499ec3 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Mon, 16 Mar 2026 18:33:15 -0400 Subject: [PATCH 09/38] chore: translated comment --- Kepware.Api.Test/ApiClient/LoadEntity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kepware.Api.Test/ApiClient/LoadEntity.cs b/Kepware.Api.Test/ApiClient/LoadEntity.cs index 8d12269..761b948 100644 --- a/Kepware.Api.Test/ApiClient/LoadEntity.cs +++ b/Kepware.Api.Test/ApiClient/LoadEntity.cs @@ -379,7 +379,7 @@ public async Task LoadEntityAsync_ShouldThrowInvalidOperationException_WhenLoadR #endregion - #region LoadEntityAsync - Single Tag mit DynamicProperties + #region LoadEntityAsync - Single Tag with DynamicProperties [Fact] public async Task LoadEntityAsync_ShouldReturnTag_WithCorrectDynamicProperties() From b178ba26e7f5d4baedec66594f5ae9448ce2d9ac Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Mon, 16 Mar 2026 18:33:46 -0400 Subject: [PATCH 10/38] chore: ensure _data files are output in build --- Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj b/Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj index 884ad48..6283af0 100644 --- a/Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj +++ b/Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj @@ -42,7 +42,7 @@ PreserveNewest - + PreserveNewest From 7c60442c51f15805e099ba5e4bba62371de33526 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Tue, 17 Mar 2026 18:38:47 -0400 Subject: [PATCH 11/38] feat(api): extended optimized recursion ProjectLoad to tag groups --- .../ApiClient/ProjectLoadTests.cs | 84 +- .../ApiClient/_TestApiClientBase.cs | 102 +- .../dte.8bitDevice.Breg.json | 53 + .../dte.8bitDevice.Kreg.json | 494 +++ .../dte.8bitDevice.Rreg.json | 494 +++ .../dte.8bitDevice.Sreg.json | 53 + .../opt.recursionDevice.Breg.json | 53 + .../opt.recursionDevice.Kreg.json | 404 +++ ....recursionDevice.RecursionTest.Level1.json | 806 +++++ ....recursionDevice.RecursionTest.Level2.json | 403 +++ ....recursionDevice.RecursionTest.Level3.json | 403 +++ ....recursionDevice.RecursionTest.Level4.json | 403 +++ .../opt.recursionDevice.Rreg.json | 404 +++ .../opt.recursionDevice.Sreg.json | 53 + Kepware.Api.Test/_data/simdemo_en-us.json | 3022 ++++++++++++++++- .../ClientHandler/ProjectApiHandler.cs | 61 +- Kepware.Api/KepwareApiClient.cs | 8 + .../Project/DeviceTagGroup.Properties.cs | 25 + Kepware.Api/Model/Project/DeviceTagGroup.cs | 22 +- 19 files changed, 7266 insertions(+), 81 deletions(-) create mode 100644 Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Breg.json create mode 100644 Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Kreg.json create mode 100644 Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Rreg.json create mode 100644 Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Sreg.json create mode 100644 Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Breg.json create mode 100644 Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Kreg.json create mode 100644 Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level1.json create mode 100644 Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level2.json create mode 100644 Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level3.json create mode 100644 Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level4.json create mode 100644 Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Rreg.json create mode 100644 Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Sreg.json create mode 100644 Kepware.Api/Model/Project/DeviceTagGroup.Properties.cs diff --git a/Kepware.Api.Test/ApiClient/ProjectLoadTests.cs b/Kepware.Api.Test/ApiClient/ProjectLoadTests.cs index 6554f41..cd1991c 100644 --- a/Kepware.Api.Test/ApiClient/ProjectLoadTests.cs +++ b/Kepware.Api.Test/ApiClient/ProjectLoadTests.cs @@ -22,63 +22,6 @@ namespace Kepware.Api.Test.ApiClient public class ProjectLoadTests : TestApiClientBase { - //private async Task ConfigureToServeEndpoints() - //{ - // var projectData = await LoadJsonTestDataAsync(); - - // var channels = projectData.Project?.Channels?.Select(c => new Channel { Name = c.Name, Description = c.Description, DynamicProperties = c.DynamicProperties }).ToList() ?? []; - - // // Serve project details - // _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project") - // .ReturnsResponse(JsonSerializer.Serialize(new Project { Description = projectData?.Project?.Description, DynamicProperties = projectData?.Project?.DynamicProperties ?? [] }), "application/json"); - - // // Serve channels without nested devices - // _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels") - // .ReturnsResponse(JsonSerializer.Serialize(channels), "application/json"); - - // foreach (var channel in projectData?.Project?.Channels ?? []) - // { - // _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + $"/config/v1/project/channels/{channel.Name}") - // .ReturnsResponse(JsonSerializer.Serialize(new Channel { Name = channel.Name, Description = channel.Description, DynamicProperties = channel.DynamicProperties }), "application/json"); - - // if (channel.Devices != null) - // { - // var devices = channel.Devices.Select(d => new Device { Name = d.Name, Description = d.Description, DynamicProperties = d.DynamicProperties }).ToList(); - // _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + $"/config/v1/project/channels/{channel.Name}/devices") - // .ReturnsResponse(JsonSerializer.Serialize(devices), "application/json"); - - // foreach (var device in channel.Devices) - // { - // var deviceEndpoint = TEST_ENDPOINT + $"/config/v1/project/channels/{channel.Name}/devices/{device.Name}"; - // _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, deviceEndpoint) - // .ReturnsResponse(JsonSerializer.Serialize(new Device { Name = device.Name, Description = device.Description, DynamicProperties = device.DynamicProperties }), "application/json"); - - - // _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, deviceEndpoint + "/tags") - // .ReturnsResponse(JsonSerializer.Serialize(device.Tags), "application/json"); - - // ConfigureToServeEndpointsTagGroupsRecursive(deviceEndpoint, device.TagGroups ?? []); - // } - // } - // } - //} - - //private void ConfigureToServeEndpointsTagGroupsRecursive(string endpoint, IEnumerable tagGroups) - //{ - // var tagGroupEndpoint = endpoint + "/tag_groups"; - - // _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, tagGroupEndpoint) - // .ReturnsResponse(JsonSerializer.Serialize(tagGroups), "application/json"); - - // foreach (var tagGroup in tagGroups) - // { - // _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, string.Concat(tagGroupEndpoint, "/", tagGroup.Name, "/tags")) - // .ReturnsResponse(JsonSerializer.Serialize(tagGroup.Tags), "application/json"); - - // ConfigureToServeEndpointsTagGroupsRecursive(string.Concat(tagGroupEndpoint, "/", tagGroup.Name), tagGroup.TagGroups ?? []); - // } - //} - [Theory] [InlineData("KEPServerEX", "12", 6, 17, true)] [InlineData("KEPServerEX", "12", 6, 16, false)] @@ -88,6 +31,12 @@ public class ProjectLoadTests : TestApiClientBase public async Task LoadProject_ShouldLoadCorrectly_BasedOnProductSupport( string productName, string productId, int majorVersion, int minorVersion, bool supportsJsonLoad) { + // This test will validate that the LoadProjectAsync method correctly loads the project structure and + // content based on whether the connected server version supports JsonProjectLoad. It will compare the loaded project against expected test data to ensure accuracy. + // For servers that support JsonProjectLoad, the test will configure the mock server to serve a full JSON project + // and validate that the loaded project matches the test data exactly. + + // Arrange ConfigureConnectedClient(productName, productId, majorVersion, minorVersion); if (supportsJsonLoad) @@ -99,8 +48,10 @@ public async Task LoadProject_ShouldLoadCorrectly_BasedOnProductSupport( await ConfigureToServeEndpoints(); } + // Act var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: true); + // Assert project.IsLoadedByProjectLoadService.ShouldBe(supportsJsonLoad); project.ShouldNotBeNull(); @@ -149,6 +100,11 @@ public async Task LoadProject_ShouldLoadCorrectly_BasedOnProductSupport( public async Task LoadProject_ShouldLoadCorrectly_Serialize_BasedOnProductSupport( string productName, string productId, int majorVersion, int minorVersion, bool supportsJsonLoad) { + // This test will validate that the LoadProjectAsync method correctly loads the project structure using the optimized recursion method. + // It will compare the loaded project against expected test data to ensure accuracy. The test will configure the mock server to serve + // endpoints to support an optimized recursion load and validate that the loaded project matches the test data exactly. + + // Arrange ConfigureConnectedClient(productName, productId, majorVersion, minorVersion); if (supportsJsonLoad) @@ -161,10 +117,16 @@ public async Task LoadProject_ShouldLoadCorrectly_Serialize_BasedOnProductSuppor throw SkipException.ForSkip($"Product {productName} v{majorVersion}.{minorVersion} (id={productId}) does not support JSON project load. Skipping full-project test case."); } - var tagLimitOverride = 100; // Set a high tag limit to ensure all tags are loaded for comparison + // Override the tag limit to ensure that we are testing the optimized recursion and selectively load objects based on the tag limit. + // See _data/simdemo_en.json and json chunks in _data/projectLoadSerializeData for data that is served by the mock server for this test. + var tagLimitOverride = 100; + + // Act var project = await _kepwareApiClient.Project.LoadProjectAsync(blnLoadFullProject: true, projectLoadTagLimit: tagLimitOverride); + + // Assert // Optimized recursion is done for this test, which will result in false. project.IsLoadedByProjectLoadService.ShouldBeFalse(); @@ -204,6 +166,12 @@ public async Task LoadProject_ShouldLoadCorrectly_Serialize_BasedOnProductSuppor CompareTagGroupsRecursive(ExpectedDevice.TagGroups, LoadedDevice.TagGroups, ExpectedDevice.Name); } } + + // Verify expected number of calls to the project load endpoints to ensure that the optimized recursion is selectively loading objects based on the tag limit. + foreach (var uri in _optimizedRecursionUris) + { + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Get, uri); + } } private static void CompareTagGroupsRecursive(DeviceTagGroupCollection? expected, DeviceTagGroupCollection? actual, string parentName) diff --git a/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs b/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs index 725a6b1..7e3e006 100644 --- a/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs +++ b/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs @@ -28,6 +28,11 @@ public abstract class TestApiClientBase protected readonly Mock _loggerFactoryMock; protected readonly KepwareApiClient _kepwareApiClient; + /// + /// URIs used to optimize recursion endpoints for tests. Populated via . + /// + protected readonly List _optimizedRecursionUris = new List(); + protected TestApiClientBase() { _httpMessageHandlerMock = new Mock(); @@ -156,6 +161,7 @@ protected async Task ConfigureToServeEndpoints(string filePath = "_data/simdemo_ "Channel1" => 2, "Simulation Examples" => 24, "Data Type Examples" => 216, + "OptRecursionTest" => 318, _ => 0 }; ch.SetDynamicProperty(Properties.Channel.StaticTagCount, staticCount); @@ -187,6 +193,7 @@ protected async Task ConfigureToServeEndpoints(string filePath = "_data/simdemo_ "Functions" => 24, "16 Bit Device" => 98, "8 Bit Device" => 118, + "RecursionTestDevice" => 318, _ => 0 }; dev.SetDynamicProperty(Properties.Device.StaticTagCount, staticCount); @@ -198,6 +205,8 @@ protected async Task ConfigureToServeEndpoints(string filePath = "_data/simdemo_ foreach (var device in channel.Devices) { var deviceEndpoint = TEST_ENDPOINT + $"/config/v1/project/channels/{channel.Name}/devices/{device.Name}"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, deviceEndpoint) .ReturnsResponse(JsonSerializer.Serialize(new Device { Name = device.Name, Description = device.Description, DynamicProperties = device.DynamicProperties }), "application/json"); @@ -215,25 +224,114 @@ protected async Task ConfigureToServeEndpoints(string filePath = "_data/simdemo_ var channel1String = await File.ReadAllTextAsync("_data/projectLoadSerializeData/channel1.json"); var sixteenBitDeviceString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/dataTypeExamples.16BitDevice.json"); var simExamplesChannelString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/simulationExamples.json"); + var dte8BitBRegTagGroupString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/dte.8bitDevice.Breg.json"); + var dte8BitKRegTagGroupString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/dte.8bitDevice.Kreg.json"); + var dte8BitRRegTagGroupString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/dte.8bitDevice.Rreg.json"); + var dte8BitSRegTagGroupString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/dte.8bitDevice.Sreg.json"); + var optRecursionDeviceBRegTagGroupString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/opt.recursionDevice.Breg.json"); + var optRecursionDeviceKRegTagGroupString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/opt.recursionDevice.Kreg.json"); + var optRecursionDeviceRRegTagGroupString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/opt.recursionDevice.Rreg.json"); + var optRecursionDeviceSRegTagGroupString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/opt.recursionDevice.Sreg.json"); + var optRecursionDeviceRecursionTestLevel1String = await File.ReadAllTextAsync("_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level1.json"); + var optRecursionDeviceRecursionTestLevel2String = await File.ReadAllTextAsync("_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level2.json"); + var optRecursionDeviceRecursionTestLevel3String = await File.ReadAllTextAsync("_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level3.json"); + var optRecursionDeviceRecursionTestLevel4String = await File.ReadAllTextAsync("_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level4.json"); _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project") .ReturnsResponse(projectPropertiesString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project"); + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Channel1?content=serialize") .ReturnsResponse(channel1String, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/Channel1?content=serialize"); + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/16 Bit Device?content=serialize") .ReturnsResponse(sixteenBitDeviceString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/16 Bit Device?content=serialize"); + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Simulation Examples?content=serialize") .ReturnsResponse(simExamplesChannelString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/Simulation Examples?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/8 Bit Device/tag_groups/B Registers?content=serialize") + .ReturnsResponse(dte8BitBRegTagGroupString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/8 Bit Device/tag_groups/B Registers?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/8 Bit Device/tag_groups/K Registers?content=serialize") + .ReturnsResponse(dte8BitKRegTagGroupString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/8 Bit Device/tag_groups/K Registers?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/8 Bit Device/tag_groups/R Registers?content=serialize") + .ReturnsResponse(dte8BitRRegTagGroupString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/8 Bit Device/tag_groups/R Registers?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/8 Bit Device/tag_groups/S Registers?content=serialize") + .ReturnsResponse(dte8BitSRegTagGroupString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/Data Type Examples/devices/8 Bit Device/tag_groups/S Registers?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/B Registers?content=serialize") + .ReturnsResponse(optRecursionDeviceBRegTagGroupString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/B Registers?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/K Registers?content=serialize") + .ReturnsResponse(optRecursionDeviceKRegTagGroupString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/K Registers?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/R Registers?content=serialize") + .ReturnsResponse(optRecursionDeviceRRegTagGroupString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/R Registers?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/S Registers?content=serialize") + .ReturnsResponse(optRecursionDeviceSRegTagGroupString, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/S Registers?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/RecursionTest/tag_groups/Level1?content=serialize") + .ReturnsResponse(optRecursionDeviceRecursionTestLevel1String, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/RecursionTest/tag_groups/Level1?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/RecursionTest/tag_groups/Level2?content=serialize") + .ReturnsResponse(optRecursionDeviceRecursionTestLevel2String, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/RecursionTest/tag_groups/Level2?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/RecursionTest/tag_groups/Level3?content=serialize") + .ReturnsResponse(optRecursionDeviceRecursionTestLevel3String, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/RecursionTest/tag_groups/Level3?content=serialize"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/RecursionTest/tag_groups/Level4?content=serialize") + .ReturnsResponse(optRecursionDeviceRecursionTestLevel4String, "application/json"); + _optimizedRecursionUris.Add(TEST_ENDPOINT + "/config/v1/project/channels/OptRecursionTest/devices/RecursionTestDevice/tag_groups/RecursionTest/tag_groups/Level4?content=serialize"); } private void ConfigureToServeEndpointsTagGroupsRecursive(string endpoint, IEnumerable tagGroups) { var tagGroupEndpoint = endpoint + "/tag_groups"; + var updatedTagGroups = tagGroups + .Select(tg => + { + var tagGrp = tg; + int staticCount = tg.Name switch + { + "B Registers" => 5, + "K Registers" => 54, + "R Registers" => 54, + "S Registers" => 5, + "RecursionTest" => 220, + "Level1" => 88, + "Level2" => 44, + "Level3" => 44, + "Level4" => 44, + "Level1_1" => 44, + _ => 0 + }; + tagGrp.SetDynamicProperty(Properties.DeviceTagGroup.TotalTagCount, staticCount); + return tagGrp; + }).ToList() ?? new List(); + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, tagGroupEndpoint) - .ReturnsResponse(JsonSerializer.Serialize(tagGroups), "application/json"); + .ReturnsResponse(JsonSerializer.Serialize(updatedTagGroups), "application/json"); - foreach (var tagGroup in tagGroups) + foreach (var tagGroup in updatedTagGroups) { _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, string.Concat(tagGroupEndpoint, "/", tagGroup.Name, "/tags")) .ReturnsResponse(JsonSerializer.Serialize(tagGroup.Tags), "application/json"); diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Breg.json b/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Breg.json new file mode 100644 index 0000000..cf3dfee --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Breg.json @@ -0,0 +1,53 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "B Registers", + "common.ALLTYPES_DESCRIPTION": "Boolean registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0001", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0002", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0003", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0004", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "BooleanArray", + "common.ALLTYPES_DESCRIPTION": "Array of 4 Boolean Registers", + "servermain.TAG_ADDRESS": "B0010 [4]", + "servermain.TAG_DATA_TYPE": 21, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Kreg.json b/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Kreg.json new file mode 100644 index 0000000..0a35190 --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Kreg.json @@ -0,0 +1,494 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "K Registers", + "common.ALLTYPES_DESCRIPTION": "Constant Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Byte1", + "common.ALLTYPES_DESCRIPTION": "8-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0200", + "servermain.TAG_DATA_TYPE": 3, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Byte2", + "common.ALLTYPES_DESCRIPTION": "8-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0201", + "servermain.TAG_DATA_TYPE": 3, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Byte3", + "common.ALLTYPES_DESCRIPTION": "8-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0202", + "servermain.TAG_DATA_TYPE": 3, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Byte4", + "common.ALLTYPES_DESCRIPTION": "8-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0203", + "servermain.TAG_DATA_TYPE": 3, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ByteArray", + "common.ALLTYPES_DESCRIPTION": "8-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0220 [4]", + "servermain.TAG_DATA_TYPE": 23, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Char1", + "common.ALLTYPES_DESCRIPTION": "8-Bit signed integer", + "servermain.TAG_ADDRESS": "K0300", + "servermain.TAG_DATA_TYPE": 2, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Char2", + "common.ALLTYPES_DESCRIPTION": "8-Bit signed integer", + "servermain.TAG_ADDRESS": "K0301", + "servermain.TAG_DATA_TYPE": 2, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Char3", + "common.ALLTYPES_DESCRIPTION": "8-Bit signed integer", + "servermain.TAG_ADDRESS": "K0302", + "servermain.TAG_DATA_TYPE": 2, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Char4", + "common.ALLTYPES_DESCRIPTION": "8-Bit signed integer", + "servermain.TAG_ADDRESS": "K0303", + "servermain.TAG_DATA_TYPE": 2, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "CharArray", + "common.ALLTYPES_DESCRIPTION": "8-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0330 [4]", + "servermain.TAG_DATA_TYPE": 22, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0416", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0424", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0508", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0512", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0608", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0612", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1216", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1224", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0708", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0712", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1116", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1124", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0804", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0806", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0904", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0906", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Rreg.json b/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Rreg.json new file mode 100644 index 0000000..f7dbe82 --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Rreg.json @@ -0,0 +1,494 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "R Registers", + "common.ALLTYPES_DESCRIPTION": "Ramping Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Byte1", + "common.ALLTYPES_DESCRIPTION": "8-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0200", + "servermain.TAG_DATA_TYPE": 3, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Byte2", + "common.ALLTYPES_DESCRIPTION": "8-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0201", + "servermain.TAG_DATA_TYPE": 3, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Byte3", + "common.ALLTYPES_DESCRIPTION": "8-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0202", + "servermain.TAG_DATA_TYPE": 3, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Byte4", + "common.ALLTYPES_DESCRIPTION": "8-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0203", + "servermain.TAG_DATA_TYPE": 3, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ByteArray", + "common.ALLTYPES_DESCRIPTION": "8-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "R0220 [4]", + "servermain.TAG_DATA_TYPE": 23, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Char1", + "common.ALLTYPES_DESCRIPTION": "8-Bit signed integer", + "servermain.TAG_ADDRESS": "R0300", + "servermain.TAG_DATA_TYPE": 2, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Char2", + "common.ALLTYPES_DESCRIPTION": "8-Bit signed integer", + "servermain.TAG_ADDRESS": "R0301", + "servermain.TAG_DATA_TYPE": 2, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Char3", + "common.ALLTYPES_DESCRIPTION": "8-Bit signed integer", + "servermain.TAG_ADDRESS": "R0302", + "servermain.TAG_DATA_TYPE": 2, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Char4", + "common.ALLTYPES_DESCRIPTION": "8-Bit signed integer", + "servermain.TAG_ADDRESS": "R0303", + "servermain.TAG_DATA_TYPE": 2, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "CharArray", + "common.ALLTYPES_DESCRIPTION": "8-Bit signed integer array", + "servermain.TAG_ADDRESS": "R0330 [4]", + "servermain.TAG_DATA_TYPE": 22, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0416", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0424", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "R0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0508", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0512", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "R0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0608", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0612", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "R0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1216", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1224", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "R1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0708", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0712", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "R0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1116", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1124", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "R1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0804", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0806", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "R0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0904", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0906", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "R0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Sreg.json b/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Sreg.json new file mode 100644 index 0000000..718fa10 --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/dte.8bitDevice.Sreg.json @@ -0,0 +1,53 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "S Registers", + "common.ALLTYPES_DESCRIPTION": "String Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "String1", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0001", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String2", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0002", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String3", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0003", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String4", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0004", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "StringArray[4]", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string array", + "servermain.TAG_ADDRESS": "S0010 [4]", + "servermain.TAG_DATA_TYPE": 20, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Breg.json b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Breg.json new file mode 100644 index 0000000..cf3dfee --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Breg.json @@ -0,0 +1,53 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "B Registers", + "common.ALLTYPES_DESCRIPTION": "Boolean registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0001", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0002", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0003", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0004", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "BooleanArray", + "common.ALLTYPES_DESCRIPTION": "Array of 4 Boolean Registers", + "servermain.TAG_ADDRESS": "B0010 [4]", + "servermain.TAG_DATA_TYPE": 21, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Kreg.json b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Kreg.json new file mode 100644 index 0000000..939be19 --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Kreg.json @@ -0,0 +1,404 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "K Registers", + "common.ALLTYPES_DESCRIPTION": "Constant Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level1.json b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level1.json new file mode 100644 index 0000000..e708b5f --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level1.json @@ -0,0 +1,806 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "Level1", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ], + "tag_groups": [ + { + "common.ALLTYPES_NAME": "Level1_1", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level2.json b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level2.json new file mode 100644 index 0000000..71954e4 --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level2.json @@ -0,0 +1,403 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "Level2", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level3.json b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level3.json new file mode 100644 index 0000000..8b86cb6 --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level3.json @@ -0,0 +1,403 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "Level3", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level4.json b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level4.json new file mode 100644 index 0000000..d7688fe --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.RecursionTest.Level4.json @@ -0,0 +1,403 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "Level4", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Rreg.json b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Rreg.json new file mode 100644 index 0000000..f4043f2 --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Rreg.json @@ -0,0 +1,404 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "R Registers", + "common.ALLTYPES_DESCRIPTION": "Ramping Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "R0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "R0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "R0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "R1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "R0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "R1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "R0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "R0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Sreg.json b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Sreg.json new file mode 100644 index 0000000..718fa10 --- /dev/null +++ b/Kepware.Api.Test/_data/projectLoadSerializeData/opt.recursionDevice.Sreg.json @@ -0,0 +1,53 @@ +{ + "tag_groups": { + "common.ALLTYPES_NAME": "S Registers", + "common.ALLTYPES_DESCRIPTION": "String Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "String1", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0001", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String2", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0002", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String3", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0003", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String4", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0004", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "StringArray[4]", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string array", + "servermain.TAG_ADDRESS": "S0010 [4]", + "servermain.TAG_DATA_TYPE": 20, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } +} \ No newline at end of file diff --git a/Kepware.Api.Test/_data/simdemo_en-us.json b/Kepware.Api.Test/_data/simdemo_en-us.json index e93f6ce..a1069e4 100644 --- a/Kepware.Api.Test/_data/simdemo_en-us.json +++ b/Kepware.Api.Test/_data/simdemo_en-us.json @@ -2,6 +2,8 @@ "project": { "common.ALLTYPES_DESCRIPTION": "Example project utilizing Simulator Driver.", "servermain.PROJECT_TITLE": "Simulation Driver Demo", + "servermain.CREATE_USER": "", + "servermain.CREATE_TIME": 1773769017, "channels": [ { "common.ALLTYPES_NAME": "Channel1", @@ -2095,6 +2097,2954 @@ } ] }, + { + "common.ALLTYPES_NAME": "OptRecursionTest", + "servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator", + "servermain.CHANNEL_DIAGNOSTICS_CAPTURE": false, + "servermain.CHANNEL_UNIQUE_ID": 2764480803, + "servermain.CHANNEL_WRITE_OPTIMIZATIONS_METHOD": 2, + "servermain.CHANNEL_WRITE_OPTIMIZATIONS_DUTY_CYCLE": 10, + "servermain.CHANNEL_NON_NORMALIZED_FLOATING_POINT_HANDLING": 0, + "simulator.CHANNEL_ITEM_PERSISTENCE": false, + "simulator.CHANNEL_ITEM_PERSISTENCE_DATA_FILE": "C:\\ProgramData\\PTC\\Kepware Server\\V7\\Simulator\\OptRecursionTest.dat", + "devices": [ + { + "common.ALLTYPES_NAME": "RecursionTestDevice", + "servermain.MULTIPLE_TYPES_DEVICE_DRIVER": "Simulator", + "servermain.DEVICE_MODEL": 0, + "servermain.DEVICE_UNIQUE_ID": 2797076252, + "servermain.DEVICE_ID_FORMAT": 1, + "servermain.DEVICE_ID_STRING": "1", + "servermain.DEVICE_ID_HEXADECIMAL": 1, + "servermain.DEVICE_ID_DECIMAL": 1, + "servermain.DEVICE_ID_OCTAL": 1, + "servermain.DEVICE_DATA_COLLECTION": true, + "servermain.DEVICE_SCAN_MODE": 0, + "servermain.DEVICE_SCAN_MODE_RATE_MS": 1000, + "servermain.DEVICE_SCAN_MODE_PROVIDE_INITIAL_UPDATES_FROM_CACHE": false, + "tag_groups": [ + { + "common.ALLTYPES_NAME": "B Registers", + "common.ALLTYPES_DESCRIPTION": "Boolean registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0001", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0002", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0003", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "Boolean register", + "servermain.TAG_ADDRESS": "B0004", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "BooleanArray", + "common.ALLTYPES_DESCRIPTION": "Array of 4 Boolean Registers", + "servermain.TAG_ADDRESS": "B0010 [4]", + "servermain.TAG_DATA_TYPE": 21, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + }, + { + "common.ALLTYPES_NAME": "K Registers", + "common.ALLTYPES_DESCRIPTION": "Constant Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + }, + { + "common.ALLTYPES_NAME": "R Registers", + "common.ALLTYPES_DESCRIPTION": "Ramping Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "R0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "R0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "R0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "R0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "R0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "R1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "R1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "R0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "R0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "R1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "R1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "R0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "R0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "R0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "R0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + }, + { + "common.ALLTYPES_NAME": "RecursionTest", + "tag_groups": [ + { + "common.ALLTYPES_NAME": "Level1", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ], + "tag_groups": [ + { + "common.ALLTYPES_NAME": "Level1_1", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } + ] + }, + { + "common.ALLTYPES_NAME": "Level2", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + }, + { + "common.ALLTYPES_NAME": "Level3", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + }, + { + "common.ALLTYPES_NAME": "Level4", + "tags": [ + { + "common.ALLTYPES_NAME": "Boolean1", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.00", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean2", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.01", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean3", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.02", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Boolean4", + "common.ALLTYPES_DESCRIPTION": "1-Bit Boolean", + "servermain.TAG_ADDRESS": "K0100.03", + "servermain.TAG_DATA_TYPE": 1, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double1", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0400", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double2", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0404", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double3", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0408", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Double4", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0412", + "servermain.TAG_DATA_TYPE": 9, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DoubleArray", + "common.ALLTYPES_DESCRIPTION": "64-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0440 [4]", + "servermain.TAG_DATA_TYPE": 29, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord1", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0500", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord2", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0502", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord3", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0504", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWord4", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0506", + "servermain.TAG_DATA_TYPE": 7, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "DWordArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0550 [4]", + "servermain.TAG_DATA_TYPE": 27, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float1", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0600", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float2", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0602", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float3", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0604", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Float4", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point", + "servermain.TAG_ADDRESS": "K0606", + "servermain.TAG_DATA_TYPE": 8, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "FloatArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit IEEE floating point array", + "servermain.TAG_ADDRESS": "K0660 [4]", + "servermain.TAG_DATA_TYPE": 28, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong1", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1200", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong2", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1204", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong3", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1208", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLong4", + "common.ALLTYPES_DESCRIPTION": "64 bit signed integer", + "servermain.TAG_ADDRESS": "K1212", + "servermain.TAG_DATA_TYPE": 13, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LLongArray", + "common.ALLTYPES_DESCRIPTION": "64-bit signed integer array", + "servermain.TAG_ADDRESS": "K1240 [4]", + "servermain.TAG_DATA_TYPE": 33, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long1", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0700", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long2", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0702", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long3", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0704", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Long4", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer", + "servermain.TAG_ADDRESS": "K0706", + "servermain.TAG_DATA_TYPE": 6, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "LongArray", + "common.ALLTYPES_DESCRIPTION": "32-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0770 [4]", + "servermain.TAG_DATA_TYPE": 26, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord1", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1100", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord2", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1104", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord3", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1108", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWord4", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned integer", + "servermain.TAG_ADDRESS": "K1112", + "servermain.TAG_DATA_TYPE": 14, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "QWordArray", + "common.ALLTYPES_DESCRIPTION": "64-bit unsigned Integer array", + "servermain.TAG_ADDRESS": "K1140 [4]", + "servermain.TAG_DATA_TYPE": 34, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short1", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0800", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short2", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0801", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short3", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0802", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Short4", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer", + "servermain.TAG_ADDRESS": "K0803", + "servermain.TAG_DATA_TYPE": 4, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "ShortArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit signed integer array", + "servermain.TAG_ADDRESS": "K0880 [4]", + "servermain.TAG_DATA_TYPE": 24, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word1", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0900", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word2", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0901", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word3", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0902", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "Word4", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer", + "servermain.TAG_ADDRESS": "K0903", + "servermain.TAG_DATA_TYPE": 5, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "WordArray", + "common.ALLTYPES_DESCRIPTION": "16-Bit unsigned integer array", + "servermain.TAG_ADDRESS": "K0990 [4]", + "servermain.TAG_DATA_TYPE": 25, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } + ] + }, + { + "common.ALLTYPES_NAME": "S Registers", + "common.ALLTYPES_DESCRIPTION": "String Registers", + "tags": [ + { + "common.ALLTYPES_NAME": "String1", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0001", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String2", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0002", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String3", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0003", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "String4", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string", + "servermain.TAG_ADDRESS": "S0004", + "servermain.TAG_DATA_TYPE": 0, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + }, + { + "common.ALLTYPES_NAME": "StringArray[4]", + "common.ALLTYPES_DESCRIPTION": "Null terminated Unicode string array", + "servermain.TAG_ADDRESS": "S0010 [4]", + "servermain.TAG_DATA_TYPE": 20, + "servermain.TAG_READ_WRITE_ACCESS": 1, + "servermain.TAG_SCAN_RATE_MILLISECONDS": 100, + "servermain.TAG_SCALING_TYPE": 0 + } + ] + } + ] + } + ] + }, { "common.ALLTYPES_NAME": "Simulation Examples", "common.ALLTYPES_DESCRIPTION": "Example Simulator Channel", @@ -2361,6 +5311,21 @@ "aeserverinterface.MIN_SUBSCRIPTION_BUFFER_TIME_MS": 1000, "aeserverinterface.MIN_KEEP_ALIVE_TIME_MS": 1000 }, + { + "common.ALLTYPES_NAME": "ddeserver", + "ddeserver.ENABLE": false, + "ddeserver.SERVICE_NAME": "ptcdde", + "ddeserver.ADVANCED_DDE": true, + "ddeserver.XLTABLE": true, + "ddeserver.CF_TEXT": true, + "ddeserver.CLIENT_UPDATE_INTERVAL_MS": 100, + "ddeserver.REQUEST_TIMEOUT_SEC": 15 + }, + { + "common.ALLTYPES_NAME": "hdaserver", + "hdaserver.ENABLE": false, + "hdaserver.ENABLE_DIAGNOSTICS": false + }, { "common.ALLTYPES_NAME": "opcdaserver", "opcdaserver.PROJECT_OPC_DA_1_ENABLED": true, @@ -2394,13 +5359,13 @@ "thingworxinterface.TRUST_ALL_CERTIFICATES": false, "thingworxinterface.DISABLE_ENCRYPTION": false, "thingworxinterface.MAX_THING_COUNT": 500, - "thingworxinterface.THING_NAME": "KEPServerEX", + "thingworxinterface.THING_NAME": "Kepware Server", "thingworxinterface.PUBLISH_FLOOR_MSEC": 1000, "thingworxinterface.LOGGING_ENABLED": false, "thingworxinterface.LOG_LEVEL": 3, "thingworxinterface.VERBOSE": false, "thingworxinterface.STORE_AND_FORWARD_ENABLED": false, - "thingworxinterface.STORAGE_PATH": "C:\\ProgramData\\Kepware\\KEPServerEX\\V6\\", + "thingworxinterface.STORAGE_PATH": "C:\\ProgramData\\PTC\\Kepware Server\\V7", "thingworxinterface.DATASTORE_MAXSIZE": 2048, "thingworxinterface.FORWARD_MODE": 0, "thingworxinterface.DATASTORE_ID": 421728385, @@ -2426,6 +5391,59 @@ "uaserverinterface.PROJECT_OPC_UA_MAX_DATA_QUEUE_SIZE": 2, "uaserverinterface.PROJECT_OPC_UA_MAX_RETRANSMIT_QUEUE_SIZE": 10, "uaserverinterface.PROJECT_OPC_UA_MAX_NOTIFICATION_PER_PUBLISH": 65536 + }, + { + "common.ALLTYPES_NAME": "wwtoolkitinterface", + "wwtoolkitinterface.ENABLED": false, + "wwtoolkitinterface.SERVICE_NAME": "server_runtime", + "wwtoolkitinterface.CLIENT_UPDATE_INTERVAL_MS": 100 + } + ], + "_ua_gateway": [ + { + "common.ALLTYPES_NAME": "_UA_Gateway", + "common.ALLTYPES_DESCRIPTION": "The parent interface of the OPC UA Gateway. Configuring this interface and managing the Instance Certificate view will allow communication between the UA Gateway service, its children interfaces, and the rest of Kepware+.", + "ua_client_interfaces": [ + { + "common.ALLTYPES_NAME": "Client Interface", + "common.ALLTYPES_DESCRIPTION": "The client interface of the OPC UA Gateway. Configuring this interface and adding client connections will allow communication between the OPC UA Gateway and other OPC UA servers.", + "client_instance_certificates": [ + { + "common.ALLTYPES_NAME": "Client Instance Certificate", + "common.ALLTYPES_DESCRIPTION": "The Client Interface Instance Certificate.", + "ua_gateway.UA_DISTINGUISHED_NAMES": "CN = PTC OPC UA Gateway Client Interface\n" + } + ] + } + ], + "ua_server_interfaces": [ + { + "common.ALLTYPES_NAME": "Server Interface", + "common.ALLTYPES_DESCRIPTION": "The server interface of the OPC UA Gateway. Configuring this interface and adding server endpoints will allow other OPC UA clients to connect to the OPC UA Gateway.", + "ua_gateway.UA_SERVER_INTERFACE_USER_IDENTITY_POLICY_ANONYMOUS": false, + "ua_gateway.UA_SERVER_INTERFACE_USER_IDENTITY_POLICY_USERNAME_PASSWORD": true, + "ua_gateway.UA_SERVER_INTERFACE_USER_IDENTITY_POLICY_X509": true, + "ua_gateway.UA_SERVER_INTERFACE_SECURITY_POLICIES_NONE": false, + "ua_gateway.UA_SERVER_INTERFACE_SECURITY_POLICIES_BASIC256SHA256": 2, + "ua_gateway.UA_SERVER_INTERFACE_SECURITY_POLICIES_AES128_SHA256_RSAOAEP": 0, + "ua_gateway.UA_SERVER_INTERFACE_SECURITY_POLICIES_AES256_SHA256_RSAPSS": 0, + "ua_gateway.LDS_REGISTRATION_ENABLED": false, + "ua_gateway.LDS_MAX_REGISTRATION_INTERVAL": 30000, + "ua_gateway.UA_SERVER_INTERFACE_MAX_SUBSCRIPTION_LIFETIME": 3600000, + "ua_gateway.UA_SERVER_INTERFACE_MIN_SUBSCRIPTION_LIFETIME": 10000, + "ua_gateway.UA_SERVER_INTERFACE_MAX_SESSION_TIMEOUT": 3600000, + "ua_gateway.UA_SERVER_INTERFACE_MIN_SESSION_TIMEOUT": 10000, + "ua_gateway.UA_SERVER_INTERFACE_MAX_NOTIFICATIONS_PER_PUBLISH": 1000, + "ua_gateway.UA_SERVER_INTERFACE_MAX_NOTIFICATIONS_QUEUE_SIZE": 100, + "server_instance_certificates": [ + { + "common.ALLTYPES_NAME": "Server Instance Certificate", + "common.ALLTYPES_DESCRIPTION": "The Server Interface Instance Certificate.", + "ua_gateway.UA_DISTINGUISHED_NAMES": "CN = PTC OPC UA Gateway Server Interface\n" + } + ] + } + ] } ] } diff --git a/Kepware.Api/ClientHandler/ProjectApiHandler.cs b/Kepware.Api/ClientHandler/ProjectApiHandler.cs index c0d41c7..32b9d34 100644 --- a/Kepware.Api/ClientHandler/ProjectApiHandler.cs +++ b/Kepware.Api/ClientHandler/ProjectApiHandler.cs @@ -270,8 +270,9 @@ public async Task LoadProjectAsync(bool blnLoadFullProject = false, int if (productInfo?.SupportsJsonProjectLoadService == true) { - // Set by as the threshold to use content=serialize based loading or full recursive loading. Default - // set by class. + // Check to see if projectLoadTagLimit parameter is set on call. If not or an invalid value, use value ] + // set by as the threshold to use content=serialize + // based loading or full recursive loading. if (projectLoadTagLimit <= 0) projectLoadTagLimit = m_kepwareApiClient.ClientOptions.ProjectLoadTagLimit; @@ -373,6 +374,10 @@ private async Task LoadProjectOptimizedRecurisveAsync(Project project, project.Channels = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(cancellationToken: cancellationToken); if (project.Channels != null) { + var query = new[] + { + new KeyValuePair("content", "serialize") + }; int totalChannelCount = project.Channels.Count; int loadedChannelCount = 0; @@ -386,10 +391,6 @@ private async Task LoadProjectOptimizedRecurisveAsync(Project project, { if (channel.GetDynamicProperty(Properties.Channel.StaticTagCount) < tagLimit) { - var query = new[] - { - new KeyValuePair("content", "serialize") - }; var loadedChannel = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(channel.Name, query, cancellationToken: cancellationToken).ConfigureAwait(false); if (loadedChannel != null) { @@ -417,10 +418,6 @@ private async Task LoadProjectOptimizedRecurisveAsync(Project project, { if (device.GetDynamicProperty(Properties.Device.StaticTagCount) < tagLimit) { - var query = new[] - { - new KeyValuePair("content", "serialize") - }; var loadedDevice = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(device.Name, channel, query, cancellationToken: cancellationToken).ConfigureAwait(false); if (loadedDevice != null) { @@ -440,7 +437,7 @@ private async Task LoadProjectOptimizedRecurisveAsync(Project project, if (device.TagGroups != null) { - await LoadTagGroupsRecursiveAsync(m_kepwareApiClient, device.TagGroups, cancellationToken: cancellationToken).ConfigureAwait(false); + await LoadTagGroupsRecursiveAsync(m_kepwareApiClient, device.TagGroups, optimizedRecursion: true, tagLimit: tagLimit, cancellationToken: cancellationToken).ConfigureAwait(false); } } })); @@ -596,20 +593,48 @@ private static void SetOwnerRecursive(IEnumerable tagGroups, Nam /// /// The API client. /// The tag groups to load. + /// A flag indicating whether to use optimized recursion with content=serialize or basic recursion. + /// This is only applicable for projects loaded with the full project load option and when the JsonProjectLoad service is + /// supported by the server. + /// Tag Limit if overridden by method call. /// The cancellation token. /// A task that represents the asynchronous operation. - internal static async Task LoadTagGroupsRecursiveAsync(KepwareApiClient apiClient, IEnumerable tagGroups, CancellationToken cancellationToken = default) + internal static async Task LoadTagGroupsRecursiveAsync(KepwareApiClient apiClient, IEnumerable tagGroups, bool optimizedRecursion = false, int tagLimit = 0, CancellationToken cancellationToken = default) { + // Falls back to the value set by if tagLimit parameter is not set or an invalid value is provided. + if (tagLimit <= 0) + tagLimit = apiClient.ClientOptions.ProjectLoadTagLimit; foreach (var tagGroup in tagGroups) { // Load the Tag Groups and Tags of the current Tag Group - tagGroup.TagGroups = await apiClient.GenericConfig.LoadCollectionAsync(tagGroup, cancellationToken: cancellationToken).ConfigureAwait(false); - tagGroup.Tags = await apiClient.GenericConfig.LoadCollectionAsync(tagGroup, cancellationToken: cancellationToken).ConfigureAwait(false); - - // Recursively load the Tag Groups and Tags of the child Tag Groups - if (tagGroup.TagGroups != null && tagGroup.TagGroups.Count > 0) + if (optimizedRecursion && tagGroup.TotalTagCount < tagLimit) { - await LoadTagGroupsRecursiveAsync(apiClient, tagGroup.TagGroups, cancellationToken).ConfigureAwait(false); + var query = new[] + { + new KeyValuePair("content", "serialize") + }; + var loadedTagGroup = await apiClient.GenericConfig.LoadEntityAsync(tagGroup.Name, tagGroup.Owner!, query, cancellationToken: cancellationToken).ConfigureAwait(false); + if (loadedTagGroup != null) + { + tagGroup.Tags = loadedTagGroup.Tags; + tagGroup.TagGroups = loadedTagGroup.TagGroups; + } + else + { + // Failed to load tag group, log warning and end without loading child tag groups. + apiClient.Logger.LogWarning("Failed to load {TagGroupName} in {OwnerName}", tagGroup.Name, tagGroup.Owner?.Name); + continue; + } + } + else + { + tagGroup.TagGroups = await apiClient.GenericConfig.LoadCollectionAsync(tagGroup, cancellationToken: cancellationToken).ConfigureAwait(false); + tagGroup.Tags = await apiClient.GenericConfig.LoadCollectionAsync(tagGroup, cancellationToken: cancellationToken).ConfigureAwait(false); + // Recursively load the Tag Groups and Tags of the child Tag Groups + if (tagGroup.TagGroups != null && tagGroup.TagGroups.Count > 0) + { + await LoadTagGroupsRecursiveAsync(apiClient, tagGroup.TagGroups, optimizedRecursion, tagLimit, cancellationToken).ConfigureAwait(false); + } } } } diff --git a/Kepware.Api/KepwareApiClient.cs b/Kepware.Api/KepwareApiClient.cs index fe59e48..f37018a 100644 --- a/Kepware.Api/KepwareApiClient.cs +++ b/Kepware.Api/KepwareApiClient.cs @@ -43,6 +43,14 @@ public partial class KepwareApiClient : IKepwareDefaultValueProvider private bool? m_hasValidCredentials = null; private ProductInfo? m_productInfo = null; + /// + /// Gets the logger instance used for logging operations. + /// + /// This property provides access to the logger, which can be used to log messages at + /// various levels. Ensure that the logger is properly initialized before use. + public ILogger Logger => m_logger; + + /// /// Gets the name of the client instance. /// diff --git a/Kepware.Api/Model/Project/DeviceTagGroup.Properties.cs b/Kepware.Api/Model/Project/DeviceTagGroup.Properties.cs new file mode 100644 index 0000000..0181cb9 --- /dev/null +++ b/Kepware.Api/Model/Project/DeviceTagGroup.Properties.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Kepware.Api.Model +{ + public partial class Properties + { + public static class DeviceTagGroup + { + + /// + /// Represents the constant name used to identify the count of tags in the local tag group. + /// + public const string LocalTagCount = Properties.NonSerialized.TagGrpTagCount; + + /// + /// Represents the constant name used to identify the total count of tags in the tag group, including all child tag groups. + /// + public const string TotalTagCount = Properties.NonSerialized.TagGrpTotalTagCount; + } + } +} diff --git a/Kepware.Api/Model/Project/DeviceTagGroup.cs b/Kepware.Api/Model/Project/DeviceTagGroup.cs index a062935..e51eadc 100644 --- a/Kepware.Api/Model/Project/DeviceTagGroup.cs +++ b/Kepware.Api/Model/Project/DeviceTagGroup.cs @@ -48,12 +48,32 @@ public DeviceTagGroup(string name, DeviceTagGroup owner) public DeviceTagGroupCollection? TagGroups { get; set; } /// - /// Get a flag indicating if the tag group is autogenerated + /// Get a flag indicating if the tag group was autogenerated /// [YamlIgnore] [JsonIgnore] public bool IsAutogenerated => GetDynamicProperty(Properties.NonSerialized.TagGroupAutogenerated); + /// + /// Gets the number of tags that are defined locally within this tag group. + /// + /// Use this property to determine how many tags are associated with the current + /// instance, excluding tags in children tag groups. NOTE: Provides the count from the Kepware instance + /// and only updates when loading the tag group configuration. + [YamlIgnore] + [JsonIgnore] + public int LocalTagCount => GetDynamicProperty(Properties.DeviceTagGroup.LocalTagCount); + + /// + /// Gets the total number of tags included in the tag group and all children tag groups. + /// + /// Use this property to determine how many tags are associated with the current + /// instance, including tags in children tag groups. NOTE: Provides the count from the Kepware instance + /// and only updates when loading the tag group configuration. + [YamlIgnore] + [JsonIgnore] + public int TotalTagCount => GetDynamicProperty(Properties.DeviceTagGroup.TotalTagCount); + /// /// Recursively cleans up the tag group and all its children /// From 4579a071f777c32cba2dde1a65be53cae5204ae7 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Tue, 17 Mar 2026 18:40:11 -0400 Subject: [PATCH 12/38] refactor: various property and comment cleanup --- Kepware.Api/Model/Project/Channel.Properties.cs | 2 +- Kepware.Api/Model/Project/Device.Properties.cs | 6 +++--- Kepware.Api/Model/Project/Project.Properties.cs | 3 +-- Kepware.Api/Model/Properties.cs | 6 +++--- 4 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Kepware.Api/Model/Project/Channel.Properties.cs b/Kepware.Api/Model/Project/Channel.Properties.cs index 2b0e131..6f1a805 100644 --- a/Kepware.Api/Model/Project/Channel.Properties.cs +++ b/Kepware.Api/Model/Project/Channel.Properties.cs @@ -48,7 +48,7 @@ public static class Channel /// /// Value of the static tag count for the channel. /// - public const string StaticTagCount = "servermain.CHANNEL_STATIC_TAG_COUNT"; + public const string StaticTagCount = Properties.NonSerialized.ChannelStaticTagCount; } } diff --git a/Kepware.Api/Model/Project/Device.Properties.cs b/Kepware.Api/Model/Project/Device.Properties.cs index 006c64f..e4de536 100644 --- a/Kepware.Api/Model/Project/Device.Properties.cs +++ b/Kepware.Api/Model/Project/Device.Properties.cs @@ -11,14 +11,14 @@ public partial class Properties public static class Device { /// - /// The driver used by this channel. + /// The driver used by this device. /// public const string DeviceDriver = "servermain.MULTIPLE_TYPES_DEVICE_DRIVER"; /// - /// Value of the static tag count for the channel. + /// Constant value for the key of the static tag count for the device. /// - public const string StaticTagCount = "servermain.DEVICE_STATIC_TAG_COUNT"; + public const string StaticTagCount = Properties.NonSerialized.DeviceStaticTagCount; } } diff --git a/Kepware.Api/Model/Project/Project.Properties.cs b/Kepware.Api/Model/Project/Project.Properties.cs index 207fba2..af0fb24 100644 --- a/Kepware.Api/Model/Project/Project.Properties.cs +++ b/Kepware.Api/Model/Project/Project.Properties.cs @@ -20,8 +20,7 @@ public static class ProjectSettings /// /// Count of tags identified in the project. /// - // TODO: Does this need to be moved to non-seralized properties? - public const string TagsDefined = "servermain.PROJECT_TAGS_DEFINED"; + public const string TagsDefined = Properties.NonSerialized.ProjectTagsDefined; #endregion diff --git a/Kepware.Api/Model/Properties.cs b/Kepware.Api/Model/Properties.cs index c77aeb2..4aacaa4 100644 --- a/Kepware.Api/Model/Properties.cs +++ b/Kepware.Api/Model/Properties.cs @@ -71,13 +71,13 @@ public static class NonSerialized /// public const string TagGrpTotalTagCount = "servermain.TAGGROUP_TOTAL_TAG_COUNT"; /// - /// The local tag count in a tag group property key. + /// The local tag count in a tag group property key. Used for tag groups within devices. /// public const string TagGrpTagCount = "servermain.TAGGROUP_LOCAL_TAG_COUNT"; /// /// The static tag count in a channel property key. /// - public const string ChannelTagCount = "servermain.CHANNEL_STATIC_TAG_COUNT"; + public const string ChannelStaticTagCount = "servermain.CHANNEL_STATIC_TAG_COUNT"; /// /// The autogenerated tag group property key. /// @@ -104,7 +104,7 @@ public static class NonSerialized ChannelAssignment, TagGrpTotalTagCount, TagGrpTagCount, - ChannelTagCount, + ChannelStaticTagCount, TagGroupAutogenerated, TagAutogenerated , DeviceStaticTagCount, From 2c288723d5c2ce509e63c60c07dcabfb34ad823d Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Thu, 19 Mar 2026 20:53:23 -0400 Subject: [PATCH 13/38] doc: updated readme --- Kepware.Api/README.md | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/Kepware.Api/README.md b/Kepware.Api/README.md index 40625b1..b793350 100644 --- a/Kepware.Api/README.md +++ b/Kepware.Api/README.md @@ -19,15 +19,17 @@ This package is designed to work with all versions of Kepware that support the C | **Connectivity**
*(Channel, Devices, Tags, Tag Groups)* | Y | Y | | **Administration**
*(User Groups, Users, UA Endpoints, Local License Server)* | Y[^1] | Y | | **Product Info and Health Status** | Y[^4] | Y | -| **Export Project** | Y[^2] | Y | +| **Export Project** | Y | Y | | **Import Project (via JsonProjectLoad Service)** | N[^2] | N | | **Import Project (via CompareAndApply)[^3]** | Y | Y | [^1]: UA Endpoints and Local License Server supported for Kepware Edge only -[^2]: JsonProjectLoad was added to Kepware Server v6.17 and later builds, the SDK detects the server version and uses the appropriate service or loads the project by multiple requests if using KepwareApiClient.LoadProject. -[^3]: CompareAndApply is handled by the SDK, it compares the source project with the server project and applies the changes. The JsonProjectLoad service is a direct call to the server to load a project. +[^2]: JsonProjectLoad was added to Kepware Server v6.17 and later builds. +[^3]: [CompareAndApply](/Kepware.Api/ClientHandler/ProjectApiHandler.cs) is handled by the SDK. It compares the source project with another server project and applies the changes. [^4]: Added to Kepware Server v6.13 and later builds +**NOTE:** Exporting a project from a Kepware server is done using the KepwareApiClient.LoadProjectAsync method. This detects the server version and uses the appropriate method to either export/load the whole project or loads the project by multiple requests. This ensures that large projects can be exported/loaded from the Kepware instance as optimally as possible based on the current API design. See [LoadProjectAsync](/Kepware.Api/ClientHandler/ProjectApiHandler.cs) for more details. + 3. Configuration API *Services* implemented: | Services | KS | KE | @@ -37,9 +39,8 @@ This package is designed to work with all versions of Kepware that support the C | **ProjectLoad and ProjectSave** | N | N | | **JsonProjectLoad\*\***
*(used for import project feature)* | Y | Y | -4. Synchronize configurations between your application and Kepware server. -5. Supports advanced operations like project comparison, entity synchronization, and driver property queries. -6. Built-in support for Dependency Injection to simplify integration. +4. Supports advanced operations like project comparison, entity synchronization, and driver property queries. +5. Built-in support for Dependency Injection to simplify integration. ## Installation @@ -51,16 +52,36 @@ Kepware.Api NuGet package is available from NuGet repository. ``` 2. Register the `KepwareApiClient` in your application using Dependency Injection: + ```csharp services.AddKepwareApiClient( name: "default", baseUrl: "https://localhost:57512", apiUserName: "Administrator", apiPassword: "StrongAdminPassword2025!", - disableCertificateValidation: true + disableCertificateValidation: false ); ``` + or + + ```csharp + var clientOptions = new KepwareClientOptions + { + HostUri = new Uri("https://localhost:57512"), + Username = "Administrator", + Password = "StrongAdminPassword2025!", + Timeout = TimeSpan.FromSeconds(60), + DisableCertifcateValidation = false, + ProjectLoadTagLimit = 100000 + }; + + services.AddKepwareApiClient( + name: "default", + options: clientOptions + ); + ``` + ## Key Methods ### Connection and Status @@ -77,9 +98,9 @@ Kepware.Api NuGet package is available from NuGet repository. Retrieves product information about the Kepware server. ### Project Management -- **Load Project:** +- **Export / Load Project:** ```csharp - var project = await api.LoadProject(blnLoadFullProject:true); + var project = await api.LoadProjectAsync(blnLoadFullProject:true); ``` Loads the current project from the Kepware server. From 13a64f639eced93edee7aa23e6446009370acb2d Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Thu, 19 Mar 2026 21:16:42 -0400 Subject: [PATCH 14/38] chore: updated with new LoadProjectAsync method call name --- KepwareSync.Service/SyncService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/KepwareSync.Service/SyncService.cs b/KepwareSync.Service/SyncService.cs index a3811ac..a524220 100644 --- a/KepwareSync.Service/SyncService.cs +++ b/KepwareSync.Service/SyncService.cs @@ -122,7 +122,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) private static async Task FetchCurrentProjectIdAsync(KepwareApiClient client, CancellationToken cancellationToken) { - var project = await client.Project.LoadProject(false, cancellationToken).ConfigureAwait(false); + var project = await client.Project.LoadProjectAsync(false, cancellationToken: cancellationToken).ConfigureAwait(false); return project?.ProjectId ?? -1; } @@ -166,7 +166,7 @@ private async Task ProcessChangeAsync(ChangeEvent changeEvent, CancellationToken internal async Task SyncFromPrimaryKepServerAsync(CancellationToken cancellationToken = default) { m_logger.LogInformation("Synchronizing full project from primary Kepware..."); - var project = await m_kepServerClient.Project.LoadProject(true); + var project = await m_kepServerClient.Project.LoadProjectAsync(true); await project.Cleanup(m_kepServerClient, true, cancellationToken); if (m_kepServerClient.ClientOptions.Tag is KepwareSyncTarget targetOptions && @@ -239,7 +239,7 @@ private async Task SyncFromSecondaryKepServerAsync(CancellationToken cancellatio m_logger.LogInformation("Syncing from secondary client {ClientHostName}...", clientToSyncFrom.ClientHostName); } - var projectFromSecondary = await clientToSyncFrom.Project.LoadProject(true, cancellationToken).ConfigureAwait(false); + var projectFromSecondary = await clientToSyncFrom.Project.LoadProjectAsync(true, cancellationToken: cancellationToken).ConfigureAwait(false); await SyncProjectToKepServerAsync("secondary", projectFromSecondary, m_kepServerClient, "Primary", onSyncedWithChanges: () => NotifyChange(new ChangeEvent { Source = ChangeSource.PrimaryKepServer, Reason = "Sync from secondary kepserver" }), cancellationToken: cancellationToken).ConfigureAwait(false); From 779b1d601ff5c12b3945f987c24c2ce889e613fe Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Fri, 20 Mar 2026 10:20:59 -0400 Subject: [PATCH 15/38] fix(test): fixed file name for case sensitive linux environments --- Kepware.Api.Test/ApiClient/_TestApiClientBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs b/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs index 7e3e006..ffc2200 100644 --- a/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs +++ b/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs @@ -221,7 +221,7 @@ protected async Task ConfigureToServeEndpoints(string filePath = "_data/simdemo_ // Additional endpoints for content=serialize mocking var projectPropertiesString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/projectProperties.json"); - var channel1String = await File.ReadAllTextAsync("_data/projectLoadSerializeData/channel1.json"); + var channel1String = await File.ReadAllTextAsync("_data/projectLoadSerializeData/Channel1.json"); var sixteenBitDeviceString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/dataTypeExamples.16BitDevice.json"); var simExamplesChannelString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/simulationExamples.json"); var dte8BitBRegTagGroupString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/dte.8bitDevice.Breg.json"); From 3af3254c002b64eff20395c8b5b3bd2ce06e3e80 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Thu, 26 Mar 2026 22:16:23 -0400 Subject: [PATCH 16/38] feat(api): generic deleteitems returns array of bool results --- Kepware.Api.Test/ApiClient/DeleteTests.cs | 46 +++++++++++++++++-- .../ClientHandler/GenericApiHandler.cs | 29 ++++++++---- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/Kepware.Api.Test/ApiClient/DeleteTests.cs b/Kepware.Api.Test/ApiClient/DeleteTests.cs index e1226c5..69142bd 100644 --- a/Kepware.Api.Test/ApiClient/DeleteTests.cs +++ b/Kepware.Api.Test/ApiClient/DeleteTests.cs @@ -4,6 +4,7 @@ using Moq.Contrib.HttpClient; using Shouldly; using System.Net; +using System.Linq; namespace Kepware.Api.Test.ApiClient; @@ -302,7 +303,8 @@ public async Task Delete_MultipleItems_WhenSuccessful_ShouldDeleteAll() var result = await _kepwareApiClient.GenericConfig.DeleteItemsAsync(tags, owner: device); // Assert - result.ShouldBeTrue(); + result.Length.ShouldBe(tags.Count); + result.All(r => r).ShouldBeTrue(); foreach (var tag in tags) { var endpoint = $"/config/v1/project/channels/{channel.Name}/devices/{device.Name}/tags/{tag.Name}"; @@ -326,7 +328,8 @@ public async Task Delete_MultipleItems_WithHttpError_ShouldLogError() var result = await _kepwareApiClient.GenericConfig.DeleteItemsAsync(tags, owner: device); // Assert - result.ShouldBeFalse(); + result.Length.ShouldBe(tags.Count); + result[0].ShouldBeFalse(); _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}", Times.Once()); _loggerMockGeneric.Verify(logger => logger.Log( @@ -354,7 +357,8 @@ public async Task Delete_MultipleItems_WithConnectionError_ShouldHandleGracefull var result = await _kepwareApiClient.GenericConfig.DeleteItemsAsync(tags, owner: device); // Assert - result.ShouldBeFalse(); + result.Length.ShouldBe(tags.Count); + result[0].ShouldBeFalse(); _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}", Times.Once()); _loggerMockGeneric.Verify(logger => logger.Log( @@ -379,7 +383,41 @@ public async Task Delete_MultipleItems_WithEmptyList_ShouldNoop() var result = await _kepwareApiClient.GenericConfig.DeleteItemsAsync(tags, owner: device); // Assert - result.ShouldBeTrue(); + result.Length.ShouldBe(0); _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}", Times.Never()); } + + [Fact] + public async Task Delete_MultipleItems_WithMixedResults_ShouldReturnOrderedResults() + { + // Arrange + var channel = new Channel { Name = "TestChannel" }; + var device = new Device { Name = "ParentDevice", Owner = channel }; + var tags = CreateTestTags(); + var firstEndpoint = $"/config/v1/project/channels/{channel.Name}/devices/{device.Name}/tags/{tags[0].Name}"; + var secondEndpoint = $"/config/v1/project/channels/{channel.Name}/devices/{device.Name}/tags/{tags[1].Name}"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{firstEndpoint}") + .ReturnsResponse(HttpStatusCode.OK); + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{secondEndpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act + var result = await _kepwareApiClient.GenericConfig.DeleteItemsAsync(tags, owner: device); + + // Assert + result.Length.ShouldBe(tags.Count); + result[0].ShouldBeTrue(); + result[1].ShouldBeFalse(); + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{firstEndpoint}", Times.Once()); + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{secondEndpoint}", Times.Once()); + _loggerMockGeneric.Verify(logger => + logger.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => true), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } } diff --git a/Kepware.Api/ClientHandler/GenericApiHandler.cs b/Kepware.Api/ClientHandler/GenericApiHandler.cs index bf510c6..b20cd65 100644 --- a/Kepware.Api/ClientHandler/GenericApiHandler.cs +++ b/Kepware.Api/ClientHandler/GenericApiHandler.cs @@ -420,11 +420,11 @@ protected async Task DeleteItemByEndpointAsync(string endpoint, Cancell /// /// /// - /// - public Task DeleteItemAsync(K item, NamedEntity? owner = null, CancellationToken cancellationToken = default) + /// A task that represents the asynchronous operation. The task result contains a boolean indicating whether the delete was successful. + public async Task DeleteItemAsync(K item, NamedEntity? owner = null, CancellationToken cancellationToken = default) where T : EntityCollection where K : NamedEntity, new() - => DeleteItemsAsync([item], owner, cancellationToken); + => (await DeleteItemsAsync([item], owner, cancellationToken).ConfigureAwait(false)).FirstOrDefault(); /// /// Deletes a list of items from the Kepware server. @@ -434,14 +434,16 @@ public Task DeleteItemAsync(K item, NamedEntity? owner = null, Cance /// /// /// - /// - // TODO: determine return options for mixed results (e.g. some deletes succeed and some fail) - currently returns false if any delete fails, but could also return a list of results for each item - public async Task DeleteItemsAsync(List items, NamedEntity? owner = null, CancellationToken cancellationToken = default) + /// A task that represents the asynchronous operation. The task result contains an array of booleans indicating whether each delete was successful. + public async Task DeleteItemsAsync(List items, NamedEntity? owner = null, CancellationToken cancellationToken = default) where T : EntityCollection where K : NamedEntity, new() { if (items.Count == 0) - return true; + return []; + + List result = []; + try { var collectionEndpoint = EndpointResolver.ResolveEndpoint(owner).TrimEnd('/'); @@ -456,17 +458,24 @@ public async Task DeleteItemsAsync(List items, NamedEntity? owner { var message = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); m_logger.LogError("Failed to delete {TypeName} from {Endpoint}: {ReasonPhrase}\n{Message}", typeof(T).Name, endpoint, response.ReasonPhrase, message); - return false; + result.Add(false); + } + else + { + result.Add(true); } } - return true; } catch (HttpRequestException httpEx) { m_logger.LogWarning(httpEx, "Failed to connect to {BaseAddress}", m_httpClient.BaseAddress); m_kepwareApiClient.OnHttpRequestException(httpEx); + + if (items.Count > result.Count) + result.AddRange(Enumerable.Repeat(false, items.Count - result.Count)); } - return false; + + return [.. result]; } #endregion From 0468978d7e107699b07e14478f656059e219ceb0 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Fri, 27 Mar 2026 13:00:33 -0400 Subject: [PATCH 17/38] feat(api): Detailed response options for ProjectLoad and compareAndApply methods --- .../ApiClient/GenericHandleTests.cs | 94 +++++++ Kepware.Api.Test/ApiClient/InsertTests.cs | 8 +- Kepware.Api.TestIntg/ApiClient/InsertTests.cs | 7 +- .../ApiClient/ProjectPermissionTests.cs | 16 +- .../ClientHandler/GenericApiHandler.cs | 241 +++++++++++++++--- .../ClientHandler/ProjectApiHandler.cs | 175 ++++++++----- Kepware.Api/Model/ApiMessages.cs | 30 +++ Kepware.Api/Model/ApplyResults.cs | 195 ++++++++++++++ Kepware.Api/Serializer/KepJsonContext.cs | 1 + 9 files changed, 659 insertions(+), 108 deletions(-) create mode 100644 Kepware.Api/Model/ApplyResults.cs diff --git a/Kepware.Api.Test/ApiClient/GenericHandleTests.cs b/Kepware.Api.Test/ApiClient/GenericHandleTests.cs index ac0c0be..599d01a 100644 --- a/Kepware.Api.Test/ApiClient/GenericHandleTests.cs +++ b/Kepware.Api.Test/ApiClient/GenericHandleTests.cs @@ -1,19 +1,113 @@ using System; using System.Collections.Generic; +using System.Net; using System.Net.Http; using System.Reflection; +using System.Text.Json; using System.Threading.Tasks; using Kepware.Api.Model; using Kepware.Api.Test.ApiClient; using Microsoft.Extensions.Logging; using Moq; using Moq.Contrib.HttpClient; +using Shouldly; using Xunit; namespace Kepware.Api.Test.ApiClient { public class GenericHandler : TestApiClientBase { + [Fact] + public async Task CompareAndApplyDetailed_ShouldCountUpdateAsFailed_When200ContainsNotApplied() + { + // Arrange + var channel = new Channel { Name = "Channel1" }; + channel.SetDynamicProperty(Properties.Channel.DeviceDriver, "Simulator"); + + var sourceDevice = new Device { Name = "Device1", Description = "new description", Owner = channel }; + var targetDevice = new Device { Name = "Device1", Description = "old description", Owner = channel }; + + var sourceCollection = new DeviceCollection { sourceDevice }; + var targetCollection = new DeviceCollection { targetDevice }; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, TEST_ENDPOINT + "/config/v1/project/channels/Channel1/devices/Device1") + .ReturnsResponse(HttpStatusCode.OK, JsonSerializer.Serialize(targetDevice), "application/json"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Put, TEST_ENDPOINT + "/config/v1/project/channels/Channel1/devices/Device1") + .ReturnsResponse(HttpStatusCode.OK, + """ + { + "not_applied": { + "servermain.DEVICE_ID_OCTAL": 1, + "servermain.DEVICE_MODEL": 0 + }, + "code": 200, + "message": "Not all properties were applied." + } + """, + "application/json"); + + // Act + var result = await _kepwareApiClient.GenericConfig.CompareAndApplyDetailed(sourceCollection, targetCollection, channel); + + // Assert + result.Updates.ShouldBe(0); + result.Failed.ShouldBe(1); + result.Failures.Count.ShouldBe(1); + result.Failures[0].Operation.ShouldBe(ApplyOperation.Update); + (result.Failures[0].AttemptedItem as Device)?.Name.ShouldBe("Device1"); + result.Failures[0].NotAppliedProperties.ShouldNotBeNull(); + result.Failures[0].NotAppliedProperties!.ShouldContain("servermain.DEVICE_ID_OCTAL"); + result.Failures[0].NotAppliedProperties!.ShouldContain("servermain.DEVICE_MODEL"); + } + + [Fact] + public async Task CompareAndApplyDetailed_ShouldMap207InsertFeedbackToItems() + { + // Arrange + var channel = new Channel { Name = "Channel1" }; + channel.SetDynamicProperty(Properties.Channel.DeviceDriver, "Simulator"); + var ownerDevice = new Device { Name = "Device1", Owner = channel }; + + var tag1 = new Tag { Name = "Tag1", TagAddress = "RAMP" }; + var tag2 = new Tag { Name = "Tag2", TagAddress = "SINE" }; + + var sourceTags = new DeviceTagCollection { tag1, tag2 }; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, TEST_ENDPOINT + "/config/v1/project/channels/Channel1/devices/Device1/tags") + .ReturnsResponse((HttpStatusCode)207, + """ + [ + { + "property": "common.ALLTYPES_NAME", + "description": "The name 'Tag1' is already used.", + "error_line": 3, + "code": 400, + "message": "Validation failed on property common.ALLTYPES_NAME in object definition at line 3: The name 'Tag1' is already used." + }, + { + "code": 201, + "message": "Created" + } + ] + """, + "application/json"); + + // Act + var result = await _kepwareApiClient.GenericConfig.CompareAndApplyDetailed(sourceTags, null, ownerDevice); + + // Assert + result.Inserts.ShouldBe(1); + result.Failed.ShouldBe(1); + result.Failures.Count.ShouldBe(1); + result.Failures[0].Operation.ShouldBe(ApplyOperation.Insert); + (result.Failures[0].AttemptedItem as Tag)?.Name.ShouldBe("Tag1"); + result.Failures[0].ResponseCode.ShouldBe(400); + result.Failures[0].Property.ShouldBe("common.ALLTYPES_NAME"); + result.Failures[0].Description.ShouldBe("The name 'Tag1' is already used."); + result.Failures[0].ErrorLine.ShouldBe(3); + } + [Fact] public void AppendQueryString_PrivateMethod_EncodesAndSkipsNullsAndAppendsCorrectly() { diff --git a/Kepware.Api.Test/ApiClient/InsertTests.cs b/Kepware.Api.Test/ApiClient/InsertTests.cs index a931cf2..6f1ac66 100644 --- a/Kepware.Api.Test/ApiClient/InsertTests.cs +++ b/Kepware.Api.Test/ApiClient/InsertTests.cs @@ -143,7 +143,7 @@ public async Task Insert_MultipleItems_WithPartialSuccess_ShouldReturnMixedResul } [Fact] - public async Task Insert_MultipleItems_WithUnsupportedDriver_ShouldSkipItems() + public async Task Insert_MultipleItems_WithUnsupportedDriver_ShouldFailUnsupportedItems() { // Arrange await ConfigureToServeDrivers(); @@ -159,8 +159,10 @@ public async Task Insert_MultipleItems_WithUnsupportedDriver_ShouldSkipItems() var results = await _kepwareApiClient.GenericConfig.InsertItemsAsync(channels); // Assert - results.Length.ShouldBe(1); // Nur der Advanced Simulator-Channel sollte eingefügt werden - results[0].ShouldBeTrue(); + results.Length.ShouldBe(2); // Both channels have results + results[0].ShouldBeFalse(); // UnsupportedDriver should fail + results[1].ShouldBeTrue(); // Advanced Simulator should succeed + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}", Times.Once()); _loggerMockGeneric.Verify(logger => logger.Log( diff --git a/Kepware.Api.TestIntg/ApiClient/InsertTests.cs b/Kepware.Api.TestIntg/ApiClient/InsertTests.cs index a74ead6..61ee9f3 100644 --- a/Kepware.Api.TestIntg/ApiClient/InsertTests.cs +++ b/Kepware.Api.TestIntg/ApiClient/InsertTests.cs @@ -84,7 +84,7 @@ public async Task Insert_MultipleItems_WithPartialSuccess_ShouldReturnMixedResul } [Fact] - public async Task Insert_MultipleItems_WithUnsupportedDriver_ShouldSkipItems() + public async Task Insert_MultipleItems_WithUnsupportedDriver_ShouldItems() { // Arrange var channel1 = CreateTestChannel("Channel1", "UnsupportedDriver"); @@ -95,8 +95,9 @@ public async Task Insert_MultipleItems_WithUnsupportedDriver_ShouldSkipItems() var results = await _kepwareApiClient.GenericConfig.InsertItemsAsync(channels); // Assert - results.Length.ShouldBe(1); // Only the Simulator channel should be inserted - results[0].ShouldBeTrue(); + results.Length.ShouldBe(2); // Both channels have results + results[0].ShouldBeFalse(); // UnsupportedDriver should fail + results[1].ShouldBeTrue(); // Simulator should succeed // Clean up await DeleteAllChannelsAsync(); diff --git a/Kepware.Api.TestIntg/ApiClient/ProjectPermissionTests.cs b/Kepware.Api.TestIntg/ApiClient/ProjectPermissionTests.cs index 181ea47..8aca032 100644 --- a/Kepware.Api.TestIntg/ApiClient/ProjectPermissionTests.cs +++ b/Kepware.Api.TestIntg/ApiClient/ProjectPermissionTests.cs @@ -55,13 +55,12 @@ public async Task UpdateProjectPermissionAsync_ShouldReturnTrue_WhenUpdateSucces await _kepwareApiClient.Admin.CreateOrUpdateServerUserGroupAsync(serverUserGroup); - var projectPermission = new ProjectPermission + var projectPermission = await _kepwareApiClient.Admin.GetProjectPermissionAsync(serverUserGroup, ProjectPermissionName.ServermainAlias); + if (projectPermission == null) { - Name = ProjectPermissionName.ServermainAlias, - AddObject = true, - EditObject = true, - DeleteObject = false - }; + Assert.Fail("Precondition failed: ProjectPermission should exist for the test user group."); + } + projectPermission.DeleteObject = false; // Change a property to trigger an update // Act var result = await _kepwareApiClient.Admin.UpdateProjectPermissionAsync(serverUserGroup, projectPermission); @@ -97,9 +96,6 @@ public async Task UpdateProjectPermissionAsync_ShouldReturnTrue_WhenUpdateSucces [Fact] public async Task UpdateProjectPermissionAsync_ShouldReturnFalse_WhenUpdateFails() { - // TODO: Currently fails. Unsure of expected behavior from Kepware when an update fails. As of v6.18 - // Kepware returns a 200 OK with content that indicates a "not applied" key in the payload , - // which is not consistent with other endpoints. // Arrange var serverUserGroup = new ServerUserGroup { Name = "Administrators" }; @@ -115,7 +111,7 @@ public async Task UpdateProjectPermissionAsync_ShouldReturnFalse_WhenUpdateFails var result = await _kepwareApiClient.Admin.UpdateProjectPermissionAsync(serverUserGroup, projectPermission); // Assert - result.ShouldBeFalse("Currently fails. Unsure of expected behavior from Kepware when an update fails. See comments in test."); + result.ShouldBeFalse(); } } } diff --git a/Kepware.Api/ClientHandler/GenericApiHandler.cs b/Kepware.Api/ClientHandler/GenericApiHandler.cs index b20cd65..0227dc8 100644 --- a/Kepware.Api/ClientHandler/GenericApiHandler.cs +++ b/Kepware.Api/ClientHandler/GenericApiHandler.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Net; using System.Text; using System.Text.Json; using System.Text.Json.Serialization.Metadata; @@ -52,22 +53,94 @@ internal GenericApiHandler(KepwareApiClient kepwareApiClient, ILogger> CompareAndApply(T? sourceCollection, T? targetCollection, NamedEntity? owner = null, CancellationToken cancellationToken = default) where T : EntityCollection where K : NamedEntity, new() + { + var result = await CompareAndApplyDetailed(sourceCollection, targetCollection, owner, cancellationToken).ConfigureAwait(false); + return result.CompareResult; + } + + /// + /// Compares two collections and applies changes while returning detailed success and failure information. + /// Left should represent the source collection and Right should represent the target collection in the Kepware server. + /// + /// The type of the entity collection. + /// The type of the entity. + /// The source collection. + /// The collection representing the current state of the API. + /// The owner of the entities. + /// The cancellation token. + /// A detailed apply result including successful counts and failed item details. + public async Task> CompareAndApplyDetailed(T? sourceCollection, T? targetCollection, NamedEntity? owner = null, CancellationToken cancellationToken = default) + where T : EntityCollection + where K : NamedEntity, new() { var compareResult = EntityCompare.Compare(sourceCollection, targetCollection); + var result = new CollectionApplyResult(compareResult); - // This are the items that are in the API but not in the source - // --> we need to delete them - await DeleteItemsAsync(compareResult.ItemsOnlyInRight.Select(i => i.Right!).ToList(), owner, cancellationToken: cancellationToken).ConfigureAwait(false); + var deleteItems = compareResult.ItemsOnlyInRight.Select(i => i.Right!).ToList(); + var deleteResult = await DeleteItemsAsync(deleteItems, owner, cancellationToken: cancellationToken).ConfigureAwait(false); + for (int i = 0; i < deleteItems.Count; i++) + { + if (i < deleteResult.Length && deleteResult[i]) + { + result.AddDeleteSuccess(); + } + else + { + result.AddFailure(new ApplyFailure + { + Operation = ApplyOperation.Delete, + AttemptedItem = deleteItems[i], + }); + } + } - // This are the items both in the API and the source - // --> we need to update them - await UpdateItemsAsync(compareResult.ChangedItems.Select(i => (i.Left!, i.Right)).ToList(), owner, cancellationToken: cancellationToken).ConfigureAwait(false); + var updatePairs = compareResult.ChangedItems.Select(i => (i.Left!, i.Right)).ToList(); + var updateResult = await UpdateItemsDetailedAsync(updatePairs, owner, cancellationToken).ConfigureAwait(false); + for (int i = 0; i < updatePairs.Count; i++) + { + if (i < updateResult.Count && updateResult[i].IsSuccess) + { + result.AddUpdateSuccess(); + } + else + { + var updateOutcome = i < updateResult.Count ? updateResult[i] : new UpdateItemOutcome(false); + result.AddFailure(new ApplyFailure + { + Operation = ApplyOperation.Update, + AttemptedItem = updatePairs[i].Item1, + ResponseCode = updateOutcome.ResponseCode, + ResponseMessage = updateOutcome.ResponseMessage, + NotAppliedProperties = updateOutcome.NotAppliedProperties, + }); + } + } - // This are the items that are in the source but not in the API - // --> we need to insert them - await InsertItemsAsync(compareResult.ItemsOnlyInLeft.Select(i => i.Left!).ToList(), owner: owner, cancellationToken: cancellationToken).ConfigureAwait(false); + var insertItems = compareResult.ItemsOnlyInLeft.Select(i => i.Left!).ToList(); + var insertResult = await InsertItemsDetailedAsync(insertItems, owner: owner, cancellationToken: cancellationToken).ConfigureAwait(false); + for (int i = 0; i < insertItems.Count; i++) + { + if (i < insertResult.Count && insertResult[i].IsSuccess) + { + result.AddInsertSuccess(); + } + else + { + var insertOutcome = i < insertResult.Count ? insertResult[i] : new InsertItemOutcome(false); + result.AddFailure(new ApplyFailure + { + Operation = ApplyOperation.Insert, + AttemptedItem = insertItems[i], + ResponseCode = insertOutcome.ResponseCode, + ResponseMessage = insertOutcome.ResponseMessage, + Property = insertOutcome.Property, + Description = insertOutcome.Description, + ErrorLine = insertOutcome.ErrorLine, + }); + } + } - return compareResult; + return result; } #endregion @@ -110,6 +183,14 @@ protected internal async Task UpdateItemAsync(string endpoint, T item, } else { + var updateMessage = await TryDeserializeUpdateMessageAsync(response, cancellationToken).ConfigureAwait(false); + if (updateMessage?.NotApplied != null && updateMessage.NotApplied.Count > 0) + { + m_logger.LogError("Partial update detected for {TypeName} on {Endpoint}. Not applied properties: {NotApplied}", + typeof(T).Name, endpoint, updateMessage.NotApplied.Keys); + return false; + } + return true; } } @@ -146,13 +227,18 @@ public async Task UpdateItemAsync(K item, K? oldItem = default, Name /// /// The task result contains an array of booleans indicating whether the update for each item was successful. public async Task UpdateItemsAsync(List<(K item, K? oldItem)> items, NamedEntity? owner = null, CancellationToken cancellationToken = default) + where T : EntityCollection + where K : NamedEntity, new() + => (await UpdateItemsDetailedAsync(items, owner, cancellationToken).ConfigureAwait(false)).Select(i => i.IsSuccess).ToArray(); + + private async Task> UpdateItemsDetailedAsync(List<(K item, K? oldItem)> items, NamedEntity? owner = null, CancellationToken cancellationToken = default) where T : EntityCollection where K : NamedEntity, new() { if (items.Count == 0) return []; - List result = new List(); + List result = []; try { var collectionEndpoint = EndpointResolver.ResolveEndpoint(owner).TrimEnd('/'); @@ -163,7 +249,7 @@ public async Task UpdateItemsAsync(List<(K item, K? oldItem)> item if (currentEntity == null) { m_logger.LogError("Failed to load {TypeName} from {Endpoint}", typeof(K).Name, endpoint); - result.Add(false); + result.Add(new UpdateItemOutcome(false)); } else { @@ -179,11 +265,21 @@ public async Task UpdateItemsAsync(List<(K item, K? oldItem)> item { var message = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); m_logger.LogError("Failed to update {TypeName} from {Endpoint}: {ReasonPhrase}\n{Message}", typeof(T).Name, endpoint, response.ReasonPhrase, message); - result.Add(false); + result.Add(new UpdateItemOutcome(false, (int)response.StatusCode, message)); } else { - result.Add(true); + var updateMessage = await TryDeserializeUpdateMessageAsync(response, cancellationToken).ConfigureAwait(false); + if (updateMessage?.NotApplied != null && updateMessage.NotApplied.Count > 0) + { + var notApplied = updateMessage.NotApplied.Keys.ToList(); + m_logger.LogError("Partial update detected for {TypeName} on {Endpoint}. Not applied properties: {NotApplied}", typeof(T).Name, endpoint, notApplied); + result.Add(new UpdateItemOutcome(false, updateMessage.ResponseStatusCode, updateMessage.Message, notApplied)); + } + else + { + result.Add(new UpdateItemOutcome(true, (int)response.StatusCode, updateMessage?.Message)); + } } } } @@ -195,9 +291,9 @@ public async Task UpdateItemsAsync(List<(K item, K? oldItem)> item } if (result.Count < items.Count) - result.AddRange(Enumerable.Repeat(false, items.Count - result.Count)); + result.AddRange(Enumerable.Repeat(new UpdateItemOutcome(false), items.Count - result.Count)); - return [..result]; + return result; } #endregion @@ -267,18 +363,27 @@ public async Task InsertItemAsync(K item, NamedEntity? owner = null, /// /// A task that represents the asynchronous operation. The task result contains an array of booleans indicating whether the each insert was successful. public async Task InsertItemsAsync(List items, int pageSize = 10, NamedEntity? owner = null, CancellationToken cancellationToken = default) + where T : EntityCollection + where K : NamedEntity, new() + => (await InsertItemsDetailedAsync(items, pageSize, owner, cancellationToken).ConfigureAwait(false)).Select(i => i.IsSuccess).ToArray(); + + private async Task> InsertItemsDetailedAsync(List items, int pageSize = 10, NamedEntity? owner = null, CancellationToken cancellationToken = default) where T : EntityCollection where K : NamedEntity, new() { if (items.Count == 0) return []; - List result = new List(); + var result = new InsertItemOutcome?[items.Count]; try { var endpoint = EndpointResolver.ResolveEndpoint(owner); + var supportedItems = new List<(int index, K item)>(); + var unsupportedItems = new List(); + var unsupportedMessage = "Unsupported driver detected for insert."; + if (typeof(K) == typeof(Channel) || typeof(K) == typeof(Device)) { @@ -290,26 +395,41 @@ public async Task InsertItemsAsync(List items, int pageSize = 1 m_cachedSupportedDrivers = await GetSupportedDriversAsync(cancellationToken).ConfigureAwait(false); } - var groupedItems = items - .GroupBy(i => - { - var driver = i.GetDynamicProperty(Properties.Channel.DeviceDriver); - return !string.IsNullOrEmpty(driver) && m_cachedSupportedDrivers.ContainsKey(driver); - }); + for (int i = 0; i < items.Count; i++) + { + var item = items[i]; + var driver = item.GetDynamicProperty(Properties.Channel.DeviceDriver); + var isSupported = !string.IsNullOrEmpty(driver) && m_cachedSupportedDrivers.ContainsKey(driver); + if (isSupported) + { + supportedItems.Add((i, item)); + } + else + { + unsupportedItems.Add(item); + result[i] = new InsertItemOutcome(false, (int)HttpStatusCode.BadRequest, unsupportedMessage); + } + } - var unsupportedItems = groupedItems.FirstOrDefault(g => !g.Key)?.ToList() ?? []; if (unsupportedItems.Count > 0) { - items = groupedItems.FirstOrDefault(g => g.Key)?.ToList() ?? []; m_logger.LogWarning("The following {NumItems} {TypeName} have unsupported drivers ({ListOfUsedUnsupportedDrivers}) and will not be inserted: {ItemsNames}", unsupportedItems.Count, typeof(K).Name, unsupportedItems.Select(i => i.GetDynamicProperty(Properties.Channel.DeviceDriver)).Distinct(), unsupportedItems.Select(i => i.Name)); } } + else + { + for (int i = 0; i < items.Count; i++) + { + supportedItems.Add((i, items[i])); + } + } - var totalPageCount = (int)Math.Ceiling((double)items.Count / pageSize); + var totalPageCount = (int)Math.Ceiling((double)supportedItems.Count / pageSize); for (int i = 0; i < totalPageCount; i++) { - var pageItems = items.Skip(i * pageSize).Take(pageSize).ToList(); + var pageItemMapping = supportedItems.Skip(i * pageSize).Take(pageSize).ToList(); + var pageItems = pageItemMapping.Select(p => p.item).ToList(); m_logger.LogInformation("Inserting {NumItems} {TypeName}(s) on {Endpoint} in batch {BatchNr} of {TotalBatches} ...", pageItems.Count, typeof(K).Name, endpoint, i + 1, totalPageCount); var jsonContent = JsonSerializer.Serialize(pageItems, KepJsonContext.GetJsonListTypeInfo()); @@ -319,7 +439,10 @@ public async Task InsertItemsAsync(List items, int pageSize = 1 { var message = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); m_logger.LogError("Failed to insert {TypeName} from {Endpoint}: {ReasonPhrase}\n{Message}", typeof(T).Name, endpoint, response.ReasonPhrase, message); - result.AddRange(Enumerable.Repeat(false, pageItems.Count)); + foreach (var pageItem in pageItemMapping) + { + result[pageItem.index] = new InsertItemOutcome(false, (int)response.StatusCode, message); + } } else if (response.StatusCode == System.Net.HttpStatusCode.MultiStatus) { @@ -330,7 +453,26 @@ public async Task InsertItemsAsync(List items, int pageSize = 1 await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), KepJsonContext.Default.ListApiResult, cancellationToken).ConfigureAwait(false) ?? []; - result.AddRange(results.Select(r => r.IsSuccessStatusCode)); + for (int entryIndex = 0; entryIndex < pageItemMapping.Count; entryIndex++) + { + var mappedItem = pageItemMapping[entryIndex]; + if (entryIndex < results.Count) + { + var itemResult = results[entryIndex]; + result[mappedItem.index] = new InsertItemOutcome( + itemResult.IsSuccessStatusCode, + itemResult.Code, + itemResult.Message, + itemResult.Property, + itemResult.Description, + itemResult.ErrorLine); + } + else + { + result[mappedItem.index] = new InsertItemOutcome(false, (int)HttpStatusCode.InternalServerError, + "Multi-status response did not contain an entry for this item."); + } + } var failedEntries = results?.Where(r => !r.IsSuccessStatusCode)?.ToList() ?? []; m_logger.LogError("{NumSuccessFull} were successfull, failed to insert {NumFailed} {TypeName} from {Endpoint}: {ReasonPhrase}\nFailed:\n{Message}", @@ -338,7 +480,10 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false } else { - result.AddRange(Enumerable.Repeat(true, pageItems.Count)); + foreach (var pageItem in pageItemMapping) + { + result[pageItem.index] = new InsertItemOutcome(true, (int)response.StatusCode, response.ReasonPhrase); + } } } } @@ -346,15 +491,45 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false { m_logger.LogWarning(httpEx, "Failed to connect to {BaseAddress}", m_httpClient.BaseAddress); m_kepwareApiClient.OnHttpRequestException(httpEx); + } - if (items.Count > result.Count) - result.AddRange(Enumerable.Repeat(false, items.Count - result.Count)); + for (int i = 0; i < result.Length; i++) + { + result[i] ??= new InsertItemOutcome(false); } - return [.. result]; + return result.Select(r => r!).ToList(); } #endregion + private async Task TryDeserializeUpdateMessageAsync(HttpResponseMessage response, CancellationToken cancellationToken) + { + var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(body)) + { + return null; + } + + try + { + return JsonSerializer.Deserialize(body, KepJsonContext.Default.UpdateApiResponseMessage); + } + catch (JsonException) + { + return null; + } + } + + private sealed record UpdateItemOutcome(bool IsSuccess, int? ResponseCode = null, string? ResponseMessage = null, IReadOnlyList? NotAppliedProperties = null); + + private sealed record InsertItemOutcome( + bool IsSuccess, + int? ResponseCode = null, + string? ResponseMessage = null, + string? Property = null, + string? Description = null, + int? ErrorLine = null); + #region Delete /// /// Deletes an item from the Kepware server. diff --git a/Kepware.Api/ClientHandler/ProjectApiHandler.cs b/Kepware.Api/ClientHandler/ProjectApiHandler.cs index 32b9d34..d65f852 100644 --- a/Kepware.Api/ClientHandler/ProjectApiHandler.cs +++ b/Kepware.Api/ClientHandler/ProjectApiHandler.cs @@ -61,9 +61,8 @@ public ProjectApiHandler(KepwareApiClient kepwareApiClient, ChannelApiHandler ch /// A task that represents the asynchronous operation. The task result contains a tuple with the counts of inserts, updates, and deletes. public async Task<(int inserts, int updates, int deletes)> CompareAndApply(Project sourceProject, CancellationToken cancellationToken = default) { - var projectFromApi = await LoadProjectAsync(blnLoadFullProject: true, cancellationToken: cancellationToken); - await projectFromApi.Cleanup(m_kepwareApiClient, true, cancellationToken).ConfigureAwait(false); - return await CompareAndApply(sourceProject, projectFromApi, cancellationToken).ConfigureAwait(false); + var result = await CompareAndApplyDetailed(sourceProject, cancellationToken).ConfigureAwait(false); + return (result.Inserts, result.Updates, result.Deletes); } /// @@ -75,74 +74,82 @@ public ProjectApiHandler(KepwareApiClient kepwareApiClient, ChannelApiHandler ch /// A task that represents the asynchronous operation. The task result contains a tuple with the counts of inserts, updates, and deletes. public async Task<(int inserts, int updates, int deletes)> CompareAndApply(Project sourceProject, Project projectFromApi, CancellationToken cancellationToken = default) { - int inserts = 0, updates = 0, deletes = 0; + var result = await CompareAndApplyDetailed(sourceProject, projectFromApi, cancellationToken).ConfigureAwait(false); + return (result.Inserts, result.Updates, result.Deletes); + } + + /// + /// Compares the source project with the project from the API and applies changes while returning detailed success and failure information. + /// + /// The source project to compare. + /// The cancellation token. + /// A detailed project apply result including counts and failed items. + public async Task CompareAndApplyDetailed(Project sourceProject, CancellationToken cancellationToken = default) + { + var projectFromApi = await LoadProjectAsync(blnLoadFullProject: true, cancellationToken: cancellationToken).ConfigureAwait(false); + await projectFromApi.Cleanup(m_kepwareApiClient, true, cancellationToken).ConfigureAwait(false); + return await CompareAndApplyDetailed(sourceProject, projectFromApi, cancellationToken).ConfigureAwait(false); + } + + /// + /// Compares the source project with the project from the API and applies changes while returning detailed success and failure information. + /// + /// The source project to compare. + /// The project loaded from the API. + /// The cancellation token. + /// A detailed project apply result including counts and failed items. + public async Task CompareAndApplyDetailed(Project sourceProject, Project projectFromApi, CancellationToken cancellationToken = default) + { + var result = new ProjectCompareAndApplyResult(); if (sourceProject.Hash != projectFromApi.Hash) { m_logger.LogInformation("Project properties has changed. Updating project properties..."); - var result = await SetProjectPropertiesAsync(sourceProject, cancellationToken: cancellationToken).ConfigureAwait(false); - if (result) + var projectPropertyFailure = await SetProjectPropertiesDetailedAsync(sourceProject, cancellationToken: cancellationToken).ConfigureAwait(false); + if (projectPropertyFailure == null) { - updates += 1; + result.AddUpdateSuccess(); } else { m_logger.LogError("Failed to update project properties..."); + result.AddFailure(projectPropertyFailure); } } - - var channelCompare = await m_kepwareApiClient.GenericConfig.CompareAndApply(sourceProject.Channels, projectFromApi.Channels, + var channelCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailed(sourceProject.Channels, projectFromApi.Channels, cancellationToken: cancellationToken).ConfigureAwait(false); + result.Add(channelCompare); - updates += channelCompare.ChangedItems.Count; - inserts += channelCompare.ItemsOnlyInLeft.Count; - deletes += channelCompare.ItemsOnlyInRight.Count; - - foreach (var channel in channelCompare.UnchangedItems.Concat(channelCompare.ChangedItems)) + foreach (var channel in channelCompare.CompareResult.UnchangedItems.Concat(channelCompare.CompareResult.ChangedItems)) { - var deviceCompare = await m_kepwareApiClient.GenericConfig.CompareAndApply(channel.Left!.Devices, channel.Right!.Devices, channel.Right, + var deviceCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailed(channel.Left!.Devices, channel.Right!.Devices, channel.Right, cancellationToken: cancellationToken).ConfigureAwait(false); + result.Add(deviceCompare); - updates += deviceCompare.ChangedItems.Count; - inserts += deviceCompare.ItemsOnlyInLeft.Count; - deletes += deviceCompare.ItemsOnlyInRight.Count; - - foreach (var device in deviceCompare.UnchangedItems.Concat(deviceCompare.ChangedItems)) + foreach (var device in deviceCompare.CompareResult.UnchangedItems.Concat(deviceCompare.CompareResult.ChangedItems)) { - var tagCompare = await m_kepwareApiClient.GenericConfig.CompareAndApply(device.Left!.Tags, device.Right!.Tags, device.Right, cancellationToken).ConfigureAwait(false); - - updates += tagCompare.ChangedItems.Count; - inserts += tagCompare.ItemsOnlyInLeft.Count; - deletes += tagCompare.ItemsOnlyInRight.Count; - - var tagGroupCompare = await m_kepwareApiClient.GenericConfig.CompareAndApply(device.Left!.TagGroups, device.Right!.TagGroups, device.Right, cancellationToken).ConfigureAwait(false); + var tagCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailed(device.Left!.Tags, device.Right!.Tags, device.Right, cancellationToken).ConfigureAwait(false); + result.Add(tagCompare); - updates += tagGroupCompare.ChangedItems.Count; - inserts += tagGroupCompare.ItemsOnlyInLeft.Count; - deletes += tagGroupCompare.ItemsOnlyInRight.Count; + var tagGroupCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailed(device.Left!.TagGroups, device.Right!.TagGroups, device.Right, cancellationToken).ConfigureAwait(false); + result.Add(tagGroupCompare); - - foreach (var tagGroup in tagGroupCompare.UnchangedItems.Concat(tagGroupCompare.ChangedItems)) + foreach (var tagGroup in tagGroupCompare.CompareResult.UnchangedItems.Concat(tagGroupCompare.CompareResult.ChangedItems)) { - var tagGroupTagCompare = await m_kepwareApiClient.GenericConfig.CompareAndApply(tagGroup.Left!.Tags, tagGroup.Right!.Tags, tagGroup.Right, cancellationToken).ConfigureAwait(false); - - updates += tagGroupTagCompare.ChangedItems.Count; - inserts += tagGroupTagCompare.ItemsOnlyInLeft.Count; - deletes += tagGroupTagCompare.ItemsOnlyInRight.Count; + var tagGroupTagCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailed(tagGroup.Left!.Tags, tagGroup.Right!.Tags, tagGroup.Right, cancellationToken).ConfigureAwait(false); + result.Add(tagGroupTagCompare); if (tagGroup.Left?.TagGroups != null) { - var result = await RecusivlyCompareTagGroup(tagGroup.Left!.TagGroups, tagGroup.Right!.TagGroups, tagGroup.Right, cancellationToken).ConfigureAwait(false); - updates += result.updates; - inserts += result.inserts; - deletes += result.deletes; + var recursiveResult = await RecusivlyCompareTagGroupDetailed(tagGroup.Left!.TagGroups, tagGroup.Right!.TagGroups, tagGroup.Right, cancellationToken).ConfigureAwait(false); + result.Add(recursiveResult); } } } } - return (inserts, updates, deletes); + return result; } #endregion @@ -169,6 +176,11 @@ public ProjectApiHandler(KepwareApiClient kepwareApiClient, ChannelApiHandler ch /// A task that represents the asynchronous operation. The task result contains a boolean indicating whether the update was successful. /// public async Task SetProjectPropertiesAsync(Project project, CancellationToken cancellationToken = default) + { + return await SetProjectPropertiesDetailedAsync(project, cancellationToken).ConfigureAwait(false) == null; + } + + private async Task SetProjectPropertiesDetailedAsync(Project project, CancellationToken cancellationToken = default) { try { @@ -199,10 +211,32 @@ public async Task SetProjectPropertiesAsync(Project project, CancellationT { var message = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); m_logger.LogError("Failed to update Project Property Settings from {Endpoint}: {ReasonPhrase}\n{Message}", endpoint, response.ReasonPhrase, message); + return new ApplyFailure + { + Operation = ApplyOperation.Update, + AttemptedItem = project, + ResponseCode = (int)response.StatusCode, + ResponseMessage = message, + }; } else { - return true; + var updateMessage = await TryDeserializeUpdateMessageAsync(response, cancellationToken).ConfigureAwait(false); + if (updateMessage?.NotApplied != null && updateMessage.NotApplied.Count > 0) + { + var notApplied = updateMessage.NotApplied.Keys.ToList(); + m_logger.LogError("Partial update detected for project properties on {Endpoint}. Not applied properties: {NotApplied}", endpoint, notApplied); + return new ApplyFailure + { + Operation = ApplyOperation.Update, + AttemptedItem = project, + ResponseCode = updateMessage.ResponseStatusCode, + ResponseMessage = updateMessage.Message, + NotAppliedProperties = notApplied, + }; + } + + return null; } } catch (HttpRequestException httpEx) @@ -211,7 +245,29 @@ public async Task SetProjectPropertiesAsync(Project project, CancellationT m_kepwareApiClient.OnHttpRequestException(httpEx); } - return false; + return new ApplyFailure + { + Operation = ApplyOperation.Update, + AttemptedItem = project, + }; + } + + private static async Task TryDeserializeUpdateMessageAsync(HttpResponseMessage response, CancellationToken cancellationToken) + { + var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + if (string.IsNullOrWhiteSpace(body)) + { + return null; + } + + try + { + return JsonSerializer.Deserialize(body, KepJsonContext.Default.UpdateApiResponseMessage); + } + catch (JsonException) + { + return null; + } } #endregion @@ -560,28 +616,29 @@ private static void SetOwnerRecursive(IEnumerable tagGroups, Nam /// A task that represents the asynchronous operation. The task result contains a tuple with the counts of inserts, updates, and deletes. internal static async Task<(int inserts, int updates, int deletes)> RecusivlyCompareTagGroup(KepwareApiClient apiClient, DeviceTagGroupCollection left, DeviceTagGroupCollection? right, NamedEntity owner, CancellationToken cancellationToken) { - (int inserts, int updates, int deletes) ret = (0, 0, 0); + var result = await RecusivlyCompareTagGroupDetailed(apiClient, left, right, owner, cancellationToken).ConfigureAwait(false); + return (result.Inserts, result.Updates, result.Deletes); + } - var tagGroupCompare = await apiClient.GenericConfig.CompareAndApply(left, right, owner, cancellationToken: cancellationToken).ConfigureAwait(false); + private Task RecusivlyCompareTagGroupDetailed(DeviceTagGroupCollection left, DeviceTagGroupCollection? right, NamedEntity owner, CancellationToken cancellationToken) + => RecusivlyCompareTagGroupDetailed(m_kepwareApiClient, left, right, owner, cancellationToken); - ret.inserts = tagGroupCompare.ItemsOnlyInLeft.Count; - ret.updates = tagGroupCompare.ChangedItems.Count; - ret.deletes = tagGroupCompare.ItemsOnlyInRight.Count; + internal static async Task RecusivlyCompareTagGroupDetailed(KepwareApiClient apiClient, DeviceTagGroupCollection left, DeviceTagGroupCollection? right, NamedEntity owner, CancellationToken cancellationToken) + { + var ret = new ProjectCompareAndApplyResult(); - foreach (var tagGroup in tagGroupCompare.UnchangedItems.Concat(tagGroupCompare.ChangedItems)) - { - var tagGroupTagCompare = await apiClient.GenericConfig.CompareAndApply(tagGroup.Left!.Tags, tagGroup.Right!.Tags, tagGroup.Right, cancellationToken: cancellationToken).ConfigureAwait(false); + var tagGroupCompare = await apiClient.GenericConfig.CompareAndApplyDetailed(left, right, owner, cancellationToken: cancellationToken).ConfigureAwait(false); + ret.Add(tagGroupCompare); - ret.inserts = tagGroupTagCompare.ItemsOnlyInLeft.Count; - ret.updates = tagGroupTagCompare.ChangedItems.Count; - ret.deletes = tagGroupTagCompare.ItemsOnlyInRight.Count; + foreach (var tagGroup in tagGroupCompare.CompareResult.UnchangedItems.Concat(tagGroupCompare.CompareResult.ChangedItems)) + { + var tagGroupTagCompare = await apiClient.GenericConfig.CompareAndApplyDetailed(tagGroup.Left!.Tags, tagGroup.Right!.Tags, tagGroup.Right, cancellationToken: cancellationToken).ConfigureAwait(false); + ret.Add(tagGroupTagCompare); if (tagGroup.Left!.TagGroups != null) { - var result = await RecusivlyCompareTagGroup(apiClient, tagGroup.Left!.TagGroups, tagGroup.Right!.TagGroups, tagGroup.Right, cancellationToken: cancellationToken).ConfigureAwait(false); - ret.updates += result.updates; - ret.deletes += result.deletes; - ret.inserts += result.inserts; + var result = await RecusivlyCompareTagGroupDetailed(apiClient, tagGroup.Left!.TagGroups, tagGroup.Right!.TagGroups, tagGroup.Right, cancellationToken: cancellationToken).ConfigureAwait(false); + ret.Add(result); } } diff --git a/Kepware.Api/Model/ApiMessages.cs b/Kepware.Api/Model/ApiMessages.cs index cb118a7..89beb63 100644 --- a/Kepware.Api/Model/ApiMessages.cs +++ b/Kepware.Api/Model/ApiMessages.cs @@ -15,6 +15,24 @@ namespace Kepware.Api.Model /// public class ApiResult { + /// + /// Gets or sets the property name associated with a validation failure. + /// + [JsonPropertyName("property")] + public string? Property { get; set; } + + /// + /// Gets or sets the description associated with a validation failure. + /// + [JsonPropertyName("description")] + public string? Description { get; set; } + + /// + /// Gets or sets the error line associated with a validation failure. + /// + [JsonPropertyName("error_line")] + public int? ErrorLine { get; set; } + /// /// Gets or sets the status code of the API result. /// @@ -40,6 +58,18 @@ public class ApiResult public bool IsSuccessStatusCode => Code >= 200 && Code < 300; } + /// + /// Represents the response from an update endpoint that may include partially applied properties. + /// + public class UpdateApiResponseMessage : ApiResponseMessage + { + /// + /// Gets or sets the properties that were not applied. + /// + [JsonPropertyName("not_applied")] + public Dictionary? NotApplied { get; set; } + } + /// /// Represents the status of the Configuration API REST service. /// diff --git a/Kepware.Api/Model/ApplyResults.cs b/Kepware.Api/Model/ApplyResults.cs new file mode 100644 index 0000000..533a901 --- /dev/null +++ b/Kepware.Api/Model/ApplyResults.cs @@ -0,0 +1,195 @@ +using Kepware.Api.Util; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; + +namespace Kepware.Api.Model +{ + /// + /// Represents the type of operation applied during compare-and-apply. + /// + public enum ApplyOperation + { + /// + /// Insert operation. + /// + Insert = 0, + + /// + /// Update operation. + /// + Update = 1, + + /// + /// Delete operation. + /// + Delete = 2, + } + + /// + /// Represents one failed apply operation and the attempted item. + /// + public sealed class ApplyFailure + { + /// + /// Gets the operation that failed. + /// + public required ApplyOperation Operation { get; init; } + + /// + /// Gets the original item used for the failed operation. + /// + public required BaseEntity AttemptedItem { get; init; } + + /// + /// Gets the response code associated with the failed operation. + /// + public int? ResponseCode { get; init; } + + /// + /// Gets the response message associated with the failed operation. + /// + public string? ResponseMessage { get; init; } + + /// + /// Gets the property name associated with an insert validation failure. + /// + public string? Property { get; init; } + + /// + /// Gets the detailed description associated with an insert validation failure. + /// + public string? Description { get; init; } + + /// + /// Gets the error line associated with an insert validation failure. + /// + public int? ErrorLine { get; init; } + + /// + /// Gets the list of update properties that were not applied. + /// + public IReadOnlyList? NotAppliedProperties { get; init; } + } + + /// + /// Represents detailed compare-and-apply results for a collection. + /// + /// The entity type. + public sealed class CollectionApplyResult + where K : NamedEntity, new() + { + private readonly List m_failures = []; + + /// + /// Initializes a new instance of the class. + /// + /// The raw compare result bucket. + public CollectionApplyResult(EntityCompare.CollectionResultBucket compareResult) + { + CompareResult = compareResult; + } + + /// + /// Gets the underlying compare result bucket. + /// + public EntityCompare.CollectionResultBucket CompareResult { get; } + + /// + /// Gets the number of successfully inserted items. + /// + public int Inserts { get; private set; } + + /// + /// Gets the number of successfully updated items. + /// + public int Updates { get; private set; } + + /// + /// Gets the number of successfully deleted items. + /// + public int Deletes { get; private set; } + + /// + /// Gets the number of failed operations. + /// + public int Failed => m_failures.Count; + + /// + /// Gets the failed operation details. + /// + public ReadOnlyCollection Failures => m_failures.AsReadOnly(); + + internal void AddInsertSuccess() => Inserts += 1; + + internal void AddUpdateSuccess() => Updates += 1; + + internal void AddDeleteSuccess() => Deletes += 1; + + internal void AddFailure(ApplyFailure failure) => m_failures.Add(failure); + } + + /// + /// Represents detailed compare-and-apply results for a full project operation. + /// + public sealed class ProjectCompareAndApplyResult + { + private readonly List m_failures = []; + + /// + /// Gets the number of successfully inserted items. + /// + public int Inserts { get; private set; } + + /// + /// Gets the number of successfully updated items. + /// + public int Updates { get; private set; } + + /// + /// Gets the number of successfully deleted items. + /// + public int Deletes { get; private set; } + + /// + /// Gets the number of failed operations. + /// + public int Failed => m_failures.Count; + + /// + /// Gets the failed operation details. + /// + public ReadOnlyCollection Failures => m_failures.AsReadOnly(); + + internal void Add(CollectionApplyResult result) + => Add(result.Inserts, result.Updates, result.Deletes, result.Failures); + + internal void Add(CollectionApplyResult result) + => Add(result.Inserts, result.Updates, result.Deletes, result.Failures); + + internal void Add(CollectionApplyResult result) + => Add(result.Inserts, result.Updates, result.Deletes, result.Failures); + + internal void Add(CollectionApplyResult result) + => Add(result.Inserts, result.Updates, result.Deletes, result.Failures); + + internal void Add(ProjectCompareAndApplyResult result) + => Add(result.Inserts, result.Updates, result.Deletes, result.Failures); + + internal void AddInsertSuccess() => Inserts += 1; + + internal void AddUpdateSuccess() => Updates += 1; + + internal void AddDeleteSuccess() => Deletes += 1; + + internal void AddFailure(ApplyFailure failure) => m_failures.Add(failure); + + private void Add(int inserts, int updates, int deletes, IReadOnlyList failures) + { + Inserts += inserts; + Updates += updates; + Deletes += deletes; + m_failures.AddRange(failures); + } + } +} diff --git a/Kepware.Api/Serializer/KepJsonContext.cs b/Kepware.Api/Serializer/KepJsonContext.cs index 5555c09..dbc07f3 100644 --- a/Kepware.Api/Serializer/KepJsonContext.cs +++ b/Kepware.Api/Serializer/KepJsonContext.cs @@ -23,6 +23,7 @@ namespace Kepware.Api.Serializer [JsonSerializable(typeof(ServerUser))] [JsonSerializable(typeof(ProjectPermission))] [JsonSerializable(typeof(ApiResponseMessage))] + [JsonSerializable(typeof(UpdateApiResponseMessage))] [JsonSerializable(typeof(JobResponseMessage))] [JsonSerializable(typeof(JobStatusMessage))] [JsonSerializable(typeof(ServiceInvocationRequest))] From 0898abc91e284720289bb8b9c99a2c2f1d483b23 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Fri, 27 Mar 2026 14:54:44 -0400 Subject: [PATCH 18/38] refactor: modified method name --- .../ApiClient/GenericHandleTests.cs | 8 +++--- .../ApiClient/ProjectApiHandlerTests.cs | 2 +- .../ApiClient/ProjectApiHandlerTests.cs | 2 +- Kepware.Api.TestIntg/appsettings.json | 4 +-- Kepware.Api/ClientHandler/DeviceApiHandler.cs | 2 +- .../ClientHandler/GenericApiHandler.cs | 6 ++-- .../ClientHandler/ProjectApiHandler.cs | 28 +++++++++---------- KepwareSync.Service/SyncService.cs | 2 +- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Kepware.Api.Test/ApiClient/GenericHandleTests.cs b/Kepware.Api.Test/ApiClient/GenericHandleTests.cs index 599d01a..8657f3c 100644 --- a/Kepware.Api.Test/ApiClient/GenericHandleTests.cs +++ b/Kepware.Api.Test/ApiClient/GenericHandleTests.cs @@ -18,7 +18,7 @@ namespace Kepware.Api.Test.ApiClient public class GenericHandler : TestApiClientBase { [Fact] - public async Task CompareAndApplyDetailed_ShouldCountUpdateAsFailed_When200ContainsNotApplied() + public async Task CompareAndApplyDetailedAsync_ShouldCountUpdateAsFailed_When200ContainsNotApplied() { // Arrange var channel = new Channel { Name = "Channel1" }; @@ -48,7 +48,7 @@ public async Task CompareAndApplyDetailed_ShouldCountUpdateAsFailed_When200Conta "application/json"); // Act - var result = await _kepwareApiClient.GenericConfig.CompareAndApplyDetailed(sourceCollection, targetCollection, channel); + var result = await _kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync(sourceCollection, targetCollection, channel); // Assert result.Updates.ShouldBe(0); @@ -62,7 +62,7 @@ public async Task CompareAndApplyDetailed_ShouldCountUpdateAsFailed_When200Conta } [Fact] - public async Task CompareAndApplyDetailed_ShouldMap207InsertFeedbackToItems() + public async Task CompareAndApplyDetailedAsync_ShouldMap207InsertFeedbackToItems() { // Arrange var channel = new Channel { Name = "Channel1" }; @@ -94,7 +94,7 @@ public async Task CompareAndApplyDetailed_ShouldMap207InsertFeedbackToItems() "application/json"); // Act - var result = await _kepwareApiClient.GenericConfig.CompareAndApplyDetailed(sourceTags, null, ownerDevice); + var result = await _kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync(sourceTags, null, ownerDevice); // Assert result.Inserts.ShouldBe(1); diff --git a/Kepware.Api.Test/ApiClient/ProjectApiHandlerTests.cs b/Kepware.Api.Test/ApiClient/ProjectApiHandlerTests.cs index 34989a0..d4a5a48 100644 --- a/Kepware.Api.Test/ApiClient/ProjectApiHandlerTests.cs +++ b/Kepware.Api.Test/ApiClient/ProjectApiHandlerTests.cs @@ -464,7 +464,7 @@ public async Task CompareAndApply_ShouldReturn2Inserts1Update1Delete_WhenSourceH newProject.SetDynamicProperty("uaserverinterface.PROJECT_OPC_UA_ANONYMOUS_LOGIN", false); // Act - var result = await _projectApiHandler.CompareAndApply(newProject); + var result = await _projectApiHandler.CompareAndApplyAsync(newProject); // Assert Assert.Equal(2, result.inserts); // 2 new channels added diff --git a/Kepware.Api.TestIntg/ApiClient/ProjectApiHandlerTests.cs b/Kepware.Api.TestIntg/ApiClient/ProjectApiHandlerTests.cs index a56b342..a901ddb 100644 --- a/Kepware.Api.TestIntg/ApiClient/ProjectApiHandlerTests.cs +++ b/Kepware.Api.TestIntg/ApiClient/ProjectApiHandlerTests.cs @@ -312,7 +312,7 @@ public async Task CompareAndApply_ShouldReturnChanges() newProject.SetDynamicProperty("uaserverinterface.PROJECT_OPC_UA_ANONYMOUS_LOGIN", !existingProperties?.ProjectProperties.OpcUaAllowAnonymousLogin); // Act - var result = await _kepwareApiClient.Project.CompareAndApply(newProject); + var result = await _kepwareApiClient.Project.CompareAndApplyAsync(newProject); // Assert Assert.Equal(2, result.inserts); // 2 new channels added diff --git a/Kepware.Api.TestIntg/appsettings.json b/Kepware.Api.TestIntg/appsettings.json index 2126b20..c450806 100644 --- a/Kepware.Api.TestIntg/appsettings.json +++ b/Kepware.Api.TestIntg/appsettings.json @@ -2,8 +2,8 @@ "TestSettings": { "IntegrationTest": true, "TestServer": { - "Host": "https://localhost", - "Port": 57512, + "Host": "http://localhost", + "Port": 57412, "UserName": "Administrator", "Password": "" } diff --git a/Kepware.Api/ClientHandler/DeviceApiHandler.cs b/Kepware.Api/ClientHandler/DeviceApiHandler.cs index 8556062..a39ac57 100644 --- a/Kepware.Api/ClientHandler/DeviceApiHandler.cs +++ b/Kepware.Api/ClientHandler/DeviceApiHandler.cs @@ -205,7 +205,7 @@ public async Task UpdateDeviceAsync(Device device, bool updateTagsAndTagGr if (updateTagsAndTagGroups) { - await m_kepwareApiClient.GenericConfig.CompareAndApply(device.Tags, currentDevice.Tags, device, cancellationToken: cancellationToken); + await m_kepwareApiClient.GenericConfig.CompareAndApplyAsync(device.Tags, currentDevice.Tags, device, cancellationToken: cancellationToken); if (device.TagGroups != null) await ProjectApiHandler.RecusivlyCompareTagGroup(m_kepwareApiClient, device.TagGroups, currentDevice.TagGroups, device, cancellationToken); diff --git a/Kepware.Api/ClientHandler/GenericApiHandler.cs b/Kepware.Api/ClientHandler/GenericApiHandler.cs index 0227dc8..e03aa4f 100644 --- a/Kepware.Api/ClientHandler/GenericApiHandler.cs +++ b/Kepware.Api/ClientHandler/GenericApiHandler.cs @@ -50,11 +50,11 @@ internal GenericApiHandler(KepwareApiClient kepwareApiClient, ILoggerThe cancellation token. /// A task that represents the asynchronous operation. The task result contains the comparison result as . - public async Task> CompareAndApply(T? sourceCollection, T? targetCollection, NamedEntity? owner = null, CancellationToken cancellationToken = default) + public async Task> CompareAndApplyAsync(T? sourceCollection, T? targetCollection, NamedEntity? owner = null, CancellationToken cancellationToken = default) where T : EntityCollection where K : NamedEntity, new() { - var result = await CompareAndApplyDetailed(sourceCollection, targetCollection, owner, cancellationToken).ConfigureAwait(false); + var result = await CompareAndApplyDetailedAsync(sourceCollection, targetCollection, owner, cancellationToken).ConfigureAwait(false); return result.CompareResult; } @@ -69,7 +69,7 @@ internal GenericApiHandler(KepwareApiClient kepwareApiClient, ILoggerThe owner of the entities. /// The cancellation token. /// A detailed apply result including successful counts and failed item details. - public async Task> CompareAndApplyDetailed(T? sourceCollection, T? targetCollection, NamedEntity? owner = null, CancellationToken cancellationToken = default) + public async Task> CompareAndApplyDetailedAsync(T? sourceCollection, T? targetCollection, NamedEntity? owner = null, CancellationToken cancellationToken = default) where T : EntityCollection where K : NamedEntity, new() { diff --git a/Kepware.Api/ClientHandler/ProjectApiHandler.cs b/Kepware.Api/ClientHandler/ProjectApiHandler.cs index d65f852..5732470 100644 --- a/Kepware.Api/ClientHandler/ProjectApiHandler.cs +++ b/Kepware.Api/ClientHandler/ProjectApiHandler.cs @@ -59,9 +59,9 @@ public ProjectApiHandler(KepwareApiClient kepwareApiClient, ChannelApiHandler ch /// The source project to compare. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains a tuple with the counts of inserts, updates, and deletes. - public async Task<(int inserts, int updates, int deletes)> CompareAndApply(Project sourceProject, CancellationToken cancellationToken = default) + public async Task<(int inserts, int updates, int deletes)> CompareAndApplyAsync(Project sourceProject, CancellationToken cancellationToken = default) { - var result = await CompareAndApplyDetailed(sourceProject, cancellationToken).ConfigureAwait(false); + var result = await CompareAndApplyDetailedAsync(sourceProject, cancellationToken).ConfigureAwait(false); return (result.Inserts, result.Updates, result.Deletes); } @@ -72,9 +72,9 @@ public ProjectApiHandler(KepwareApiClient kepwareApiClient, ChannelApiHandler ch /// The project loaded from the API. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains a tuple with the counts of inserts, updates, and deletes. - public async Task<(int inserts, int updates, int deletes)> CompareAndApply(Project sourceProject, Project projectFromApi, CancellationToken cancellationToken = default) + public async Task<(int inserts, int updates, int deletes)> CompareAndApplyAsync(Project sourceProject, Project projectFromApi, CancellationToken cancellationToken = default) { - var result = await CompareAndApplyDetailed(sourceProject, projectFromApi, cancellationToken).ConfigureAwait(false); + var result = await CompareAndApplyDetailedAsync(sourceProject, projectFromApi, cancellationToken).ConfigureAwait(false); return (result.Inserts, result.Updates, result.Deletes); } @@ -84,11 +84,11 @@ public ProjectApiHandler(KepwareApiClient kepwareApiClient, ChannelApiHandler ch /// The source project to compare. /// The cancellation token. /// A detailed project apply result including counts and failed items. - public async Task CompareAndApplyDetailed(Project sourceProject, CancellationToken cancellationToken = default) + public async Task CompareAndApplyDetailedAsync(Project sourceProject, CancellationToken cancellationToken = default) { var projectFromApi = await LoadProjectAsync(blnLoadFullProject: true, cancellationToken: cancellationToken).ConfigureAwait(false); await projectFromApi.Cleanup(m_kepwareApiClient, true, cancellationToken).ConfigureAwait(false); - return await CompareAndApplyDetailed(sourceProject, projectFromApi, cancellationToken).ConfigureAwait(false); + return await CompareAndApplyDetailedAsync(sourceProject, projectFromApi, cancellationToken).ConfigureAwait(false); } /// @@ -98,7 +98,7 @@ public async Task CompareAndApplyDetailed(Project /// The project loaded from the API. /// The cancellation token. /// A detailed project apply result including counts and failed items. - public async Task CompareAndApplyDetailed(Project sourceProject, Project projectFromApi, CancellationToken cancellationToken = default) + public async Task CompareAndApplyDetailedAsync(Project sourceProject, Project projectFromApi, CancellationToken cancellationToken = default) { var result = new ProjectCompareAndApplyResult(); @@ -117,27 +117,27 @@ public async Task CompareAndApplyDetailed(Project } } - var channelCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailed(sourceProject.Channels, projectFromApi.Channels, + var channelCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync(sourceProject.Channels, projectFromApi.Channels, cancellationToken: cancellationToken).ConfigureAwait(false); result.Add(channelCompare); foreach (var channel in channelCompare.CompareResult.UnchangedItems.Concat(channelCompare.CompareResult.ChangedItems)) { - var deviceCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailed(channel.Left!.Devices, channel.Right!.Devices, channel.Right, + var deviceCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync(channel.Left!.Devices, channel.Right!.Devices, channel.Right, cancellationToken: cancellationToken).ConfigureAwait(false); result.Add(deviceCompare); foreach (var device in deviceCompare.CompareResult.UnchangedItems.Concat(deviceCompare.CompareResult.ChangedItems)) { - var tagCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailed(device.Left!.Tags, device.Right!.Tags, device.Right, cancellationToken).ConfigureAwait(false); + var tagCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync(device.Left!.Tags, device.Right!.Tags, device.Right, cancellationToken).ConfigureAwait(false); result.Add(tagCompare); - var tagGroupCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailed(device.Left!.TagGroups, device.Right!.TagGroups, device.Right, cancellationToken).ConfigureAwait(false); + var tagGroupCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync(device.Left!.TagGroups, device.Right!.TagGroups, device.Right, cancellationToken).ConfigureAwait(false); result.Add(tagGroupCompare); foreach (var tagGroup in tagGroupCompare.CompareResult.UnchangedItems.Concat(tagGroupCompare.CompareResult.ChangedItems)) { - var tagGroupTagCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailed(tagGroup.Left!.Tags, tagGroup.Right!.Tags, tagGroup.Right, cancellationToken).ConfigureAwait(false); + var tagGroupTagCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync(tagGroup.Left!.Tags, tagGroup.Right!.Tags, tagGroup.Right, cancellationToken).ConfigureAwait(false); result.Add(tagGroupTagCompare); if (tagGroup.Left?.TagGroups != null) @@ -627,12 +627,12 @@ internal static async Task RecusivlyCompareTagGrou { var ret = new ProjectCompareAndApplyResult(); - var tagGroupCompare = await apiClient.GenericConfig.CompareAndApplyDetailed(left, right, owner, cancellationToken: cancellationToken).ConfigureAwait(false); + var tagGroupCompare = await apiClient.GenericConfig.CompareAndApplyDetailedAsync(left, right, owner, cancellationToken: cancellationToken).ConfigureAwait(false); ret.Add(tagGroupCompare); foreach (var tagGroup in tagGroupCompare.CompareResult.UnchangedItems.Concat(tagGroupCompare.CompareResult.ChangedItems)) { - var tagGroupTagCompare = await apiClient.GenericConfig.CompareAndApplyDetailed(tagGroup.Left!.Tags, tagGroup.Right!.Tags, tagGroup.Right, cancellationToken: cancellationToken).ConfigureAwait(false); + var tagGroupTagCompare = await apiClient.GenericConfig.CompareAndApplyDetailedAsync(tagGroup.Left!.Tags, tagGroup.Right!.Tags, tagGroup.Right, cancellationToken: cancellationToken).ConfigureAwait(false); ret.Add(tagGroupTagCompare); if (tagGroup.Left!.TagGroups != null) diff --git a/KepwareSync.Service/SyncService.cs b/KepwareSync.Service/SyncService.cs index a524220..caa6465 100644 --- a/KepwareSync.Service/SyncService.cs +++ b/KepwareSync.Service/SyncService.cs @@ -285,7 +285,7 @@ private async Task SyncProjectToKepServerAsync(string projectSource, Project pro if (await kepServerClient.TestConnectionAsync(cancellationToken).ConfigureAwait(false)) { - var (inserts, updates, deletes) = await kepServerClient.Project.CompareAndApply(project, cancellationToken).ConfigureAwait(false); + var (inserts, updates, deletes) = await kepServerClient.Project.CompareAndApplyAsync(project, cancellationToken).ConfigureAwait(false); if (updates > 0 || deletes > 0 || inserts > 0) { From 389d9234c808cf6c15d84715c3fe1f91c5db074a Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Fri, 27 Mar 2026 15:24:18 -0400 Subject: [PATCH 19/38] chore: removed excess logging --- Kepware.Api/ClientHandler/ProjectApiHandler.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/Kepware.Api/ClientHandler/ProjectApiHandler.cs b/Kepware.Api/ClientHandler/ProjectApiHandler.cs index 5732470..fe4d70d 100644 --- a/Kepware.Api/ClientHandler/ProjectApiHandler.cs +++ b/Kepware.Api/ClientHandler/ProjectApiHandler.cs @@ -316,9 +316,7 @@ public async Task LoadProjectAsync(bool blnLoadFullProject = false, int // If not loading full project, return with just project properties. if (!blnLoadFullProject) { - m_logger.LogInformation("Loaded project properties in {ElapsedMilliseconds} ms", stopwatch.ElapsedMilliseconds); return project; - } From 9231406799b8e96c495b32fe63fb60b646409bca Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Fri, 27 Mar 2026 15:44:27 -0400 Subject: [PATCH 20/38] refactor: renamed properties --- .../ApiClient/GenericHandleTests.cs | 30 +++++++++---------- Kepware.Api/Model/ApplyResults.cs | 18 +++++------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Kepware.Api.Test/ApiClient/GenericHandleTests.cs b/Kepware.Api.Test/ApiClient/GenericHandleTests.cs index 8657f3c..cb6931b 100644 --- a/Kepware.Api.Test/ApiClient/GenericHandleTests.cs +++ b/Kepware.Api.Test/ApiClient/GenericHandleTests.cs @@ -52,13 +52,13 @@ public async Task CompareAndApplyDetailedAsync_ShouldCountUpdateAsFailed_When200 // Assert result.Updates.ShouldBe(0); - result.Failed.ShouldBe(1); - result.Failures.Count.ShouldBe(1); - result.Failures[0].Operation.ShouldBe(ApplyOperation.Update); - (result.Failures[0].AttemptedItem as Device)?.Name.ShouldBe("Device1"); - result.Failures[0].NotAppliedProperties.ShouldNotBeNull(); - result.Failures[0].NotAppliedProperties!.ShouldContain("servermain.DEVICE_ID_OCTAL"); - result.Failures[0].NotAppliedProperties!.ShouldContain("servermain.DEVICE_MODEL"); + result.Failures.ShouldBe(1); + result.FailureList.Count.ShouldBe(1); + result.FailureList[0].Operation.ShouldBe(ApplyOperation.Update); + (result.FailureList[0].AttemptedItem as Device)?.Name.ShouldBe("Device1"); + result.FailureList[0].NotAppliedProperties.ShouldNotBeNull(); + result.FailureList[0].NotAppliedProperties!.ShouldContain("servermain.DEVICE_ID_OCTAL"); + result.FailureList[0].NotAppliedProperties!.ShouldContain("servermain.DEVICE_MODEL"); } [Fact] @@ -98,14 +98,14 @@ public async Task CompareAndApplyDetailedAsync_ShouldMap207InsertFeedbackToItems // Assert result.Inserts.ShouldBe(1); - result.Failed.ShouldBe(1); - result.Failures.Count.ShouldBe(1); - result.Failures[0].Operation.ShouldBe(ApplyOperation.Insert); - (result.Failures[0].AttemptedItem as Tag)?.Name.ShouldBe("Tag1"); - result.Failures[0].ResponseCode.ShouldBe(400); - result.Failures[0].Property.ShouldBe("common.ALLTYPES_NAME"); - result.Failures[0].Description.ShouldBe("The name 'Tag1' is already used."); - result.Failures[0].ErrorLine.ShouldBe(3); + result.Failures.ShouldBe(1); + result.FailureList.Count.ShouldBe(1); + result.FailureList[0].Operation.ShouldBe(ApplyOperation.Insert); + (result.FailureList[0].AttemptedItem as Tag)?.Name.ShouldBe("Tag1"); + result.FailureList[0].ResponseCode.ShouldBe(400); + result.FailureList[0].Property.ShouldBe("common.ALLTYPES_NAME"); + result.FailureList[0].Description.ShouldBe("The name 'Tag1' is already used."); + result.FailureList[0].ErrorLine.ShouldBe(3); } [Fact] diff --git a/Kepware.Api/Model/ApplyResults.cs b/Kepware.Api/Model/ApplyResults.cs index 533a901..cbc3429 100644 --- a/Kepware.Api/Model/ApplyResults.cs +++ b/Kepware.Api/Model/ApplyResults.cs @@ -113,12 +113,12 @@ public CollectionApplyResult(EntityCompare.CollectionResultBucket compareResu /// /// Gets the number of failed operations. /// - public int Failed => m_failures.Count; + public int Failures => m_failures.Count; /// /// Gets the failed operation details. /// - public ReadOnlyCollection Failures => m_failures.AsReadOnly(); + public ReadOnlyCollection FailureList => m_failures.AsReadOnly(); internal void AddInsertSuccess() => Inserts += 1; @@ -154,27 +154,27 @@ public sealed class ProjectCompareAndApplyResult /// /// Gets the number of failed operations. /// - public int Failed => m_failures.Count; + public int Failures => m_failures.Count; /// /// Gets the failed operation details. /// - public ReadOnlyCollection Failures => m_failures.AsReadOnly(); + public ReadOnlyCollection FailureList => m_failures.AsReadOnly(); internal void Add(CollectionApplyResult result) - => Add(result.Inserts, result.Updates, result.Deletes, result.Failures); + => Add(result.Inserts, result.Updates, result.Deletes, result.FailureList); internal void Add(CollectionApplyResult result) - => Add(result.Inserts, result.Updates, result.Deletes, result.Failures); + => Add(result.Inserts, result.Updates, result.Deletes, result.FailureList); internal void Add(CollectionApplyResult result) - => Add(result.Inserts, result.Updates, result.Deletes, result.Failures); + => Add(result.Inserts, result.Updates, result.Deletes, result.FailureList); internal void Add(CollectionApplyResult result) - => Add(result.Inserts, result.Updates, result.Deletes, result.Failures); + => Add(result.Inserts, result.Updates, result.Deletes, result.FailureList); internal void Add(ProjectCompareAndApplyResult result) - => Add(result.Inserts, result.Updates, result.Deletes, result.Failures); + => Add(result.Inserts, result.Updates, result.Deletes, result.FailureList); internal void AddInsertSuccess() => Inserts += 1; From 2256870f180b3eee289813e671f4330337c92482 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Fri, 27 Mar 2026 15:59:19 -0400 Subject: [PATCH 21/38] feat(sync): utilized new Project CompareAndApplyDetailedAsync method to monitor failure counts --- KepwareSync.Service/SyncService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/KepwareSync.Service/SyncService.cs b/KepwareSync.Service/SyncService.cs index caa6465..44ab1f4 100644 --- a/KepwareSync.Service/SyncService.cs +++ b/KepwareSync.Service/SyncService.cs @@ -285,12 +285,12 @@ private async Task SyncProjectToKepServerAsync(string projectSource, Project pro if (await kepServerClient.TestConnectionAsync(cancellationToken).ConfigureAwait(false)) { - var (inserts, updates, deletes) = await kepServerClient.Project.CompareAndApplyAsync(project, cancellationToken).ConfigureAwait(false); + var compareAndApplyResults = await kepServerClient.Project.CompareAndApplyDetailedAsync(project, cancellationToken).ConfigureAwait(false); - if (updates > 0 || deletes > 0 || inserts > 0) + if (compareAndApplyResults.Updates > 0 || compareAndApplyResults.Deletes > 0 || compareAndApplyResults.Inserts > 0 || compareAndApplyResults.Failures > 0) { - m_logger.LogInformation("Completed synchronisation from {ProjectSource} to {ClientName}-kepserver ({ClientHostName}): {NumUpdates} updates, {NumInserts} inserts, {NumDeletes} deletes", - projectSource, clientName, kepServerClient.ClientHostName, updates, inserts, deletes); + m_logger.LogInformation("Completed synchronisation from {ProjectSource} to {ClientName}-kepserver ({ClientHostName}): {NumUpdates} updates, {NumInserts} inserts, {NumDeletes} deletes, {NumFailures} failures", + projectSource, clientName, kepServerClient.ClientHostName, compareAndApplyResults.Updates, compareAndApplyResults.Inserts, compareAndApplyResults.Deletes, compareAndApplyResults.Failures); onSyncedWithChanges?.Invoke(); } else From 27cf682fa14280a395f2e478686ce07f11f9b878 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Fri, 27 Mar 2026 16:26:24 -0400 Subject: [PATCH 22/38] refactor: changed access scope --- Kepware.Api/ClientHandler/GenericApiHandler.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Kepware.Api/ClientHandler/GenericApiHandler.cs b/Kepware.Api/ClientHandler/GenericApiHandler.cs index e03aa4f..ac3c5e3 100644 --- a/Kepware.Api/ClientHandler/GenericApiHandler.cs +++ b/Kepware.Api/ClientHandler/GenericApiHandler.cs @@ -231,7 +231,7 @@ public async Task UpdateItemsAsync(List<(K item, K? oldItem)> item where K : NamedEntity, new() => (await UpdateItemsDetailedAsync(items, owner, cancellationToken).ConfigureAwait(false)).Select(i => i.IsSuccess).ToArray(); - private async Task> UpdateItemsDetailedAsync(List<(K item, K? oldItem)> items, NamedEntity? owner = null, CancellationToken cancellationToken = default) + protected internal async Task> UpdateItemsDetailedAsync(List<(K item, K? oldItem)> items, NamedEntity? owner = null, CancellationToken cancellationToken = default) where T : EntityCollection where K : NamedEntity, new() { @@ -295,6 +295,10 @@ private async Task> UpdateItemsDetailedAsync(List< return result; } + + + protected internal sealed record UpdateItemOutcome(bool IsSuccess, int? ResponseCode = null, string? ResponseMessage = null, IReadOnlyList? NotAppliedProperties = null); + #endregion #region Insert @@ -367,7 +371,7 @@ public async Task InsertItemsAsync(List items, int pageSize = 1 where K : NamedEntity, new() => (await InsertItemsDetailedAsync(items, pageSize, owner, cancellationToken).ConfigureAwait(false)).Select(i => i.IsSuccess).ToArray(); - private async Task> InsertItemsDetailedAsync(List items, int pageSize = 10, NamedEntity? owner = null, CancellationToken cancellationToken = default) + protected internal async Task> InsertItemsDetailedAsync(List items, int pageSize = 10, NamedEntity? owner = null, CancellationToken cancellationToken = default) where T : EntityCollection where K : NamedEntity, new() { @@ -520,9 +524,8 @@ await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false } } - private sealed record UpdateItemOutcome(bool IsSuccess, int? ResponseCode = null, string? ResponseMessage = null, IReadOnlyList? NotAppliedProperties = null); - private sealed record InsertItemOutcome( + protected internal sealed record InsertItemOutcome( bool IsSuccess, int? ResponseCode = null, string? ResponseMessage = null, From b49cedb7a3cdb853d6851aa570d21514ef3fc81b Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Fri, 27 Mar 2026 16:34:27 -0400 Subject: [PATCH 23/38] chore(doc): updated docstrings --- Kepware.Api/ClientHandler/ProjectApiHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Kepware.Api/ClientHandler/ProjectApiHandler.cs b/Kepware.Api/ClientHandler/ProjectApiHandler.cs index fe4d70d..6eaec08 100644 --- a/Kepware.Api/ClientHandler/ProjectApiHandler.cs +++ b/Kepware.Api/ClientHandler/ProjectApiHandler.cs @@ -83,7 +83,7 @@ public ProjectApiHandler(KepwareApiClient kepwareApiClient, ChannelApiHandler ch /// /// The source project to compare. /// The cancellation token. - /// A detailed project apply result including counts and failed items. + /// A including counts and failed items. public async Task CompareAndApplyDetailedAsync(Project sourceProject, CancellationToken cancellationToken = default) { var projectFromApi = await LoadProjectAsync(blnLoadFullProject: true, cancellationToken: cancellationToken).ConfigureAwait(false); @@ -97,7 +97,7 @@ public async Task CompareAndApplyDetailedAsync(Pro /// The source project to compare. /// The project loaded from the API. /// The cancellation token. - /// A detailed project apply result including counts and failed items. + /// A including counts and failed items. public async Task CompareAndApplyDetailedAsync(Project sourceProject, Project projectFromApi, CancellationToken cancellationToken = default) { var result = new ProjectCompareAndApplyResult(); From 7c8d6c86107f7a2d84b50f9f3192ffdd370b9c12 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Tue, 31 Mar 2026 16:46:25 -0400 Subject: [PATCH 24/38] chore: Updated property options to appsettings.json --- KepwareSync.Service/appsettings.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/KepwareSync.Service/appsettings.json b/KepwareSync.Service/appsettings.json index faa757c..e835447 100644 --- a/KepwareSync.Service/appsettings.json +++ b/KepwareSync.Service/appsettings.json @@ -20,6 +20,12 @@ ] }, "Storage": { - "Directory": "ExportedYaml" + "Directory": "ExportedYaml", + "PersistDefaultValue": true + }, + "Sync": { + "SyncMode": "OneWay", + "SyncDirection": "KepwareToDiskAndSecondary", + "SyncThrottlingMs": 1000 } } \ No newline at end of file From 886264c0e3a5cbe2f4c21c4c9e9c9ff96d653879 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Tue, 31 Mar 2026 18:11:49 -0400 Subject: [PATCH 25/38] feat(api): IoT GW added models and associate tests --- .../Model/IotGateway/IotGatewayModelTests.cs | 496 ++++++++++++++++++ Kepware.Api/Model/EntityFactory.cs | 4 + .../Model/Project/IotGateway/IotAgent.cs | 116 ++++ .../IotGateway/IotGateway.Properties.cs | 280 ++++++++++ .../IotGateway/IotGatewayCollections.cs | 50 ++ .../Project/IotGateway/IotGatewayEnums.cs | 160 ++++++ .../Model/Project/IotGateway/IotItem.cs | 102 ++++ .../Project/IotGateway/MqttClientAgent.cs | 169 ++++++ .../Project/IotGateway/PublishingIotAgent.cs | 135 +++++ .../Project/IotGateway/RestClientAgent.cs | 101 ++++ .../Project/IotGateway/RestServerAgent.cs | 92 ++++ Kepware.Api/Model/Properties.cs | 15 + Kepware.Api/Serializer/KepJsonContext.cs | 42 +- 13 files changed, 1761 insertions(+), 1 deletion(-) create mode 100644 Kepware.Api.Test/Model/IotGateway/IotGatewayModelTests.cs create mode 100644 Kepware.Api/Model/Project/IotGateway/IotAgent.cs create mode 100644 Kepware.Api/Model/Project/IotGateway/IotGateway.Properties.cs create mode 100644 Kepware.Api/Model/Project/IotGateway/IotGatewayCollections.cs create mode 100644 Kepware.Api/Model/Project/IotGateway/IotGatewayEnums.cs create mode 100644 Kepware.Api/Model/Project/IotGateway/IotItem.cs create mode 100644 Kepware.Api/Model/Project/IotGateway/MqttClientAgent.cs create mode 100644 Kepware.Api/Model/Project/IotGateway/PublishingIotAgent.cs create mode 100644 Kepware.Api/Model/Project/IotGateway/RestClientAgent.cs create mode 100644 Kepware.Api/Model/Project/IotGateway/RestServerAgent.cs diff --git a/Kepware.Api.Test/Model/IotGateway/IotGatewayModelTests.cs b/Kepware.Api.Test/Model/IotGateway/IotGatewayModelTests.cs new file mode 100644 index 0000000..3807cf6 --- /dev/null +++ b/Kepware.Api.Test/Model/IotGateway/IotGatewayModelTests.cs @@ -0,0 +1,496 @@ +using Kepware.Api.Model; +using Kepware.Api.Serializer; +using System.Text.Json; + +namespace Kepware.Api.Test.Model.IotGateway +{ + public class IotGatewayModelTests + { + #region MqttClientAgent Tests + + [Fact] + public void MqttClientAgent_Deserialize_ShouldPopulateProperties() + { + var json = """ + { + "common.ALLTYPES_NAME": "TestMqttAgent", + "common.ALLTYPES_DESCRIPTION": "A test MQTT client agent", + "iot_gateway.AGENTTYPES_TYPE": "MQTT Client", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_gateway.IGNORE_QUALITY_CHANGES": false, + "iot_gateway.MQTT_CLIENT_URL": "tcp://localhost:1883", + "iot_gateway.MQTT_CLIENT_TOPIC": "iotgateway", + "iot_gateway.MQTT_CLIENT_QOS": 1, + "iot_gateway.AGENTTYPES_PUBLISH_TYPE": 0, + "iot_gateway.AGENTTYPES_RATE_MS": 10000, + "iot_gateway.AGENTTYPES_PUBLISH_FORMAT": 0, + "iot_gateway.AGENTTYPES_MAX_EVENTS": 1000, + "iot_gateway.AGENTTYPES_TIMEOUT_S": 5, + "iot_gateway.AGENTTYPES_MESSAGE_FORMAT": 0, + "iot_gateway.AGENTTYPES_SEND_INITIAL_UPDATE": true, + "iot_gateway.MQTT_CLIENT_ENABLE_LAST_WILL": false, + "iot_gateway.MQTT_CLIENT_ENABLE_WRITE_TOPIC": false, + "iot_gateway.MQTT_TLS_VERSION": 0, + "iot_gateway.MQTT_CLIENT_CERTIFICATE": false + } + """; + + var agent = JsonSerializer.Deserialize(json, KepJsonContext.Default.MqttClientAgent); + + Assert.NotNull(agent); + Assert.Equal("TestMqttAgent", agent.Name); + Assert.Equal("A test MQTT client agent", agent.Description); + Assert.Equal("MQTT Client", agent.AgentType); + Assert.True(agent.Enabled); + Assert.False(agent.IgnoreQualityChanges); + Assert.Equal("tcp://localhost:1883", agent.Url); + Assert.Equal("iotgateway", agent.Topic); + Assert.Equal(MqttQos.AtLeastOnce, agent.Qos); + Assert.Equal(IotPublishType.Interval, agent.PublishType); + Assert.Equal(10000, agent.RateMs); + Assert.Equal(IotPublishFormat.NarrowFormat, agent.PublishFormat); + Assert.Equal(1000, agent.MaxEventsPerPublish); + Assert.Equal(5, agent.TransactionTimeoutS); + Assert.Equal(IotMessageFormat.StandardTemplate, agent.MessageFormat); + Assert.True(agent.SendInitialUpdate); + Assert.False(agent.EnableLastWill); + Assert.False(agent.EnableWriteTopic); + Assert.Equal(MqttTlsVersion.Default, agent.TlsVersion); + + Assert.False(agent.ClientCertificate); + } + + [Fact] + public void MqttClientAgent_SetProperties_ShouldUpdateDynamicProperties() + { + var agent = new MqttClientAgent("TestAgent"); + + agent.Url = "tcp://broker:1883"; + agent.Topic = "test/topic"; + agent.Qos = MqttQos.ExactlyOnce; + agent.Enabled = true; + agent.PublishType = IotPublishType.OnDataChange; + agent.RateMs = 5000; + agent.TlsVersion = MqttTlsVersion.V1_2; + agent.EnableLastWill = true; + agent.LastWillTopic = "lwt/topic"; + agent.LastWillMessage = "offline"; + agent.EnableWriteTopic = true; + agent.WriteTopic = "write/topic"; + + Assert.Equal("tcp://broker:1883", agent.Url); + Assert.Equal("test/topic", agent.Topic); + Assert.Equal(MqttQos.ExactlyOnce, agent.Qos); + Assert.True(agent.Enabled); + Assert.Equal(IotPublishType.OnDataChange, agent.PublishType); + Assert.Equal(5000, agent.RateMs); + Assert.Equal(MqttTlsVersion.V1_2, agent.TlsVersion); + Assert.True(agent.EnableLastWill); + Assert.Equal("lwt/topic", agent.LastWillTopic); + Assert.Equal("offline", agent.LastWillMessage); + Assert.True(agent.EnableWriteTopic); + Assert.Equal("write/topic", agent.WriteTopic); + } + + [Fact] + public void MqttClientAgent_RoundTrip_ShouldPreserveProperties() + { + var agent = new MqttClientAgent("RoundTripAgent"); + agent.Enabled = true; + agent.Url = "tcp://localhost:1883"; + agent.Topic = "test"; + agent.Qos = MqttQos.AtLeastOnce; + agent.PublishType = IotPublishType.Interval; + agent.RateMs = 10000; + + var json = JsonSerializer.Serialize(agent, KepJsonContext.Default.MqttClientAgent); + var deserialized = JsonSerializer.Deserialize(json, KepJsonContext.Default.MqttClientAgent); + + Assert.NotNull(deserialized); + Assert.Equal("RoundTripAgent", deserialized.Name); + Assert.True(deserialized.Enabled); + Assert.Equal("tcp://localhost:1883", deserialized.Url); + Assert.Equal("test", deserialized.Topic); + Assert.Equal(MqttQos.AtLeastOnce, deserialized.Qos); + Assert.Equal(IotPublishType.Interval, deserialized.PublishType); + Assert.Equal(10000, deserialized.RateMs); + } + + [Fact] + public void MqttClientAgent_WithIotItems_ShouldDeserializeChildren() + { + var json = """ + { + "common.ALLTYPES_NAME": "AgentWithItems", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_items": [ + { + "common.ALLTYPES_NAME": "Item1", + "iot_gateway.IOT_ITEM_SERVER_TAG": "Channel1.Device1.Tag1", + "iot_gateway.IOT_ITEM_ENABLED": true, + "iot_gateway.IOT_ITEM_SCAN_RATE_MS": 1000 + } + ] + } + """; + + var agent = JsonSerializer.Deserialize(json, KepJsonContext.Default.MqttClientAgent); + + Assert.NotNull(agent); + Assert.NotNull(agent.IotItems); + Assert.Single(agent.IotItems); + Assert.Equal("Item1", agent.IotItems[0].Name); + Assert.Equal("Channel1.Device1.Tag1", agent.IotItems[0].ServerTag); + Assert.True(agent.IotItems[0].Enabled); + Assert.Equal(1000, agent.IotItems[0].ScanRateMs); + } + + #endregion + + #region RestClientAgent Tests + + [Fact] + public void RestClientAgent_Deserialize_ShouldPopulateProperties() + { + var json = """ + { + "common.ALLTYPES_NAME": "TestRestClient", + "iot_gateway.AGENTTYPES_TYPE": "REST Client", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_gateway.REST_CLIENT_URL": "http://127.0.0.1:3000", + "iot_gateway.REST_CLIENT_METHOD": 0, + "iot_gateway.AGENTTYPES_PUBLISH_TYPE": 1, + "iot_gateway.AGENTTYPES_RATE_MS": 5000, + "iot_gateway.AGENTTYPES_PUBLISH_FORMAT": 1, + "iot_gateway.AGENTTYPES_MESSAGE_FORMAT": 1, + "iot_gateway.REST_CLIENT_PUBLISH_MEDIA_TYPE": 0, + "iot_gateway.BUFFER_ON_FAILED_PUBLISH": true, + "iot_gateway.AGENTTYPES_SEND_INITIAL_UPDATE": true + } + """; + + var agent = JsonSerializer.Deserialize(json, KepJsonContext.Default.RestClientAgent); + + Assert.NotNull(agent); + Assert.Equal("TestRestClient", agent.Name); + Assert.Equal("REST Client", agent.AgentType); + Assert.True(agent.Enabled); + Assert.Equal("http://127.0.0.1:3000", agent.Url); + Assert.Equal(RestClientHttpMethod.Post, agent.HttpMethod); + Assert.Equal(IotPublishType.OnDataChange, agent.PublishType); + Assert.Equal(5000, agent.RateMs); + Assert.Equal(IotPublishFormat.WideFormat, agent.PublishFormat); + Assert.Equal(IotMessageFormat.AdvancedTemplate, agent.MessageFormat); + Assert.Equal(RestClientMediaType.ApplicationJson, agent.PublishMediaType); + Assert.True(agent.BufferOnFailedPublish); + Assert.True(agent.SendInitialUpdate); + } + + [Fact] + public void RestClientAgent_SetProperties_ShouldUpdateDynamicProperties() + { + var agent = new RestClientAgent("TestAgent"); + + agent.Url = "https://api.example.com"; + agent.HttpMethod = RestClientHttpMethod.Put; + agent.HttpHeader = "Authorization: Bearer token123"; + agent.PublishMediaType = RestClientMediaType.TextPlain; + agent.Username = "user"; + agent.Password = "pass"; + agent.BufferOnFailedPublish = false; + + Assert.Equal("https://api.example.com", agent.Url); + Assert.Equal(RestClientHttpMethod.Put, agent.HttpMethod); + Assert.Equal("Authorization: Bearer token123", agent.HttpHeader); + Assert.Equal(RestClientMediaType.TextPlain, agent.PublishMediaType); + Assert.Equal("user", agent.Username); + Assert.Equal("pass", agent.Password); + Assert.False(agent.BufferOnFailedPublish); + } + + [Fact] + public void RestClientAgent_RoundTrip_ShouldPreserveProperties() + { + var agent = new RestClientAgent("RoundTripRestClient"); + agent.Enabled = true; + agent.Url = "https://api.example.com"; + agent.HttpMethod = RestClientHttpMethod.Put; + agent.PublishType = IotPublishType.OnDataChange; + agent.RateMs = 5000; + agent.PublishFormat = IotPublishFormat.WideFormat; + agent.BufferOnFailedPublish = true; + + var json = JsonSerializer.Serialize(agent, KepJsonContext.Default.RestClientAgent); + var deserialized = JsonSerializer.Deserialize(json, KepJsonContext.Default.RestClientAgent); + + Assert.NotNull(deserialized); + Assert.Equal("RoundTripRestClient", deserialized.Name); + Assert.True(deserialized.Enabled); + Assert.Equal("https://api.example.com", deserialized.Url); + Assert.Equal(RestClientHttpMethod.Put, deserialized.HttpMethod); + Assert.Equal(IotPublishType.OnDataChange, deserialized.PublishType); + Assert.Equal(5000, deserialized.RateMs); + Assert.Equal(IotPublishFormat.WideFormat, deserialized.PublishFormat); + Assert.True(deserialized.BufferOnFailedPublish); + } + + #endregion + + #region RestServerAgent Tests + + [Fact] + public void RestServerAgent_Deserialize_ShouldPopulateProperties() + { + var json = """ + { + "common.ALLTYPES_NAME": "TestRestServer", + "iot_gateway.AGENTTYPES_TYPE": "REST Server", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_gateway.IGNORE_QUALITY_CHANGES": false, + "iot_gateway.REST_SERVER_PORT_NUMBER": 39320, + "iot_gateway.REST_SERVER_USE_HTTPS": true, + "iot_gateway.REST_SERVER_ENABLE_WRITE_ENDPOINT": false, + "iot_gateway.REST_SERVER_ALLOW_ANONYMOUS_LOGIN": false + } + """; + + var agent = JsonSerializer.Deserialize(json, KepJsonContext.Default.RestServerAgent); + + Assert.NotNull(agent); + Assert.Equal("TestRestServer", agent.Name); + Assert.Equal("REST Server", agent.AgentType); + Assert.True(agent.Enabled); + Assert.False(agent.IgnoreQualityChanges); + Assert.Equal(39320, agent.PortNumber); + Assert.True(agent.UseHttps); + Assert.False(agent.EnableWriteEndpoint); + Assert.False(agent.AllowAnonymousLogin); + } + + [Fact] + public void RestServerAgent_ShouldNotHavePublishProperties() + { + // Verify that RestServerAgent inherits from IotAgent directly, not PublishingIotAgent + Assert.False(typeof(RestServerAgent).IsSubclassOf(typeof(PublishingIotAgent))); + Assert.True(typeof(RestServerAgent).IsSubclassOf(typeof(IotAgent))); + } + + [Fact] + public void RestServerAgent_SetProperties_ShouldUpdateDynamicProperties() + { + var agent = new RestServerAgent("TestServer"); + + agent.PortNumber = 8080; + agent.UseHttps = false; + agent.EnableWriteEndpoint = true; + agent.AllowAnonymousLogin = true; + agent.CorsAllowedOrigins = "http://localhost:3000,http://example.com"; + agent.Enabled = true; + + Assert.Equal(8080, agent.PortNumber); + Assert.False(agent.UseHttps); + Assert.True(agent.EnableWriteEndpoint); + Assert.True(agent.AllowAnonymousLogin); + Assert.Equal("http://localhost:3000,http://example.com", agent.CorsAllowedOrigins); + Assert.True(agent.Enabled); + } + + [Fact] + public void RestServerAgent_RoundTrip_ShouldPreserveProperties() + { + var agent = new RestServerAgent("RoundTripRestServer"); + agent.Enabled = true; + agent.PortNumber = 8080; + agent.UseHttps = false; + agent.EnableWriteEndpoint = true; + agent.AllowAnonymousLogin = true; + agent.CorsAllowedOrigins = "http://localhost:3000"; + + var json = JsonSerializer.Serialize(agent, KepJsonContext.Default.RestServerAgent); + var deserialized = JsonSerializer.Deserialize(json, KepJsonContext.Default.RestServerAgent); + + Assert.NotNull(deserialized); + Assert.Equal("RoundTripRestServer", deserialized.Name); + Assert.True(deserialized.Enabled); + Assert.Equal(8080, deserialized.PortNumber); + Assert.False(deserialized.UseHttps); + Assert.True(deserialized.EnableWriteEndpoint); + Assert.True(deserialized.AllowAnonymousLogin); + Assert.Equal("http://localhost:3000", deserialized.CorsAllowedOrigins); + } + + #endregion + + #region IotItem Tests + + [Fact] + public void IotItem_Deserialize_ShouldPopulateProperties() + { + var json = """ + { + "common.ALLTYPES_NAME": "TestItem", + "common.ALLTYPES_DESCRIPTION": "A test IoT item", + "iot_gateway.IOT_ITEM_SERVER_TAG": "Channel1.Device1.Tag1", + "iot_gateway.IOT_ITEM_USE_SCAN_RATE": true, + "iot_gateway.IOT_ITEM_SCAN_RATE_MS": 500, + "iot_gateway.IOT_ITEM_SEND_EVERY_SCAN": false, + "iot_gateway.IOT_ITEM_DEADBAND_PERCENT": 2.5, + "iot_gateway.IOT_ITEM_ENABLED": true, + "iot_gateway.IOT_ITEM_DATA_TYPE": -1 + } + """; + + var item = JsonSerializer.Deserialize(json, KepJsonContext.Default.IotItem); + + Assert.NotNull(item); + Assert.Equal("TestItem", item.Name); + Assert.Equal("A test IoT item", item.Description); + Assert.Equal("Channel1.Device1.Tag1", item.ServerTag); + Assert.True(item.UseScanRate); + Assert.Equal(500, item.ScanRateMs); + Assert.False(item.PublishEveryScan); + Assert.Equal(2.5, item.DeadbandPercent); + Assert.True(item.Enabled); + Assert.Equal(IotItemDataType.Default, item.DataType); + } + + [Fact] + public void IotItem_SetProperties_ShouldUpdateDynamicProperties() + { + var item = new IotItem("Item1"); + + item.ServerTag = "Channel1.Device1.Tag2"; + item.UseScanRate = false; + item.ScanRateMs = 2000; + item.PublishEveryScan = true; + item.DeadbandPercent = 5.0; + item.Enabled = false; + item.DataType = IotItemDataType.Float; + + Assert.Equal("Channel1.Device1.Tag2", item.ServerTag); + Assert.False(item.UseScanRate); + Assert.Equal(2000, item.ScanRateMs); + Assert.True(item.PublishEveryScan); + Assert.Equal(5.0, item.DeadbandPercent); + Assert.False(item.Enabled); + Assert.Equal(IotItemDataType.Float, item.DataType); + } + + [Fact] + public void IotItem_RoundTrip_ShouldPreserveProperties() + { + var item = new IotItem("RoundTripItem"); + item.ServerTag = "Ch1.Dev1.Tag1"; + item.Enabled = true; + item.ScanRateMs = 1000; + item.DataType = IotItemDataType.Double; + + var json = JsonSerializer.Serialize(item, KepJsonContext.Default.IotItem); + var deserialized = JsonSerializer.Deserialize(json, KepJsonContext.Default.IotItem); + + Assert.NotNull(deserialized); + Assert.Equal("RoundTripItem", deserialized.Name); + Assert.Equal("Ch1.Dev1.Tag1", deserialized.ServerTag); + Assert.True(deserialized.Enabled); + Assert.Equal(1000, deserialized.ScanRateMs); + Assert.Equal(IotItemDataType.Double, deserialized.DataType); + } + + #endregion + + #region Inheritance Tests + + [Fact] + public void MqttClientAgent_ShouldInheritFromPublishingIotAgent() + { + Assert.True(typeof(MqttClientAgent).IsSubclassOf(typeof(PublishingIotAgent))); + Assert.True(typeof(MqttClientAgent).IsSubclassOf(typeof(IotAgent))); + Assert.True(typeof(MqttClientAgent).IsSubclassOf(typeof(NamedEntity))); + } + + [Fact] + public void RestClientAgent_ShouldInheritFromPublishingIotAgent() + { + Assert.True(typeof(RestClientAgent).IsSubclassOf(typeof(PublishingIotAgent))); + Assert.True(typeof(RestClientAgent).IsSubclassOf(typeof(IotAgent))); + Assert.True(typeof(RestClientAgent).IsSubclassOf(typeof(NamedEntity))); + } + + [Fact] + public void RestServerAgent_ShouldInheritFromIotAgentDirectly() + { + Assert.True(typeof(RestServerAgent).IsSubclassOf(typeof(IotAgent))); + Assert.True(typeof(RestServerAgent).IsSubclassOf(typeof(NamedEntity))); + Assert.False(typeof(RestServerAgent).IsSubclassOf(typeof(PublishingIotAgent))); + } + + [Fact] + public void IotItem_ShouldInheritFromNamedEntity() + { + Assert.True(typeof(IotItem).IsSubclassOf(typeof(NamedEntity))); + Assert.False(typeof(IotItem).IsSubclassOf(typeof(IotAgent))); + } + + #endregion + + #region Collection Deserialization Tests + + [Fact] + public void MqttClientAgentCollection_Deserialize_ShouldWork() + { + var json = """ + [ + { + "common.ALLTYPES_NAME": "Agent1", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_gateway.MQTT_CLIENT_URL": "tcp://localhost:1883" + }, + { + "common.ALLTYPES_NAME": "Agent2", + "iot_gateway.AGENTTYPES_ENABLED": false, + "iot_gateway.MQTT_CLIENT_URL": "tcp://broker:1883" + } + ] + """; + + var agents = JsonSerializer.Deserialize>(json, KepJsonContext.Default.ListMqttClientAgent); + + Assert.NotNull(agents); + Assert.Equal(2, agents.Count); + Assert.Equal("Agent1", agents[0].Name); + Assert.Equal("Agent2", agents[1].Name); + Assert.True(agents[0].Enabled); + Assert.False(agents[1].Enabled); + } + + [Fact] + public void IotItemCollection_Deserialize_ShouldWork() + { + var json = """ + [ + { + "common.ALLTYPES_NAME": "Item1", + "iot_gateway.IOT_ITEM_SERVER_TAG": "Ch1.Dev1.Tag1", + "iot_gateway.IOT_ITEM_ENABLED": true + }, + { + "common.ALLTYPES_NAME": "Item2", + "iot_gateway.IOT_ITEM_SERVER_TAG": "Ch1.Dev1.Tag2", + "iot_gateway.IOT_ITEM_ENABLED": false + } + ] + """; + + var items = JsonSerializer.Deserialize>(json, KepJsonContext.Default.ListIotItem); + + Assert.NotNull(items); + Assert.Equal(2, items.Count); + Assert.Equal("Item1", items[0].Name); + Assert.Equal("Item2", items[1].Name); + Assert.True(items[0].Enabled); + Assert.False(items[1].Enabled); + } + + #endregion + } +} diff --git a/Kepware.Api/Model/EntityFactory.cs b/Kepware.Api/Model/EntityFactory.cs index 39b1e4b..0606186 100644 --- a/Kepware.Api/Model/EntityFactory.cs +++ b/Kepware.Api/Model/EntityFactory.cs @@ -19,6 +19,10 @@ static EntityFactory() Factories[typeof(Tag)] = () => new Tag(); Factories[typeof(DefaultEntity)] = () => new DefaultEntity(); Factories[typeof(NamedEntity)] = () => new NamedEntity(); + Factories[typeof(MqttClientAgent)] = () => new MqttClientAgent(); + Factories[typeof(RestClientAgent)] = () => new RestClientAgent(); + Factories[typeof(RestServerAgent)] = () => new RestServerAgent(); + Factories[typeof(IotItem)] = () => new IotItem(); } public static BaseEntity CreateInstance(Type type) diff --git a/Kepware.Api/Model/Project/IotGateway/IotAgent.cs b/Kepware.Api/Model/Project/IotGateway/IotAgent.cs new file mode 100644 index 0000000..5ce3794 --- /dev/null +++ b/Kepware.Api/Model/Project/IotGateway/IotAgent.cs @@ -0,0 +1,116 @@ +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; + +namespace Kepware.Api.Model +{ + /// + /// Base class for all IoT Gateway agent types. Contains the universal properties + /// shared by MQTT Client, REST Client, and REST Server agents. + /// + public class IotAgent : NamedEntity + { + /// + /// Initializes a new instance of the class. + /// + public IotAgent() + { + } + + /// + /// Initializes a new instance of the class with the specified name. + /// + /// The name of the agent. + public IotAgent(string name) : base(name) + { + } + + #region Properties + + /// + /// Gets or sets the IoT Items in this agent. + /// + [YamlIgnore] + [JsonPropertyName("iot_items")] + [JsonPropertyOrder(100)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IotItemCollection? IotItems { get; set; } + + /// + /// Gets the agent type identifier (read-only). + /// + [YamlIgnore, JsonIgnore] + public string? AgentType + { + get => GetDynamicProperty(Properties.IotAgent.AgentType); + } + + /// + /// Gets or sets whether the agent is enabled. + /// + [YamlIgnore, JsonIgnore] + public bool? Enabled + { + get => GetDynamicProperty(Properties.IotAgent.Enabled); + set => SetDynamicProperty(Properties.IotAgent.Enabled, value); + } + + /// + /// Gets or sets whether quality changes should be ignored. + /// + [YamlIgnore, JsonIgnore] + public bool? IgnoreQualityChanges + { + get => GetDynamicProperty(Properties.IotAgent.IgnoreQualityChanges); + set => SetDynamicProperty(Properties.IotAgent.IgnoreQualityChanges, value); + } + + /// + /// Gets the total number of tags configured under this agent (read-only). + /// + [YamlIgnore, JsonIgnore] + public int? ThisAgentTotal + { + get => GetDynamicProperty(Properties.IotAgent.ThisAgentTotal); + } + + /// + /// Gets the total number of tags configured under all agents (read-only). + /// + [YamlIgnore, JsonIgnore] + public int? AllAgentsTotal + { + get => GetDynamicProperty(Properties.IotAgent.AllAgentsTotal); + } + + /// + /// Gets the license limit for configured tags (read-only). + /// + [YamlIgnore, JsonIgnore] + public string? LicenseLimit + { + get => GetDynamicProperty(Properties.IotAgent.LicenseLimit); + } + + #endregion + + /// + /// Recursively cleans up the agent and all IoT Items. + /// + /// The default value provider. + /// Whether to remove the project ID. + /// A token to cancel the operation. + /// A task that represents the asynchronous cleanup operation. + public override async Task Cleanup(IKepwareDefaultValueProvider defaultValueProvider, bool blnRemoveProjectId = false, CancellationToken cancellationToken = default) + { + await base.Cleanup(defaultValueProvider, blnRemoveProjectId, cancellationToken).ConfigureAwait(false); + + if (IotItems != null) + { + foreach (var item in IotItems) + { + await item.Cleanup(defaultValueProvider, blnRemoveProjectId, cancellationToken).ConfigureAwait(false); + } + } + } + } +} diff --git a/Kepware.Api/Model/Project/IotGateway/IotGateway.Properties.cs b/Kepware.Api/Model/Project/IotGateway/IotGateway.Properties.cs new file mode 100644 index 0000000..9933d20 --- /dev/null +++ b/Kepware.Api/Model/Project/IotGateway/IotGateway.Properties.cs @@ -0,0 +1,280 @@ +namespace Kepware.Api.Model +{ + public partial class Properties + { + /// + /// Property key constants shared by all IoT Gateway agent types. + /// + public static class IotAgent + { + /// + /// The agent type identifier (read-only). + /// + public const string AgentType = "iot_gateway.AGENTTYPES_TYPE"; + + /// + /// Specifies if the agent is enabled. + /// + public const string Enabled = "iot_gateway.AGENTTYPES_ENABLED"; + + /// + /// Specifies if changes in quality should be ignored. + /// + public const string IgnoreQualityChanges = "iot_gateway.IGNORE_QUALITY_CHANGES"; + + /// + /// Specifies the publish type (Interval or On Data Change). + /// + public const string PublishType = "iot_gateway.AGENTTYPES_PUBLISH_TYPE"; + + /// + /// Specifies the publish rate in milliseconds. + /// + public const string RateMs = "iot_gateway.AGENTTYPES_RATE_MS"; + + /// + /// Specifies the publish format (Wide or Narrow). + /// + public const string PublishFormat = "iot_gateway.AGENTTYPES_PUBLISH_FORMAT"; + + /// + /// Maximum number of tag events per publish in narrow format. + /// + public const string MaxEventsPerPublish = "iot_gateway.AGENTTYPES_MAX_EVENTS"; + + /// + /// Transaction timeout in seconds. + /// + public const string TransactionTimeoutS = "iot_gateway.AGENTTYPES_TIMEOUT_S"; + + /// + /// Specifies the message format (Standard or Advanced Template). + /// + public const string MessageFormat = "iot_gateway.AGENTTYPES_MESSAGE_FORMAT"; + + /// + /// Standard template for message formatting. + /// + public const string StandardTemplate = "iot_gateway.AGENTTYPES_STANDARD_TEMPLATE"; + + /// + /// Expansion of |VALUES| in standard template. + /// + public const string ExpansionOfValues = "iot_gateway.AGENTTYPES_EXPANSION_OF_VALUES"; + + /// + /// Advanced template for message formatting. + /// + public const string AdvancedTemplate = "iot_gateway.AGENTTYPES_ADVANCED_TEMPLATE"; + + /// + /// Specifies if the initial update should be sent when the agent starts. + /// + public const string SendInitialUpdate = "iot_gateway.AGENTTYPES_SEND_INITIAL_UPDATE"; + + /// + /// Total number of tags configured under this agent (read-only). + /// + public const string ThisAgentTotal = Properties.NonSerialized.ThisAgentTotal; + + /// + /// Total number of tags configured under all agents (read-only). + /// + public const string AllAgentsTotal = Properties.NonSerialized.AllAgentsTotal; + + /// + /// Maximum number of configured tags allowed by the license (read-only). + /// + public const string LicenseLimit = Properties.NonSerialized.LicenseLimit; + } + + /// + /// Property key constants specific to MQTT Client agents. + /// + public static class MqttClientAgent + { + /// + /// URL of the MQTT broker endpoint. + /// + public const string Url = "iot_gateway.MQTT_CLIENT_URL"; + + /// + /// Topic name for publishing data on the broker. + /// + public const string Topic = "iot_gateway.MQTT_CLIENT_TOPIC"; + + /// + /// MQTT Quality of Service level. + /// + public const string Qos = "iot_gateway.MQTT_CLIENT_QOS"; + + /// + /// Unique client identity for broker communication. + /// + public const string ClientId = "iot_gateway.MQTT_CLIENT_CLIENT_ID"; + + /// + /// Username for broker authentication. + /// + public const string Username = "iot_gateway.MQTT_CLIENT_USERNAME"; + + /// + /// Password for broker authentication. + /// + public const string Password = "iot_gateway.MQTT_CLIENT_PASSWORD"; + + /// + /// TLS version for secure connections. + /// + public const string TlsVersion = "iot_gateway.MQTT_TLS_VERSION"; + + /// + /// Enable client certificate for application-based authentication. + /// + public const string ClientCertificate = "iot_gateway.MQTT_CLIENT_CERTIFICATE"; + + /// + /// Enable Last Will and Testament. + /// + public const string EnableLastWill = "iot_gateway.MQTT_CLIENT_ENABLE_LAST_WILL"; + + /// + /// Topic for Last Will and Testament message. + /// + public const string LastWillTopic = "iot_gateway.MQTT_CLIENT_LAST_WILL_TOPIC"; + + /// + /// Last Will and Testament message text. + /// + public const string LastWillMessage = "iot_gateway.MQTT_CLIENT_LAST_WILL_MESSAGE"; + + /// + /// Enable listening for write requests. + /// + public const string EnableWriteTopic = "iot_gateway.MQTT_CLIENT_ENABLE_WRITE_TOPIC"; + + /// + /// Topic for write request subscriptions. + /// + public const string WriteTopic = "iot_gateway.MQTT_CLIENT_WRITE_TOPIC"; + } + + /// + /// Property key constants specific to REST Client agents. + /// + public static class RestClientAgent + { + /// + /// URL of the REST endpoint. + /// + public const string Url = "iot_gateway.REST_CLIENT_URL"; + + /// + /// HTTP method for publishing data (POST or PUT). + /// + public const string HttpMethod = "iot_gateway.REST_CLIENT_METHOD"; + + /// + /// HTTP header name-value pairs sent with each connection. + /// + public const string HttpHeader = "iot_gateway.REST_CLIENT_HTTP_HEADER"; + + /// + /// Content-type header for published data. + /// + public const string PublishMediaType = "iot_gateway.REST_CLIENT_PUBLISH_MEDIA_TYPE"; + + /// + /// Username for basic HTTP authentication. + /// + public const string Username = "iot_gateway.REST_CLIENT_USERNAME"; + + /// + /// Password for basic HTTP authentication. + /// + public const string Password = "iot_gateway.REST_CLIENT_PASSWORD"; + + /// + /// Buffer updates when a publish fails. + /// + public const string BufferOnFailedPublish = "iot_gateway.BUFFER_ON_FAILED_PUBLISH"; + } + + /// + /// Property key constants specific to REST Server agents. + /// + public static class RestServerAgent + { + /// + /// Network adapter for the REST server endpoint. + /// + public const string NetworkAdapter = "iot_gateway.REST_SERVER_NETWORK_ADAPTER"; + + /// + /// Port number for the REST server. + /// + public const string PortNumber = "iot_gateway.REST_SERVER_PORT_NUMBER"; + + /// + /// CORS allowed origins (comma-delimited). + /// + public const string CorsAllowedOrigins = "iot_gateway.REST_SERVER_CORS_ALLOWED_ORIGINS"; + + /// + /// Enable HTTPS encryption. + /// + public const string UseHttps = "iot_gateway.REST_SERVER_USE_HTTPS"; + + /// + /// Enable write endpoint for tag writes. + /// + public const string EnableWriteEndpoint = "iot_gateway.REST_SERVER_ENABLE_WRITE_ENDPOINT"; + + /// + /// Allow unauthenticated access. + /// + public const string AllowAnonymousLogin = "iot_gateway.REST_SERVER_ALLOW_ANONYMOUS_LOGIN"; + } + + /// + /// Property key constants for IoT Items. + /// + public static class IotItem + { + /// + /// Full channel.device.name of the referenced server tag. + /// + public const string ServerTag = "iot_gateway.IOT_ITEM_SERVER_TAG"; + + /// + /// Use scan rate to collect data from the device. + /// + public const string UseScanRate = "iot_gateway.IOT_ITEM_USE_SCAN_RATE"; + + /// + /// Scan rate in milliseconds. + /// + public const string ScanRateMs = "iot_gateway.IOT_ITEM_SCAN_RATE_MS"; + + /// + /// Force publish on every scan regardless of value change. + /// + public const string PublishEveryScan = "iot_gateway.IOT_ITEM_SEND_EVERY_SCAN"; + + /// + /// Deadband percentage for publish threshold. + /// + public const string DeadbandPercent = "iot_gateway.IOT_ITEM_DEADBAND_PERCENT"; + + /// + /// Enable or disable the IoT Item. + /// + public const string Enabled = "iot_gateway.IOT_ITEM_ENABLED"; + + /// + /// Data type of the referenced tag. + /// + public const string DataType = "iot_gateway.IOT_ITEM_DATA_TYPE"; + } + } +} diff --git a/Kepware.Api/Model/Project/IotGateway/IotGatewayCollections.cs b/Kepware.Api/Model/Project/IotGateway/IotGatewayCollections.cs new file mode 100644 index 0000000..d480d37 --- /dev/null +++ b/Kepware.Api/Model/Project/IotGateway/IotGatewayCollections.cs @@ -0,0 +1,50 @@ +namespace Kepware.Api.Model +{ + /// + /// Represents the collection of MQTT Client agents in the IoT Gateway. + /// + [Endpoint("/config/v1/project/_iot_gateway/mqtt_clients")] + public class MqttClientAgentCollection : EntityCollection + { + /// + /// Initializes a new instance of the class. + /// + public MqttClientAgentCollection() { } + } + + /// + /// Represents the collection of REST Client agents in the IoT Gateway. + /// + [Endpoint("/config/v1/project/_iot_gateway/rest_clients")] + public class RestClientAgentCollection : EntityCollection + { + /// + /// Initializes a new instance of the class. + /// + public RestClientAgentCollection() { } + } + + /// + /// Represents the collection of REST Server agents in the IoT Gateway. + /// + [Endpoint("/config/v1/project/_iot_gateway/rest_servers")] + public class RestServerAgentCollection : EntityCollection + { + /// + /// Initializes a new instance of the class. + /// + public RestServerAgentCollection() { } + } + + /// + /// Represents the collection of IoT Items in an IoT Gateway agent. + /// + [Endpoint("/config/v1/project/_iot_gateway/mqtt_clients/{agentName}/iot_items")] + public class IotItemCollection : EntityCollection + { + /// + /// Initializes a new instance of the class. + /// + public IotItemCollection() { } + } +} diff --git a/Kepware.Api/Model/Project/IotGateway/IotGatewayEnums.cs b/Kepware.Api/Model/Project/IotGateway/IotGatewayEnums.cs new file mode 100644 index 0000000..b6fe7e2 --- /dev/null +++ b/Kepware.Api/Model/Project/IotGateway/IotGatewayEnums.cs @@ -0,0 +1,160 @@ +namespace Kepware.Api.Model +{ + /// + /// Specifies the publish type for IoT Gateway agents. + /// + public enum IotPublishType + { + /// Publish at a set interval rate. + Interval = 0, + /// Publish on data change. + OnDataChange = 1 + } + + /// + /// Specifies the publish format for IoT Gateway agents. + /// + public enum IotPublishFormat + { + /// Output based on tags that have changed value or quality. + NarrowFormat = 0, + /// Output includes all enabled tags regardless of changes. + WideFormat = 1 + } + + /// + /// Specifies the message format for IoT Gateway agents. + /// + public enum IotMessageFormat + { + /// Standard template format. + StandardTemplate = 0, + /// Advanced template format. + AdvancedTemplate = 1 + } + + /// + /// Specifies the MQTT Quality of Service level. + /// + public enum MqttQos + { + /// At most once delivery (fire and forget). + AtMostOnce = 0, + /// At least once delivery (acknowledged delivery). + AtLeastOnce = 1, + /// Exactly once delivery (assured delivery). + ExactlyOnce = 2 + } + + /// + /// Specifies the TLS version for MQTT secure connections. + /// + public enum MqttTlsVersion + { + /// Default TLS version. + Default = 0, + /// TLS version 1.0. + V1_0 = 1, + /// TLS version 1.1. + V1_1 = 2, + /// TLS version 1.2. + V1_2 = 3 + } + + /// + /// Specifies the HTTP method for REST Client agents. + /// + public enum RestClientHttpMethod + { + /// HTTP POST method. + Post = 0, + /// HTTP PUT method. + Put = 1 + } + + /// + /// Specifies the content-type header for REST Client agent publish. + /// + public enum RestClientMediaType + { + /// application/json + ApplicationJson = 0, + /// application/xml + ApplicationXml = 1, + /// application/xhtml+xml + ApplicationXhtmlXml = 2, + /// text/plain + TextPlain = 3, + /// text/html + TextHtml = 4 + } + + /// + /// Specifies the data type for IoT Items. + /// + public enum IotItemDataType + { + /// Default / auto-detect. + Default = -1, + /// String data type. + String = 0, + /// Boolean data type. + Boolean = 1, + /// Char data type. + Char = 2, + /// Byte data type. + Byte = 3, + /// Short (16-bit signed integer) data type. + Short = 4, + /// Word (16-bit unsigned integer) data type. + Word = 5, + /// Long (32-bit signed integer) data type. + Long = 6, + /// DWord (32-bit unsigned integer) data type. + DWord = 7, + /// Float (32-bit floating point) data type. + Float = 8, + /// Double (64-bit floating point) data type. + Double = 9, + /// BCD (binary-coded decimal) data type. + BCD = 10, + /// LBCD (long binary-coded decimal) data type. + LBCD = 11, + /// Date data type. + Date = 12, + /// LLong (64-bit signed integer) data type. + LLong = 13, + /// QWord (64-bit unsigned integer) data type. + QWord = 14, + /// String array data type. + StringArray = 20, + /// Boolean array data type. + BooleanArray = 21, + /// Char array data type. + CharArray = 22, + /// Byte array data type. + ByteArray = 23, + /// Short array data type. + ShortArray = 24, + /// Word array data type. + WordArray = 25, + /// Long array data type. + LongArray = 26, + /// DWord array data type. + DWordArray = 27, + /// Float array data type. + FloatArray = 28, + /// Double array data type. + DoubleArray = 29, + /// BCD array data type. + BCDArray = 30, + /// LBCD array data type. + LBCDArray = 31, + /// Date array data type. + DateArray = 32, + /// LLong array data type. + LLongArray = 33, + /// QWord array data type. + QWordArray = 34 + } +} diff --git a/Kepware.Api/Model/Project/IotGateway/IotItem.cs b/Kepware.Api/Model/Project/IotGateway/IotItem.cs new file mode 100644 index 0000000..623996e --- /dev/null +++ b/Kepware.Api/Model/Project/IotGateway/IotItem.cs @@ -0,0 +1,102 @@ +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; + +namespace Kepware.Api.Model +{ + /// + /// Represents an IoT Item in the IoT Gateway. IoT Items are children of agent types + /// and identify the data objects exposed by the agent. + /// + [Endpoint("/config/v1/project/_iot_gateway/mqtt_clients/{agentName}/iot_items/{name}")] + public class IotItem : NamedEntity + { + /// + /// Initializes a new instance of the class. + /// + public IotItem() + { + } + + /// + /// Initializes a new instance of the class with the specified name. + /// + /// The name of the IoT Item. + public IotItem(string name) : base(name) + { + } + + #region Properties + + /// + /// Gets or sets the full channel.device.name of the referenced server tag. + /// + [YamlIgnore, JsonIgnore] + public string? ServerTag + { + get => GetDynamicProperty(Properties.IotItem.ServerTag); + set => SetDynamicProperty(Properties.IotItem.ServerTag, value); + } + + /// + /// Gets or sets whether to use scan rate to collect data from the device. + /// + [YamlIgnore, JsonIgnore] + public bool? UseScanRate + { + get => GetDynamicProperty(Properties.IotItem.UseScanRate); + set => SetDynamicProperty(Properties.IotItem.UseScanRate, value); + } + + /// + /// Gets or sets the scan rate in milliseconds. + /// + [YamlIgnore, JsonIgnore] + public int? ScanRateMs + { + get => GetDynamicProperty(Properties.IotItem.ScanRateMs); + set => SetDynamicProperty(Properties.IotItem.ScanRateMs, value); + } + + /// + /// Gets or sets whether to publish on every scan regardless of value change. + /// + [YamlIgnore, JsonIgnore] + public bool? PublishEveryScan + { + get => GetDynamicProperty(Properties.IotItem.PublishEveryScan); + set => SetDynamicProperty(Properties.IotItem.PublishEveryScan, value); + } + + /// + /// Gets or sets the deadband percentage for publish threshold. + /// + [YamlIgnore, JsonIgnore] + public double? DeadbandPercent + { + get => GetDynamicProperty(Properties.IotItem.DeadbandPercent); + set => SetDynamicProperty(Properties.IotItem.DeadbandPercent, value); + } + + /// + /// Gets or sets whether the IoT Item is enabled. + /// + [YamlIgnore, JsonIgnore] + public bool? Enabled + { + get => GetDynamicProperty(Properties.IotItem.Enabled); + set => SetDynamicProperty(Properties.IotItem.Enabled, value); + } + + /// + /// Gets or sets the data type of the referenced tag. + /// + [YamlIgnore, JsonIgnore] + public IotItemDataType? DataType + { + get => (IotItemDataType?)GetDynamicProperty(Properties.IotItem.DataType); + set => SetDynamicProperty(Properties.IotItem.DataType, (int?)value); + } + + #endregion + } +} diff --git a/Kepware.Api/Model/Project/IotGateway/MqttClientAgent.cs b/Kepware.Api/Model/Project/IotGateway/MqttClientAgent.cs new file mode 100644 index 0000000..54b60ab --- /dev/null +++ b/Kepware.Api/Model/Project/IotGateway/MqttClientAgent.cs @@ -0,0 +1,169 @@ +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; + +namespace Kepware.Api.Model +{ + /// + /// Represents an MQTT Client agent in the IoT Gateway. + /// + [Endpoint("/config/v1/project/_iot_gateway/mqtt_clients/{name}")] + public class MqttClientAgent : PublishingIotAgent + { + /// + /// Initializes a new instance of the class. + /// + public MqttClientAgent() + { + } + + /// + /// Initializes a new instance of the class with the specified name. + /// + /// The name of the agent. + public MqttClientAgent(string name) : base(name) + { + } + + #region MQTT Properties + + /// + /// Gets or sets the MQTT broker URL. + /// + [YamlIgnore, JsonIgnore] + public string? Url + { + get => GetDynamicProperty(Properties.MqttClientAgent.Url); + set => SetDynamicProperty(Properties.MqttClientAgent.Url, value); + } + + /// + /// Gets or sets the MQTT topic for publishing data. + /// + [YamlIgnore, JsonIgnore] + public string? Topic + { + get => GetDynamicProperty(Properties.MqttClientAgent.Topic); + set => SetDynamicProperty(Properties.MqttClientAgent.Topic, value); + } + + /// + /// Gets or sets the MQTT Quality of Service level. + /// + [YamlIgnore, JsonIgnore] + public MqttQos? Qos + { + get => (MqttQos?)GetDynamicProperty(Properties.MqttClientAgent.Qos); + set => SetDynamicProperty(Properties.MqttClientAgent.Qos, (int?)value); + } + + /// + /// Gets or sets the client ID for broker communication. + /// + [YamlIgnore, JsonIgnore] + public string? ClientId + { + get => GetDynamicProperty(Properties.MqttClientAgent.ClientId); + set => SetDynamicProperty(Properties.MqttClientAgent.ClientId, value); + } + + /// + /// Gets or sets the username for broker authentication. + /// + [YamlIgnore, JsonIgnore] + public string? Username + { + get => GetDynamicProperty(Properties.MqttClientAgent.Username); + set => SetDynamicProperty(Properties.MqttClientAgent.Username, value); + } + + /// + /// Gets or sets the password for broker authentication. + /// + [YamlIgnore, JsonIgnore] + public string? Password + { + get => GetDynamicProperty(Properties.MqttClientAgent.Password); + set => SetDynamicProperty(Properties.MqttClientAgent.Password, value); + } + + /// + /// Gets or sets the TLS version for secure connections. + /// + [YamlIgnore, JsonIgnore] + public MqttTlsVersion? TlsVersion + { + get => (MqttTlsVersion?)GetDynamicProperty(Properties.MqttClientAgent.TlsVersion); + set => SetDynamicProperty(Properties.MqttClientAgent.TlsVersion, (int?)value); + } + + /// + /// Gets or sets whether client certificate authentication is enabled. + /// + [YamlIgnore, JsonIgnore] + public bool? ClientCertificate + { + get => GetDynamicProperty(Properties.MqttClientAgent.ClientCertificate); + set => SetDynamicProperty(Properties.MqttClientAgent.ClientCertificate, value); + } + + #endregion + + #region Last Will Properties + + /// + /// Gets or sets whether Last Will and Testament is enabled. + /// + [YamlIgnore, JsonIgnore] + public bool? EnableLastWill + { + get => GetDynamicProperty(Properties.MqttClientAgent.EnableLastWill); + set => SetDynamicProperty(Properties.MqttClientAgent.EnableLastWill, value); + } + + /// + /// Gets or sets the Last Will and Testament topic. + /// + [YamlIgnore, JsonIgnore] + public string? LastWillTopic + { + get => GetDynamicProperty(Properties.MqttClientAgent.LastWillTopic); + set => SetDynamicProperty(Properties.MqttClientAgent.LastWillTopic, value); + } + + /// + /// Gets or sets the Last Will and Testament message. + /// + [YamlIgnore, JsonIgnore] + public string? LastWillMessage + { + get => GetDynamicProperty(Properties.MqttClientAgent.LastWillMessage); + set => SetDynamicProperty(Properties.MqttClientAgent.LastWillMessage, value); + } + + #endregion + + #region Subscription Properties + + /// + /// Gets or sets whether write request listening is enabled. + /// + [YamlIgnore, JsonIgnore] + public bool? EnableWriteTopic + { + get => GetDynamicProperty(Properties.MqttClientAgent.EnableWriteTopic); + set => SetDynamicProperty(Properties.MqttClientAgent.EnableWriteTopic, value); + } + + /// + /// Gets or sets the topic for write request subscriptions. + /// + [YamlIgnore, JsonIgnore] + public string? WriteTopic + { + get => GetDynamicProperty(Properties.MqttClientAgent.WriteTopic); + set => SetDynamicProperty(Properties.MqttClientAgent.WriteTopic, value); + } + + #endregion + } +} diff --git a/Kepware.Api/Model/Project/IotGateway/PublishingIotAgent.cs b/Kepware.Api/Model/Project/IotGateway/PublishingIotAgent.cs new file mode 100644 index 0000000..1b332ca --- /dev/null +++ b/Kepware.Api/Model/Project/IotGateway/PublishingIotAgent.cs @@ -0,0 +1,135 @@ +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; + +namespace Kepware.Api.Model +{ + /// + /// Base class for IoT Gateway agent types that support publishing (MQTT Client and REST Client). + /// Contains publish configuration and message template properties not applicable to REST Server agents. + /// + public class PublishingIotAgent : IotAgent + { + /// + /// Initializes a new instance of the class. + /// + public PublishingIotAgent() + { + } + + /// + /// Initializes a new instance of the class with the specified name. + /// + /// The name of the agent. + public PublishingIotAgent(string name) : base(name) + { + } + + #region Publish Properties + + /// + /// Gets or sets the publish type (Interval or On Data Change). + /// + [YamlIgnore, JsonIgnore] + public IotPublishType? PublishType + { + get => (IotPublishType?)GetDynamicProperty(Properties.IotAgent.PublishType); + set => SetDynamicProperty(Properties.IotAgent.PublishType, (int?)value); + } + + /// + /// Gets or sets the publish rate in milliseconds. + /// + [YamlIgnore, JsonIgnore] + public int? RateMs + { + get => GetDynamicProperty(Properties.IotAgent.RateMs); + set => SetDynamicProperty(Properties.IotAgent.RateMs, value); + } + + /// + /// Gets or sets the publish format (Wide or Narrow). + /// + [YamlIgnore, JsonIgnore] + public IotPublishFormat? PublishFormat + { + get => (IotPublishFormat?)GetDynamicProperty(Properties.IotAgent.PublishFormat); + set => SetDynamicProperty(Properties.IotAgent.PublishFormat, (int?)value); + } + + /// + /// Gets or sets the maximum number of tag events per publish in narrow format. + /// + [YamlIgnore, JsonIgnore] + public int? MaxEventsPerPublish + { + get => GetDynamicProperty(Properties.IotAgent.MaxEventsPerPublish); + set => SetDynamicProperty(Properties.IotAgent.MaxEventsPerPublish, value); + } + + /// + /// Gets or sets the transaction timeout in seconds. + /// + [YamlIgnore, JsonIgnore] + public int? TransactionTimeoutS + { + get => GetDynamicProperty(Properties.IotAgent.TransactionTimeoutS); + set => SetDynamicProperty(Properties.IotAgent.TransactionTimeoutS, value); + } + + /// + /// Gets or sets whether the initial update should be sent when the agent starts. + /// + [YamlIgnore, JsonIgnore] + public bool? SendInitialUpdate + { + get => GetDynamicProperty(Properties.IotAgent.SendInitialUpdate); + set => SetDynamicProperty(Properties.IotAgent.SendInitialUpdate, value); + } + + #endregion + + #region Message Properties + + /// + /// Gets or sets the message format (Standard or Advanced Template). + /// + [YamlIgnore, JsonIgnore] + public IotMessageFormat? MessageFormat + { + get => (IotMessageFormat?)GetDynamicProperty(Properties.IotAgent.MessageFormat); + set => SetDynamicProperty(Properties.IotAgent.MessageFormat, (int?)value); + } + + /// + /// Gets or sets the standard template for message formatting. + /// + [YamlIgnore, JsonIgnore] + public string? StandardTemplate + { + get => GetDynamicProperty(Properties.IotAgent.StandardTemplate); + set => SetDynamicProperty(Properties.IotAgent.StandardTemplate, value); + } + + /// + /// Gets or sets the expansion of |VALUES| in the standard template. + /// + [YamlIgnore, JsonIgnore] + public string? ExpansionOfValues + { + get => GetDynamicProperty(Properties.IotAgent.ExpansionOfValues); + set => SetDynamicProperty(Properties.IotAgent.ExpansionOfValues, value); + } + + /// + /// Gets or sets the advanced template for message formatting. + /// + [YamlIgnore, JsonIgnore] + public string? AdvancedTemplate + { + get => GetDynamicProperty(Properties.IotAgent.AdvancedTemplate); + set => SetDynamicProperty(Properties.IotAgent.AdvancedTemplate, value); + } + + #endregion + } +} diff --git a/Kepware.Api/Model/Project/IotGateway/RestClientAgent.cs b/Kepware.Api/Model/Project/IotGateway/RestClientAgent.cs new file mode 100644 index 0000000..f3d8acf --- /dev/null +++ b/Kepware.Api/Model/Project/IotGateway/RestClientAgent.cs @@ -0,0 +1,101 @@ +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; + +namespace Kepware.Api.Model +{ + /// + /// Represents a REST Client agent in the IoT Gateway. + /// + [Endpoint("/config/v1/project/_iot_gateway/rest_clients/{name}")] + public class RestClientAgent : PublishingIotAgent + { + /// + /// Initializes a new instance of the class. + /// + public RestClientAgent() + { + } + + /// + /// Initializes a new instance of the class with the specified name. + /// + /// The name of the agent. + public RestClientAgent(string name) : base(name) + { + } + + #region REST Client Properties + + /// + /// Gets or sets the REST endpoint URL. + /// + [YamlIgnore, JsonIgnore] + public string? Url + { + get => GetDynamicProperty(Properties.RestClientAgent.Url); + set => SetDynamicProperty(Properties.RestClientAgent.Url, value); + } + + /// + /// Gets or sets the HTTP method for publishing data (POST or PUT). + /// + [YamlIgnore, JsonIgnore] + public RestClientHttpMethod? HttpMethod + { + get => (RestClientHttpMethod?)GetDynamicProperty(Properties.RestClientAgent.HttpMethod); + set => SetDynamicProperty(Properties.RestClientAgent.HttpMethod, (int?)value); + } + + /// + /// Gets or sets the HTTP header name-value pairs. + /// + [YamlIgnore, JsonIgnore] + public string? HttpHeader + { + get => GetDynamicProperty(Properties.RestClientAgent.HttpHeader); + set => SetDynamicProperty(Properties.RestClientAgent.HttpHeader, value); + } + + /// + /// Gets or sets the content-type for published data. + /// + [YamlIgnore, JsonIgnore] + public RestClientMediaType? PublishMediaType + { + get => (RestClientMediaType?)GetDynamicProperty(Properties.RestClientAgent.PublishMediaType); + set => SetDynamicProperty(Properties.RestClientAgent.PublishMediaType, (int?)value); + } + + /// + /// Gets or sets the username for basic HTTP authentication. + /// + [YamlIgnore, JsonIgnore] + public string? Username + { + get => GetDynamicProperty(Properties.RestClientAgent.Username); + set => SetDynamicProperty(Properties.RestClientAgent.Username, value); + } + + /// + /// Gets or sets the password for basic HTTP authentication. + /// + [YamlIgnore, JsonIgnore] + public string? Password + { + get => GetDynamicProperty(Properties.RestClientAgent.Password); + set => SetDynamicProperty(Properties.RestClientAgent.Password, value); + } + + /// + /// Gets or sets whether updates should be buffered when a publish fails. + /// + [YamlIgnore, JsonIgnore] + public bool? BufferOnFailedPublish + { + get => GetDynamicProperty(Properties.RestClientAgent.BufferOnFailedPublish); + set => SetDynamicProperty(Properties.RestClientAgent.BufferOnFailedPublish, value); + } + + #endregion + } +} diff --git a/Kepware.Api/Model/Project/IotGateway/RestServerAgent.cs b/Kepware.Api/Model/Project/IotGateway/RestServerAgent.cs new file mode 100644 index 0000000..e64b349 --- /dev/null +++ b/Kepware.Api/Model/Project/IotGateway/RestServerAgent.cs @@ -0,0 +1,92 @@ +using System.Text.Json.Serialization; +using YamlDotNet.Serialization; + +namespace Kepware.Api.Model +{ + /// + /// Represents a REST Server agent in the IoT Gateway. + /// REST Server agents do not have publish or message template properties. + /// + [Endpoint("/config/v1/project/_iot_gateway/rest_servers/{name}")] + public class RestServerAgent : IotAgent + { + /// + /// Initializes a new instance of the class. + /// + public RestServerAgent() + { + } + + /// + /// Initializes a new instance of the class with the specified name. + /// + /// The name of the agent. + public RestServerAgent(string name) : base(name) + { + } + + #region REST Server Properties + + /// + /// Gets or sets the network adapter for the REST server endpoint. + /// + [YamlIgnore, JsonIgnore] + public string? NetworkAdapter + { + get => GetDynamicProperty(Properties.RestServerAgent.NetworkAdapter); + set => SetDynamicProperty(Properties.RestServerAgent.NetworkAdapter, value); + } + + /// + /// Gets or sets the port number for the REST server. + /// + [YamlIgnore, JsonIgnore] + public int? PortNumber + { + get => GetDynamicProperty(Properties.RestServerAgent.PortNumber); + set => SetDynamicProperty(Properties.RestServerAgent.PortNumber, value); + } + + /// + /// Gets or sets the CORS allowed origins (comma-delimited list). + /// + [YamlIgnore, JsonIgnore] + public string? CorsAllowedOrigins + { + get => GetDynamicProperty(Properties.RestServerAgent.CorsAllowedOrigins); + set => SetDynamicProperty(Properties.RestServerAgent.CorsAllowedOrigins, value); + } + + /// + /// Gets or sets whether HTTPS encryption is enabled. + /// + [YamlIgnore, JsonIgnore] + public bool? UseHttps + { + get => GetDynamicProperty(Properties.RestServerAgent.UseHttps); + set => SetDynamicProperty(Properties.RestServerAgent.UseHttps, value); + } + + /// + /// Gets or sets whether the write endpoint is enabled. + /// + [YamlIgnore, JsonIgnore] + public bool? EnableWriteEndpoint + { + get => GetDynamicProperty(Properties.RestServerAgent.EnableWriteEndpoint); + set => SetDynamicProperty(Properties.RestServerAgent.EnableWriteEndpoint, value); + } + + /// + /// Gets or sets whether anonymous login is allowed. + /// + [YamlIgnore, JsonIgnore] + public bool? AllowAnonymousLogin + { + get => GetDynamicProperty(Properties.RestServerAgent.AllowAnonymousLogin); + set => SetDynamicProperty(Properties.RestServerAgent.AllowAnonymousLogin, value); + } + + #endregion + } +} diff --git a/Kepware.Api/Model/Properties.cs b/Kepware.Api/Model/Properties.cs index 4aacaa4..4b7cae2 100644 --- a/Kepware.Api/Model/Properties.cs +++ b/Kepware.Api/Model/Properties.cs @@ -96,6 +96,21 @@ public static class NonSerialized /// public const string ProjectTagsDefined = "servermain.PROJECT_TAGS_DEFINED"; + /// + /// Total number of tags configured under this agent (read-only). + /// + public const string ThisAgentTotal = "iot_gateway.AGENTTYPES_THIS_AGENT_TOTAL"; + + /// + /// Total number of tags configured under all agents (read-only). + /// + public const string AllAgentsTotal = "iot_gateway.AGENTTYPES_ALL_AGENTS_TOTAL"; + + /// + /// Maximum number of configured tags allowed by the license (read-only). + /// + public const string LicenseLimit = "iot_gateway.AGENTTYPES_LICENSE_LIMIT"; + /// /// A set of non-serialized properties. /// diff --git a/Kepware.Api/Serializer/KepJsonContext.cs b/Kepware.Api/Serializer/KepJsonContext.cs index dbc07f3..9d0640a 100644 --- a/Kepware.Api/Serializer/KepJsonContext.cs +++ b/Kepware.Api/Serializer/KepJsonContext.cs @@ -27,7 +27,11 @@ namespace Kepware.Api.Serializer [JsonSerializable(typeof(JobResponseMessage))] [JsonSerializable(typeof(JobStatusMessage))] [JsonSerializable(typeof(ServiceInvocationRequest))] - + [JsonSerializable(typeof(MqttClientAgent))] + [JsonSerializable(typeof(RestClientAgent))] + [JsonSerializable(typeof(RestServerAgent))] + [JsonSerializable(typeof(IotItem))] + [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] @@ -37,6 +41,10 @@ namespace Kepware.Api.Serializer [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] @@ -96,6 +104,22 @@ public static JsonTypeInfo GetJsonTypeInfo() { return (JsonTypeInfo)(object)Default.DefaultEntity; } + else if (typeof(T) == typeof(MqttClientAgent)) + { + return (JsonTypeInfo)(object)Default.MqttClientAgent; + } + else if (typeof(T) == typeof(RestClientAgent)) + { + return (JsonTypeInfo)(object)Default.RestClientAgent; + } + else if (typeof(T) == typeof(RestServerAgent)) + { + return (JsonTypeInfo)(object)Default.RestServerAgent; + } + else if (typeof(T) == typeof(IotItem)) + { + return (JsonTypeInfo)(object)Default.IotItem; + } else { throw new NotSupportedException(); @@ -141,6 +165,22 @@ public static JsonTypeInfo> GetJsonListTypeInfo() { return (JsonTypeInfo>)(object)Default.ListDefaultEntity; } + else if (typeof(T) == typeof(MqttClientAgent)) + { + return (JsonTypeInfo>)(object)Default.ListMqttClientAgent; + } + else if (typeof(T) == typeof(RestClientAgent)) + { + return (JsonTypeInfo>)(object)Default.ListRestClientAgent; + } + else if (typeof(T) == typeof(RestServerAgent)) + { + return (JsonTypeInfo>)(object)Default.ListRestServerAgent; + } + else if (typeof(T) == typeof(IotItem)) + { + return (JsonTypeInfo>)(object)Default.ListIotItem; + } else { throw new NotSupportedException(); From 63f34381730fbc40190bbb55f38fd286cf1dacdd Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Tue, 31 Mar 2026 19:14:04 -0400 Subject: [PATCH 26/38] feat(api): IoT GW Add client handlers and testing --- Kepware.Api.Test/ApiClient/IotGatewayTests.cs | 1093 +++++++++++++++++ .../ApiClient/_TestApiClientBase.cs | 4 + .../ClientHandler/IotGatewayApiHandler.cs | 520 ++++++++ Kepware.Api/KepwareApiClient.cs | 7 + 4 files changed, 1624 insertions(+) create mode 100644 Kepware.Api.Test/ApiClient/IotGatewayTests.cs create mode 100644 Kepware.Api/ClientHandler/IotGatewayApiHandler.cs diff --git a/Kepware.Api.Test/ApiClient/IotGatewayTests.cs b/Kepware.Api.Test/ApiClient/IotGatewayTests.cs new file mode 100644 index 0000000..1bd8387 --- /dev/null +++ b/Kepware.Api.Test/ApiClient/IotGatewayTests.cs @@ -0,0 +1,1093 @@ +using Kepware.Api.ClientHandler; +using Kepware.Api.Model; +using Kepware.Api.Serializer; +using Microsoft.Extensions.Logging; +using Moq; +using Moq.Contrib.HttpClient; +using Shouldly; +using System.Net; +using System.Text.Json; + +namespace Kepware.Api.Test.ApiClient; + +public class IotGatewayTests : TestApiClientBase +{ + #region ServerTagToItemName Tests + + [Theory] + [InlineData("Channel1.Device1.Tag1", "Channel1_Device1_Tag1")] + [InlineData("_System._Time", "System__Time")] + [InlineData("_System._Date", "System__Date")] + [InlineData("Channel1.Device1._InternalTag", "Channel1_Device1__InternalTag")] + [InlineData("SimpleTag", "SimpleTag")] + [InlineData("_LeadingUnderscore", "LeadingUnderscore")] + public void ServerTagToItemName_ShouldConvertCorrectly(string serverTag, string expectedName) + { + var result = IotGatewayApiHandler.ServerTagToItemName(serverTag); + result.ShouldBe(expectedName); + } + + #endregion + + #region MQTT Client Agent Tests + + [Fact] + public async Task GetOrCreateMqttClientAgent_WhenNotExists_ShouldCreateAgent() + { + // Arrange + var getEndpoint = "/config/v1/project/_iot_gateway/mqtt_clients/TestAgent"; + var postEndpoint = "/config/v1/project/_iot_gateway/mqtt_clients"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{getEndpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{postEndpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.IotGateway.GetOrCreateMqttClientAgentAsync("TestAgent"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestAgent"); + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{postEndpoint}", Times.Once()); + } + + [Fact] + public async Task GetOrCreateMqttClientAgent_WhenExists_ShouldReturnExistingAgent() + { + // Arrange + var agentJson = """ + { + "common.ALLTYPES_NAME": "TestAgent", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_gateway.MQTT_CLIENT_URL": "tcp://localhost:1883" + } + """; + var getEndpoint = "/config/v1/project/_iot_gateway/mqtt_clients/TestAgent"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{getEndpoint}") + .ReturnsResponse(HttpStatusCode.OK, agentJson, "application/json"); + + // Act + var result = await _kepwareApiClient.IotGateway.GetOrCreateMqttClientAgentAsync("TestAgent"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestAgent"); + result.Url.ShouldBe("tcp://localhost:1883"); + // Should NOT have called POST + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Post, $"{TEST_ENDPOINT}/config/v1/project/_iot_gateway/mqtt_clients", Times.Never()); + } + + [Fact] + public async Task GetOrCreateMqttClientAgent_WhenCreateFails_ShouldThrowInvalidOperationException() + { + // Arrange + var getEndpoint = "/config/v1/project/_iot_gateway/mqtt_clients/TestAgent"; + var postEndpoint = "/config/v1/project/_iot_gateway/mqtt_clients"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{getEndpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{postEndpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act & Assert + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.GetOrCreateMqttClientAgentAsync("TestAgent")); + } + + [Fact] + public async Task GetOrCreateMqttClientAgent_WithEmptyName_ShouldThrowArgumentException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.GetOrCreateMqttClientAgentAsync("")); + } + + + [Fact] + public async Task CreateMqttClientAgent_WhenSuccessful_ShouldReturnAgent() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.IotGateway.CreateMqttClientAgentAsync("TestAgent"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestAgent"); + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}", Times.Once()); + } + + [Fact] + public async Task CreateMqttClientAgent_WithProperties_ShouldSetProperties() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + var properties = new Dictionary + { + { Properties.MqttClientAgent.Url, "tcp://localhost:1883" }, + { Properties.MqttClientAgent.Topic, "test/topic" } + }; + + // Act + var result = await _kepwareApiClient.IotGateway.CreateMqttClientAgentAsync("TestAgent", properties); + + // Assert + result.ShouldNotBeNull(); + result.Url.ShouldBe("tcp://localhost:1883"); + result.Topic.ShouldBe("test/topic"); + } + + [Fact] + public async Task CreateMqttClientAgent_WithHttpError_ShouldReturnNull() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act + var result = await _kepwareApiClient.IotGateway.CreateMqttClientAgentAsync("TestAgent"); + + // Assert + result.ShouldBeNull(); + _loggerMockGeneric.Verify(logger => + logger.Log( + LogLevel.Error, + It.IsAny(), + It.Is((v, t) => true), + It.IsAny(), + It.Is>((v, t) => true)), + Times.Once); + } + + [Fact] + public async Task CreateMqttClientAgent_WithEmptyName_ShouldThrowArgumentException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.CreateMqttClientAgentAsync("")); + } + + [Fact] + public async Task GetMqttClientAgent_WhenExists_ShouldReturnAgent() + { + // Arrange + var agentJson = """ + { + "common.ALLTYPES_NAME": "TestAgent", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_gateway.MQTT_CLIENT_URL": "tcp://localhost:1883" + } + """; + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/TestAgent"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK, agentJson, "application/json"); + + // Act + var result = await _kepwareApiClient.IotGateway.GetMqttClientAgentAsync("TestAgent"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestAgent"); + result.Enabled.ShouldBe(true); + result.Url.ShouldBe("tcp://localhost:1883"); + } + + [Fact] + public async Task GetMqttClientAgent_WhenNotFound_ShouldReturnNull() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/NonExistent"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + // Act + var result = await _kepwareApiClient.IotGateway.GetMqttClientAgentAsync("NonExistent"); + + // Assert + result.ShouldBeNull(); + } + + [Fact] + public async Task DeleteMqttClientAgent_ByEntity_WhenSuccessful_ShouldReturnTrue() + { + // Arrange + var agent = new MqttClientAgent("TestAgent"); + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/TestAgent"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.IotGateway.DeleteMqttClientAgentAsync(agent); + + // Assert + result.ShouldBeTrue(); + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}", Times.Once()); + } + + [Fact] + public async Task DeleteMqttClientAgent_ByName_WhenSuccessful_ShouldReturnTrue() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/TestAgent"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.IotGateway.DeleteMqttClientAgentAsync("TestAgent"); + + // Assert + result.ShouldBeTrue(); + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}", Times.Once()); + } + + [Fact] + public async Task DeleteMqttClientAgent_WithHttpError_ShouldReturnFalse() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/TestAgent"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act + var result = await _kepwareApiClient.IotGateway.DeleteMqttClientAgentAsync("TestAgent"); + + // Assert + result.ShouldBeFalse(); + } + + [Fact] + public async Task DeleteMqttClientAgent_WithConnectionError_ShouldReturnFalse() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/TestAgent"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .Throws(new HttpRequestException("Connection error")); + + // Act + var result = await _kepwareApiClient.IotGateway.DeleteMqttClientAgentAsync("TestAgent"); + + // Assert + result.ShouldBeFalse(); + } + + #endregion + + #region REST Client Agent Tests + + [Fact] + public async Task GetOrCreateRestClientAgent_WhenNotExists_ShouldCreateAgent() + { + // Arrange + var getEndpoint = "/config/v1/project/_iot_gateway/rest_clients/TestRestClient"; + var postEndpoint = "/config/v1/project/_iot_gateway/rest_clients"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{getEndpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{postEndpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.IotGateway.GetOrCreateRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestClient"); + } + + [Fact] + public async Task GetOrCreateRestClientAgent_WhenExists_ShouldReturnAgent() + { + // Arrange + var agentJson = """ + { + "common.ALLTYPES_NAME": "TestRestClient", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_gateway.REST_CLIENT_URL": "https://api.example.com" + } + """; + var endpoint = "/config/v1/project/_iot_gateway/rest_clients/TestRestClient"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK, agentJson, "application/json"); + + // Act + var result = await _kepwareApiClient.IotGateway.GetOrCreateRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestClient"); + result.Url.ShouldBe("https://api.example.com"); + } + + [Fact] + public async Task GetOrCreateRestClientAgent_WhenCreateFails_ShouldThrowInvalidOperationException() + { + // Arrange + var getEndpoint = "/config/v1/project/_iot_gateway/rest_clients/TestRestClient"; + var postEndpoint = "/config/v1/project/_iot_gateway/rest_clients"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{getEndpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{postEndpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act & Assert + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.GetOrCreateRestClientAgentAsync("TestRestClient")); + } + + [Fact] + public async Task GetOrCreateRestClientAgent_WithEmptyName_ShouldThrowArgumentException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.GetOrCreateRestClientAgentAsync("")); + } + + [Fact] + public async Task CreateRestClientAgent_WhenSuccessful_ShouldReturnAgent() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_clients"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.IotGateway.CreateRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestClient"); + } + + [Fact] + public async Task CreateRestClientAgent_WithProperties_ShouldSetProperties() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_clients"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + var properties = new Dictionary + { + { Properties.RestClientAgent.Url, "https://api.example.com" } + }; + + // Act + var result = await _kepwareApiClient.IotGateway.CreateRestClientAgentAsync("TestRestClient", properties); + + // Assert + result.ShouldNotBeNull(); + result.Url.ShouldBe("https://api.example.com"); + } + + [Fact] + public async Task CreateRestClientAgent_WithHttpError_ShouldReturnNull() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_clients"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act + var result = await _kepwareApiClient.IotGateway.CreateRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldBeNull(); + } + + [Fact] + public async Task CreateRestClientAgent_WithEmptyName_ShouldThrowArgumentException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.CreateRestClientAgentAsync("")); + } + + [Fact] + public async Task GetRestClientAgent_WhenExists_ShouldReturnAgent() + { + // Arrange + var agentJson = """ + { + "common.ALLTYPES_NAME": "TestRestClient", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_gateway.REST_CLIENT_URL": "https://api.example.com" + } + """; + var endpoint = "/config/v1/project/_iot_gateway/rest_clients/TestRestClient"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK, agentJson, "application/json"); + + // Act + var result = await _kepwareApiClient.IotGateway.GetRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestClient"); + result.Url.ShouldBe("https://api.example.com"); + } + + [Fact] + public async Task GetRestClientAgent_WhenNotFound_ShouldReturnNull() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_clients/NonExistent"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + // Act + var result = await _kepwareApiClient.IotGateway.GetRestClientAgentAsync("NonExistent"); + + // Assert + result.ShouldBeNull(); + } + + [Fact] + public async Task DeleteRestClientAgent_ByEntity_WhenSuccessful_ShouldReturnTrue() + { + // Arrange + var agent = new RestClientAgent("TestRestClient"); + var endpoint = "/config/v1/project/_iot_gateway/rest_clients/TestRestClient"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.IotGateway.DeleteRestClientAgentAsync(agent); + + // Assert + result.ShouldBeTrue(); + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}", Times.Once()); + } + + [Fact] + public async Task DeleteRestClientAgent_ByName_WhenSuccessful_ShouldReturnTrue() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_clients/TestRestClient"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.IotGateway.DeleteRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldBeTrue(); + } + + [Fact] + public async Task DeleteRestClientAgent_WithHttpError_ShouldReturnFalse() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_clients/TestRestClient"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act + var result = await _kepwareApiClient.IotGateway.DeleteRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldBeFalse(); + } + + #endregion + + #region REST Server Agent Tests + + [Fact] + public async Task GetOrCreateRestServerAgent_WhenNotExists_ShouldCreateAgent() + { + // Arrange + var getEndpoint = "/config/v1/project/_iot_gateway/rest_servers/TestRestServer"; + var postEndpoint = "/config/v1/project/_iot_gateway/rest_servers"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{getEndpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{postEndpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.IotGateway.GetOrCreateRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestServer"); + } + + [Fact] + public async Task GetOrCreateRestServerAgent_WhenExists_ShouldReturnAgent() + { + // Arrange + var agentJson = """ + { + "common.ALLTYPES_NAME": "TestRestServer", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_gateway.REST_SERVER_PORT_NUMBER": 39320 + } + """; + var endpoint = "/config/v1/project/_iot_gateway/rest_servers/TestRestServer"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK, agentJson, "application/json"); + + // Act + var result = await _kepwareApiClient.IotGateway.GetOrCreateRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestServer"); + result.PortNumber.ShouldBe(39320); + } + + [Fact] + public async Task GetOrCreateRestServerAgent_WhenCreateFails_ShouldThrowInvalidOperationException() + { + // Arrange + var getEndpoint = "/config/v1/project/_iot_gateway/rest_servers/TestRestServer"; + var postEndpoint = "/config/v1/project/_iot_gateway/rest_servers"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{getEndpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{postEndpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act & Assert + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.GetOrCreateRestServerAgentAsync("TestRestServer")); + } + + [Fact] + public async Task GetOrCreateRestServerAgent_WithEmptyName_ShouldThrowArgumentException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.GetOrCreateRestServerAgentAsync("")); + } + + [Fact] + public async Task CreateRestServerAgent_WhenSuccessful_ShouldReturnAgent() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_servers"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.IotGateway.CreateRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestServer"); + } + + [Fact] + public async Task CreateRestServerAgent_WithProperties_ShouldSetProperties() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_servers"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + var properties = new Dictionary + { + { Properties.RestServerAgent.PortNumber, 39320 } + }; + + // Act + var result = await _kepwareApiClient.IotGateway.CreateRestServerAgentAsync("TestRestServer", properties); + + // Assert + result.ShouldNotBeNull(); + result.PortNumber.ShouldBe(39320); + } + + [Fact] + public async Task CreateRestServerAgent_WithHttpError_ShouldReturnNull() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_servers"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act + var result = await _kepwareApiClient.IotGateway.CreateRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldBeNull(); + } + + [Fact] + public async Task CreateRestServerAgent_WithEmptyName_ShouldThrowArgumentException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.CreateRestServerAgentAsync("")); + } + + [Fact] + public async Task GetRestServerAgent_WhenExists_ShouldReturnAgent() + { + // Arrange + var agentJson = """ + { + "common.ALLTYPES_NAME": "TestRestServer", + "iot_gateway.AGENTTYPES_ENABLED": true, + "iot_gateway.REST_SERVER_PORT_NUMBER": 39320 + } + """; + var endpoint = "/config/v1/project/_iot_gateway/rest_servers/TestRestServer"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK, agentJson, "application/json"); + + // Act + var result = await _kepwareApiClient.IotGateway.GetRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestServer"); + result.PortNumber.ShouldBe(39320); + } + + [Fact] + public async Task GetRestServerAgent_WhenNotFound_ShouldReturnNull() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_servers/NonExistent"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + // Act + var result = await _kepwareApiClient.IotGateway.GetRestServerAgentAsync("NonExistent"); + + // Assert + result.ShouldBeNull(); + } + + [Fact] + public async Task DeleteRestServerAgent_ByEntity_WhenSuccessful_ShouldReturnTrue() + { + // Arrange + var agent = new RestServerAgent("TestRestServer"); + var endpoint = "/config/v1/project/_iot_gateway/rest_servers/TestRestServer"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.IotGateway.DeleteRestServerAgentAsync(agent); + + // Assert + result.ShouldBeTrue(); + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}", Times.Once()); + } + + [Fact] + public async Task DeleteRestServerAgent_ByName_WhenSuccessful_ShouldReturnTrue() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_servers/TestRestServer"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.IotGateway.DeleteRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldBeTrue(); + } + + [Fact] + public async Task DeleteRestServerAgent_WithHttpError_ShouldReturnFalse() + { + // Arrange + var endpoint = "/config/v1/project/_iot_gateway/rest_servers/TestRestServer"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act + var result = await _kepwareApiClient.IotGateway.DeleteRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldBeFalse(); + } + + #endregion + + #region IoT Item Tests + + [Fact] + public async Task GetOrCreateIotItem_WhenNotExists_ShouldCreateWithDerivedName() + { + // Arrange - server tag "Channel1.Device1.Tag1" should query with name "Channel1_Device1_Tag1" + var parentAgent = new MqttClientAgent("ParentAgent"); + var getEndpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items/Channel1_Device1_Tag1"; + var postEndpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{getEndpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{postEndpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentAgent); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("Channel1_Device1_Tag1"); + result.ServerTag.ShouldBe("Channel1.Device1.Tag1"); + } + + [Fact] + public async Task GetOrCreateIotItem_WhenExists_ShouldReturnExistingItem() + { + // Arrange + var parentAgent = new MqttClientAgent("ParentAgent"); + var itemJson = """ + { + "common.ALLTYPES_NAME": "Channel1_Device1_Tag1", + "iot_gateway.IOT_ITEM_SERVER_TAG": "Channel1.Device1.Tag1", + "iot_gateway.IOT_ITEM_ENABLED": true + } + """; + var getEndpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items/Channel1_Device1_Tag1"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{getEndpoint}") + .ReturnsResponse(HttpStatusCode.OK, itemJson, "application/json"); + + // Act + var result = await _kepwareApiClient.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentAgent); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("Channel1_Device1_Tag1"); + // Should NOT have called POST + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Post, $"{TEST_ENDPOINT}/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items", Times.Never()); + } + + [Fact] + public async Task GetOrCreateIotItem_WhenCreateFails_ShouldThrowInvalidOperationException() + { + // Arrange + var parentAgent = new MqttClientAgent("ParentAgent"); + var getEndpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items/Channel1_Device1_Tag1"; + var postEndpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{getEndpoint}") + .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{postEndpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act & Assert + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentAgent)); + } + + [Fact] + public async Task CreateIotItem_ShouldDeriveNameFromServerTag() + { + // Arrange - "Channel1.Device1.Tag1" should produce item named "Channel1_Device1_Tag1" + var parentAgent = new MqttClientAgent("ParentAgent"); + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", parentAgent); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("Channel1_Device1_Tag1"); + result.ServerTag.ShouldBe("Channel1.Device1.Tag1"); + } + + [Fact] + public async Task CreateIotItem_WithSystemTag_ShouldStripLeadingUnderscore() + { + // Arrange - "_System._Time" should produce item named "System__Time" + var parentAgent = new MqttClientAgent("ParentAgent"); + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.IotGateway.CreateIotItemAsync("_System._Time", parentAgent); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("System__Time"); + result.ServerTag.ShouldBe("_System._Time"); + } + + [Fact] + public async Task CreateIotItem_WithHttpError_ShouldReturnNull() + { + // Arrange + var parentAgent = new MqttClientAgent("ParentAgent"); + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Post, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act + var result = await _kepwareApiClient.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", parentAgent); + + // Assert + result.ShouldBeNull(); + } + + [Fact] + public async Task CreateIotItem_WithEmptyServerTag_ShouldThrowArgumentException() + { + var parentAgent = new MqttClientAgent("ParentAgent"); + + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.CreateIotItemAsync("", parentAgent)); + } + + [Fact] + public async Task CreateIotItem_WithNullParent_ShouldThrowArgumentNullException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", null!)); + } + + [Fact] + public async Task GetIotItem_ShouldTranslateServerTagToItemName() + { + // Arrange - querying by server tag "Channel1.Device1.Tag1" should GET using name "Channel1_Device1_Tag1" + var parentAgent = new MqttClientAgent("ParentAgent"); + var itemJson = """ + { + "common.ALLTYPES_NAME": "Channel1_Device1_Tag1", + "iot_gateway.IOT_ITEM_SERVER_TAG": "Channel1.Device1.Tag1", + "iot_gateway.IOT_ITEM_ENABLED": true + } + """; + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items/Channel1_Device1_Tag1"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Get, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK, itemJson, "application/json"); + + // Act + var result = await _kepwareApiClient.IotGateway.GetIotItemAsync("Channel1.Device1.Tag1", parentAgent); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("Channel1_Device1_Tag1"); + result.ServerTag.ShouldBe("Channel1.Device1.Tag1"); + result.Enabled.ShouldBe(true); + } + + [Fact] + public async Task DeleteIotItem_ByEntity_WhenSuccessful_ShouldReturnTrue() + { + // Arrange + var parentAgent = new MqttClientAgent("ParentAgent"); + var item = new IotItem("Channel1_Device1_Tag1") { Owner = parentAgent }; + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items/Channel1_Device1_Tag1"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.IotGateway.DeleteIotItemAsync(item); + + // Assert + result.ShouldBeTrue(); + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}", Times.Once()); + } + + [Fact] + public async Task DeleteIotItem_ByServerTag_ShouldTranslateToItemName() + { + // Arrange - deleting by server tag "Channel1.Device1.Tag1" should DELETE using name "Channel1_Device1_Tag1" + var parentAgent = new MqttClientAgent("ParentAgent"); + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items/Channel1_Device1_Tag1"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.OK); + + // Act + var result = await _kepwareApiClient.IotGateway.DeleteIotItemAsync("Channel1.Device1.Tag1", parentAgent); + + // Assert + result.ShouldBeTrue(); + _httpMessageHandlerMock.VerifyRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}", Times.Once()); + } + + [Fact] + public async Task DeleteIotItem_WithHttpError_ShouldReturnFalse() + { + // Arrange + var parentAgent = new MqttClientAgent("ParentAgent"); + var item = new IotItem("Channel1_Device1_Tag1") { Owner = parentAgent }; + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items/Channel1_Device1_Tag1"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); + + // Act + var result = await _kepwareApiClient.IotGateway.DeleteIotItemAsync(item); + + // Assert + result.ShouldBeFalse(); + } + + [Fact] + public async Task DeleteIotItem_WithConnectionError_ShouldReturnFalse() + { + // Arrange + var parentAgent = new MqttClientAgent("ParentAgent"); + var item = new IotItem("Channel1_Device1_Tag1") { Owner = parentAgent }; + var endpoint = "/config/v1/project/_iot_gateway/mqtt_clients/ParentAgent/iot_items/Channel1_Device1_Tag1"; + + _httpMessageHandlerMock.SetupRequest(HttpMethod.Delete, $"{TEST_ENDPOINT}{endpoint}") + .Throws(new HttpRequestException("Connection error")); + + // Act + var result = await _kepwareApiClient.IotGateway.DeleteIotItemAsync(item); + + // Assert + result.ShouldBeFalse(); + } + + #endregion + + #region Argument Validation Tests + + [Fact] + public async Task GetMqttClientAgent_WithEmptyName_ShouldThrowArgumentException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.GetMqttClientAgentAsync("")); + } + + [Fact] + public async Task GetRestClientAgent_WithEmptyName_ShouldThrowArgumentException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.GetRestClientAgentAsync("")); + } + + [Fact] + public async Task GetRestServerAgent_WithEmptyName_ShouldThrowArgumentException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.GetRestServerAgentAsync("")); + } + + [Fact] + public async Task DeleteMqttClientAgent_WithNullEntity_ShouldThrowArgumentNullException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.DeleteMqttClientAgentAsync((MqttClientAgent)null!)); + } + + [Fact] + public async Task DeleteRestClientAgent_WithNullEntity_ShouldThrowArgumentNullException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.DeleteRestClientAgentAsync((RestClientAgent)null!)); + } + + [Fact] + public async Task DeleteRestServerAgent_WithNullEntity_ShouldThrowArgumentNullException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.DeleteRestServerAgentAsync((RestServerAgent)null!)); + } + + [Fact] + public async Task GetIotItem_WithEmptyServerTag_ShouldThrowArgumentException() + { + var parentAgent = new MqttClientAgent("ParentAgent"); + + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.GetIotItemAsync("", parentAgent)); + } + + [Fact] + public async Task GetIotItem_WithNullParent_ShouldThrowArgumentNullException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.GetIotItemAsync("Channel1.Device1.Tag1", null!)); + } + + [Fact] + public async Task DeleteIotItem_WithNullEntity_ShouldThrowArgumentNullException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.DeleteIotItemAsync((IotItem)null!)); + } + + [Fact] + public async Task GetOrCreateIotItem_WithEmptyServerTag_ShouldThrowArgumentException() + { + var parentAgent = new MqttClientAgent("ParentAgent"); + + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.GetOrCreateIotItemAsync("", parentAgent)); + } + + [Fact] + public async Task GetOrCreateIotItem_WithNullParent_ShouldThrowArgumentNullException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", null!)); + } + + [Fact] + public async Task DeleteIotItem_ByServerTag_WithEmptyTag_ShouldThrowArgumentException() + { + var parentAgent = new MqttClientAgent("ParentAgent"); + + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.DeleteIotItemAsync("", parentAgent)); + } + + [Fact] + public async Task DeleteIotItem_ByServerTag_WithNullParent_ShouldThrowArgumentNullException() + { + await Should.ThrowAsync(async () => + await _kepwareApiClient.IotGateway.DeleteIotItemAsync("Channel1.Device1.Tag1", null!)); + } + + #endregion +} diff --git a/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs b/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs index ffc2200..d05c64c 100644 --- a/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs +++ b/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs @@ -25,6 +25,7 @@ public abstract class TestApiClientBase protected readonly Mock> _loggerMockAdmin; protected readonly Mock> _loggerMockProject; protected readonly Mock> _loggerMockGeneric; + protected readonly Mock> _loggerMockIotGateway; protected readonly Mock _loggerFactoryMock; protected readonly KepwareApiClient _kepwareApiClient; @@ -40,6 +41,7 @@ protected TestApiClientBase() _loggerMockAdmin = new Mock>(); _loggerMockGeneric = new Mock>(); _loggerMockProject = new Mock>(); + _loggerMockIotGateway = new Mock>(); _loggerFactoryMock = new Mock(); _loggerFactoryMock.Setup(factory => factory.CreateLogger(It.IsAny())).Returns((string name) => @@ -52,6 +54,8 @@ protected TestApiClientBase() return _loggerMockGeneric.Object; else if (name == typeof(ProjectApiHandler).FullName) return _loggerMockProject.Object; + else if (name == typeof(IotGatewayApiHandler).FullName) + return _loggerMockIotGateway.Object; else return Mock.Of(); }); diff --git a/Kepware.Api/ClientHandler/IotGatewayApiHandler.cs b/Kepware.Api/ClientHandler/IotGatewayApiHandler.cs new file mode 100644 index 0000000..44f0bf8 --- /dev/null +++ b/Kepware.Api/ClientHandler/IotGatewayApiHandler.cs @@ -0,0 +1,520 @@ +using Kepware.Api.Model; +using Microsoft.Extensions.Logging; + +namespace Kepware.Api.ClientHandler +{ + /// + /// Handles operations related to IoT Gateway agent configurations in the Kepware server. + /// Supports MQTT Client, REST Client, and REST Server agent types and their child IoT Items. + /// + public class IotGatewayApiHandler + { + private readonly KepwareApiClient m_kepwareApiClient; + private readonly ILogger m_logger; + + /// + /// Initializes a new instance of the class. + /// + /// The Kepware Configuration API client. + /// The logger instance. + public IotGatewayApiHandler(KepwareApiClient kepwareApiClient, ILogger logger) + { + m_kepwareApiClient = kepwareApiClient; + m_logger = logger; + } + + #region MQTT Client Agent + + /// + /// Gets or creates an MQTT Client agent with the specified name. + /// If the agent exists, it is loaded and returned. If it does not exist, it is created with the specified properties. + /// + /// The name of the agent. + /// Optional properties to set on the agent. + /// The cancellation token. + /// A task that represents the asynchronous operation. The task result contains the created or loaded . + /// Thrown when the name is null or empty. + /// Thrown when the agent cannot be created or loaded. + public async Task GetOrCreateMqttClientAgentAsync(string name, IDictionary? properties = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + + var agent = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken: cancellationToken); + + if (agent == null) + { + agent = await CreateMqttClientAgentAsync(name, properties, cancellationToken); + if (agent == null) + { + throw new InvalidOperationException($"Failed to create or load MQTT Client agent '{name}'"); + } + } + + return agent; + } + + /// + /// Gets an MQTT Client agent with the specified name. + /// + /// The name of the agent. + /// The cancellation token. + /// The loaded or null if not found. + /// Thrown when the name is null or empty. + public async Task GetMqttClientAgentAsync(string name, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + + return await m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken: cancellationToken); + } + + /// + /// Creates a new MQTT Client agent with the specified name. + /// + /// The name of the agent. + /// Optional properties to set on the agent. + /// The cancellation token. + /// The created , or null if creation failed. + /// Thrown when the name is null or empty. + public async Task CreateMqttClientAgentAsync(string name, IDictionary? properties = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + + var agent = new MqttClientAgent(name); + if (properties != null) + { + foreach (var property in properties) + { + agent.SetDynamicProperty(property.Key, property.Value); + } + } + + if (await m_kepwareApiClient.GenericConfig.InsertItemAsync(agent, cancellationToken: cancellationToken)) + { + return agent; + } + + return null; + } + + /// + /// Updates the specified MQTT Client agent. + /// + /// The agent to update. + /// The cancellation token. + /// A boolean indicating whether the update was successful. + public Task UpdateMqttClientAgentAsync(MqttClientAgent agent, CancellationToken cancellationToken = default) + => m_kepwareApiClient.GenericConfig.UpdateItemAsync(agent, oldItem: null, cancellationToken); + + /// + /// Deletes the specified MQTT Client agent. + /// + /// The agent to delete. + /// The cancellation token. + /// A boolean indicating whether the deletion was successful. + /// Thrown when the agent is null. + public Task DeleteMqttClientAgentAsync(MqttClientAgent agent, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(agent); + return m_kepwareApiClient.GenericConfig.DeleteItemAsync(agent, cancellationToken: cancellationToken); + } + + /// + /// Deletes the MQTT Client agent with the specified name. + /// + /// The name of the agent to delete. + /// The cancellation token. + /// A boolean indicating whether the deletion was successful. + /// Thrown when the name is null or empty. + public Task DeleteMqttClientAgentAsync(string name, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + return m_kepwareApiClient.GenericConfig.DeleteItemAsync(name, cancellationToken: cancellationToken); + } + + #endregion + + #region REST Client Agent + + /// + /// Gets or creates a REST Client agent with the specified name. + /// If the agent exists, it is loaded and returned. If it does not exist, it is created with the specified properties. + /// + /// The name of the agent. + /// Optional properties to set on the agent. + /// The cancellation token. + /// A task that represents the asynchronous operation. The task result contains the created or loaded . + /// Thrown when the name is null or empty. + /// Thrown when the agent cannot be created or loaded. + public async Task GetOrCreateRestClientAgentAsync(string name, IDictionary? properties = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + + var agent = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken: cancellationToken); + + if (agent == null) + { + agent = await CreateRestClientAgentAsync(name, properties, cancellationToken); + if (agent == null) + { + throw new InvalidOperationException($"Failed to create or load REST Client agent '{name}'"); + } + } + + return agent; + } + + /// + /// Gets a REST Client agent with the specified name. + /// + /// The name of the agent. + /// The cancellation token. + /// The loaded or null if not found. + /// Thrown when the name is null or empty. + public async Task GetRestClientAgentAsync(string name, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + + return await m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken: cancellationToken); + } + + /// + /// Creates a new REST Client agent with the specified name. + /// + /// The name of the agent. + /// Optional properties to set on the agent. + /// The cancellation token. + /// The created , or null if creation failed. + /// Thrown when the name is null or empty. + public async Task CreateRestClientAgentAsync(string name, IDictionary? properties = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + + var agent = new RestClientAgent(name); + if (properties != null) + { + foreach (var property in properties) + { + agent.SetDynamicProperty(property.Key, property.Value); + } + } + + if (await m_kepwareApiClient.GenericConfig.InsertItemAsync(agent, cancellationToken: cancellationToken)) + { + return agent; + } + + return null; + } + + /// + /// Updates the specified REST Client agent. + /// + /// The agent to update. + /// The cancellation token. + /// A boolean indicating whether the update was successful. + public Task UpdateRestClientAgentAsync(RestClientAgent agent, CancellationToken cancellationToken = default) + => m_kepwareApiClient.GenericConfig.UpdateItemAsync(agent, oldItem: null, cancellationToken); + + /// + /// Deletes the specified REST Client agent. + /// + /// The agent to delete. + /// The cancellation token. + /// A boolean indicating whether the deletion was successful. + /// Thrown when the agent is null. + public Task DeleteRestClientAgentAsync(RestClientAgent agent, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(agent); + return m_kepwareApiClient.GenericConfig.DeleteItemAsync(agent, cancellationToken: cancellationToken); + } + + /// + /// Deletes the REST Client agent with the specified name. + /// + /// The name of the agent to delete. + /// The cancellation token. + /// A boolean indicating whether the deletion was successful. + /// Thrown when the name is null or empty. + public Task DeleteRestClientAgentAsync(string name, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + return m_kepwareApiClient.GenericConfig.DeleteItemAsync(name, cancellationToken: cancellationToken); + } + + #endregion + + #region REST Server Agent + + /// + /// Gets or creates a REST Server agent with the specified name. + /// If the agent exists, it is loaded and returned. If it does not exist, it is created with the specified properties. + /// + /// The name of the agent. + /// Optional properties to set on the agent. + /// The cancellation token. + /// A task that represents the asynchronous operation. The task result contains the created or loaded . + /// Thrown when the name is null or empty. + /// Thrown when the agent cannot be created or loaded. + public async Task GetOrCreateRestServerAgentAsync(string name, IDictionary? properties = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + + var agent = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken: cancellationToken); + + if (agent == null) + { + agent = await CreateRestServerAgentAsync(name, properties, cancellationToken); + if (agent == null) + { + throw new InvalidOperationException($"Failed to create or load REST Server agent '{name}'"); + } + } + + return agent; + } + + /// + /// Gets a REST Server agent with the specified name. + /// + /// The name of the agent. + /// The cancellation token. + /// The loaded or null if not found. + /// Thrown when the name is null or empty. + public async Task GetRestServerAgentAsync(string name, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + + return await m_kepwareApiClient.GenericConfig.LoadEntityAsync(name, cancellationToken: cancellationToken); + } + + /// + /// Creates a new REST Server agent with the specified name. + /// + /// The name of the agent. + /// Optional properties to set on the agent. + /// The cancellation token. + /// The created , or null if creation failed. + /// Thrown when the name is null or empty. + public async Task CreateRestServerAgentAsync(string name, IDictionary? properties = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + + var agent = new RestServerAgent(name); + if (properties != null) + { + foreach (var property in properties) + { + agent.SetDynamicProperty(property.Key, property.Value); + } + } + + if (await m_kepwareApiClient.GenericConfig.InsertItemAsync(agent, cancellationToken: cancellationToken)) + { + return agent; + } + + return null; + } + + /// + /// Updates the specified REST Server agent. + /// + /// The agent to update. + /// The cancellation token. + /// A boolean indicating whether the update was successful. + public Task UpdateRestServerAgentAsync(RestServerAgent agent, CancellationToken cancellationToken = default) + => m_kepwareApiClient.GenericConfig.UpdateItemAsync(agent, oldItem: null, cancellationToken); + + /// + /// Deletes the specified REST Server agent. + /// + /// The agent to delete. + /// The cancellation token. + /// A boolean indicating whether the deletion was successful. + /// Thrown when the agent is null. + public Task DeleteRestServerAgentAsync(RestServerAgent agent, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(agent); + return m_kepwareApiClient.GenericConfig.DeleteItemAsync(agent, cancellationToken: cancellationToken); + } + + /// + /// Deletes the REST Server agent with the specified name. + /// + /// The name of the agent to delete. + /// The cancellation token. + /// A boolean indicating whether the deletion was successful. + /// Thrown when the name is null or empty. + public Task DeleteRestServerAgentAsync(string name, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(name)) + throw new ArgumentException("Agent name cannot be null or empty", nameof(name)); + return m_kepwareApiClient.GenericConfig.DeleteItemAsync(name, cancellationToken: cancellationToken); + } + + #endregion + + #region IoT Items + + /// + /// Converts a dot-delimited server tag name to the IoT Item name used by the Kepware API. + /// Replaces dots with underscores and strips a leading underscore if present. + /// For example, "Channel1.Device1.Tag1" becomes "Channel1_Device1_Tag1" + /// and "_System._Time" becomes "System__Time". + /// + /// The dot-delimited server tag name. + /// The converted IoT Item name. + internal static string ServerTagToItemName(string serverTag) + { + var name = serverTag.Replace('.', '_'); + if (name.StartsWith('_')) + name = name[1..]; + return name; + } + + /// + /// Gets or creates an IoT Item for the specified server tag under the given parent agent. + /// If the item exists, it is loaded and returned. If it does not exist, it is created with the specified properties. + /// The IoT Item name is derived from the server tag by replacing dots with underscores and stripping any leading underscore. + /// + /// The dot-delimited server tag reference (e.g., "Channel1.Device1.Tag1"). + /// The parent agent that will own the IoT Item. + /// Optional properties to set on the IoT Item. + /// The cancellation token. + /// A task that represents the asynchronous operation. The task result contains the created or loaded . + /// Thrown when the server tag is null or empty. + /// Thrown when the parent agent is null. + /// Thrown when the item cannot be created or loaded. + public async Task GetOrCreateIotItemAsync(string serverTag, IotAgent parentAgent, IDictionary? properties = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(serverTag)) + throw new ArgumentException("Server tag cannot be null or empty", nameof(serverTag)); + ArgumentNullException.ThrowIfNull(parentAgent); + + var itemName = ServerTagToItemName(serverTag); + var item = await m_kepwareApiClient.GenericConfig.LoadEntityAsync(itemName, parentAgent, cancellationToken: cancellationToken); + + if (item == null) + { + item = await CreateIotItemAsync(serverTag, parentAgent, properties, cancellationToken); + if (item == null) + { + throw new InvalidOperationException($"Failed to create or load IoT Item for server tag '{serverTag}'"); + } + } + + return item; + } + + /// + /// Gets an IoT Item by its server tag name under the given parent agent. + /// The server tag is converted to the IoT Item name by replacing dots with underscores and stripping any leading underscore. + /// + /// The dot-delimited server tag name (e.g., "Channel1.Device1.Tag1"). + /// The parent agent that owns the IoT Item. + /// The cancellation token. + /// The loaded or null if not found. + /// Thrown when the server tag is null or empty. + /// Thrown when the parent agent is null. + public async Task GetIotItemAsync(string serverTag, IotAgent parentAgent, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(serverTag)) + throw new ArgumentException("Server tag cannot be null or empty", nameof(serverTag)); + ArgumentNullException.ThrowIfNull(parentAgent); + + var itemName = ServerTagToItemName(serverTag); + return await m_kepwareApiClient.GenericConfig.LoadEntityAsync(itemName, parentAgent, cancellationToken: cancellationToken); + } + + /// + /// Creates a new IoT Item for the specified server tag under the given parent agent. + /// The IoT Item name is derived from the server tag by replacing dots with underscores and stripping any leading underscore. + /// + /// The dot-delimited server tag reference (e.g., "Channel1.Device1.Tag1"). + /// The parent agent that will own the IoT Item. + /// Optional properties to set on the IoT Item. + /// The cancellation token. + /// The created , or null if creation failed. + /// Thrown when the server tag is null or empty. + /// Thrown when the parent agent is null. + public async Task CreateIotItemAsync(string serverTag, IotAgent parentAgent, IDictionary? properties = null, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(serverTag)) + throw new ArgumentException("Server tag cannot be null or empty", nameof(serverTag)); + ArgumentNullException.ThrowIfNull(parentAgent); + + var itemName = ServerTagToItemName(serverTag); + var item = new IotItem(itemName) { Owner = parentAgent }; + item.SetDynamicProperty(Properties.IotItem.ServerTag, serverTag); + + if (properties != null) + { + foreach (var property in properties) + { + item.SetDynamicProperty(property.Key, property.Value); + } + } + + if (await m_kepwareApiClient.GenericConfig.InsertItemAsync(item, parentAgent, cancellationToken: cancellationToken)) + { + return item; + } + + return null; + } + + /// + /// Updates the specified IoT Item. + /// + /// The IoT Item to update. + /// The cancellation token. + /// A boolean indicating whether the update was successful. + public Task UpdateIotItemAsync(IotItem item, CancellationToken cancellationToken = default) + => m_kepwareApiClient.GenericConfig.UpdateItemAsync(item, oldItem: null, cancellationToken); + + /// + /// Deletes the specified IoT Item. + /// + /// The IoT Item to delete. + /// The cancellation token. + /// A boolean indicating whether the deletion was successful. + /// Thrown when the item is null. + public Task DeleteIotItemAsync(IotItem item, CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(item); + return m_kepwareApiClient.GenericConfig.DeleteItemAsync(item, cancellationToken: cancellationToken); + } + + /// + /// Deletes the IoT Item identified by the specified server tag under the given parent agent. + /// The server tag is converted to the IoT Item name by replacing dots with underscores and stripping any leading underscore. + /// + /// The dot-delimited server tag name (e.g., "Channel1.Device1.Tag1"). + /// The parent agent that owns the IoT Item. + /// The cancellation token. + /// A boolean indicating whether the deletion was successful. + /// Thrown when the server tag is null or empty. + /// Thrown when the parent agent is null. + public Task DeleteIotItemAsync(string serverTag, IotAgent parentAgent, CancellationToken cancellationToken = default) + { + if (string.IsNullOrEmpty(serverTag)) + throw new ArgumentException("Server tag cannot be null or empty", nameof(serverTag)); + ArgumentNullException.ThrowIfNull(parentAgent); + var itemName = ServerTagToItemName(serverTag); + return m_kepwareApiClient.GenericConfig.DeleteItemAsync([parentAgent.Name!, itemName], cancellationToken: cancellationToken); + } + + #endregion + } +} diff --git a/Kepware.Api/KepwareApiClient.cs b/Kepware.Api/KepwareApiClient.cs index f37018a..487059d 100644 --- a/Kepware.Api/KepwareApiClient.cs +++ b/Kepware.Api/KepwareApiClient.cs @@ -98,6 +98,12 @@ public partial class KepwareApiClient : IKepwareDefaultValueProvider /// See for method references. public ServicesApiHandler ApiServices { get; init; } + /// + /// Gets the IoT Gateway handlers. + /// + /// See for method references. + public IotGatewayApiHandler IotGateway { get; init; } + internal HttpClient HttpClient { get { return m_httpClient; } } #region Constructors @@ -131,6 +137,7 @@ internal KepwareApiClient(string name, KepwareApiClientOptions options, ILoggerF Project = new ProjectApiHandler(this, channelsApiHandler, devicesApiHandler, loggerFactory.CreateLogger()); Admin = new AdminApiHandler(this, loggerFactory.CreateLogger()); ApiServices = new ServicesApiHandler(this, loggerFactory.CreateLogger()); + IotGateway = new IotGatewayApiHandler(this, loggerFactory.CreateLogger()); } #endregion From 8b003d2118fe4ade38b6a1e89d055a2e3946c82c Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Tue, 31 Mar 2026 19:27:04 -0400 Subject: [PATCH 27/38] refactor: Moved IoT GW into Project namespace for client handlers --- Kepware.Api.Test/ApiClient/IotGatewayTests.cs | 132 +++++++++--------- .../ClientHandler/ProjectApiHandler.cs | 10 +- Kepware.Api/KepwareApiClient.cs | 10 +- 3 files changed, 77 insertions(+), 75 deletions(-) diff --git a/Kepware.Api.Test/ApiClient/IotGatewayTests.cs b/Kepware.Api.Test/ApiClient/IotGatewayTests.cs index 1bd8387..59b2899 100644 --- a/Kepware.Api.Test/ApiClient/IotGatewayTests.cs +++ b/Kepware.Api.Test/ApiClient/IotGatewayTests.cs @@ -45,7 +45,7 @@ public async Task GetOrCreateMqttClientAgent_WhenNotExists_ShouldCreateAgent() .ReturnsResponse(HttpStatusCode.OK); // Act - var result = await _kepwareApiClient.IotGateway.GetOrCreateMqttClientAgentAsync("TestAgent"); + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateMqttClientAgentAsync("TestAgent"); // Assert result.ShouldNotBeNull(); @@ -70,7 +70,7 @@ public async Task GetOrCreateMqttClientAgent_WhenExists_ShouldReturnExistingAgen .ReturnsResponse(HttpStatusCode.OK, agentJson, "application/json"); // Act - var result = await _kepwareApiClient.IotGateway.GetOrCreateMqttClientAgentAsync("TestAgent"); + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateMqttClientAgentAsync("TestAgent"); // Assert result.ShouldNotBeNull(); @@ -95,14 +95,14 @@ public async Task GetOrCreateMqttClientAgent_WhenCreateFails_ShouldThrowInvalidO // Act & Assert await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.GetOrCreateMqttClientAgentAsync("TestAgent")); + await _kepwareApiClient.Project.IotGateway.GetOrCreateMqttClientAgentAsync("TestAgent")); } [Fact] public async Task GetOrCreateMqttClientAgent_WithEmptyName_ShouldThrowArgumentException() { await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.GetOrCreateMqttClientAgentAsync("")); + await _kepwareApiClient.Project.IotGateway.GetOrCreateMqttClientAgentAsync("")); } @@ -116,7 +116,7 @@ public async Task CreateMqttClientAgent_WhenSuccessful_ShouldReturnAgent() .ReturnsResponse(HttpStatusCode.OK); // Act - var result = await _kepwareApiClient.IotGateway.CreateMqttClientAgentAsync("TestAgent"); + var result = await _kepwareApiClient.Project.IotGateway.CreateMqttClientAgentAsync("TestAgent"); // Assert result.ShouldNotBeNull(); @@ -140,7 +140,7 @@ public async Task CreateMqttClientAgent_WithProperties_ShouldSetProperties() }; // Act - var result = await _kepwareApiClient.IotGateway.CreateMqttClientAgentAsync("TestAgent", properties); + var result = await _kepwareApiClient.Project.IotGateway.CreateMqttClientAgentAsync("TestAgent", properties); // Assert result.ShouldNotBeNull(); @@ -158,7 +158,7 @@ public async Task CreateMqttClientAgent_WithHttpError_ShouldReturnNull() .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); // Act - var result = await _kepwareApiClient.IotGateway.CreateMqttClientAgentAsync("TestAgent"); + var result = await _kepwareApiClient.Project.IotGateway.CreateMqttClientAgentAsync("TestAgent"); // Assert result.ShouldBeNull(); @@ -176,7 +176,7 @@ public async Task CreateMqttClientAgent_WithHttpError_ShouldReturnNull() public async Task CreateMqttClientAgent_WithEmptyName_ShouldThrowArgumentException() { await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.CreateMqttClientAgentAsync("")); + await _kepwareApiClient.Project.IotGateway.CreateMqttClientAgentAsync("")); } [Fact] @@ -196,7 +196,7 @@ public async Task GetMqttClientAgent_WhenExists_ShouldReturnAgent() .ReturnsResponse(HttpStatusCode.OK, agentJson, "application/json"); // Act - var result = await _kepwareApiClient.IotGateway.GetMqttClientAgentAsync("TestAgent"); + var result = await _kepwareApiClient.Project.IotGateway.GetMqttClientAgentAsync("TestAgent"); // Assert result.ShouldNotBeNull(); @@ -215,7 +215,7 @@ public async Task GetMqttClientAgent_WhenNotFound_ShouldReturnNull() .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); // Act - var result = await _kepwareApiClient.IotGateway.GetMqttClientAgentAsync("NonExistent"); + var result = await _kepwareApiClient.Project.IotGateway.GetMqttClientAgentAsync("NonExistent"); // Assert result.ShouldBeNull(); @@ -232,7 +232,7 @@ public async Task DeleteMqttClientAgent_ByEntity_WhenSuccessful_ShouldReturnTrue .ReturnsResponse(HttpStatusCode.OK); // Act - var result = await _kepwareApiClient.IotGateway.DeleteMqttClientAgentAsync(agent); + var result = await _kepwareApiClient.Project.IotGateway.DeleteMqttClientAgentAsync(agent); // Assert result.ShouldBeTrue(); @@ -249,7 +249,7 @@ public async Task DeleteMqttClientAgent_ByName_WhenSuccessful_ShouldReturnTrue() .ReturnsResponse(HttpStatusCode.OK); // Act - var result = await _kepwareApiClient.IotGateway.DeleteMqttClientAgentAsync("TestAgent"); + var result = await _kepwareApiClient.Project.IotGateway.DeleteMqttClientAgentAsync("TestAgent"); // Assert result.ShouldBeTrue(); @@ -266,7 +266,7 @@ public async Task DeleteMqttClientAgent_WithHttpError_ShouldReturnFalse() .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); // Act - var result = await _kepwareApiClient.IotGateway.DeleteMqttClientAgentAsync("TestAgent"); + var result = await _kepwareApiClient.Project.IotGateway.DeleteMqttClientAgentAsync("TestAgent"); // Assert result.ShouldBeFalse(); @@ -282,7 +282,7 @@ public async Task DeleteMqttClientAgent_WithConnectionError_ShouldReturnFalse() .Throws(new HttpRequestException("Connection error")); // Act - var result = await _kepwareApiClient.IotGateway.DeleteMqttClientAgentAsync("TestAgent"); + var result = await _kepwareApiClient.Project.IotGateway.DeleteMqttClientAgentAsync("TestAgent"); // Assert result.ShouldBeFalse(); @@ -306,7 +306,7 @@ public async Task GetOrCreateRestClientAgent_WhenNotExists_ShouldCreateAgent() .ReturnsResponse(HttpStatusCode.OK); // Act - var result = await _kepwareApiClient.IotGateway.GetOrCreateRestClientAgentAsync("TestRestClient"); + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateRestClientAgentAsync("TestRestClient"); // Assert result.ShouldNotBeNull(); @@ -330,7 +330,7 @@ public async Task GetOrCreateRestClientAgent_WhenExists_ShouldReturnAgent() .ReturnsResponse(HttpStatusCode.OK, agentJson, "application/json"); // Act - var result = await _kepwareApiClient.IotGateway.GetOrCreateRestClientAgentAsync("TestRestClient"); + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateRestClientAgentAsync("TestRestClient"); // Assert result.ShouldNotBeNull(); @@ -353,14 +353,14 @@ public async Task GetOrCreateRestClientAgent_WhenCreateFails_ShouldThrowInvalidO // Act & Assert await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.GetOrCreateRestClientAgentAsync("TestRestClient")); + await _kepwareApiClient.Project.IotGateway.GetOrCreateRestClientAgentAsync("TestRestClient")); } [Fact] public async Task GetOrCreateRestClientAgent_WithEmptyName_ShouldThrowArgumentException() { await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.GetOrCreateRestClientAgentAsync("")); + await _kepwareApiClient.Project.IotGateway.GetOrCreateRestClientAgentAsync("")); } [Fact] @@ -373,7 +373,7 @@ public async Task CreateRestClientAgent_WhenSuccessful_ShouldReturnAgent() .ReturnsResponse(HttpStatusCode.OK); // Act - var result = await _kepwareApiClient.IotGateway.CreateRestClientAgentAsync("TestRestClient"); + var result = await _kepwareApiClient.Project.IotGateway.CreateRestClientAgentAsync("TestRestClient"); // Assert result.ShouldNotBeNull(); @@ -395,7 +395,7 @@ public async Task CreateRestClientAgent_WithProperties_ShouldSetProperties() }; // Act - var result = await _kepwareApiClient.IotGateway.CreateRestClientAgentAsync("TestRestClient", properties); + var result = await _kepwareApiClient.Project.IotGateway.CreateRestClientAgentAsync("TestRestClient", properties); // Assert result.ShouldNotBeNull(); @@ -412,7 +412,7 @@ public async Task CreateRestClientAgent_WithHttpError_ShouldReturnNull() .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); // Act - var result = await _kepwareApiClient.IotGateway.CreateRestClientAgentAsync("TestRestClient"); + var result = await _kepwareApiClient.Project.IotGateway.CreateRestClientAgentAsync("TestRestClient"); // Assert result.ShouldBeNull(); @@ -422,7 +422,7 @@ public async Task CreateRestClientAgent_WithHttpError_ShouldReturnNull() public async Task CreateRestClientAgent_WithEmptyName_ShouldThrowArgumentException() { await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.CreateRestClientAgentAsync("")); + await _kepwareApiClient.Project.IotGateway.CreateRestClientAgentAsync("")); } [Fact] @@ -442,7 +442,7 @@ public async Task GetRestClientAgent_WhenExists_ShouldReturnAgent() .ReturnsResponse(HttpStatusCode.OK, agentJson, "application/json"); // Act - var result = await _kepwareApiClient.IotGateway.GetRestClientAgentAsync("TestRestClient"); + var result = await _kepwareApiClient.Project.IotGateway.GetRestClientAgentAsync("TestRestClient"); // Assert result.ShouldNotBeNull(); @@ -460,7 +460,7 @@ public async Task GetRestClientAgent_WhenNotFound_ShouldReturnNull() .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); // Act - var result = await _kepwareApiClient.IotGateway.GetRestClientAgentAsync("NonExistent"); + var result = await _kepwareApiClient.Project.IotGateway.GetRestClientAgentAsync("NonExistent"); // Assert result.ShouldBeNull(); @@ -477,7 +477,7 @@ public async Task DeleteRestClientAgent_ByEntity_WhenSuccessful_ShouldReturnTrue .ReturnsResponse(HttpStatusCode.OK); // Act - var result = await _kepwareApiClient.IotGateway.DeleteRestClientAgentAsync(agent); + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestClientAgentAsync(agent); // Assert result.ShouldBeTrue(); @@ -494,7 +494,7 @@ public async Task DeleteRestClientAgent_ByName_WhenSuccessful_ShouldReturnTrue() .ReturnsResponse(HttpStatusCode.OK); // Act - var result = await _kepwareApiClient.IotGateway.DeleteRestClientAgentAsync("TestRestClient"); + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestClientAgentAsync("TestRestClient"); // Assert result.ShouldBeTrue(); @@ -510,7 +510,7 @@ public async Task DeleteRestClientAgent_WithHttpError_ShouldReturnFalse() .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); // Act - var result = await _kepwareApiClient.IotGateway.DeleteRestClientAgentAsync("TestRestClient"); + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestClientAgentAsync("TestRestClient"); // Assert result.ShouldBeFalse(); @@ -534,7 +534,7 @@ public async Task GetOrCreateRestServerAgent_WhenNotExists_ShouldCreateAgent() .ReturnsResponse(HttpStatusCode.OK); // Act - var result = await _kepwareApiClient.IotGateway.GetOrCreateRestServerAgentAsync("TestRestServer"); + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateRestServerAgentAsync("TestRestServer"); // Assert result.ShouldNotBeNull(); @@ -558,7 +558,7 @@ public async Task GetOrCreateRestServerAgent_WhenExists_ShouldReturnAgent() .ReturnsResponse(HttpStatusCode.OK, agentJson, "application/json"); // Act - var result = await _kepwareApiClient.IotGateway.GetOrCreateRestServerAgentAsync("TestRestServer"); + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateRestServerAgentAsync("TestRestServer"); // Assert result.ShouldNotBeNull(); @@ -581,14 +581,14 @@ public async Task GetOrCreateRestServerAgent_WhenCreateFails_ShouldThrowInvalidO // Act & Assert await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.GetOrCreateRestServerAgentAsync("TestRestServer")); + await _kepwareApiClient.Project.IotGateway.GetOrCreateRestServerAgentAsync("TestRestServer")); } [Fact] public async Task GetOrCreateRestServerAgent_WithEmptyName_ShouldThrowArgumentException() { await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.GetOrCreateRestServerAgentAsync("")); + await _kepwareApiClient.Project.IotGateway.GetOrCreateRestServerAgentAsync("")); } [Fact] @@ -601,7 +601,7 @@ public async Task CreateRestServerAgent_WhenSuccessful_ShouldReturnAgent() .ReturnsResponse(HttpStatusCode.OK); // Act - var result = await _kepwareApiClient.IotGateway.CreateRestServerAgentAsync("TestRestServer"); + var result = await _kepwareApiClient.Project.IotGateway.CreateRestServerAgentAsync("TestRestServer"); // Assert result.ShouldNotBeNull(); @@ -623,7 +623,7 @@ public async Task CreateRestServerAgent_WithProperties_ShouldSetProperties() }; // Act - var result = await _kepwareApiClient.IotGateway.CreateRestServerAgentAsync("TestRestServer", properties); + var result = await _kepwareApiClient.Project.IotGateway.CreateRestServerAgentAsync("TestRestServer", properties); // Assert result.ShouldNotBeNull(); @@ -640,7 +640,7 @@ public async Task CreateRestServerAgent_WithHttpError_ShouldReturnNull() .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); // Act - var result = await _kepwareApiClient.IotGateway.CreateRestServerAgentAsync("TestRestServer"); + var result = await _kepwareApiClient.Project.IotGateway.CreateRestServerAgentAsync("TestRestServer"); // Assert result.ShouldBeNull(); @@ -650,7 +650,7 @@ public async Task CreateRestServerAgent_WithHttpError_ShouldReturnNull() public async Task CreateRestServerAgent_WithEmptyName_ShouldThrowArgumentException() { await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.CreateRestServerAgentAsync("")); + await _kepwareApiClient.Project.IotGateway.CreateRestServerAgentAsync("")); } [Fact] @@ -670,7 +670,7 @@ public async Task GetRestServerAgent_WhenExists_ShouldReturnAgent() .ReturnsResponse(HttpStatusCode.OK, agentJson, "application/json"); // Act - var result = await _kepwareApiClient.IotGateway.GetRestServerAgentAsync("TestRestServer"); + var result = await _kepwareApiClient.Project.IotGateway.GetRestServerAgentAsync("TestRestServer"); // Assert result.ShouldNotBeNull(); @@ -688,7 +688,7 @@ public async Task GetRestServerAgent_WhenNotFound_ShouldReturnNull() .ReturnsResponse(HttpStatusCode.NotFound, "Not Found"); // Act - var result = await _kepwareApiClient.IotGateway.GetRestServerAgentAsync("NonExistent"); + var result = await _kepwareApiClient.Project.IotGateway.GetRestServerAgentAsync("NonExistent"); // Assert result.ShouldBeNull(); @@ -705,7 +705,7 @@ public async Task DeleteRestServerAgent_ByEntity_WhenSuccessful_ShouldReturnTrue .ReturnsResponse(HttpStatusCode.OK); // Act - var result = await _kepwareApiClient.IotGateway.DeleteRestServerAgentAsync(agent); + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestServerAgentAsync(agent); // Assert result.ShouldBeTrue(); @@ -722,7 +722,7 @@ public async Task DeleteRestServerAgent_ByName_WhenSuccessful_ShouldReturnTrue() .ReturnsResponse(HttpStatusCode.OK); // Act - var result = await _kepwareApiClient.IotGateway.DeleteRestServerAgentAsync("TestRestServer"); + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestServerAgentAsync("TestRestServer"); // Assert result.ShouldBeTrue(); @@ -738,7 +738,7 @@ public async Task DeleteRestServerAgent_WithHttpError_ShouldReturnFalse() .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); // Act - var result = await _kepwareApiClient.IotGateway.DeleteRestServerAgentAsync("TestRestServer"); + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestServerAgentAsync("TestRestServer"); // Assert result.ShouldBeFalse(); @@ -763,7 +763,7 @@ public async Task GetOrCreateIotItem_WhenNotExists_ShouldCreateWithDerivedName() .ReturnsResponse(HttpStatusCode.OK); // Act - var result = await _kepwareApiClient.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentAgent); + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentAgent); // Assert result.ShouldNotBeNull(); @@ -789,7 +789,7 @@ public async Task GetOrCreateIotItem_WhenExists_ShouldReturnExistingItem() .ReturnsResponse(HttpStatusCode.OK, itemJson, "application/json"); // Act - var result = await _kepwareApiClient.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentAgent); + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentAgent); // Assert result.ShouldNotBeNull(); @@ -814,7 +814,7 @@ public async Task GetOrCreateIotItem_WhenCreateFails_ShouldThrowInvalidOperation // Act & Assert await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentAgent)); + await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentAgent)); } [Fact] @@ -828,7 +828,7 @@ public async Task CreateIotItem_ShouldDeriveNameFromServerTag() .ReturnsResponse(HttpStatusCode.OK); // Act - var result = await _kepwareApiClient.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", parentAgent); + var result = await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", parentAgent); // Assert result.ShouldNotBeNull(); @@ -847,7 +847,7 @@ public async Task CreateIotItem_WithSystemTag_ShouldStripLeadingUnderscore() .ReturnsResponse(HttpStatusCode.OK); // Act - var result = await _kepwareApiClient.IotGateway.CreateIotItemAsync("_System._Time", parentAgent); + var result = await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("_System._Time", parentAgent); // Assert result.ShouldNotBeNull(); @@ -866,7 +866,7 @@ public async Task CreateIotItem_WithHttpError_ShouldReturnNull() .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); // Act - var result = await _kepwareApiClient.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", parentAgent); + var result = await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", parentAgent); // Assert result.ShouldBeNull(); @@ -878,14 +878,14 @@ public async Task CreateIotItem_WithEmptyServerTag_ShouldThrowArgumentException( var parentAgent = new MqttClientAgent("ParentAgent"); await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.CreateIotItemAsync("", parentAgent)); + await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("", parentAgent)); } [Fact] public async Task CreateIotItem_WithNullParent_ShouldThrowArgumentNullException() { await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", null!)); + await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", null!)); } [Fact] @@ -906,7 +906,7 @@ public async Task GetIotItem_ShouldTranslateServerTagToItemName() .ReturnsResponse(HttpStatusCode.OK, itemJson, "application/json"); // Act - var result = await _kepwareApiClient.IotGateway.GetIotItemAsync("Channel1.Device1.Tag1", parentAgent); + var result = await _kepwareApiClient.Project.IotGateway.GetIotItemAsync("Channel1.Device1.Tag1", parentAgent); // Assert result.ShouldNotBeNull(); @@ -927,7 +927,7 @@ public async Task DeleteIotItem_ByEntity_WhenSuccessful_ShouldReturnTrue() .ReturnsResponse(HttpStatusCode.OK); // Act - var result = await _kepwareApiClient.IotGateway.DeleteIotItemAsync(item); + var result = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync(item); // Assert result.ShouldBeTrue(); @@ -945,7 +945,7 @@ public async Task DeleteIotItem_ByServerTag_ShouldTranslateToItemName() .ReturnsResponse(HttpStatusCode.OK); // Act - var result = await _kepwareApiClient.IotGateway.DeleteIotItemAsync("Channel1.Device1.Tag1", parentAgent); + var result = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync("Channel1.Device1.Tag1", parentAgent); // Assert result.ShouldBeTrue(); @@ -964,7 +964,7 @@ public async Task DeleteIotItem_WithHttpError_ShouldReturnFalse() .ReturnsResponse(HttpStatusCode.InternalServerError, "Server Error"); // Act - var result = await _kepwareApiClient.IotGateway.DeleteIotItemAsync(item); + var result = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync(item); // Assert result.ShouldBeFalse(); @@ -982,7 +982,7 @@ public async Task DeleteIotItem_WithConnectionError_ShouldReturnFalse() .Throws(new HttpRequestException("Connection error")); // Act - var result = await _kepwareApiClient.IotGateway.DeleteIotItemAsync(item); + var result = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync(item); // Assert result.ShouldBeFalse(); @@ -996,42 +996,42 @@ public async Task DeleteIotItem_WithConnectionError_ShouldReturnFalse() public async Task GetMqttClientAgent_WithEmptyName_ShouldThrowArgumentException() { await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.GetMqttClientAgentAsync("")); + await _kepwareApiClient.Project.IotGateway.GetMqttClientAgentAsync("")); } [Fact] public async Task GetRestClientAgent_WithEmptyName_ShouldThrowArgumentException() { await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.GetRestClientAgentAsync("")); + await _kepwareApiClient.Project.IotGateway.GetRestClientAgentAsync("")); } [Fact] public async Task GetRestServerAgent_WithEmptyName_ShouldThrowArgumentException() { await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.GetRestServerAgentAsync("")); + await _kepwareApiClient.Project.IotGateway.GetRestServerAgentAsync("")); } [Fact] public async Task DeleteMqttClientAgent_WithNullEntity_ShouldThrowArgumentNullException() { await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.DeleteMqttClientAgentAsync((MqttClientAgent)null!)); + await _kepwareApiClient.Project.IotGateway.DeleteMqttClientAgentAsync((MqttClientAgent)null!)); } [Fact] public async Task DeleteRestClientAgent_WithNullEntity_ShouldThrowArgumentNullException() { await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.DeleteRestClientAgentAsync((RestClientAgent)null!)); + await _kepwareApiClient.Project.IotGateway.DeleteRestClientAgentAsync((RestClientAgent)null!)); } [Fact] public async Task DeleteRestServerAgent_WithNullEntity_ShouldThrowArgumentNullException() { await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.DeleteRestServerAgentAsync((RestServerAgent)null!)); + await _kepwareApiClient.Project.IotGateway.DeleteRestServerAgentAsync((RestServerAgent)null!)); } [Fact] @@ -1040,21 +1040,21 @@ public async Task GetIotItem_WithEmptyServerTag_ShouldThrowArgumentException() var parentAgent = new MqttClientAgent("ParentAgent"); await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.GetIotItemAsync("", parentAgent)); + await _kepwareApiClient.Project.IotGateway.GetIotItemAsync("", parentAgent)); } [Fact] public async Task GetIotItem_WithNullParent_ShouldThrowArgumentNullException() { await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.GetIotItemAsync("Channel1.Device1.Tag1", null!)); + await _kepwareApiClient.Project.IotGateway.GetIotItemAsync("Channel1.Device1.Tag1", null!)); } [Fact] public async Task DeleteIotItem_WithNullEntity_ShouldThrowArgumentNullException() { await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.DeleteIotItemAsync((IotItem)null!)); + await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync((IotItem)null!)); } [Fact] @@ -1063,14 +1063,14 @@ public async Task GetOrCreateIotItem_WithEmptyServerTag_ShouldThrowArgumentExcep var parentAgent = new MqttClientAgent("ParentAgent"); await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.GetOrCreateIotItemAsync("", parentAgent)); + await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("", parentAgent)); } [Fact] public async Task GetOrCreateIotItem_WithNullParent_ShouldThrowArgumentNullException() { await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", null!)); + await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", null!)); } [Fact] @@ -1079,14 +1079,14 @@ public async Task DeleteIotItem_ByServerTag_WithEmptyTag_ShouldThrowArgumentExce var parentAgent = new MqttClientAgent("ParentAgent"); await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.DeleteIotItemAsync("", parentAgent)); + await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync("", parentAgent)); } [Fact] public async Task DeleteIotItem_ByServerTag_WithNullParent_ShouldThrowArgumentNullException() { await Should.ThrowAsync(async () => - await _kepwareApiClient.IotGateway.DeleteIotItemAsync("Channel1.Device1.Tag1", null!)); + await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync("Channel1.Device1.Tag1", null!)); } #endregion diff --git a/Kepware.Api/ClientHandler/ProjectApiHandler.cs b/Kepware.Api/ClientHandler/ProjectApiHandler.cs index 6eaec08..9954149 100644 --- a/Kepware.Api/ClientHandler/ProjectApiHandler.cs +++ b/Kepware.Api/ClientHandler/ProjectApiHandler.cs @@ -36,20 +36,28 @@ public class ProjectApiHandler /// See for method references. public DeviceApiHandler Devices { get; } + /// + /// Gets the IoT Gateway handlers. + /// + /// See for method references. + public IotGatewayApiHandler IotGateway { get; } + /// /// Initializes a new instance of the class. /// /// The Kepware API client. /// The channel API handler. /// The device API handler. + /// The IoT Gateway API handler. /// The logger instance. - public ProjectApiHandler(KepwareApiClient kepwareApiClient, ChannelApiHandler channelApiHandler, DeviceApiHandler deviceApiHandler, ILogger logger) + public ProjectApiHandler(KepwareApiClient kepwareApiClient, ChannelApiHandler channelApiHandler, DeviceApiHandler deviceApiHandler, IotGatewayApiHandler iotGatewayApiHandler, ILogger logger) { m_kepwareApiClient = kepwareApiClient; m_logger = logger; Channels = channelApiHandler; Devices = deviceApiHandler; + IotGateway = iotGatewayApiHandler; } #region CompareAndApply diff --git a/Kepware.Api/KepwareApiClient.cs b/Kepware.Api/KepwareApiClient.cs index 487059d..5a87ca2 100644 --- a/Kepware.Api/KepwareApiClient.cs +++ b/Kepware.Api/KepwareApiClient.cs @@ -98,12 +98,6 @@ public partial class KepwareApiClient : IKepwareDefaultValueProvider /// See for method references. public ServicesApiHandler ApiServices { get; init; } - /// - /// Gets the IoT Gateway handlers. - /// - /// See for method references. - public IotGatewayApiHandler IotGateway { get; init; } - internal HttpClient HttpClient { get { return m_httpClient; } } #region Constructors @@ -134,10 +128,10 @@ internal KepwareApiClient(string name, KepwareApiClientOptions options, ILoggerF var channelsApiHandler = new ChannelApiHandler(this, loggerFactory.CreateLogger()); var devicesApiHandler = new DeviceApiHandler(this, loggerFactory.CreateLogger()); - Project = new ProjectApiHandler(this, channelsApiHandler, devicesApiHandler, loggerFactory.CreateLogger()); + var iotGatewayApiHandler = new IotGatewayApiHandler(this, loggerFactory.CreateLogger()); + Project = new ProjectApiHandler(this, channelsApiHandler, devicesApiHandler, iotGatewayApiHandler, loggerFactory.CreateLogger()); Admin = new AdminApiHandler(this, loggerFactory.CreateLogger()); ApiServices = new ServicesApiHandler(this, loggerFactory.CreateLogger()); - IotGateway = new IotGatewayApiHandler(this, loggerFactory.CreateLogger()); } #endregion From c6e511085c1eaef1318945068d73d2af2ed1ebe7 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Wed, 1 Apr 2026 11:58:41 -0400 Subject: [PATCH 28/38] feat(api): Integrate IoT GW into Project context and management --- .../IotGatewayProjectIntegrationTests.cs | 465 ++++++++++++++++++ .../ClientHandler/ProjectApiHandler.cs | 109 ++++ Kepware.Api/Model/ApplyResults.cs | 12 + .../Project/IotGateway/IotGatewayContainer.cs | 41 ++ Kepware.Api/Model/Project/Project.cs | 21 +- .../Project/ProjectPropertiesJsonConverter.cs | 88 +++- Kepware.Api/Serializer/KepJsonContext.cs | 1 + 7 files changed, 734 insertions(+), 3 deletions(-) create mode 100644 Kepware.Api.Test/Model/IotGateway/IotGatewayProjectIntegrationTests.cs create mode 100644 Kepware.Api/Model/Project/IotGateway/IotGatewayContainer.cs diff --git a/Kepware.Api.Test/Model/IotGateway/IotGatewayProjectIntegrationTests.cs b/Kepware.Api.Test/Model/IotGateway/IotGatewayProjectIntegrationTests.cs new file mode 100644 index 0000000..1ca6328 --- /dev/null +++ b/Kepware.Api.Test/Model/IotGateway/IotGatewayProjectIntegrationTests.cs @@ -0,0 +1,465 @@ +using System.Text.Json; +using Kepware.Api.Model; +using Kepware.Api.Serializer; +using Xunit; + +namespace Kepware.Api.Test.Model.IotGateway +{ + public class IotGatewayProjectIntegrationTests + { + #region Project Deserialization + + [Fact] + public void ProjectWithIotGateway_ShouldDeserializeFromJson() + { + var json = @"{ + ""PROJECT_ID"": 42, + ""common.ALLTYPES_DESCRIPTION"": ""Test project"", + ""_iot_gateway"": [ + { + ""common.ALLTYPES_NAME"": ""_IoT_Gateway"", + ""mqtt_clients"": [ + { + ""common.ALLTYPES_NAME"": ""MqttAgent1"", + ""common.ALLTYPES_DESCRIPTION"": ""Test MQTT agent"", + ""iot_gateway.AGENTTYPES_ENABLED"": true, + ""iot_gateway.MQTT_CLIENT_URL"": ""tcp://broker:1883"", + ""iot_items"": [ + { + ""common.ALLTYPES_NAME"": ""Item1"", + ""iot_gateway.IOT_ITEM_SERVER_TAG"": ""Channel1.Device1.Tag1"", + ""iot_gateway.IOT_ITEM_ENABLED"": true + }, + { + ""common.ALLTYPES_NAME"": ""_MqttItem2"", + ""iot_gateway.IOT_ITEM_SERVER_TAG"": ""Channel1.Device1.Tag5"", + ""iot_gateway.IOT_ITEM_ENABLED"": false + } + ] + } + ], + ""rest_clients"": [ + { + ""common.ALLTYPES_NAME"": ""RestClient1"", + ""iot_gateway.AGENTTYPES_ENABLED"": true, + ""iot_gateway.REST_CLIENT_URL"": ""https://api.example.com"", + ""iot_items"": [ + { + ""common.ALLTYPES_NAME"": ""_RestClientItem1"", + ""iot_gateway.IOT_ITEM_SERVER_TAG"": ""Channel1.Device1.Tag2"", + ""iot_gateway.IOT_ITEM_ENABLED"": true + }, + { + ""common.ALLTYPES_NAME"": ""RestClientItem2"", + ""iot_gateway.IOT_ITEM_SERVER_TAG"": ""Channel1.Device1.Tag3"", + ""iot_gateway.IOT_ITEM_ENABLED"": false + } + ] + } + ], + ""rest_servers"": [ + { + ""common.ALLTYPES_NAME"": ""RestServer1"", + ""iot_gateway.AGENTTYPES_ENABLED"": false, + ""iot_gateway.REST_SERVER_PORT_NUMBER"": 39320, + ""iot_items"": [ + { + ""common.ALLTYPES_NAME"": ""RestServerItem1"", + ""iot_gateway.IOT_ITEM_SERVER_TAG"": ""Channel1.Device1.Tag4"", + ""iot_gateway.IOT_ITEM_ENABLED"": true + }, + { + ""common.ALLTYPES_NAME"": ""_RestServerItem2"", + ""iot_gateway.IOT_ITEM_SERVER_TAG"": ""Channel2.Device2.Tag1"", + ""iot_gateway.IOT_ITEM_ENABLED"": false + } + ] + } + ] + } + ] + }"; + + var project = JsonSerializer.Deserialize(json, KepJsonContext.Default.Project); + + Assert.NotNull(project); + Assert.Equal(42, project.ProjectId); + Assert.Equal("Test project", project.Description); + + Assert.NotNull(project.IotGateway); + Assert.False(project.IotGateway.IsEmpty); + + // MQTT Client + Assert.NotNull(project.IotGateway.MqttClientAgents); + Assert.Single(project.IotGateway.MqttClientAgents); + var mqtt = project.IotGateway.MqttClientAgents[0]; + Assert.Equal("MqttAgent1", mqtt.Name); + Assert.True(mqtt.Enabled); + Assert.Equal("tcp://broker:1883", mqtt.Url); + + // MQTT Client IoT Items + Assert.NotNull(mqtt.IotItems); + Assert.Equal(2, mqtt.IotItems.Count); + Assert.Equal("Item1", mqtt.IotItems[0].Name); + Assert.Equal("Channel1.Device1.Tag1", mqtt.IotItems[0].ServerTag); + Assert.True(mqtt.IotItems[0].Enabled); + Assert.Equal("_MqttItem2", mqtt.IotItems[1].Name); + Assert.Equal("Channel1.Device1.Tag5", mqtt.IotItems[1].ServerTag); + Assert.False(mqtt.IotItems[1].Enabled); + + // REST Client + Assert.NotNull(project.IotGateway.RestClientAgents); + Assert.Single(project.IotGateway.RestClientAgents); + var restClient = project.IotGateway.RestClientAgents[0]; + Assert.Equal("RestClient1", restClient.Name); + Assert.True(restClient.Enabled); + + // REST Client IoT Items + Assert.NotNull(restClient.IotItems); + Assert.Equal(2, restClient.IotItems.Count); + Assert.Equal("_RestClientItem1", restClient.IotItems[0].Name); + Assert.Equal("Channel1.Device1.Tag2", restClient.IotItems[0].ServerTag); + Assert.True(restClient.IotItems[0].Enabled); + Assert.Equal("RestClientItem2", restClient.IotItems[1].Name); + Assert.Equal("Channel1.Device1.Tag3", restClient.IotItems[1].ServerTag); + Assert.False(restClient.IotItems[1].Enabled); + + // REST Server + Assert.NotNull(project.IotGateway.RestServerAgents); + Assert.Single(project.IotGateway.RestServerAgents); + var restServer = project.IotGateway.RestServerAgents[0]; + Assert.Equal("RestServer1", restServer.Name); + Assert.False(restServer.Enabled); + Assert.Equal(39320, restServer.PortNumber); + + // REST Server IoT Items + Assert.NotNull(restServer.IotItems); + Assert.Equal(2, restServer.IotItems.Count); + Assert.Equal("RestServerItem1", restServer.IotItems[0].Name); + Assert.Equal("Channel1.Device1.Tag4", restServer.IotItems[0].ServerTag); + Assert.True(restServer.IotItems[0].Enabled); + Assert.Equal("_RestServerItem2", restServer.IotItems[1].Name); + Assert.Equal("Channel2.Device2.Tag1", restServer.IotItems[1].ServerTag); + Assert.False(restServer.IotItems[1].Enabled); + } + + [Fact] + public void ProjectWithoutIotGateway_ShouldDeserializeCorrectly() + { + var json = @"{ + ""PROJECT_ID"": 1, + ""common.ALLTYPES_DESCRIPTION"": ""No IoT"" + }"; + + var project = JsonSerializer.Deserialize(json, KepJsonContext.Default.Project); + + Assert.NotNull(project); + Assert.Null(project.IotGateway); + } + + [Fact] + public void ProjectWithEmptyIotGateway_ShouldNotSerializeIotGateway() + { + var project = new Project + { + IotGateway = new IotGatewayContainer() + }; + + var json = JsonSerializer.Serialize(project, KepJsonContext.Default.Project); + + Assert.DoesNotContain("_iot_gateway", json); + } + + #endregion + + #region Round-trip Serialization + + [Fact] + public void ProjectWithIotGateway_ShouldRoundTripSerialize() + { + var project = new Project(); + project.IotGateway = new IotGatewayContainer + { + MqttClientAgents = new MqttClientAgentCollection + { + new MqttClientAgent + { + Name = "MqttAgent1", + Enabled = true, + Url = "tcp://broker:1883", + Topic = "test/topic", + IotItems = new IotItemCollection + { + new IotItem + { + Name = "Item1", + ServerTag = "Channel1.Device1.Tag1", + Enabled = true + }, + new IotItem + { + Name = "_MqttItem2", + ServerTag = "Channel1.Device1.Tag5", + Enabled = false + } + } + } + }, + RestClientAgents = new RestClientAgentCollection + { + new RestClientAgent + { + Name = "RestClient1", + Enabled = true, + Url = "https://api.example.com", + IotItems = new IotItemCollection + { + new IotItem + { + Name = "_RestClientItem1", + ServerTag = "Channel1.Device1.Tag2", + Enabled = true + }, + new IotItem + { + Name = "RestClientItem2", + ServerTag = "Channel1.Device1.Tag3", + Enabled = false + } + } + } + }, + RestServerAgents = new RestServerAgentCollection + { + new RestServerAgent + { + Name = "RestServer1", + Enabled = false, + PortNumber = 39320, + IotItems = new IotItemCollection + { + new IotItem + { + Name = "RestServerItem1", + ServerTag = "Channel1.Device1.Tag4", + Enabled = true + }, + new IotItem + { + Name = "_RestServerItem2", + ServerTag = "Channel2.Device2.Tag1", + Enabled = false + } + } + } + } + }; + + var json = JsonSerializer.Serialize(project, KepJsonContext.Default.Project); + var deserialized = JsonSerializer.Deserialize(json, KepJsonContext.Default.Project); + + Assert.NotNull(deserialized); + Assert.NotNull(deserialized.IotGateway); + Assert.False(deserialized.IotGateway.IsEmpty); + + // MQTT + Assert.NotNull(deserialized.IotGateway.MqttClientAgents); + Assert.Single(deserialized.IotGateway.MqttClientAgents); + Assert.Equal("MqttAgent1", deserialized.IotGateway.MqttClientAgents[0].Name); + Assert.Equal("tcp://broker:1883", deserialized.IotGateway.MqttClientAgents[0].Url); + Assert.Equal("test/topic", deserialized.IotGateway.MqttClientAgents[0].Topic); + + // MQTT Items + Assert.NotNull(deserialized.IotGateway.MqttClientAgents[0].IotItems); + Assert.Equal(2, deserialized.IotGateway.MqttClientAgents[0].IotItems!.Count); + Assert.Equal("Item1", deserialized.IotGateway.MqttClientAgents[0].IotItems![0].Name); + Assert.Equal("_MqttItem2", deserialized.IotGateway.MqttClientAgents[0].IotItems![1].Name); + Assert.False(deserialized.IotGateway.MqttClientAgents[0].IotItems![1].Enabled); + + // REST Client + Assert.NotNull(deserialized.IotGateway.RestClientAgents); + Assert.Single(deserialized.IotGateway.RestClientAgents); + Assert.Equal("RestClient1", deserialized.IotGateway.RestClientAgents[0].Name); + + // REST Client Items + Assert.NotNull(deserialized.IotGateway.RestClientAgents[0].IotItems); + Assert.Equal(2, deserialized.IotGateway.RestClientAgents[0].IotItems!.Count); + Assert.Equal("_RestClientItem1", deserialized.IotGateway.RestClientAgents[0].IotItems![0].Name); + Assert.Equal("Channel1.Device1.Tag2", deserialized.IotGateway.RestClientAgents[0].IotItems![0].ServerTag); + Assert.Equal("RestClientItem2", deserialized.IotGateway.RestClientAgents[0].IotItems![1].Name); + Assert.Equal("Channel1.Device1.Tag3", deserialized.IotGateway.RestClientAgents[0].IotItems![1].ServerTag); + + // REST Server + Assert.NotNull(deserialized.IotGateway.RestServerAgents); + Assert.Single(deserialized.IotGateway.RestServerAgents); + Assert.Equal("RestServer1", deserialized.IotGateway.RestServerAgents[0].Name); + Assert.Equal(39320, deserialized.IotGateway.RestServerAgents[0].PortNumber); + + // REST Server Items + Assert.NotNull(deserialized.IotGateway.RestServerAgents[0].IotItems); + Assert.Equal(2, deserialized.IotGateway.RestServerAgents[0].IotItems!.Count); + Assert.Equal("RestServerItem1", deserialized.IotGateway.RestServerAgents[0].IotItems![0].Name); + Assert.Equal("Channel1.Device1.Tag4", deserialized.IotGateway.RestServerAgents[0].IotItems![0].ServerTag); + Assert.Equal("_RestServerItem2", deserialized.IotGateway.RestServerAgents[0].IotItems![1].Name); + Assert.Equal("Channel2.Device2.Tag1", deserialized.IotGateway.RestServerAgents[0].IotItems![1].ServerTag); + } + + #endregion + + #region Project.IsEmpty + + [Fact] + public void Project_IsEmpty_WithNullIotGateway_ShouldBeTrue() + { + var project = new Project(); + Assert.True(project.IsEmpty); + } + + [Fact] + public void Project_IsEmpty_WithEmptyIotGateway_ShouldBeTrue() + { + var project = new Project + { + IotGateway = new IotGatewayContainer() + }; + Assert.True(project.IsEmpty); + } + + [Fact] + public void Project_IsEmpty_WithIotGatewayAgents_ShouldBeFalse() + { + var project = new Project + { + IotGateway = new IotGatewayContainer + { + MqttClientAgents = new MqttClientAgentCollection + { + new MqttClientAgent { Name = "Agent1" } + } + } + }; + Assert.False(project.IsEmpty); + } + + #endregion + + #region JsonProjectRoot + + [Fact] + public void FullProjectJson_ShouldDeserializeWithIotGatewayInProjectRoot() + { + var json = @"{ + ""project"": { + ""PROJECT_ID"": 100, + ""common.ALLTYPES_DESCRIPTION"": ""Full project"", + ""_iot_gateway"": [ + { + ""common.ALLTYPES_NAME"": ""_IoT_Gateway"", + ""mqtt_clients"": [ + { + ""common.ALLTYPES_NAME"": ""Mqtt1"", + ""iot_gateway.AGENTTYPES_ENABLED"": true, + ""iot_items"": [ + { + ""common.ALLTYPES_NAME"": ""_MqttRootItem1"", + ""iot_gateway.IOT_ITEM_SERVER_TAG"": ""Channel1.Device1.Tag1"", + ""iot_gateway.IOT_ITEM_ENABLED"": true + } + ] + } + ], + ""rest_servers"": [ + { + ""common.ALLTYPES_NAME"": ""RestSrv1"", + ""iot_gateway.REST_SERVER_PORT_NUMBER"": 8080, + ""iot_items"": [ + { + ""common.ALLTYPES_NAME"": ""_RestSrvItem1"", + ""iot_gateway.IOT_ITEM_SERVER_TAG"": ""Channel1.Device1.Tag2"", + ""iot_gateway.IOT_ITEM_ENABLED"": false + } + ] + } + ] + } + ], + ""channels"": [ + { + ""common.ALLTYPES_NAME"": ""Channel1"", + ""servermain.MULTIPLE_TYPES_DEVICE_DRIVER"": ""Simulator"" + } + ] + } + }"; + + var root = JsonSerializer.Deserialize(json, KepJsonContext.Default.JsonProjectRoot); + + Assert.NotNull(root); + Assert.NotNull(root.Project); + Assert.Equal(100, root.Project.ProjectId); + + // Channels + Assert.NotNull(root.Project.Channels); + Assert.Single(root.Project.Channels); + Assert.Equal("Channel1", root.Project.Channels[0].Name); + + // IoT Gateway + Assert.NotNull(root.Project.IotGateway); + Assert.NotNull(root.Project.IotGateway.MqttClientAgents); + Assert.Single(root.Project.IotGateway.MqttClientAgents); + Assert.Equal("Mqtt1", root.Project.IotGateway.MqttClientAgents[0].Name); + Assert.True(root.Project.IotGateway.MqttClientAgents[0].Enabled); + Assert.NotNull(root.Project.IotGateway.MqttClientAgents[0].IotItems); + Assert.Single(root.Project.IotGateway.MqttClientAgents[0].IotItems!); + Assert.Equal("_MqttRootItem1", root.Project.IotGateway.MqttClientAgents[0].IotItems![0].Name); + Assert.Equal("Channel1.Device1.Tag1", root.Project.IotGateway.MqttClientAgents[0].IotItems![0].ServerTag); + + Assert.NotNull(root.Project.IotGateway.RestServerAgents); + Assert.Single(root.Project.IotGateway.RestServerAgents); + Assert.Equal("RestSrv1", root.Project.IotGateway.RestServerAgents[0].Name); + Assert.Equal(8080, root.Project.IotGateway.RestServerAgents[0].PortNumber); + Assert.NotNull(root.Project.IotGateway.RestServerAgents[0].IotItems); + Assert.Single(root.Project.IotGateway.RestServerAgents[0].IotItems!); + Assert.Equal("_RestSrvItem1", root.Project.IotGateway.RestServerAgents[0].IotItems![0].Name); + Assert.False(root.Project.IotGateway.RestServerAgents[0].IotItems![0].Enabled); + } + + #endregion + + #region IotGatewayContainer.IsEmpty + + [Fact] + public void IotGatewayContainer_IsEmpty_WithNullCollections_ShouldBeTrue() + { + var container = new IotGatewayContainer(); + Assert.True(container.IsEmpty); + } + + [Fact] + public void IotGatewayContainer_IsEmpty_WithEmptyCollections_ShouldBeTrue() + { + var container = new IotGatewayContainer + { + MqttClientAgents = new MqttClientAgentCollection(), + RestClientAgents = new RestClientAgentCollection(), + RestServerAgents = new RestServerAgentCollection() + }; + Assert.True(container.IsEmpty); + } + + [Fact] + public void IotGatewayContainer_IsEmpty_WithAgents_ShouldBeFalse() + { + var container = new IotGatewayContainer + { + RestClientAgents = new RestClientAgentCollection + { + new RestClientAgent { Name = "Agent1" } + } + }; + Assert.False(container.IsEmpty); + } + + #endregion + } +} diff --git a/Kepware.Api/ClientHandler/ProjectApiHandler.cs b/Kepware.Api/ClientHandler/ProjectApiHandler.cs index 9954149..c239b94 100644 --- a/Kepware.Api/ClientHandler/ProjectApiHandler.cs +++ b/Kepware.Api/ClientHandler/ProjectApiHandler.cs @@ -157,6 +157,9 @@ public async Task CompareAndApplyDetailedAsync(Pro } } + // Compare and apply IoT Gateway agents and their IoT Items + await CompareAndApplyIotGatewayDetailedAsync(sourceProject.IotGateway, projectFromApi.IotGateway, result, cancellationToken).ConfigureAwait(false); + return result; } #endregion @@ -429,6 +432,18 @@ private static void SetOwnersFullProject(Project project) SetOwnerRecursive(device.TagGroups, device); } } + + if (project.IotGateway != null) + { + foreach (var agent in (project.IotGateway.MqttClientAgents ?? []).Cast() + .Concat(project.IotGateway.RestClientAgents ?? []) + .Concat(project.IotGateway.RestServerAgents ?? [])) + { + if (agent.IotItems != null) + foreach (var item in agent.IotItems) + item.Owner = agent; + } + } } private async Task LoadProjectOptimizedRecurisveAsync(Project project, int tagLimit, CancellationToken cancellationToken = default) @@ -532,6 +547,9 @@ private async Task LoadProjectOptimizedRecurisveAsync(Project project, } } + // Load IoT Gateway agents and their IoT Items + await LoadIotGatewayRecursiveAsync(project, cancellationToken).ConfigureAwait(false); + return project; } @@ -580,11 +598,102 @@ await Task.WhenAll(channel.Devices.Select(async device => project = new Project(); } } + + // Load IoT Gateway agents and their IoT Items + await LoadIotGatewayRecursiveAsync(project, cancellationToken).ConfigureAwait(false); + return project; } + + private async Task LoadIotGatewayRecursiveAsync(Project project, CancellationToken cancellationToken) + { + MqttClientAgentCollection? mqttClients; + RestClientAgentCollection? restClients; + RestServerAgentCollection? restServers; + + try + { + mqttClients = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + restClients = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + restServers = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(cancellationToken: cancellationToken).ConfigureAwait(false); + } + catch (InvalidOperationException ex) + { + // IoT Gateway plug-in may not be installed on the server + m_logger.LogDebug(ex, "IoT Gateway plug-in not available, skipping IoT Gateway loading"); + return; + } + + if ((mqttClients != null && mqttClients.Count > 0) || + (restClients != null && restClients.Count > 0) || + (restServers != null && restServers.Count > 0)) + { + project.IotGateway = new IotGatewayContainer + { + MqttClientAgents = mqttClients, + RestClientAgents = restClients, + RestServerAgents = restServers + }; + + // Load IoT Items for each agent + var agentTasks = new List(); + foreach (var agent in (mqttClients ?? []).Cast() + .Concat(restClients ?? []) + .Concat(restServers ?? [])) + { + agentTasks.Add(Task.Run(async () => + { + agent.IotItems = await m_kepwareApiClient.GenericConfig.LoadCollectionAsync(agent, cancellationToken: cancellationToken).ConfigureAwait(false); + })); + } + await Task.WhenAll(agentTasks).ConfigureAwait(false); + } + } #endregion #region recursive methods + + private async Task CompareAndApplyIotGatewayDetailedAsync( + IotGatewayContainer? source, IotGatewayContainer? current, + ProjectCompareAndApplyResult result, CancellationToken cancellationToken) + { + // Compare MQTT Client agents + var mqttCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync( + source?.MqttClientAgents, current?.MqttClientAgents, cancellationToken: cancellationToken).ConfigureAwait(false); + result.Add(mqttCompare); + + foreach (var agent in mqttCompare.CompareResult.UnchangedItems.Concat(mqttCompare.CompareResult.ChangedItems)) + { + var itemCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync( + agent.Left!.IotItems, agent.Right!.IotItems, agent.Right, cancellationToken).ConfigureAwait(false); + result.Add(itemCompare); + } + + // Compare REST Client agents + var restClientCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync( + source?.RestClientAgents, current?.RestClientAgents, cancellationToken: cancellationToken).ConfigureAwait(false); + result.Add(restClientCompare); + + foreach (var agent in restClientCompare.CompareResult.UnchangedItems.Concat(restClientCompare.CompareResult.ChangedItems)) + { + var itemCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync( + agent.Left!.IotItems, agent.Right!.IotItems, agent.Right, cancellationToken).ConfigureAwait(false); + result.Add(itemCompare); + } + + // Compare REST Server agents + var restServerCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync( + source?.RestServerAgents, current?.RestServerAgents, cancellationToken: cancellationToken).ConfigureAwait(false); + result.Add(restServerCompare); + + foreach (var agent in restServerCompare.CompareResult.UnchangedItems.Concat(restServerCompare.CompareResult.ChangedItems)) + { + var itemCompare = await m_kepwareApiClient.GenericConfig.CompareAndApplyDetailedAsync( + agent.Left!.IotItems, agent.Right!.IotItems, agent.Right, cancellationToken).ConfigureAwait(false); + result.Add(itemCompare); + } + } + private static void SetOwnerRecursive(IEnumerable tagGroups, NamedEntity owner) { foreach (var tagGroup in tagGroups) diff --git a/Kepware.Api/Model/ApplyResults.cs b/Kepware.Api/Model/ApplyResults.cs index cbc3429..d817afb 100644 --- a/Kepware.Api/Model/ApplyResults.cs +++ b/Kepware.Api/Model/ApplyResults.cs @@ -173,6 +173,18 @@ internal void Add(CollectionApplyResult result) internal void Add(CollectionApplyResult result) => Add(result.Inserts, result.Updates, result.Deletes, result.FailureList); + internal void Add(CollectionApplyResult result) + => Add(result.Inserts, result.Updates, result.Deletes, result.FailureList); + + internal void Add(CollectionApplyResult result) + => Add(result.Inserts, result.Updates, result.Deletes, result.FailureList); + + internal void Add(CollectionApplyResult result) + => Add(result.Inserts, result.Updates, result.Deletes, result.FailureList); + + internal void Add(CollectionApplyResult result) + => Add(result.Inserts, result.Updates, result.Deletes, result.FailureList); + internal void Add(ProjectCompareAndApplyResult result) => Add(result.Inserts, result.Updates, result.Deletes, result.FailureList); diff --git a/Kepware.Api/Model/Project/IotGateway/IotGatewayContainer.cs b/Kepware.Api/Model/Project/IotGateway/IotGatewayContainer.cs new file mode 100644 index 0000000..3af217f --- /dev/null +++ b/Kepware.Api/Model/Project/IotGateway/IotGatewayContainer.cs @@ -0,0 +1,41 @@ +using System.Text.Json.Serialization; + +namespace Kepware.Api.Model +{ + /// + /// Container class representing the _iot_gateway node in the full project JSON. + /// Holds the three agent collections (MQTT Client, REST Client, REST Server). + /// + public class IotGatewayContainer + { + /// + /// Gets or sets the MQTT Client agents. + /// + [JsonPropertyName("mqtt_clients")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public MqttClientAgentCollection? MqttClientAgents { get; set; } + + /// + /// Gets or sets the REST Client agents. + /// + [JsonPropertyName("rest_clients")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public RestClientAgentCollection? RestClientAgents { get; set; } + + /// + /// Gets or sets the REST Server agents. + /// + [JsonPropertyName("rest_servers")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public RestServerAgentCollection? RestServerAgents { get; set; } + + /// + /// Returns true if all three agent collections are null or empty. + /// + [JsonIgnore] + public bool IsEmpty => + (MqttClientAgents == null || MqttClientAgents.Count == 0) && + (RestClientAgents == null || RestClientAgents.Count == 0) && + (RestServerAgents == null || RestServerAgents.Count == 0); + } +} diff --git a/Kepware.Api/Model/Project/Project.cs b/Kepware.Api/Model/Project/Project.cs index f441603..3b25b2b 100644 --- a/Kepware.Api/Model/Project/Project.cs +++ b/Kepware.Api/Model/Project/Project.cs @@ -27,7 +27,7 @@ public class Project : DefaultEntity /// /// If this is true, it indicates that this is an empty project object that was instantiated without data from the server. /// - public bool IsEmpty => Channels == null && DynamicProperties.Count == 0; + public bool IsEmpty => Channels == null && (IotGateway == null || IotGateway.IsEmpty) && DynamicProperties.Count == 0; /// /// Initializes a new instance of the class. @@ -58,6 +58,15 @@ public Project() [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public ChannelCollection? Channels { get; set; } + /// + /// Gets or sets the IoT Gateway container holding MQTT Client, REST Client, and REST Server agent collections. + /// + [YamlIgnore] + [JsonPropertyName("_iot_gateway")] + [JsonPropertyOrder(101)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IotGatewayContainer? IotGateway { get; set; } + /// /// Recursively cleans up the project and all its children /// @@ -77,6 +86,16 @@ public override async Task Cleanup(IKepwareDefaultValueProvider defaultValueProv await channel.Cleanup(defaultValueProvider, blnRemoveProjectId, cancellationToken).ConfigureAwait(false); } } + + if (IotGateway != null) + { + foreach (var agent in (IotGateway.MqttClientAgents ?? []).Cast() + .Concat(IotGateway.RestClientAgents ?? []) + .Concat(IotGateway.RestServerAgents ?? [])) + { + await agent.Cleanup(defaultValueProvider, blnRemoveProjectId, cancellationToken).ConfigureAwait(false); + } + } } public async Task CloneAsync(CancellationToken cancellationToken = default) diff --git a/Kepware.Api/Model/Project/ProjectPropertiesJsonConverter.cs b/Kepware.Api/Model/Project/ProjectPropertiesJsonConverter.cs index 3401c56..db038cc 100644 --- a/Kepware.Api/Model/Project/ProjectPropertiesJsonConverter.cs +++ b/Kepware.Api/Model/Project/ProjectPropertiesJsonConverter.cs @@ -53,10 +53,10 @@ public class ProjectPropertiesJsonConverter : JsonConverter // Handle exposed properties supporting Project model // This includes properties inherited from BaseEntity such as PROJECT_ID, DESCRIPTION, etc. // TODO: Expand as needed for other known properties or consider common approach for BaseEntity properties - if (root.TryGetProperty("channels", out var channelsProp)) + if (prop.Name == "channels") { var jsonTypeInfo = (JsonTypeInfo>)options.GetTypeInfo(typeof(List)); - var channels = JsonSerializer.Deserialize(channelsProp.GetRawText(), jsonTypeInfo); + var channels = JsonSerializer.Deserialize(prop.Value.GetRawText(), jsonTypeInfo); if (channels != null) { project.Channels = new ChannelCollection(); @@ -66,6 +66,56 @@ public class ProjectPropertiesJsonConverter : JsonConverter } } } + else if (prop.Name == "_iot_gateway") + { + // _iot_gateway is an array containing a single wrapper object with + // "common.ALLTYPES_NAME": "_IoT_Gateway" and the agent collection arrays. + if (prop.Value.ValueKind == JsonValueKind.Array) + { + foreach (var gwElement in prop.Value.EnumerateArray()) + { + if (gwElement.ValueKind == JsonValueKind.Object) + { + var container = new IotGatewayContainer(); + + if (gwElement.TryGetProperty("mqtt_clients", out var mqttProp)) + { + var mqttTypeInfo = (JsonTypeInfo>)options.GetTypeInfo(typeof(List)); + var agents = JsonSerializer.Deserialize(mqttProp.GetRawText(), mqttTypeInfo); + if (agents != null) + { + container.MqttClientAgents = new MqttClientAgentCollection(); + foreach (var agent in agents) container.MqttClientAgents.Add(agent); + } + } + + if (gwElement.TryGetProperty("rest_clients", out var restClientProp)) + { + var restClientTypeInfo = (JsonTypeInfo>)options.GetTypeInfo(typeof(List)); + var agents = JsonSerializer.Deserialize(restClientProp.GetRawText(), restClientTypeInfo); + if (agents != null) + { + container.RestClientAgents = new RestClientAgentCollection(); + foreach (var agent in agents) container.RestClientAgents.Add(agent); + } + } + + if (gwElement.TryGetProperty("rest_servers", out var restServerProp)) + { + var restServerTypeInfo = (JsonTypeInfo>)options.GetTypeInfo(typeof(List)); + var agents = JsonSerializer.Deserialize(restServerProp.GetRawText(), restServerTypeInfo); + if (agents != null) + { + container.RestServerAgents = new RestServerAgentCollection(); + foreach (var agent in agents) container.RestServerAgents.Add(agent); + } + } + + project.IotGateway = container; + } + } + } + } else if (prop.Name == "PROJECT_ID") { if (prop.Value.TryGetInt64(out var projectId)) @@ -104,6 +154,40 @@ public override void Write(Utf8JsonWriter writer, Project value, JsonSerializerO JsonSerializer.Serialize(writer, value.Channels.ToList(), jsonTypeInfo); } + // Emit IoT Gateway container as array envelope (if present) + if (value.IotGateway != null && !value.IotGateway.IsEmpty) + { + writer.WritePropertyName("_iot_gateway"); + writer.WriteStartArray(); + writer.WriteStartObject(); + + writer.WriteString("common.ALLTYPES_NAME", "_IoT_Gateway"); + + if (value.IotGateway.MqttClientAgents != null && value.IotGateway.MqttClientAgents.Count > 0) + { + writer.WritePropertyName("mqtt_clients"); + var mqttTypeInfo = (JsonTypeInfo>)options.GetTypeInfo(typeof(List)); + JsonSerializer.Serialize(writer, value.IotGateway.MqttClientAgents.ToList(), mqttTypeInfo); + } + + if (value.IotGateway.RestClientAgents != null && value.IotGateway.RestClientAgents.Count > 0) + { + writer.WritePropertyName("rest_clients"); + var restClientTypeInfo = (JsonTypeInfo>)options.GetTypeInfo(typeof(List)); + JsonSerializer.Serialize(writer, value.IotGateway.RestClientAgents.ToList(), restClientTypeInfo); + } + + if (value.IotGateway.RestServerAgents != null && value.IotGateway.RestServerAgents.Count > 0) + { + writer.WritePropertyName("rest_servers"); + var restServerTypeInfo = (JsonTypeInfo>)options.GetTypeInfo(typeof(List)); + JsonSerializer.Serialize(writer, value.IotGateway.RestServerAgents.ToList(), restServerTypeInfo); + } + + writer.WriteEndObject(); + writer.WriteEndArray(); + } + // Build grouped client_interfaces element from flattened dynamic properties var clientInterfacesElement = ClientInterfacesFlattener.BuildClientInterfacesArrayFromDynamicProperties(value.DynamicProperties); if (clientInterfacesElement.HasValue) diff --git a/Kepware.Api/Serializer/KepJsonContext.cs b/Kepware.Api/Serializer/KepJsonContext.cs index 9d0640a..513827f 100644 --- a/Kepware.Api/Serializer/KepJsonContext.cs +++ b/Kepware.Api/Serializer/KepJsonContext.cs @@ -31,6 +31,7 @@ namespace Kepware.Api.Serializer [JsonSerializable(typeof(RestClientAgent))] [JsonSerializable(typeof(RestServerAgent))] [JsonSerializable(typeof(IotItem))] + [JsonSerializable(typeof(IotGatewayContainer))] [JsonSerializable(typeof(List))] [JsonSerializable(typeof(List))] From a0882f7311bb24c22396e988542399be5f96c021 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Wed, 1 Apr 2026 16:19:38 -0400 Subject: [PATCH 29/38] chore: Add TODO for future review --- Kepware.Api/ClientHandler/ChannelApiHandler.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Kepware.Api/ClientHandler/ChannelApiHandler.cs b/Kepware.Api/ClientHandler/ChannelApiHandler.cs index ffb2dbd..5e395da 100644 --- a/Kepware.Api/ClientHandler/ChannelApiHandler.cs +++ b/Kepware.Api/ClientHandler/ChannelApiHandler.cs @@ -52,6 +52,7 @@ public async Task GetOrCreateChannelAsync(string name, string driverNam channel = await CreateChannelAsync(name, driverName, properties, cancellationToken); if (channel != null) { + // TODO: Review this section. This should not do an update if (properties != null) { var currentHash = channel.Hash; From aaabe45127453a4c8fef88c1f118c9ca13ab5e4f Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Wed, 1 Apr 2026 16:20:10 -0400 Subject: [PATCH 30/38] chore: updated gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8d0032d..5b67a39 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ docs/docfx/api/ docs/docfx/_* docs/docfx/Kepware* docs/docfx/*.md +.API * # User-specific files *.rsuser From 123a4ea001e86f9ffc1eb60498a6a7d096226750 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Wed, 1 Apr 2026 22:06:48 -0400 Subject: [PATCH 31/38] test: Added IoT GW Integration Tests --- .../ApiClient/IotGatewayTests.cs | 973 ++++++++++++++++++ .../ApiClient/_TestIntgApiClientBase.cs | 91 ++ 2 files changed, 1064 insertions(+) create mode 100644 Kepware.Api.TestIntg/ApiClient/IotGatewayTests.cs diff --git a/Kepware.Api.TestIntg/ApiClient/IotGatewayTests.cs b/Kepware.Api.TestIntg/ApiClient/IotGatewayTests.cs new file mode 100644 index 0000000..ab60c58 --- /dev/null +++ b/Kepware.Api.TestIntg/ApiClient/IotGatewayTests.cs @@ -0,0 +1,973 @@ +using Kepware.Api.ClientHandler; +using Kepware.Api.Model; +using Kepware.Api.Serializer; +using Microsoft.Extensions.Logging; +using Moq; +using Moq.Contrib.HttpClient; +using Shouldly; +using System.Net; +using System.Text.Json; + +namespace Kepware.Api.TestIntg.ApiClient; + +public class IotGatewayTests : TestIntgApiClientBase +{ + + #region MQTT Client Agent Tests + + [Fact] + public async Task GetOrCreateMqttClientAgent_WhenNotExists_ShouldCreateAgent() + { + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateMqttClientAgentAsync("TestMqttAgent"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestMqttAgent"); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [Fact] + public async Task GetOrCreateMqttClientAgent_WhenExists_ShouldReturnExistingAgent() + { + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.MQTT_CLIENT_URL", JsonDocument.Parse($"\"tcp://localhost:1883\"").RootElement} }; + var agent = await AddTestMqttClientAgent("TestMqttAgent", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateMqttClientAgentAsync("TestMqttAgent"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe(agent.Name); + result.Url.ShouldBe(agent.Url); + result.Enabled.ShouldBe(agent.Enabled); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + + [Fact] + public async Task CreateMqttClientAgent_WhenSuccessful_ShouldReturnAgent() + { + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateMqttClientAgentAsync("TestMqttAgent"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestMqttAgent"); + + // Clean up + await DeleteAllIoTAgentsAsync(); + } + + [Fact] + public async Task CreateMqttClientAgent_WithProperties_ShouldSetProperties() + { + // Arrange + + var properties = new Dictionary + { + { Properties.MqttClientAgent.Url, "tcp://localhost:1883" }, + { Properties.MqttClientAgent.Topic, "test/topic" } + }; + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateMqttClientAgentAsync("TestAgent", properties); + + // Assert + result.ShouldNotBeNull(); + result.Url.ShouldBe("tcp://localhost:1883"); + result.Topic.ShouldBe("test/topic"); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [Fact] + public async Task CreateMqttClientAgent_WithHttpError_AlreadyExists_ShouldReturnNull() + { + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.MQTT_CLIENT_URL", JsonDocument.Parse($"\"tcp://localhost:1883\"").RootElement} }; + var agent = await AddTestMqttClientAgent("TestMqttAgent", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateMqttClientAgentAsync("TestMqttAgent"); + + // Assert + result.ShouldBeNull(); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [Fact] + public async Task GetMqttClientAgent_WhenExists_ShouldReturnAgent() + { + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.MQTT_CLIENT_URL", JsonDocument.Parse($"\"tcp://localhost:1883\"").RootElement} }; + var agent = await AddTestMqttClientAgent("TestMqttAgent", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetMqttClientAgentAsync("TestMqttAgent"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestMqttAgent"); + result.Enabled.ShouldBe(true); + result.Url.ShouldBe("tcp://localhost:1883"); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [Fact] + public async Task GetMqttClientAgent_WhenNotFound_ShouldReturnNull() + { + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetMqttClientAgentAsync("NonExistent"); + + // Assert + result.ShouldBeNull(); + } + + [Fact] + public async Task DeleteMqttClientAgent_ByEntity_WhenSuccessful_ShouldReturnTrue() + { + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.MQTT_CLIENT_URL", JsonDocument.Parse($"\"tcp://localhost:1883\"").RootElement} }; + var agent = await AddTestMqttClientAgent("TestMqttAgent", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteMqttClientAgentAsync(agent); + + // Assert + result.ShouldBeTrue(); + + // Clean up + await DeleteAllIoTAgentsAsync(); + } + + [Fact] + public async Task DeleteMqttClientAgent_ByName_WhenSuccessful_ShouldReturnTrue() + { + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.MQTT_CLIENT_URL", JsonDocument.Parse($"\"tcp://localhost:1883\"").RootElement} }; + var agent = await AddTestMqttClientAgent("TestMqttAgent", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteMqttClientAgentAsync("TestMqttAgent"); + + // Assert + result.ShouldBeTrue(); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [Fact] + public async Task DeleteMqttClientAgent_WithHttpError_ShouldReturnFalse() + { + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteMqttClientAgentAsync("TestMqttAgent"); + + // Assert + result.ShouldBeFalse(); + } + + + #endregion + + #region REST Client Agent Tests + + + [SkippableFact] + public async Task GetOrCreateRestClientAgent_WhenNotExists_ShouldCreateAgent() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestClient"); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [SkippableFact] + public async Task GetOrCreateRestClientAgent_WhenExists_ShouldReturnExistingAgent() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.REST_CLIENT_URL", JsonDocument.Parse($"\"https://api.example.com\"").RootElement} }; + var agent = await AddTestRestClientAgent("TestRestClient", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe(agent.Name); + result.Url.ShouldBe(agent.Url); + result.Enabled.ShouldBe(agent.Enabled); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + + [SkippableFact] + public async Task CreateRestClientAgent_WhenSuccessful_ShouldReturnAgent() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestClient"); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [SkippableFact] + public async Task CreateRestClientAgent_WithProperties_ShouldSetProperties() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + + var properties = new Dictionary + { + { Properties.RestClientAgent.Url, "https://api.example.com" } + }; + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateRestClientAgentAsync("TestRestClient", properties); + + // Assert + result.ShouldNotBeNull(); + result.Url.ShouldBe("https://api.example.com"); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [SkippableFact] + public async Task CreateRestClientAgent_WithHttpError_AlreadyExists_ShouldReturnNull() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.REST_CLIENT_URL", JsonDocument.Parse($"\"https://api.example.com\"").RootElement} }; + var agent = await AddTestRestClientAgent("TestRestClient", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldBeNull(); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + + [SkippableFact] + public async Task GetRestClientAgent_WhenExists_ShouldReturnAgent() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.REST_CLIENT_URL", JsonDocument.Parse($"\"https://api.example.com\"").RootElement} }; + var agent = await AddTestRestClientAgent("TestRestClient", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestClient"); + result.Url.ShouldBe("https://api.example.com"); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [SkippableFact] + public async Task GetRestClientAgent_WhenNotFound_ShouldReturnNull() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetRestClientAgentAsync("NonExistent"); + + // Assert + result.ShouldBeNull(); + } + + [SkippableFact] + public async Task DeleteRestClientAgent_ByEntity_WhenSuccessful_ShouldReturnTrue() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.REST_CLIENT_URL", JsonDocument.Parse($"\"https://api.example.com\"").RootElement} }; + var agent = await AddTestRestClientAgent("TestRestClient", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestClientAgentAsync(agent); + + // Assert + result.ShouldBeTrue(); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [SkippableFact] + public async Task DeleteRestClientAgent_ByName_WhenSuccessful_ShouldReturnTrue() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.REST_CLIENT_URL", JsonDocument.Parse($"\"https://api.example.com\"").RootElement} }; + var agent = await AddTestRestClientAgent("TestRestClient", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldBeTrue(); + + // Clean up + await DeleteAllIoTAgentsAsync(); + } + + [SkippableFact] + public async Task DeleteRestClientAgent_WithHttpError_ShouldReturnFalse() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestClientAgentAsync("TestRestClient"); + + // Assert + result.ShouldBeFalse(); + } + + #endregion + + #region REST Server Agent Tests + + [SkippableFact] + public async Task GetOrCreateRestServerAgent_WhenNotExists_ShouldCreateAgent() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestServer"); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [SkippableFact] + public async Task GetOrCreateRestServerAgent_WhenExists_ShouldReturnExistingAgent() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.REST_SERVER_PORT_NUMBER", JsonDocument.Parse("39320").RootElement} }; + var agent = await AddTestRestServerAgent("TestRestServer", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetOrCreateRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe(agent.Name); + result.PortNumber.ShouldBe(agent.PortNumber); + result.Enabled.ShouldBe(agent.Enabled); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + + [SkippableFact] + public async Task CreateRestServerAgent_WhenSuccessful_ShouldReturnAgent() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestServer"); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [SkippableFact] + public async Task CreateRestServerAgent_WithProperties_ShouldSetProperties() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + + var properties = new Dictionary + { + { Properties.RestServerAgent.PortNumber, 39320 } + }; + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateRestServerAgentAsync("TestRestServer", properties); + + // Assert + result.ShouldNotBeNull(); + result.PortNumber.ShouldBe(39320); + + // Clean up + await DeleteAllIoTAgentsAsync(); + } + + [SkippableFact] + public async Task CreateRestServerAgent_WithHttpError_AlreadyExists_ShouldReturnNull() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.REST_SERVER_PORT_NUMBER", JsonDocument.Parse("39320").RootElement} }; + var agent = await AddTestRestServerAgent("TestRestServer", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.CreateRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldBeNull(); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + + [SkippableFact] + public async Task GetRestServerAgent_WhenExists_ShouldReturnAgent() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.REST_SERVER_PORT_NUMBER", JsonDocument.Parse("39320").RootElement} }; + var agent = await AddTestRestServerAgent("TestRestServer", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldNotBeNull(); + result.Name.ShouldBe("TestRestServer"); + result.PortNumber.ShouldBe(39320); + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [SkippableFact] + public async Task GetRestServerAgent_WhenNotFound_ShouldReturnNull() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.GetRestServerAgentAsync("NonExistent"); + + // Assert + result.ShouldBeNull(); + } + + [SkippableFact] + public async Task DeleteRestServerAgent_ByEntity_WhenSuccessful_ShouldReturnTrue() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.REST_SERVER_PORT_NUMBER", JsonDocument.Parse("39320").RootElement} }; + var agent = await AddTestRestServerAgent("TestRestServer", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestServerAgentAsync(agent); + + // Assert + result.ShouldBeTrue(); + + // Clean up + await DeleteAllIoTAgentsAsync(); + } + + [SkippableFact] + public async Task DeleteRestServerAgent_ByName_WhenSuccessful_ShouldReturnTrue() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Arrange + var agentProperties = new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("true").RootElement }, + {"iot_gateway.REST_SERVER_PORT_NUMBER", JsonDocument.Parse("39320").RootElement} }; + var agent = await AddTestRestServerAgent("TestRestServer", agentProperties); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldBeTrue(); + + // Clean up + await DeleteAllIoTAgentsAsync(); + } + + [SkippableFact] + public async Task DeleteRestServerAgent_WithHttpError_ShouldReturnFalse() + { + // Skip the test if the product is not "Server" productId + Skip.If(_productInfo.ProductId != "012", "Test only applicable for Server productIds"); + + // Act + var result = await _kepwareApiClient.Project.IotGateway.DeleteRestServerAgentAsync("TestRestServer"); + + // Assert + result.ShouldBeFalse(); + } + + #endregion + + #region IoT Item Tests + + [Fact] + public async Task GetOrCreateIotItem_WhenNotExists_ShouldCreateWithDerivedName() + { + // Arrange - server tag "Channel1.Device1.Tag1" should query with name "Channel1_Device1_Tag1" + var channel = await AddTestChannel("Channel1"); + var device = await AddTestDevice(channel, "Device1"); + var tag = await AddSimulatorTestTag(device, "Tag1"); + + var parentMqttAgent = await AddTestMqttClientAgent("ParentMqttAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // Act + var resultMqttClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent); + + + // Act - create a system tag item to test the leading underscore logic + var systemTagItem = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("_System._Time", parentMqttAgent); + + // Assert + resultMqttClient.ShouldNotBeNull(); + resultMqttClient.Name.ShouldBe("Channel1_Device1_Tag1"); + resultMqttClient.ServerTag.ShouldBe("Channel1.Device1.Tag1"); + + systemTagItem.ShouldNotBeNull(); + systemTagItem.Name.ShouldBe("System__Time"); + systemTagItem.ServerTag.ShouldBe("_System._Time"); + + + // If Kepware Server, test that REST Client and REST Server agents also create items with the same name and server tag + if (_productInfo.ProductId == "012") + { + // Arrange + var parentRestClientAgent = await AddTestRestClientAgent("ParentRestClientAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + var parentRestServerAgent = await AddTestRestServerAgent("ParentRestServerAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + //Act + var resultRestClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent); + var resultRestServer = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent); + + // Assert + resultRestClient.ShouldNotBeNull(); + resultRestClient.Name.ShouldBe("Channel1_Device1_Tag1"); + resultRestClient.ServerTag.ShouldBe("Channel1.Device1.Tag1"); + + resultRestServer.ShouldNotBeNull(); + resultRestServer.Name.ShouldBe("Channel1_Device1_Tag1"); + resultRestServer.ServerTag.ShouldBe("Channel1.Device1.Tag1"); + } + + + // Clean up + await DeleteAllIoTAgentsAsync(); + await DeleteAllChannelsAsync(); + } + + [Fact] + public async Task GetOrCreateIotItem_WhenExists_ShouldReturnExistingItem() + { + // Arrange + var channel = await AddTestChannel("Channel1"); + var device = await AddTestDevice(channel, "Device1"); + var tag = await AddSimulatorTestTag(device, "Tag1"); + + var parentMqttAgent = await AddTestMqttClientAgent("ParentMqttAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // TODO: add items via client base method + var itemMqttClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent); + + // Act + var resultMqttClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent); + + + // Act - create a system tag item to test the leading underscore logic + var systemTagItem = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("_System._Time", parentMqttAgent); + + // Assert + resultMqttClient.ShouldNotBeNull(); + resultMqttClient.Name.ShouldBe(itemMqttClient.Name); + resultMqttClient.ServerTag.ShouldBe(itemMqttClient.ServerTag); + + systemTagItem.ShouldNotBeNull(); + systemTagItem.Name.ShouldBe(systemTagItem.Name); + systemTagItem.ServerTag.ShouldBe(systemTagItem.ServerTag); + + // If Kepware Server, test that REST Client and REST Server agents also return items with the same name and server tag + if (_productInfo.ProductId == "012") + { + // Arrange + var parentRestClientAgent = await AddTestRestClientAgent("ParentRestClientAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + var parentRestServerAgent = await AddTestRestServerAgent("ParentRestServerAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // TODO: add items via client base method + var itemRestClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent); + var itemRestServer = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent); + + // Act + var resultRestClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent); + var resultRestServer = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent); + + // Assert + resultRestClient.ShouldNotBeNull(); + resultRestClient.Name.ShouldBe(itemRestClient.Name); + resultRestClient.ServerTag.ShouldBe(itemRestClient.ServerTag); + + resultRestServer.ShouldNotBeNull(); + resultRestServer.Name.ShouldBe(itemRestServer.Name); + resultRestServer.ServerTag.ShouldBe(itemRestServer.ServerTag); + } + + // Clean up + await DeleteAllIoTAgentsAsync(); + await DeleteAllChannelsAsync(); + } + + [Fact] + public async Task GetOrCreateIotItem_WhenCreateFails_ShouldThrowInvalidOperationException() + { + // Arrange + var parentMqttAgent = await AddTestMqttClientAgent("ParentMqttAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // Act + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent)); + + if (_productInfo.ProductId == "012") + { + // Arrange + var parentRestClientAgent = await AddTestRestClientAgent("ParentRestClientAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + var parentRestServerAgent = await AddTestRestServerAgent("ParentRestServerAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // Act + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent)); + await Should.ThrowAsync(async () => + await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent)); + } + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + [Fact] + public async Task CreateIotItem_ShouldDeriveNameFromServerTag() + { + // Arrange - server tag "Channel1.Device1.Tag1" should query with name "Channel1_Device1_Tag1" + var channel = await AddTestChannel("Channel1"); + var device = await AddTestDevice(channel, "Device1"); + var tag = await AddSimulatorTestTag(device, "Tag1"); + + var parentMqttAgent = await AddTestMqttClientAgent("ParentMqttAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + + // Act + var resultMqttClient = await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent); + + + // Act - create a system tag item to test the leading underscore logic + var systemTagItem = await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("_System._Time", parentMqttAgent); + + // Assert + resultMqttClient.ShouldNotBeNull(); + resultMqttClient.Name.ShouldBe("Channel1_Device1_Tag1"); + resultMqttClient.ServerTag.ShouldBe("Channel1.Device1.Tag1"); + + systemTagItem.ShouldNotBeNull(); + systemTagItem.Name.ShouldBe("System__Time"); + systemTagItem.ServerTag.ShouldBe("_System._Time"); + + if (_productInfo.ProductId == "012") + { + // Arrange + var parentRestClientAgent = await AddTestRestClientAgent("ParentRestClientAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + var parentRestServerAgent = await AddTestRestServerAgent("ParentRestServerAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // Act + var resultRestClient = await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent); + var resultRestServer = await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent); + + // Assert + resultRestClient.ShouldNotBeNull(); + resultRestClient.Name.ShouldBe("Channel1_Device1_Tag1"); + resultRestClient.ServerTag.ShouldBe("Channel1.Device1.Tag1"); + + resultRestServer.ShouldNotBeNull(); + resultRestServer.Name.ShouldBe("Channel1_Device1_Tag1"); + resultRestServer.ServerTag.ShouldBe("Channel1.Device1.Tag1"); + + } + // Clean up + await DeleteAllIoTAgentsAsync(); + await DeleteAllChannelsAsync(); + } + + [Fact] + public async Task CreateIotItem_WithHttpError_AlreadyExists_ShouldReturnNull() + { + // Arrange + var channel = await AddTestChannel("Channel1"); + var device = await AddTestDevice(channel, "Device1"); + var tag = await AddSimulatorTestTag(device, "Tag1"); + + var parentMqttAgent = await AddTestMqttClientAgent("ParentMqttAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // TODO: add items via client base method + var itemMqttClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent); + + // Act + var resultMqttClient = await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent); + + // Assert + resultMqttClient.ShouldBeNull(); + + if (_productInfo.ProductId == "012") + { + // Arrange + var parentRestClientAgent = await AddTestRestClientAgent("ParentRestClientAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + var parentRestServerAgent = await AddTestRestServerAgent("ParentRestServerAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // TODO: add items via client base method + + var itemRestClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent); + var itemRestServer = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent); + + // Act + + var resultRestClient = await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent); + var resultRestServer = await _kepwareApiClient.Project.IotGateway.CreateIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent); + + resultRestClient.ShouldBeNull(); + resultRestServer.ShouldBeNull(); + } + + // Clean up + await DeleteAllIoTAgentsAsync(); + await DeleteAllChannelsAsync(); + } + + + [Fact] + public async Task DeleteIotItem_ByEntity_WhenSuccessful_ShouldReturnTrue() + { + // Arrange + var channel = await AddTestChannel("Channel1"); + var device = await AddTestDevice(channel, "Device1"); + var tag = await AddSimulatorTestTag(device, "Tag1"); + + var parentMqttAgent = await AddTestMqttClientAgent("ParentMqttAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // TODO: add items via client base method + var itemMqttClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent); + + // Act + var resultMqttClient = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync(itemMqttClient); + + // Assert + resultMqttClient.ShouldBeTrue(); + + if (_productInfo.ProductId == "012") + { + // Arrange + var parentRestClientAgent = await AddTestRestClientAgent("ParentRestClientAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + var parentRestServerAgent = await AddTestRestServerAgent("ParentRestServerAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // TODO: add items via client base method + + var itemRestClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent); + var itemRestServer = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent); + + // Act + + var resultRestClient = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync(itemRestClient); + var resultRestServer = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync(itemRestServer); + + // Assert + resultRestClient.ShouldBeTrue(); + resultRestServer.ShouldBeTrue(); + } + + // Clean up + await DeleteAllIoTAgentsAsync(); + await DeleteAllChannelsAsync(); + + } + + [Fact] + public async Task DeleteIotItem_ByServerTag_ShouldTranslateToItemName() + { + // Arrange + var channel = await AddTestChannel("Channel1"); + var device = await AddTestDevice(channel, "Device1"); + var tag = await AddSimulatorTestTag(device, "Tag1"); + + var parentMqttAgent = await AddTestMqttClientAgent("ParentMqttAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // TODO: add items via client base method + var itemMqttClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent); + + // Act + var resultMqttClient = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent); + + // Assert + resultMqttClient.ShouldBeTrue(); + + if (_productInfo.ProductId == "012") + { + + var parentRestClientAgent = await AddTestRestClientAgent("ParentRestClientAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + var parentRestServerAgent = await AddTestRestServerAgent("ParentRestServerAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // TODO: add items via client base method + var itemRestClient = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent); + var itemRestServer = await _kepwareApiClient.Project.IotGateway.GetOrCreateIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent); + + // Act + var resultRestClient = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent); + var resultRestServer = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent); + + // Assert + resultRestClient.ShouldBeTrue(); + resultRestServer.ShouldBeTrue(); + } + // Clean up + await DeleteAllIoTAgentsAsync(); + await DeleteAllChannelsAsync(); + + } + + [Fact] + public async Task DeleteIotItem_WithHttpError_ShouldReturnFalse() + { + // Arrange + var parentMqttAgent = await AddTestMqttClientAgent("ParentMqttAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // Act + var resultMqttClient = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync("Channel1.Device1.Tag1", parentMqttAgent); + + // Assert + resultMqttClient.ShouldBeFalse(); + + if (_productInfo.ProductId == "012") + { + // Arrange + var parentRestClientAgent = await AddTestRestClientAgent("ParentRestClientAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + var parentRestServerAgent = await AddTestRestServerAgent("ParentRestServerAgent", new Dictionary { { "iot_gateway.AGENTTYPES_ENABLED", JsonDocument.Parse("false").RootElement }}); + + // Act + var resultRestClient = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync("Channel1.Device1.Tag1", parentRestClientAgent); + var resultRestServer = await _kepwareApiClient.Project.IotGateway.DeleteIotItemAsync("Channel1.Device1.Tag1", parentRestServerAgent); + + // Assert + resultRestClient.ShouldBeFalse(); + resultRestServer.ShouldBeFalse(); + } + + // Clean up + await DeleteAllIoTAgentsAsync(); + + } + + #endregion + +} diff --git a/Kepware.Api.TestIntg/ApiClient/_TestIntgApiClientBase.cs b/Kepware.Api.TestIntg/ApiClient/_TestIntgApiClientBase.cs index 1f16211..1b2962f 100644 --- a/Kepware.Api.TestIntg/ApiClient/_TestIntgApiClientBase.cs +++ b/Kepware.Api.TestIntg/ApiClient/_TestIntgApiClientBase.cs @@ -216,5 +216,96 @@ protected async Task AddTestTagGroup(DeviceTagGroup owner, strin return await _kepwareApiClient.GenericConfig.LoadEntityAsync(tagGroup.Name, owner) ?? throw new Exception($"Failed to load tag group '{tagGroup.Name}' after insertion."); } + protected async Task AddTestMqttClientAgent(string name = "MqttTestAgent", Dictionary? properties = null) + { + var agent = new MqttClientAgent(name); + if (properties != null) + { + foreach (var kvp in properties) + { + agent.DynamicProperties[kvp.Key] = kvp.Value; + } + } + + + await _kepwareApiClient.GenericConfig.InsertItemAsync(agent, cancellationToken: CancellationToken.None); + return await _kepwareApiClient.GenericConfig.LoadEntityAsync(agent.Name, cancellationToken: CancellationToken.None) ?? throw new Exception($"Failed to create MQTT Client Agent '{name}'."); + } + + protected async Task AddTestRestClientAgent(string name = "RestClientTestAgent", Dictionary? properties = null) + { + var agent = new RestClientAgent(name); + if (properties != null) + { + foreach (var kvp in properties) + { + agent.DynamicProperties[kvp.Key] = kvp.Value; + } + } + await _kepwareApiClient.GenericConfig.InsertItemAsync(agent, cancellationToken: CancellationToken.None); + return await _kepwareApiClient.GenericConfig.LoadEntityAsync(agent.Name, cancellationToken: CancellationToken.None) ?? throw new Exception($"Failed to create REST Client Agent '{name}'."); + } + + protected async Task AddTestRestServerAgent(string name = "RestServerTestAgent", Dictionary? properties = null) + { + var agent = new RestServerAgent(name); + if (properties != null) + { + foreach (var kvp in properties) + { + agent.DynamicProperties[kvp.Key] = kvp.Value; + } + } + await _kepwareApiClient.GenericConfig.InsertItemAsync(agent, cancellationToken: CancellationToken.None); + return await _kepwareApiClient.GenericConfig.LoadEntityAsync(agent.Name, cancellationToken: CancellationToken.None) ?? throw new Exception($"Failed to create REST Server Agent '{name}'."); + } + + protected async Task DeleteAllIoTAgentsAsync() + { + var mqttAgents = await _kepwareApiClient.GenericConfig.LoadCollectionAsync(); + if (mqttAgents != null) + { + foreach (var agent in mqttAgents) + { + await _kepwareApiClient.Project.IotGateway.DeleteMqttClientAgentAsync(agent.Name); + } + } + + var restClientAgents = await _kepwareApiClient.GenericConfig.LoadCollectionAsync(); + if (restClientAgents != null) + { + foreach (var agent in restClientAgents) + { + await _kepwareApiClient.Project.IotGateway.DeleteRestClientAgentAsync(agent.Name); + } + } + + var restServerAgents = await _kepwareApiClient.GenericConfig.LoadCollectionAsync(); + if (restServerAgents != null) + { + foreach (var agent in restServerAgents) + { + await _kepwareApiClient.Project.IotGateway.DeleteRestServerAgentAsync(agent.Name); + } + } + + } + + // protected async Task AddTestIotItem(string name = "IotItemTest", Dictionary? properties = null) + // { + // var item = new IotItem(name); + // if (properties != null) + // { + // foreach (var kvp in properties) + // { + // item.DynamicProperties[kvp.Key] = kvp.Value; + // } + // } + // await _kepwareApiClient.GenericConfig.InsertItemAsync(item, cancellationToken: CancellationToken.None); + // return await _kepwareApiClient.GenericConfig.LoadEntityAsync(item.Name, cancellationToken: CancellationToken.None) ?? throw new Exception($"Failed to create IoT Item '{name}'."); + // } + + + } } \ No newline at end of file From 344cd174842c90f3f6082af812e90db2e6934cba Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Wed, 1 Apr 2026 22:16:04 -0400 Subject: [PATCH 32/38] test: Update UaEndpoint tests --- .../ApiClient/UaEndpointTests.cs | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/Kepware.Api.TestIntg/ApiClient/UaEndpointTests.cs b/Kepware.Api.TestIntg/ApiClient/UaEndpointTests.cs index cf79bb2..4723106 100644 --- a/Kepware.Api.TestIntg/ApiClient/UaEndpointTests.cs +++ b/Kepware.Api.TestIntg/ApiClient/UaEndpointTests.cs @@ -13,12 +13,10 @@ namespace Kepware.Api.TestIntg.ApiClient { - [TestCaseOrderer("Xunit.Extensions.Ordering.TestCaseOrderer", "Xunit.Extensions.Ordering")] public class UaEndpointTests : TestIntgApiClientBase { [SkippableFact] - [Order(1)] public async Task CreateOrUpdateUaEndpointAsync_ShouldCreateUaEndpoint_WhenItDoesNotExist() { // Skip the test if the product is not "Edge" productId @@ -32,10 +30,13 @@ public async Task CreateOrUpdateUaEndpointAsync_ShouldCreateUaEndpoint_WhenItDoe // Assert result.ShouldBeTrue(); + + // Clean up + await _kepwareApiClient.Admin.DeleteUaEndpointAsync(uaEndpoint.Name); + } [SkippableFact] - [Order(2)] public async Task CreateOrUpdateUaEndpointAsync_ShouldUpdateUaEndpoint_WhenItExists() { // Skip the test if the product is not "Edge" productId @@ -43,6 +44,10 @@ public async Task CreateOrUpdateUaEndpointAsync_ShouldUpdateUaEndpoint_WhenItExi // Arrange var uaEndpoint = CreateTestUaEndpoint(); + await _kepwareApiClient.GenericConfig.InsertItemAsync(uaEndpoint); + + uaEndpoint = await _kepwareApiClient.GenericConfig.LoadEntityAsync(uaEndpoint.Name); + uaEndpoint.ShouldNotBeNull(); uaEndpoint.Port = 4840; // Act @@ -50,10 +55,12 @@ public async Task CreateOrUpdateUaEndpointAsync_ShouldUpdateUaEndpoint_WhenItExi // Assert result.ShouldBeTrue(); + + // Clean up + await _kepwareApiClient.Admin.DeleteUaEndpointAsync(uaEndpoint.Name); } [SkippableFact] - [Order(3)] public async Task GetUaEndpointAsync_ShouldReturnUaEndpoint_WhenApiRespondsSuccessfully() { // Skip the test if the product is not "Edge" productId @@ -61,6 +68,7 @@ public async Task GetUaEndpointAsync_ShouldReturnUaEndpoint_WhenApiRespondsSucce // Arrange var uaEndpoint = CreateTestUaEndpoint(); + await _kepwareApiClient.GenericConfig.InsertItemAsync(uaEndpoint); // Act var result = await _kepwareApiClient.Admin.GetUaEndpointAsync(uaEndpoint.Name); @@ -69,10 +77,13 @@ public async Task GetUaEndpointAsync_ShouldReturnUaEndpoint_WhenApiRespondsSucce result.ShouldNotBeNull(); result.Name.ShouldBe(uaEndpoint.Name); result.Port.ShouldBeOfType(); + + // Clean up + await _kepwareApiClient.Admin.DeleteUaEndpointAsync(uaEndpoint.Name); + } [SkippableFact] - [Order(4)] public async Task GetUaEndpointsAsync_ShouldReturnUaEndpointCollection_WhenApiRespondsSuccessfully() { // Skip the test if the product is not "Edge" productId @@ -83,12 +94,10 @@ public async Task GetUaEndpointsAsync_ShouldReturnUaEndpointCollection_WhenApiRe // Assert result.ShouldNotBeNull(); - // result.Count.ShouldBe(2); } [SkippableFact] - [Order(5)] public async Task DeleteUaEndpointAsync_ShouldReturnTrue_WhenDeleteSuccessful() { // Skip the test if the product is not "Edge" productId @@ -96,6 +105,7 @@ public async Task DeleteUaEndpointAsync_ShouldReturnTrue_WhenDeleteSuccessful() // Arrange var uaEndpoint = CreateTestUaEndpoint(); + await _kepwareApiClient.GenericConfig.InsertItemAsync(uaEndpoint); // Act var result = await _kepwareApiClient.Admin.DeleteUaEndpointAsync(uaEndpoint.Name); @@ -105,7 +115,6 @@ public async Task DeleteUaEndpointAsync_ShouldReturnTrue_WhenDeleteSuccessful() } [SkippableFact] - [Order(6)] public async Task DeleteUaEndpointAsync_ShouldReturnFalse_WhenDeleteFails() { // Skip the test if the product is not "Edge" productId From fd46092e6b1cb8d5a1c20b809e9063f33254c498 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Wed, 1 Apr 2026 22:40:30 -0400 Subject: [PATCH 33/38] feat(sample):: updated sample with IoT GW --- Kepware.Api.Sample/Program.cs | 41 +++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/Kepware.Api.Sample/Program.cs b/Kepware.Api.Sample/Program.cs index b323b24..e77c49a 100644 --- a/Kepware.Api.Sample/Program.cs +++ b/Kepware.Api.Sample/Program.cs @@ -57,6 +57,47 @@ static async Task Main(string[] args) await api.Project.Devices.UpdateDeviceAsync(device, true); + // --- IoT Gateway: MQTT Client Agent --- + var mqttAgent = await api.Project.IotGateway.GetOrCreateMqttClientAgentAsync("MQTT Agent by Api"); + mqttAgent.Url = "tcp://broker.example.com:1883"; + mqttAgent.Topic = "kepware/data"; + await api.Project.IotGateway.UpdateMqttClientAgentAsync(mqttAgent); + + // Add an IoT Item referencing a tag on the device created above + var mqttItem = await api.Project.IotGateway.GetOrCreateIotItemAsync( + $"{channel1.Name}.{device.Name}.BooleanByApi", mqttAgent); + mqttItem.ScanRateMs = 500; + await api.Project.IotGateway.UpdateIotItemAsync(mqttItem); + + // Clean up MQTT IoT Item and agent + await api.Project.IotGateway.DeleteIotItemAsync(mqttItem); + await api.Project.IotGateway.DeleteMqttClientAgentAsync(mqttAgent); + + // --- IoT Gateway: REST Client Agent --- + var restClientAgent = await api.Project.IotGateway.GetOrCreateRestClientAgentAsync("REST Client by Api"); + restClientAgent.Url = "https://api.example.com/data"; + restClientAgent.HttpMethod = RestClientHttpMethod.Post; + await api.Project.IotGateway.UpdateRestClientAgentAsync(restClientAgent); + + // Add an IoT Item to the REST Client agent + var restClientItem = await api.Project.IotGateway.GetOrCreateIotItemAsync( + $"{channel1.Name}.{device.Name}.SineByApi", restClientAgent); + await api.Project.IotGateway.DeleteIotItemAsync(restClientItem); + await api.Project.IotGateway.DeleteRestClientAgentAsync(restClientAgent); + + // --- IoT Gateway: REST Server Agent --- + var restServerAgent = await api.Project.IotGateway.GetOrCreateRestServerAgentAsync("REST Server by Api"); + restServerAgent.PortNumber = 39321; + restServerAgent.EnableWriteEndpoint = true; + await api.Project.IotGateway.UpdateRestServerAgentAsync(restServerAgent); + + // Add an IoT Item to the REST Server agent + var restServerItem = await api.Project.IotGateway.GetOrCreateIotItemAsync( + $"{channel1.Name}.{device.Name}.RampByApi", restServerAgent); + await api.Project.IotGateway.DeleteIotItemAsync(restServerItem); + await api.Project.IotGateway.DeleteRestServerAgentAsync(restServerAgent); + + // Clean up channel and device await api.Project.Devices.DeleteDeviceAsync(device); await api.Project.Channels.DeleteChannelAsync(channel1); From 24d9355ffe702478dc79b4161929b85db17e429a Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Wed, 1 Apr 2026 22:40:49 -0400 Subject: [PATCH 34/38] doc: Updated Readme with IoT GW --- Kepware.Api/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Kepware.Api/README.md b/Kepware.Api/README.md index 7011306..6d7d2f3 100644 --- a/Kepware.Api/README.md +++ b/Kepware.Api/README.md @@ -18,6 +18,7 @@ This package is designed to work with all versions of Kepware that support the C | :----------: | :----------: | :----------: | | **Project Properties** | Y | Y | | **Connectivity**
*(Channel, Devices, Tags, Tag Groups)* | Y | Y | +| **IoT Gateway**
*(Agents, IoT Items)* | Y | Y | | **Administration**
*(User Groups, Users, UA Endpoints, Local License Server)* | Y[^1] | Y | | **Product Info and Health Status** | Y[^4] | Y | | **Export Project** | Y | Y | From fd83c25c14169395049450b1912ee1197d78f6e9 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Wed, 1 Apr 2026 23:31:17 -0400 Subject: [PATCH 35/38] fix: test data copy issue during test builds --- Kepware.Api.Test/Kepware.Api.Test.csproj | 4 ++-- Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Kepware.Api.Test/Kepware.Api.Test.csproj b/Kepware.Api.Test/Kepware.Api.Test.csproj index e120c72..e88a0ef 100644 --- a/Kepware.Api.Test/Kepware.Api.Test.csproj +++ b/Kepware.Api.Test/Kepware.Api.Test.csproj @@ -34,9 +34,9 @@ - + PreserveNewest - +
diff --git a/Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj b/Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj index 6283af0..fe6ee90 100644 --- a/Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj +++ b/Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj @@ -39,12 +39,12 @@ - + PreserveNewest - - + + PreserveNewest - +
From 69a6afe9df470961905aac920f3d381a97d26f40 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Wed, 1 Apr 2026 23:59:08 -0400 Subject: [PATCH 36/38] fix: test data copy issue during test builds --- Kepware.Api.Test/Kepware.Api.Test.csproj | 2 +- Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Kepware.Api.Test/Kepware.Api.Test.csproj b/Kepware.Api.Test/Kepware.Api.Test.csproj index e88a0ef..2b4a190 100644 --- a/Kepware.Api.Test/Kepware.Api.Test.csproj +++ b/Kepware.Api.Test/Kepware.Api.Test.csproj @@ -35,7 +35,7 @@ - PreserveNewest + Always diff --git a/Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj b/Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj index fe6ee90..f1285c3 100644 --- a/Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj +++ b/Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj @@ -40,10 +40,10 @@ - PreserveNewest + Always - PreserveNewest + Always From 87f4b61b5e861c5b7bff92816dcef91778b9dea7 Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Thu, 2 Apr 2026 13:11:54 -0400 Subject: [PATCH 37/38] refactor: fixed issue with test data naming for linux testing --- .github/workflows/nuget-test-and-build.yml | 33 +++++++++++++++++++ .../ApiClient/_TestApiClientBase.cs | 2 +- Kepware.Api.Test/Kepware.Api.Test.csproj | 4 +-- .../Kepware.Api.TestIntg.csproj | 6 ++-- 4 files changed, 39 insertions(+), 6 deletions(-) diff --git a/.github/workflows/nuget-test-and-build.yml b/.github/workflows/nuget-test-and-build.yml index 86d2529..e74c607 100644 --- a/.github/workflows/nuget-test-and-build.yml +++ b/.github/workflows/nuget-test-and-build.yml @@ -71,6 +71,39 @@ jobs: run: dotnet restore - name: Build run: dotnet build --no-restore --configuration release + + # - name: Show current directory (debug) + # run: | + # echo "Current directory: $(pwd)" + # - name: Show repo files (debug) + # if: ${{ matrix.platform == 'ubuntu' }} + # run: | + # echo "Repo root: $(pwd)" + # ls -la + # echo "List _data:" + # ls -la Kepware.Api.Test/_data || true + # echo "List _data/projectLoadSerializeData:" + # ls -la Kepware.Api.Test/_data/projectLoadSerializeData || true + + # - name: Build test project (debug) + # if: ${{ matrix.platform == 'ubuntu' }} + # run: dotnet build Kepware.Api.Test/Kepware.Api.Test.csproj -c Release + + # - name: Show build output (debug) + # if: ${{ matrix.platform == 'ubuntu' }} + # run: ls -la Kepware.Api.Test/bin/Release || true + + # - name: Show TFM outputs (debug) + # if: ${{ matrix.platform == 'ubuntu' }} + # run: ls -la Kepware.Api.Test/bin/Release/* || true + + # - name: Show test _data (debug) + # if: ${{ matrix.platform == 'ubuntu' }} + # run: ls -la Kepware.Api.Test/bin/Release/net8.0/_data || true + # - name: Show test _data/projectLoadSerializeData (debug) + # if: ${{ matrix.platform == 'ubuntu' }} + # run: ls -la Kepware.Api.Test/bin/Release/net8.0/_data/projectLoadSerializeData || true + - name: Test run: dotnet test Kepware.Api.Test/Kepware.Api.Test.csproj --no-build --verbosity normal --configuration Release --logger "trx;LogFilePrefix=${{ matrix.platform }}-test-results" - name: Publish Test Reports (${{ matrix.platform }}) diff --git a/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs b/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs index d05c64c..2b445e6 100644 --- a/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs +++ b/Kepware.Api.Test/ApiClient/_TestApiClientBase.cs @@ -226,7 +226,7 @@ protected async Task ConfigureToServeEndpoints(string filePath = "_data/simdemo_ // Additional endpoints for content=serialize mocking var projectPropertiesString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/projectProperties.json"); var channel1String = await File.ReadAllTextAsync("_data/projectLoadSerializeData/Channel1.json"); - var sixteenBitDeviceString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/dataTypeExamples.16BitDevice.json"); + var sixteenBitDeviceString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/dataTypeExamples.16bitDevice.json"); var simExamplesChannelString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/simulationExamples.json"); var dte8BitBRegTagGroupString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/dte.8bitDevice.Breg.json"); var dte8BitKRegTagGroupString = await File.ReadAllTextAsync("_data/projectLoadSerializeData/dte.8bitDevice.Kreg.json"); diff --git a/Kepware.Api.Test/Kepware.Api.Test.csproj b/Kepware.Api.Test/Kepware.Api.Test.csproj index 2b4a190..9827459 100644 --- a/Kepware.Api.Test/Kepware.Api.Test.csproj +++ b/Kepware.Api.Test/Kepware.Api.Test.csproj @@ -34,8 +34,8 @@ - - Always + + PreserveNewest diff --git a/Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj b/Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj index f1285c3..a639398 100644 --- a/Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj +++ b/Kepware.Api.TestIntg/Kepware.Api.TestIntg.csproj @@ -40,10 +40,10 @@ - Always + PreserveNewest - - Always + + PreserveNewest From fe7846f0d2922c25bbe22990f34044707df10acb Mon Sep 17 00:00:00 2001 From: Ray Labbe Date: Thu, 2 Apr 2026 15:53:45 -0400 Subject: [PATCH 38/38] chore(sdk): update versioning to 1.1 --- Kepware.Api/version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Kepware.Api/version.json b/Kepware.Api/version.json index 2bd0bfe..cfd5584 100644 --- a/Kepware.Api/version.json +++ b/Kepware.Api/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "1.0", + "version": "1.1", "pathFilters": [ "." ], "publicReleaseRefSpec": [ "^refs/heads/main$",