diff --git a/MetasysServices.Tests/MetasysClientTests.cs b/MetasysServices.Tests/MetasysClientTests.cs index 72b6415..66ccf50 100644 --- a/MetasysServices.Tests/MetasysClientTests.cs +++ b/MetasysServices.Tests/MetasysClientTests.cs @@ -1901,6 +1901,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 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 4e8c6a0..cd0eb79 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 id 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(Object 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; diff --git a/MetasysServices/MetasysClient.cs b/MetasysServices/MetasysClient.cs index ec54d82..69d2f74 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; } } @@ -513,10 +514,12 @@ public async Task> GetObjectsAsync(ObjectId id, int l 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); diff --git a/MetasysServices/Models/MetasysObject.cs b/MetasysServices/Models/MetasysObject.cs index 5c05587..782ed3c 100644 --- a/MetasysServices/Models/MetasysObject.cs +++ b/MetasysServices/Models/MetasysObject.cs @@ -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,29 @@ 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 Type = type; + Initialize(token, version); + } + private void Initialize(JToken token, ApiVersion version) + { JObject jobj = token.ToObject(); try {