From 389a28d7e780f268e2743a91043c8498e192da53 Mon Sep 17 00:00:00 2001 From: Daniel Kluge Date: Mon, 4 Dec 2023 16:08:04 +0100 Subject: [PATCH] Restructured the AID parsing for the (hopefully) better --- lib/src/parser/AIDParser.ts | 265 ++++++++++++++++++++++++++++++------ lib/src/types/aidConf.ts | 167 ++++++++++++----------- lib/src/types/common.ts | 2 +- 3 files changed, 312 insertions(+), 122 deletions(-) diff --git a/lib/src/parser/AIDParser.ts b/lib/src/parser/AIDParser.ts index 0a5d525..d7ed443 100644 --- a/lib/src/parser/AIDParser.ts +++ b/lib/src/parser/AIDParser.ts @@ -1,7 +1,8 @@ import * as aasCore from "@aas-core-works/aas-core3.0-typescript"; -import type { AssetInterfacesDescription, EndpointMetadata, InterfaceDescription, InterfaceMetadata, InterfaceAction, InterfaceEvent, InterfaceProperty, InterfacePropertyForm } from "../types/aidConf"; +import type { AssetInterfacesDescription, EndpointMetadata, InterfaceDescription, InterfaceMetadata, InterfaceAction, InterfaceEvent, InterfaceProperty, InterfacePropertyForm, InterfaceActionForm, InterfaceFormBase, InterfaceFormBaseHTTP, InterfaceEventForm } from "../types/aidConf"; import { AvailableEndpoint, endpointAvailable } from "../types/common"; import Traverser from "../helper/traverser"; +import { Property } from "@aas-core-works/aas-core3.0-typescript/dist/types/types"; /** * Class for static methods to parse the AssetInterfacesDescription Submodel @@ -118,11 +119,20 @@ export default class AIDParser { if (actions === null || !aasCore.types.isSubmodelElementList(actions) || !actions.value) return null; // Mandatory for (const element of actions.overValueOrEmpty()) { if (!aasCore.types.isSubmodelElementCollection(element)) continue; - const parsedAction = AIDParser.parseInterfaceMetadataProperty(element, endpointProtocol); + const parsedAction = AIDParser.parseInterfaceMetadataAction(element, endpointProtocol); if (parsedAction === null) continue; parsed.actions.push(parsedAction); } + const events = Traverser.findElement(interfaceMeta, element => (element as any).idShort?.toLocaleLowerCase() === "events"); + if (events === null || !aasCore.types.isSubmodelElementList(events) || !events.value) return null; // Mandatory + for (const element of events.overValueOrEmpty()) { + if (!aasCore.types.isSubmodelElementCollection(element)) continue; + const parsedEvent = AIDParser.parseInterfaceMetadataEvent(element, endpointProtocol); + if (parsedEvent === null) continue; + parsed.events.push(parsedEvent); + } + return parsed as InterfaceMetadata; } @@ -136,22 +146,101 @@ export default class AIDParser { public static parseInterfaceMetadataProperty(prop: aasCore.types.SubmodelElementCollection, endpointProtocol?: AvailableEndpoint): InterfaceProperty | null { if (!aasCore.types.isSubmodelElementCollection(prop) || !prop.value) return null; - const parsed = {} as InterfaceProperty; + const parsed = {} as any; - // This does not matter in any way for the interface - /* for (const key of ["title", "observeable", "type"]) { - const prop = AASStore.findElement(prop, element => (element as any).idShort?.toLocaleLowerCase() === key.toLocaleLowerCase()); - if (prop === null || !aasCore.types.isProperty(prop) || !prop.value) continue; // Not mandatory - else parsed[key] = prop.value; - } */; + // This does not really matter but... + for (const key of ["title", "observeable", "type"]) { + const propProp = Traverser.findElement(prop, element => (element as any).idShort?.toLocaleLowerCase() === key.toLocaleLowerCase()); + if (propProp === null || !aasCore.types.isProperty(propProp) || !propProp.value) continue; + else parsed[key] = propProp.value; + }; + const parsedForms: InterfacePropertyForm[] = []; const forms = Traverser.findElement(prop, element => (element as any).idShort?.toLocaleLowerCase() === "forms"); - if (forms === null || !aasCore.types.isSubmodelElementCollection(forms) || !forms.value) return null; // Mandatory - const parsedForm = AIDParser.parseInterfaceMetadataPropertyForm(forms, endpointProtocol); - if (parsedForm === null) return null; - // TODO - // Other values based on endpoint protocol - return { ...parsed, forms: parsedForm } as InterfaceProperty; + if (forms === null || !aasCore.types.isSubmodelElementList(forms) || !forms.value) return null; // Mandatory + for (const form of forms.overValueOrEmpty()) { + if (!aasCore.types.isSubmodelElementCollection(form)) continue; + const parsedForm = AIDParser.parseInterfaceMetadataForm(form, "property", endpointProtocol) as InterfacePropertyForm; + if (parsedForm === null) continue; + parsedForms.push(parsedForm); + } + + if (parsedForms.length === 0) return null; + + return { ...parsed, forms: parsedForms } as InterfaceProperty; + } + + /** + * Parse a single Action InterfaceMetadata + * @param prop SubmodelElementCollection of one Action + * @param endpointProtocol Protocol used in the endpoint, so additional properties can be parsed + * @returns Parsed Action InterfaceMetadata or null if not parsable + * @beta + */ + public static parseInterfaceMetadataAction(action: aasCore.types.SubmodelElementCollection, endpointProtocol?: AvailableEndpoint): InterfaceAction | null { + if (!aasCore.types.isSubmodelElementCollection(action) || !action.value) return null; + + const parsed = {} as any; + + // This does not really matter but... + for (const key of ["title", "type"]) { + const actionProp = Traverser.findElement(action, element => (element as any).idShort?.toLocaleLowerCase() === key.toLocaleLowerCase()); + if (actionProp === null || !aasCore.types.isProperty(actionProp) || !actionProp.value) continue; + else parsed[key] = actionProp.value; + }; + + // But this is really important! + const asyncProp = Traverser.findElement(action, element => (element as any).idShort?.toLocaleLowerCase() === "async"); + if (asyncProp === null || !aasCore.types.isProperty(asyncProp) || !asyncProp.value) {} + else parsed["async"] = asyncProp.value === "true"; + + const parsedForms: InterfaceActionForm[] = []; + const forms = Traverser.findElement(action, element => (element as any).idShort?.toLocaleLowerCase() === "forms"); + if (forms === null || !aasCore.types.isSubmodelElementList(forms) || !forms.value) return null; // Mandatory + for (const form of forms.overValueOrEmpty()) { + if (!aasCore.types.isSubmodelElementCollection(form)) continue; + const parsedForm = AIDParser.parseInterfaceMetadataForm(form, "action", endpointProtocol) as InterfaceActionForm; + if (parsedForm === null) continue; + parsedForms.push(parsedForm); + } + + if (parsedForms.length === 0) return null; + + return { ...parsed, forms: parsedForms } as InterfaceAction; + } + + /** + * Parse a single Event InterfaceMetadata + * @param prop SubmodelElementCollection of one Event + * @param endpointProtocol Protocol used in the endpoint, so additional properties can be parsed + * @returns Parsed Event InterfaceMetadata or null if not parsable + * @beta + */ + public static parseInterfaceMetadataEvent(event: aasCore.types.SubmodelElementCollection, endpointProtocol?: AvailableEndpoint): InterfaceEvent | null { + if (!aasCore.types.isSubmodelElementCollection(event) || !event.value) return null; + + const parsed = {} as any; + + // This does not really matter but... + for (const key of ["title", "type"]) { + const eventProp = Traverser.findElement(event, element => (element as any).idShort?.toLocaleLowerCase() === key.toLocaleLowerCase()); + if (eventProp === null || !aasCore.types.isProperty(eventProp) || !eventProp.value) continue; + else parsed[key] = eventProp.value; + }; + + const parsedForms: InterfaceEventForm[] = []; + const forms = Traverser.findElement(event, element => (element as any).idShort?.toLocaleLowerCase() === "forms"); + if (forms === null || !aasCore.types.isSubmodelElementList(forms) || !forms.value) return null; // Mandatory + for (const form of forms.overValueOrEmpty()) { + if (!aasCore.types.isSubmodelElementCollection(form)) continue; + const parsedForm = AIDParser.parseInterfaceMetadataForm(form, "event", endpointProtocol) as InterfaceEventForm; + if (parsedForm === null) continue; + parsedForms.push(parsedForm); + } + + if (parsedForms.length === 0) return null; + + return { ...parsed, forms: parsedForms } as InterfaceEvent; } /** @@ -160,44 +249,142 @@ export default class AIDParser { * @param endpointProtocol Protocol used in the endpoint, so additional properties can be parsed * @returns Parsed Form or null if not parsable */ - private static parseInterfaceMetadataPropertyForm(form: aasCore.types.SubmodelElementCollection, endpointProtocol?: AvailableEndpoint): InterfacePropertyForm | null { + private static parseInterfaceMetadataForm(form: aasCore.types.SubmodelElementCollection, element: "property" | "action" | "event", endpointProtocol?: AvailableEndpoint): InterfacePropertyForm | InterfaceActionForm | InterfaceEventForm | null { if (!form.value) return null; const parsed = {} as any; - for (const key of ["href", "contentType"]) { + for (const key of ["href", "contentType", "op"]) { const prop = Traverser.findElement(form, element => (element as any).idShort?.toLocaleLowerCase() === key.toLocaleLowerCase()); if (prop === null || !aasCore.types.isProperty(prop) || !prop.value) return null; // Mandatory else parsed[key] = prop.value; } - // TODO - // Other values based on endpoint protocol + if (!AIDParser.isValidFormOperation(parsed.op, element, endpointProtocol)) return null; + + switch (element) { + case "property": return AIDParser.parseAdditionalFormProperties(form, parsed, endpointProtocol) as InterfacePropertyForm; + case "action": return AIDParser.parseAdditionalFormProperties(form, parsed, endpointProtocol) as InterfaceActionForm; + case "event": return AIDParser.parseAdditionalFormProperties(form, parsed, endpointProtocol) as InterfaceEventForm; + } + } - return parsed as InterfacePropertyForm; + private static isValidFormOperation(operation: string, element: "property" | "action" | "event", endpoint?: AvailableEndpoint) { + const validOperations: { [key in AvailableEndpoint]: { [key in "property" | "action" | "event"]: string[] } } = { + "HTTP": { + "property": ["readproperty", "writeproperty"], + "action": ["invokeaction"], + "event": [] + }, + "MQTT": { + "property": ["readproperty", "writeproperty", "observeproperty", "unobserveproperty"], + "action": ["invokeaction"], + "event": ["subscribeevent", "unsubscribeevent"] + } + } + + if (!endpoint) return true; + + return validOperations[endpoint][element].includes(operation); + } + + private static parseAdditionalFormProperties(form: aasCore.types.SubmodelElementCollection, currentlyParsed: any, endpointProtocol?: AvailableEndpoint): InterfacePropertyForm | InterfaceActionForm | InterfaceEventForm { + switch (endpointProtocol) { + case "HTTP": { + const method = Traverser.findElement(form, element => (element as any).idShort?.toLocaleLowerCase() === "htv:methodname"); + if (method !== null && aasCore.types.isProperty(method)) { + // Next line is kinda long so heres a short explanation: + // If method is set and it's a valid HTTP method we take it, else we use the default + // setting for read/write + const methodName = ["GET", "PUT", "POST", "DELETE", "PATCH"].includes(method.value?.toLocaleUpperCase() ?? "") ? method.value?.toLocaleUpperCase() : AIDParser.getDefaultMethod(currentlyParsed.op) ?? "GET"; + currentlyParsed["htv:methodName"] = methodName; + } + + const headers = Traverser.findElement(form, element => (element as any).idShort?.toLocaleLowerCase() === "htv:headers"); + if (headers !== null && aasCore.types.isSubmodelElementList(headers)) { + const parsedHeaders: { "htv:fieldName": string, "htv:fieldValue": string }[] = []; + + for (const header of headers.overValueOrEmpty()) { + if (!aasCore.types.isSubmodelElementCollection(header) || !header.value) continue; + const fieldName = Traverser.findElement(header, element => (element as any).idShort?.toLocaleLowerCase() === "htv:fieldname"); + const fieldValue = Traverser.findElement(header, element => (element as any).idShort?.toLocaleLowerCase() === "htv:fieldvalue"); + if (fieldName === null || !aasCore.types.isProperty(fieldName) || !fieldName.value || fieldValue === null || !aasCore.types.isProperty(fieldValue) || !fieldValue.value) continue; + parsedHeaders.push({ "htv:fieldName": fieldName.value, "htv:fieldValue": fieldValue.value }); + } + currentlyParsed["htv:headers"] = parsedHeaders; + } + break; + } + case "MQTT": { + const controlPacket = Traverser.findElement(form, element => (element as any).idShort?.toLocaleLowerCase() === "mqv:controlpacketvalue"); + if (controlPacket !== null && aasCore.types.isProperty(controlPacket)) { + // Next line is kinda long so heres a short explanation: + // If controlPacket is set and it's a valid MQTT control packet we take it, else we use the default + // setting for read/write + const controlPacketValue = ["PUBLISH", "SUBSCRIBE", "UNSUBSCRIBE"].includes(controlPacket.value?.toLocaleUpperCase() ?? "") ? controlPacket.value?.toLocaleUpperCase() : AIDParser.getDefaultMethod(currentlyParsed.op); + currentlyParsed["mqv:controlPacketValue"] = controlPacketValue; + } + + const options = Traverser.findElement(form, element => (element as any).idShort?.toLocaleLowerCase() === "mqv:options"); + if (options !== null && aasCore.types.isSubmodelElementList(options)) { + const parsedOptions: ({ "mqv:optionName": string, "mqv:optionValue": number | boolean })[] = []; + + for (const option of options.overValueOrEmpty()) { + if (!aasCore.types.isSubmodelElementCollection(option) || !option.value) continue; + const optionName = Traverser.findElement(option, element => (element as any).idShort?.toLocaleLowerCase() === "mqv:optionname"); + const optionValue = Traverser.findElement(option, element => (element as any).idShort?.toLocaleLowerCase() === "mqv:optionvalue"); + if (optionName === null || !aasCore.types.isProperty(optionName) || !optionName.value || optionValue === null || !aasCore.types.isProperty(optionValue) || !optionValue.value) continue; + const value = Number.isNaN(Number.parseInt(optionValue.value)) ? optionValue.value === "true" : Number.parseInt(optionValue.value); + parsedOptions.push({ "mqv:optionName": optionName.value, "mqv:optionValue": value }); + } + currentlyParsed["mqv:options"] = parsedOptions; + } + + break; + } + default: + break; + } + + return currentlyParsed as InterfacePropertyForm; } /** - * Parse a single Action InterfaceMetadata - * @param prop SubmodelElementCollection of one Action - * @param endpointProtocol Protocol used in the endpoint, so additional properties can be parsed - * @returns Parsed Action InterfaceMetadata or null if not parsable - * @alpha + * Get the default protocol method for an operation + * @param op Operation, e.g. "readproperty" + * @param endpoint + * @returns */ - public static parseInterfaceMetadataAction(action: aasCore.types.SubmodelElementCollection, endpointProtocol?: AvailableEndpoint): InterfaceAction | null { - // TODO - return null; - } + private static getDefaultMethod(op: string, endpoint?: AvailableEndpoint): string | undefined { + const defaults: any = { + "readproperty": { + "HTTP": "GET", + "MQTT": "SUBSCRIBE" + }, + "writeproperty": { + "HTTP": "PUT", + "MQTT": "PUBLISH" + }, + "observeproperty": { + "MQTT": "SUBSCRIBE" + }, + "unobserveproperty": { + "MQTT": "UNSUBSCRIBE" + }, + "invokeaction": { + "HTTP": "POST", + "MQTT": "PUBLISH" + }, + "subscribeevent": { + "MQTT": "SUBSCRIBE" + }, + "unsubscribeevent": { + "MQTT": "UNSUBSCRIBE" + } + }; - /** - * Parse a single Event InterfaceMetadata - * @param prop SubmodelElementCollection of one Event - * @param endpointProtocol Protocol used in the endpoint, so additional properties can be parsed - * @returns Parsed Event InterfaceMetadata or null if not parsable - * @alpha - */ - public static parseInterfaceMetadataEvent(event: aasCore.types.SubmodelElementCollection, endpointProtocol?: AvailableEndpoint): InterfaceEvent | null { - // TODO - return null; + op = op.toLocaleLowerCase(); + + return defaults[op] === undefined ? defaults[op][endpoint ?? ""] ?? null : null; } } \ No newline at end of file diff --git a/lib/src/types/aidConf.ts b/lib/src/types/aidConf.ts index 9bdf3a7..42178b1 100644 --- a/lib/src/types/aidConf.ts +++ b/lib/src/types/aidConf.ts @@ -48,11 +48,83 @@ It should be pretty easy to extend this. So let's derive some of this from the WoT and see what we can achieve. */ +type FormOp = "readproperty" | +"writeproperty" | +"observeproperty" | +"unobserveproperty" | +"invokeaction" | +"subscribeevent" | +"unsubscribeevent"; +// The following are defined in WoT but not useful in our case +//"readallproperties" | +//"writeallproperties" | +//"readmultipleproperties" | +//"writemultipleproperties"; + +type FormOpHTTPProp = "readproperty" | "writeproperty"; +type FormOpHTTPAction = "invokeaction"; + +type FormOpMQTTProp = "readproperty" | "writeproperty" | "observeproperty" | "unobserveproperty"; +type FormOpMQTTAction = "invokeaction"; +type FormOpMQTTEvent = "subscribeevent" | "unsubscribeevent"; + +export type InterfaceFormBase = { + href: string; + op: FormOp; + contentType: string; +} + +export type InterfaceFormBaseHTTP = InterfaceFormBase & { + "htv:methodName": "GET" | "PUT" | "POST" | "DELETE" | "PATCH"; + "htv:headers": { + "htv:fieldName": string; + "htv:fieldValue": string; + }[]; + op: FormOpHTTPProp | FormOpHTTPAction; +} + +type IFormHTTPProp = InterfaceFormBaseHTTP & { + op: FormOpHTTPProp; +} + +type IFormHTTPAction = InterfaceFormBaseHTTP & { + op: FormOpHTTPAction; +} + + +export type InterfaceFormBaseMQTT = InterfaceFormBase & { + "mqv:controlPacketValue": "PUBLISH" | "SUBSCRIBE" | "UNSUBSCRIBE"; + "mqv:options": ({ + "mqv:optionName": "qos"; + "mqv:optionValue": 0 | 1 | 2; + } | { + "mqv:optionName": "retain" | "dup"; + "mqv:optionValue": boolean; + })[]; + op: FormOpMQTTProp | FormOpMQTTAction | FormOpMQTTEvent; +} + +type IFormMQTTProp = InterfaceFormBaseMQTT & { + op: FormOpMQTTProp; +} + +type IFormMQTTAction = InterfaceFormBaseMQTT & { + op: FormOpMQTTAction; +} + +type IFormMQTTEvent = InterfaceFormBaseMQTT & { + op: FormOpMQTTEvent; +} + +export type InterfacePropertyForm = IFormHTTPProp | IFormMQTTProp; +export type InterfaceActionForm = IFormHTTPAction | IFormMQTTAction; +export type InterfaceEventForm = IFormMQTTEvent; + export type InterfaceProperty = { title?: string; observable?: boolean; - forms: InterfacePropertyForm; - type?: string; + forms: InterfacePropertyForm[]; + type: string; // enum?: string[]; const?: any; default?: any; @@ -66,95 +138,26 @@ export type InterfaceProperty = { // maxItems?: number; } -type FormOp = "readproperty" | -"writeproperty" | -"observeproperty" | -"unobserveproperty" | -"invokeaction" | -"subscribeevent" | -"unsubscribeevent" | -"readallproperties" | -"writeallproperties" | -"readmultipleproperties" | -"writemultipleproperties"; - -type FormOpHTTP = "readproperty" | -"writeproperty" | -"invokeaction" | -"readallproperties" | -"writeallproperties" | -"readmultipleproperties" | -"writemultipleproperties"; - -type IForm = { - href: string; - op: FormOp; - contentType: string; -} - -type IFormHTTP = IForm & { - "htv:methodName": "GET" | "PUT" | "POST" | "DELETE" | "PATCH"; - "htv:headers": { - "htv:fieldName": string; - "htv:fieldValue": string; - }[]; - op: FormOpHTTP; -} - -export type InterfacePropertyForm = InterfacePropertyFormHTTP | InterfacePropertyFormMQTT | InterfacePropertyFormModbus; - -type InterfacePropertyFormBase = { - href: string; - contentType: string; -} - -type InterfacePropertyFormHTTP = InterfacePropertyFormBase & { - methodName: string; - headers?: Record; -} - -type InterfacePropertyFormModbus = InterfacePropertyFormBase & { - function: "readCoil" | - "readDeviceIdentification" | - "readDiscreteInput" | - "readHoldingRegisters" | - "readInputRegisters" | - "writeMultipleCoils" | - "writeMultipleHoldingRegisters" | - "writeSingleCoil" | - "writeSingleHoldingRegister"; - entity?: "Coil" | "DiscreteInput" | "HoldingRegister" | "InputRegister"; - zeroBasedAddressing?: boolean; -} - -type InterfacePropertyFormMQTT = InterfacePropertyFormBase & { - retain?: boolean; - controlPacket: "publish" | "subscribe" | "unsubscribe"; - qos?: 0 | 1 | 2; -} - -/** @alpha */ -export type InterfaceAction = { - title: string; - -} +/** @beta */ +export type InterfaceAction = InterfaceActionSync | InterfaceActionAsync; type InterfaceActionBase = { - title: string; + title?: string; type: string; - const?: boolean; + forms: InterfaceActionForm[]; } -type InterfaceActionSync = { - - +type InterfaceActionSync = InterfaceActionBase & { + async: false; } type InterfaceActionAsync = { - + async: true; } -/** @alpha */ +/** @beta */ export type InterfaceEvent = { - // TODO + title?: string; + type: string; + forms: InterfaceEventForm[]; } \ No newline at end of file diff --git a/lib/src/types/common.ts b/lib/src/types/common.ts index 5b95e57..ff5a571 100644 --- a/lib/src/types/common.ts +++ b/lib/src/types/common.ts @@ -1,6 +1,6 @@ import type { types } from "@aas-core-works/aas-core3.0-typescript"; -export const availableEndpoints = ["HTTP", "MQTT", "Modbus"] as const; +export const availableEndpoints = ["HTTP", "MQTT"] as const; export type AvailableEndpoint = typeof availableEndpoints[number]; export function endpointAvailable(endpoint: string): AvailableEndpoint | null {