From de60e48f724abd01c173abc8d8505cb163052b17 Mon Sep 17 00:00:00 2001 From: danielpeintner Date: Wed, 18 Oct 2023 17:25:30 +0200 Subject: [PATCH] feat: support special AID treatment for ranges fixes https://github.com/eclipse-thingweb/node-wot/issues/1128 --- .../src/util/asset-interface-description.ts | 98 +++++++++++++++-- .../test/AssetInterfaceDescriptionTest.ts | 102 +++++++++++++++++- .../td-tools/test/util/inverterModbus.json | 61 +++++++++++ 3 files changed, 254 insertions(+), 7 deletions(-) diff --git a/packages/td-tools/src/util/asset-interface-description.ts b/packages/td-tools/src/util/asset-interface-description.ts index ebb0cf6e5..bfd381650 100644 --- a/packages/td-tools/src/util/asset-interface-description.ts +++ b/packages/td-tools/src/util/asset-interface-description.ts @@ -340,12 +340,22 @@ export class AssetInterfaceDescriptionUtil { for (const v of iv.value) { // Binding if (v.idShort === "href") { - if (v.value != null && isAbsoluteUrl(v.value)) { - form.href = v.value; - } else if (form.href && form.href.length > 0) { - form.href = form.href + v.value; // TODO handle leading/trailing slashes - } else { - form.href = v.value; + if (v.value != null) { + const hrefValue: string = v.value; + if (isAbsoluteUrl(hrefValue)) { + form.href = hrefValue; + } else if (form.href && form.href.length > 0) { + // handle leading/trailing slashes + if (form.href.endsWith("/") && hrefValue.startsWith("/")) { + form.href = form.href + hrefValue.substring(1); + } else if (!form.href.endsWith("/") && !hrefValue.startsWith("/")) { + form.href = form.href + "/" + hrefValue; + } else { + form.href = form.href + hrefValue; + } + } else { + form.href = hrefValue; + } } } else if (typeof v.idShort === "string" && v.idShort.length > 0) { // pick *any* value (and possibly override, e.g. contentType) @@ -632,6 +642,39 @@ export class AssetInterfaceDescriptionUtil { thing.properties[key].readOnly = interactionValue.value === "true"; } else if (interactionValue.idShort === "writeOnly") { thing.properties[key].writeOnly = interactionValue.value === "true"; + } else if (interactionValue.idShort === "min_max") { + // special treatment + if (thing.properties[key].type == null) { + thing.properties[key].type = "number"; + } + if (interactionValue.min != null) { + thing.properties[key].minimum = Number(interactionValue.min); + } + if (interactionValue.max != null) { + thing.properties[key].maximum = Number(interactionValue.max); + } + } else if (interactionValue.idShort === "itemsRange") { + // special treatment + if (thing.properties[key].type == null) { + thing.properties[key].type = "array"; + } + if (interactionValue.min != null) { + thing.properties[key].minItems = Number(interactionValue.min); + } + if (interactionValue.max != null) { + thing.properties[key].maxItems = Number(interactionValue.max); + } + } else if (interactionValue.idShort === "lengthRange") { + // special treatment + if (thing.properties[key].type == null) { + thing.properties[key].type = "string"; + } + if (interactionValue.min != null) { + thing.properties[key].minLength = Number(interactionValue.min); + } + if (interactionValue.max != null) { + thing.properties[key].maxLength = Number(interactionValue.max); + } } else if (interactionValue.idShort === "forms") { // will be handled below } else { @@ -807,6 +850,49 @@ export class AssetInterfaceDescriptionUtil { value: propertyValue.type, modelType: "Property", }); + // special AID treatment + if (propertyValue.minimum != null || propertyValue.maximum != null) { + const minMax: { [k: string]: unknown } = { + idShort: "min_max", + valueType: "xs:integer", + modelType: "Range", + }; + if (propertyValue.minimum != null) { + minMax.min = propertyValue.minimum.toString(); + } + if (propertyValue.maximum != null) { + minMax.max = propertyValue.maximum.toString(); + } + propertyValues.push(minMax); + } + if (propertyValue.minItems != null || propertyValue.maxItems != null) { + const itemsRange: { [k: string]: unknown } = { + idShort: "itemsRange", + valueType: "xs:integer", + modelType: "Range", + }; + if (propertyValue.minItems != null) { + itemsRange.min = propertyValue.minItems.toString(); + } + if (propertyValue.maxItems != null) { + itemsRange.max = propertyValue.maxItems.toString(); + } + propertyValues.push(itemsRange); + } + if (propertyValue.minLength != null || propertyValue.maxLength != null) { + const lengthRange: { [k: string]: unknown } = { + idShort: "lengthRange", + valueType: "xs:integer", + modelType: "Range", + }; + if (propertyValue.minLength != null) { + lengthRange.min = propertyValue.minLength.toString(); + } + if (propertyValue.maxLength != null) { + lengthRange.max = propertyValue.maxLength.toString(); + } + propertyValues.push(lengthRange); + } } // title if (propertyValue.title != null) { diff --git a/packages/td-tools/test/AssetInterfaceDescriptionTest.ts b/packages/td-tools/test/AssetInterfaceDescriptionTest.ts index 0a4b4e282..34c4904c2 100644 --- a/packages/td-tools/test/AssetInterfaceDescriptionTest.ts +++ b/packages/td-tools/test/AssetInterfaceDescriptionTest.ts @@ -178,7 +178,7 @@ class AssetInterfaceDescriptionUtilTest { expect(tdObj).to.have.property("security").to.be.an("array").to.have.lengthOf(1); expect(tdObj.securityDefinitions[tdObj.security[0]]).to.have.property("scheme").that.equals("nosec"); - // check device_name property + // check property device_name expect(tdObj).to.have.property("properties").to.have.property("device_name"); expect(tdObj) .to.have.property("properties") @@ -208,6 +208,39 @@ class AssetInterfaceDescriptionUtilTest { .to.have.property("contentType") .to.eql("application/octet-stream"); expect(tdObj.properties.device_name.forms[0]).not.to.have.property("security"); + + // check property soc + expect(tdObj).to.have.property("properties").to.have.property("soc"); + expect(tdObj) + .to.have.property("properties") + .to.have.property("soc") + .to.have.property("type") + .that.equals("integer"); + expect(tdObj).to.have.property("properties").to.have.property("soc").to.have.property("minimum").that.equals(0); + expect(tdObj) + .to.have.property("properties") + .to.have.property("soc") + .to.have.property("maximum") + .that.equals(100); + expect(tdObj) + .to.have.property("properties") + .to.have.property("soc") + .to.have.property("title") + .that.equals("Battery SoC scaled in %"); + expect(tdObj) + .to.have.property("properties") + .to.have.property("soc") + .to.have.property("forms") + .to.be.an("array") + .to.have.lengthOf(1); + expect(tdObj.properties.soc.forms[0]).to.have.property("op").to.eql("readproperty"); + expect(tdObj.properties.soc.forms[0]) + .to.have.property("href") + .to.eql("modbus+tcp://192.168.178.146:502/40361?quantity=1"); + expect(tdObj.properties.soc.forms[0]).to.have.property("modbus:function").to.eql("readHoldingRegisters"); + expect(tdObj.properties.soc.forms[0]).to.have.property("modbus:type").to.eql("uint16be"); + expect(tdObj.properties.soc.forms[0]).to.have.property("contentType").to.eql("application/octet-stream"); + expect(tdObj.properties.device_name.forms[0]).not.to.have.property("security"); } @test async "should correctly roundtrip inverterModbus from/to AID"() { @@ -290,6 +323,7 @@ class AssetInterfaceDescriptionUtilTest { .to.be.an("array") .to.have.lengthOf.greaterThan(0); let hasPropertyDeviceName = false; + let hasPropertySOC = false; for (const propertyValue of interactionValues.value) { if (propertyValue.idShort === "device_name") { hasPropertyDeviceName = true; @@ -350,9 +384,75 @@ class AssetInterfaceDescriptionUtilTest { expect(hasType).to.equal(true); expect(hasTitle).to.equal(true); expect(hasForms).to.equal(true); + } else if (propertyValue.idShort === "soc") { + hasPropertySOC = true; + expect(propertyValue) + .to.have.property("value") + .to.be.an("array") + .to.have.lengthOf.greaterThan(0); + let hasType = false; + let hasTitle = false; + let hasMinMax = false; + let hasForms = false; + for (const propProperty of propertyValue.value) { + if (propProperty.idShort === "type") { + hasType = true; + expect(propProperty.value).to.equal("integer"); + } else if (propProperty.idShort === "title") { + hasTitle = true; + expect(propProperty.value).to.equal("Battery SoC scaled in %"); + } else if (propProperty.idShort === "min_max") { + hasMinMax = true; + expect(propProperty.min).to.equal("0"); + expect(propProperty.max).to.equal("100"); + } else if (propProperty.idShort === "forms") { + hasForms = true; + expect(propProperty) + .to.have.property("value") + .to.be.an("array") + .to.have.lengthOf.greaterThan(0); + let hasHref = false; + let hasOp = false; + let hasContentType = false; + let hasModbusFunction = false; + let hasModbusType = false; + for (const formEntry of propProperty.value) { + if (formEntry.idShort === "href") { + hasHref = true; + expect(formEntry.value).to.equal( + "modbus+tcp://192.168.178.146:502/40361?quantity=1" + ); + } else if (formEntry.idShort === "op") { + hasOp = true; + expect(formEntry.value).to.equal("readproperty"); + } else if (formEntry.idShort === "contentType") { + hasContentType = true; + expect(formEntry.value).to.equal("application/octet-stream"); + } else if (formEntry.idShort === "modbus_function") { + // vs. "modbus:function" + hasModbusFunction = true; + expect(formEntry.value).to.equal("readHoldingRegisters"); + } else if (formEntry.idShort === "modbus_type") { + // vs. "modbus:type" + hasModbusType = true; + expect(formEntry.value).to.equal("uint16be"); + } + } + expect(hasHref).to.equal(true); + expect(hasOp).to.equal(true); + expect(hasContentType).to.equal(true); + expect(hasModbusFunction).to.equal(true); + expect(hasModbusType).to.equal(true); + } + } + expect(hasType).to.equal(true); + expect(hasTitle).to.equal(true); + expect(hasMinMax).to.equal(true); + expect(hasForms).to.equal(true); } } expect(hasPropertyDeviceName).to.equal(true); + expect(hasPropertySOC).to.equal(true); } } expect(hasProperties).to.equal(true); diff --git a/packages/td-tools/test/util/inverterModbus.json b/packages/td-tools/test/util/inverterModbus.json index d220784ab..6d49ba355 100644 --- a/packages/td-tools/test/util/inverterModbus.json +++ b/packages/td-tools/test/util/inverterModbus.json @@ -139,6 +139,67 @@ } ], "modelType": "SubmodelElementCollection" + }, + { + "idShort": "soc", + "value": [ + { + "idShort": "type", + "valueType": "xs:string", + "value": "integer", + "modelType": "Property" + }, + { + "idShort": "title", + "valueType": "xs:string", + "value": "Battery SoC scaled in %", + "modelType": "Property" + }, + { + "idShort": "min_max", + "valueType": "xs:integer", + "min": "0", + "max": "100", + "modelType": "Range" + }, + { + "idShort": "forms", + "value": [ + { + "idShort": "op", + "valueType": "xs:string", + "value": "readproperty", + "modelType": "Property" + }, + { + "idShort": "href", + "valueType": "xs:string", + "value": "/40361?quantity=1", + "modelType": "Property" + }, + { + "idShort": "modbus:function", + "valueType": "xs:string", + "value": "readHoldingRegisters", + "modelType": "Property" + }, + { + "idShort": "modbus:type", + "valueType": "xs:string", + "value": "uint16be", + "modelType": "Property" + }, + { + "idShort": "contentType", + "valueType": "xs:string", + "value": "application/octet-stream", + "modelType": "Property" + } + ], + "modelType": "SubmodelElementCollection" + } + ], + "modelType": "SubmodelElementCollection" } ], "modelType": "SubmodelElementCollection"