Restructured the AID parsing for the (hopefully) better

This commit is contained in:
Daniel Kluge
2023-12-04 16:11:43 +01:00
parent 43daad147f
commit 389a28d7e7
3 changed files with 312 additions and 122 deletions
+226 -39
View File
@@ -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;
}
}
+85 -82
View File
@@ -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<string, string>;
}
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[];
}
+1 -1
View File
@@ -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 {