diff --git a/MetasysServices.Tests/MetasysClientTests.cs b/MetasysServices.Tests/MetasysClientTests.cs index 630d923..e6759ca 100644 --- a/MetasysServices.Tests/MetasysClientTests.cs +++ b/MetasysServices.Tests/MetasysClientTests.cs @@ -1914,6 +1914,127 @@ public void TestGetObjectsUnauthorizedThrowsException() PrintMessage($"TestGetObjectsUnauthorizedThrowsException: {e.Message}"); } + + #region Large JSON Example + const string GetObjectsResponseThreeLevelsV4 = @" + { + ""self"": ""https://r12adsdaily.cg.na.jci.com/api/v4/objects/9b9b3a80-bc5b-582a-8879-b1f1d1a0c5e4/objects?flatten=false&includeExtensions=true&includeInternal=false&depth=2"", + ""items"": [ + { + ""itemReference"": ""R12AdsDaily:R12AdsDaily-e2"", + ""hasChildrenMatchingQuery"": true, + ""name"": ""R12AdsDaily-e2"", + ""id"": ""9b9b3a80-bc5b-582a-8879-b1f1d1a0c5e4"", + ""objectType"": ""objectTypeEnumSet.n50Class"", + ""classification"": ""device"", + ""items"": [ + { + ""itemReference"": ""R12AdsDaily:R12AdsDaily-e2/Field Bus MSTP1"", + ""hasChildrenMatchingQuery"": true, + ""name"": ""Field Bus MSTP1"", + ""id"": ""aaeb5c40-9f36-5066-904d-fa81bab16fa6"", + ""objectType"": ""objectTypeEnumSet.fieldBusClass"", + ""classification"": ""integration"", + ""items"": [ + { + ""itemReference"": ""R12AdsDaily:R12AdsDaily-e2/Field Bus MSTP1.FAC2612-5"", + ""hasChildrenMatchingQuery"": true, + ""name"": ""AHU-1"", + ""id"": ""773e4df4-af2d-5c3c-8011-2795e7c6fcbc"", + ""objectType"": ""objectTypeEnumSet.fieldDeviceClass"", + ""classification"": ""controller"", + ""items"": [] + }, + { + ""itemReference"": ""R12AdsDaily:R12AdsDaily-e2/Field Bus MSTP1.AHU-1B"", + ""hasChildrenMatchingQuery"": true, + ""name"": ""SDA-F1-AHU-1"", + ""id"": ""3644d0b6-130f-5131-b573-71298f414b41"", + ""objectType"": ""objectTypeEnumSet.fieldDeviceClass"", + ""classification"": ""controller"", + ""items"": [] + } + ] + }, + { + ""itemReference"": ""R12AdsDaily:R12AdsDaily-e2/Field Bus MSTP2"", + ""hasChildrenMatchingQuery"": true, + ""name"": ""Field Bus MSTP2"", + ""id"": ""f2c7cae9-5942-5769-8afc-58060ae67727"", + ""objectType"": ""objectTypeEnumSet.fieldBusClass"", + ""classification"": ""integration"", + ""items"": [ + { + ""itemReference"": ""R12AdsDaily:R12AdsDaily-e2/Field Bus MSTP2.AHU-2"", + ""hasChildrenMatchingQuery"": true, + ""name"": ""SD-F2-AHU-2"", + ""id"": ""ff796531-9b5f-572d-b21a-a44681acbc36"", + ""objectType"": ""objectTypeEnumSet.fieldDeviceClass"", + ""classification"": ""controller"", + ""items"": [] + } + ] + }, + { + ""itemReference"": ""R12AdsDaily:R12AdsDaily-e2/VRF"", + ""hasChildrenMatchingQuery"": true, + ""name"": ""VRF"", + ""id"": ""524e084d-159e-53cd-bae9-c91ab3f21d03"", + ""objectType"": ""objectTypeEnumSet.containerClass"", + ""classification"": ""folder"", + ""items"": [ + { + ""itemReference"": ""R12AdsDaily:R12AdsDaily-e2/VRF.VRF-1"", + ""hasChildrenMatchingQuery"": true, + ""name"": ""IDU-1"", + ""id"": ""06de2cd1-2c08-536f-bd50-bf883d55dedb"", + ""objectType"": ""objectTypeEnumSet.containerClass"", + ""classification"": ""folder"", + ""items"": [] + } + ] + } + ] + } + ] + } + "; + + #endregion + + [Test] + public void TestObjectsLevel3WithV4() + { + // This should not recurse like earlier versions. It should just fetch with a depth of 3 (one call) + // and assemble the correct response, which should leave off the root object per library design + // Arrange + httpTest.RespondWith(GetObjectsResponseThreeLevelsV4); + var originalApiVersion = client.Version; + try + { + + client.Version = ApiVersion.v4; + + // Act + var objects = client.GetObjects(mockid, 3); + + // Assert + httpTest.ShouldHaveCalled($"https://hostname/api/v4/objects/{mockid}/objects") + .WithQueryParamValue("flatten", "false") + .WithQueryParamValue("includeInternal", "false") + .WithQueryParamValue("includeExtensions", "false") + .Times(1); + + Assert.That(httpTest.CallLog.Count, Is.EqualTo(1)); + + Assert.That(objects.Count(), Is.EqualTo(3)); + } + finally + { + client.Version = originalApiVersion; + } + } + #endregion #region GetSpaces Tests @@ -2559,4 +2680,4 @@ private class TestData } #endregion } -} \ No newline at end of file +} diff --git a/MetasysServices.Tests/MetasysObjectTests.cs b/MetasysServices.Tests/MetasysObjectTests.cs new file mode 100644 index 0000000..61bf6f2 --- /dev/null +++ b/MetasysServices.Tests/MetasysObjectTests.cs @@ -0,0 +1,97 @@ +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NUnit.Framework; + +namespace JohnsonControls.Metasys.BasicServices; + +public class MetasysObjectTests +{ + + + [Test] + public void Test() + { + // Arrange + var objectTree = JsonConvert.DeserializeObject(ObjectTreeThreeLevels); + + // Act + var metasysObject = new MetasysObject(objectTree, ApiVersion.v4); + + // Assertions + Assert.That(metasysObject.ChildrenCount == 2); + Assert.That(metasysObject.Children.First().ChildrenCount == 3); + } + + + + private const string ObjectTreeThreeLevels = @" + { + ""itemReference"": ""R12AdsDaily:R12AdsDaily/Graphics"", + ""name"": ""Graphics"", + ""id"": ""76609398-fa4b-51e0-bfa3-eab94d289241"", + ""objectType"": ""objectTypeEnumSet.containerClass"", + ""classification"": ""folder"", + ""items"": [ + { + ""itemReference"": ""R12AdsDaily:R12AdsDaily/Graphics.Common"", + ""name"": ""Common"", + ""id"": ""55bd3987-6e6a-567d-8894-59684d2358d9"", + ""objectType"": ""objectTypeEnumSet.containerClass"", + ""classification"": ""folder"", + ""items"": [ + { + ""itemReference"": ""R12AdsDaily:R12AdsDaily/Graphics.Common.Lighting"", + ""name"": ""Lighting"", + ""id"": ""ccaf1ca6-4ad5-5d07-a177-c38f68ae8235"", + ""objectType"": ""objectTypeEnumSet.xamlGraphicClass"", + ""classification"": """", + ""items"": [] + }, + { + ""itemReference"": ""R12AdsDaily:R12AdsDaily/Graphics.Common.Lighting_Alt"", + ""name"": ""Lighting_Alt"", + ""id"": ""f80ba7bb-6f9e-571f-a8c1-e1162ff20870"", + ""objectType"": ""objectTypeEnumSet.xamlGraphicClass"", + ""classification"": """", + ""items"": [] + }, + { + ""itemReference"": ""R12AdsDaily:R12AdsDaily/Graphics.Common.Weather Data"", + ""name"": ""Weather Data"", + ""id"": ""a7a4bd74-a4c4-53cb-b316-2c14f627e8bc"", + ""objectType"": ""objectTypeEnumSet.xamlGraphicClass"", + ""classification"": """", + ""items"": [] + } + ] + }, + { + ""itemReference"": ""R12AdsDaily:R12AdsDaily/Graphics.Hospital"", + ""name"": ""Hospital"", + ""id"": ""dca754ae-811e-5e7a-896c-f9fee7446b82"", + ""objectType"": ""objectTypeEnumSet.containerClass"", + ""classification"": ""folder"", + ""items"": [ + { + ""itemReference"": ""R12AdsDaily:R12AdsDaily/Graphics.Hospital.AHU-2"", + ""name"": ""AHU-2"", + ""id"": ""22a56b58-f5b6-55fb-9828-c7f0ce96ae2e"", + ""objectType"": ""objectTypeEnumSet.xamlGraphicClass"", + ""classification"": """", + ""items"": [] + }, + { + ""itemReference"": ""R12AdsDaily:R12AdsDaily/Graphics.Hospital.2ND_FLR"", + ""name"": ""2ND_FLR"", + ""id"": ""61839195-b443-56d4-a768-5bc7c0e61416"", + ""objectType"": ""objectTypeEnumSet.xamlGraphicClass"", + ""classification"": """", + ""items"": [] + } + ] + } + ] + }"; + +} diff --git a/MetasysServices/BasicServiceProvider.cs b/MetasysServices/BasicServiceProvider.cs index 1800d50..76f80ad 100644 --- a/MetasysServices/BasicServiceProvider.cs +++ b/MetasysServices/BasicServiceProvider.cs @@ -162,12 +162,53 @@ protected List ToNetworkDevice(List items, ApiVersion ver return objects; } + + /// + /// Gets objects from the server + /// + /// + /// This method requires that Version 4 or greater of the API is being used. It overrides + /// the value of any flatten parameter to be `false` so that we get back a tree of + /// objects. + /// + /// + /// + /// + /// If is less than + /// + /// If is specified then this method returns the children of the specified object (and any of their children if level > 1). + /// If id is null then this method returns the root object (and it's children; unless level is 0). + /// + protected async Task> GetObjectsAsync(Guid? id, Dictionary parameters = null) + { + if (Version <= ApiVersion.v3) + { + throw new InvalidOperationException("This method requires v4 or later of the REST API"); + } + + object[] pathSegments = id == null ? ((List)[]).ToArray() : [id.ToString(), "objects"]; + + parameters["flatten"] = "false"; + var result = await GetRequestAsync("objects", parameters, pathSegments); + var firstNode = result["items"][0]; + var rootObject = new MetasysObject(firstNode, Version); + + if (id == null) + { + return [rootObject]; + } + return rootObject.Children.ToList(); + } + + /// /// Gets all child objects given a parent Guid asynchronously by requesting each available page. /// Level indicates how deep to retrieve objects. /// /// /// A level of 1 only retrieves immediate children of the parent object. + /// This should not be called on a site that supports REST API 4 or later as it makes a series of recursive calls that can + /// be handled in one call to the api /// /// The id of the object. /// Query string parameters in Key/Value format. @@ -176,6 +217,10 @@ protected List ToNetworkDevice(List items, ApiVersion ver /// protected async Task> GetObjectChildrenAsync(Guid id, Dictionary parameters = null, int levels = 1) { + if (Version > ApiVersion.v3) + { + throw new InvalidOperationException("This operation doesn't exist for API > 3"); + } if (levels < 1) { return null; @@ -293,7 +338,6 @@ public async Task> GetResourceTypesAsync(string r } /// - /// Gets the type from a token retrieved from a typeUrl /// /// /// @@ -501,7 +545,6 @@ protected async Task GetRequestAsync(string resource, Dictionary GetRequestAsync(string resource, Dictionary - /// Get typed items for the given resource asynchronously. /// /// Optionally accepts query string parameters and additional path segments. /// @@ -543,7 +585,6 @@ protected async Task> GetPagedResultsAsync(string resource, Di /// - /// Gets all items for the given resource asynchronously by requesting each available page. /// /// The main resource to read. /// Query string parameters in Key Value format. @@ -674,7 +715,6 @@ protected async Task GetBatchRequestAsync(string endpoint, IEnumerable(); - // Concatenate batch segment to use batch request and prepare the list of requests foreach (var id in ids) { foreach (var r in resources) @@ -716,7 +756,6 @@ protected async Task PostBatchRequestAsync(string endpoint, IEnumerable< // Concatenate batch segment to use batch request and prepare the list of requests url.AppendPathSegments("batch"); var objectsRequests = new List(); - // Concatenate batch segment to use batch request and prepare the list of requests foreach (var r in requests) { Url relativeUrl = new Url(r.ObjectId.ToString()); @@ -778,7 +817,6 @@ protected async Task PutBatchRequestAsync(string endpoint, IEnumerable(); - // Concatenate batch segment to use batch request and prepare the list of requests foreach (var r in requests) { Url relativeUrl = new Url(r.ObjectId.ToString()); @@ -851,7 +889,6 @@ protected async Task PatchBatchRequestAsync(string endpoint, IEnumerable // Concatenate batch segment to use batch request and prepare the list of requests url.AppendPathSegments("batch"); var objectsRequests = new List(); - // Concatenate batch segment to use batch request and prepare the list of requests foreach (var r in requests) { string activityManagementStatus = ""; diff --git a/MetasysServices/Interfaces/IMetasysClient.cs b/MetasysServices/Interfaces/IMetasysClient.cs index edcfa1c..6acd31e 100644 --- a/MetasysServices/Interfaces/IMetasysClient.cs +++ b/MetasysServices/Interfaces/IMetasysClient.cs @@ -193,10 +193,10 @@ public interface IMetasysClient : IBasicService /// Read many attribute values given the Guids of the objects. /// /// - /// A list of VariantMultiple with all the specified attributes (if existing). + /// A list of VariantMultiple with all the specified attributes (if existing). /// /// - /// + /// /// /// IEnumerable ReadPropertyMultiple(IEnumerable ids, IEnumerable attributeNames); @@ -306,11 +306,13 @@ public interface IMetasysClient : IBasicService /// /// The ID of the parent object. /// The depth of the children to retrieve. - /// Set it to true to see also internal objects that are not displayed in the Metasys tree. - /// Set it to true to get also the extensions of the object. + /// Set it to true to see also internal objects that are not displayed in the Metasys tree. + /// Set it to true to get also the extensions of the object. /// The flag includeInternalObjects applies since Metasys API v3. /// /// + /// A collection of zero or more objects that are children of the specified object with the given + /// IEnumerable GetObjects(Guid id, int levels = 1, bool includeInternalObjects = false, bool includeExtensions = false); /// Task> GetObjectsAsync(Guid id, int levels = 1, bool includeInternalObjects = false, bool includeExtensions = false); @@ -321,7 +323,7 @@ public interface IMetasysClient : IBasicService /// /// The object type enum set. /// - /// + /// IEnumerable GetObjects(Guid objectId, string objectType); /// @@ -429,7 +431,7 @@ public interface IMetasysClient : IBasicService /// /// Send an HTTP request as an asynchronous operation. - /// + /// /// /// This method currently only supports 1 value per header rather than multiple. In a future revision, this is planned to be addressed. /// diff --git a/MetasysServices/MetasysClient.cs b/MetasysServices/MetasysClient.cs index 77e1d60..8062103 100644 --- a/MetasysServices/MetasysClient.cs +++ b/MetasysServices/MetasysClient.cs @@ -124,6 +124,7 @@ public int Timeout if (Spaces != null) { Spaces.Version = version.Value; } if (Streams != null) { Streams.Version = version.Value; } if (Trends != null) { Trends.Version = version.Value; } + base.Version = value; } } @@ -325,7 +326,7 @@ public async Task RefreshAsync() var response = await Client.Request("refreshToken") .GetJsonAsync() .ConfigureAwait(false); - // Since it's a refresh, get issue info from the current token + // Since it's a refresh, get issue info from the current token CreateAccessToken(AccessToken.Issuer, AccessToken.IssuedTo, response); // Set the new value of the Token to the StreamClient if (Streams != null) @@ -512,10 +513,12 @@ public async Task> GetObjectsAsync(Guid id, int level parameters = new Dictionary { { "depth", levels.ToString() }, - { "flatten", "true".ToString() }, //This parameter is needed to get the data in a 'flat' way and keep consistency in the logic to retrieve the objects - { "includeExtensions", includeExtensions.ToString() }, - { "includeInternal", includeInternalObjects.ToString() } //This param has different name when version > v3 + { "flatten", "false" }, + { "includeExtensions", includeExtensions.ToString().ToLower() }, + { "includeInternal", includeInternalObjects.ToString().ToLower() } //This param has different name when version > v3 }; + + return await GetObjectsAsync(id, parameters); } var objects = await GetObjectChildrenAsync(id, parameters, levels).ConfigureAwait(false); @@ -564,7 +567,7 @@ public async Task GetObjectIdentifierAsync(string itemReference) { // Sanitize given itemReference var normalizedItemReference = itemReference.Trim().ToUpper(); - // Returns cached value when available, otherwise perform request + // Returns cached value when available, otherwise perform request if (!IdentifiersDictionary.ContainsKey(normalizedItemReference)) { JToken response = null; @@ -957,7 +960,7 @@ private void ScheduleRefresh() //{ // DateTime now = DateTime.UtcNow; // TimeSpan delay = AccessToken.Expires - now.AddSeconds(-1); // minimum renew gap of 1 sec in advance - // // Renew one minute before expiration if there is more than one minute time + // // Renew one minute before expiration if there is more than one minute time // if (delay > new TimeSpan(0, 1, 0)) // { // delay.Subtract(new TimeSpan(0, 1, 0)); @@ -982,7 +985,7 @@ private void ScheduleRefresh() /// - /// Overload of ReadPropertyAsync for internal use where Exception suppress is needed, e.g. ReadPropertyMultiple + /// Overload of ReadPropertyAsync for internal use where Exception suppress is needed, e.g. ReadPropertyMultiple /// /// /// @@ -1003,7 +1006,7 @@ private async Task ReadPropertyAsync(Guid id, string attributeName, boo /// /// Creates the body for the WriteProperty and WritePropertyMultiple requests as a dictionary. /// - /// The (attribute, value) pairs. + /// The (attribute, value) pairs. /// Dictionary of the attribute, value pairs. private Dictionary GetWritePropertyBody(IEnumerable<(string Attribute, object Value)> attributeValues) { @@ -1040,7 +1043,7 @@ private async Task WritePropertyRequestAsync(Guid id, Dictionary } ///// - ///// Gets the type from a token retrieved from a typeUrl + ///// Gets the type from a token retrieved from a typeUrl ///// ///// ///// @@ -1144,7 +1147,7 @@ private IEnumerable ToVariantMultiples(JToken response) var m = multiples.SingleOrDefault(s => s.Id == objId); if (m == null) { - // Add a new multiple for the current object + // Add a new multiple for the current object multiples.Add(new VariantMultiple(objId, values)); } else @@ -1241,4 +1244,3 @@ private Url GetUrlFromHttpRequest(HttpRequestMessage requestMessage) } } - diff --git a/MetasysServices/MetasysServices.csproj b/MetasysServices/MetasysServices.csproj index d4c9356..db20f28 100644 --- a/MetasysServices/MetasysServices.csproj +++ b/MetasysServices/MetasysServices.csproj @@ -1,9 +1,9 @@ - + netstandard2.0 true JohnsonControls.Metasys.BasicServices - 7.3 + 12 true © 2020-2024 Johnson Controls Johnson Controls Int. @@ -18,7 +18,7 @@ - Cleaned up unnecessary code LICENSE README.md - 6.0.4 + 6.0.4 @@ -67,7 +67,7 @@ README.md - + diff --git a/MetasysServices/Models/MetasysObject.cs b/MetasysServices/Models/MetasysObject.cs index 4ba59d2..2c08f2f 100644 --- a/MetasysServices/Models/MetasysObject.cs +++ b/MetasysServices/Models/MetasysObject.cs @@ -33,13 +33,13 @@ public class MetasysObject : Utils.ObjectUtil public MetasysObjectTypeEnum? Type { get; set; } /// - /// The resource type detail reference. + /// The resource type detail reference. /// /// This is available only on Metasys API v2 and v1. public string TypeUrl { get; set; } /// - /// The resource type detail reference. + /// The resource type detail reference. /// /// This is available since Metasys API v3. public string ObjectType { get; set; } @@ -65,8 +65,8 @@ public class MetasysObject : Utils.ObjectUtil /// /// The number of direct children objects. /// - /// The number of children or -1 if there is no children data. - public int ChildrenCount { set; get; } + /// The number of children. + public int ChildrenCount => Children.Count(); /// /// Name of the Equipment Definition mapped with the Equipment Instance @@ -84,12 +84,30 @@ public MetasysObject() { } public string Classification { get; set; } private const string DefaultClassification = "object"; + /// + /// Create a tree of objects out of a root node + /// + /// + /// + internal MetasysObject(JToken token, ApiVersion version) + { + var root = token; + var children = root["items"] as JArray; + Initialize(root, version); + + Children = children?.Select(child => new MetasysObject(child, version)).ToList() ?? []; + } + internal MetasysObject(JToken token, ApiVersion version, IEnumerable children = null, MetasysObjectTypeEnum? type = null) { - Children = children ?? new List(); // Return empty list by convention for null - ChildrenCount = Children?.Count() ?? 0; // Children count is 0 when children is null + Children = children ?? new List(); // Return empty list by convention for null + // = Children?.Count() ?? 0; // Children count is 0 when children is null Type = type; + Initialize(token, version); + } + private void Initialize(JToken token, ApiVersion version) + { JObject jobj = token.ToObject(); try { @@ -153,7 +171,7 @@ internal MetasysObject(JToken token, ApiVersion version, IEnumerable() : null; } catch