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 * 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 { AvailableEndpoint, endpointAvailable } from "../types/common";
import Traverser from "../helper/traverser"; 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 * 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 if (actions === null || !aasCore.types.isSubmodelElementList(actions) || !actions.value) return null; // Mandatory
for (const element of actions.overValueOrEmpty()) { for (const element of actions.overValueOrEmpty()) {
if (!aasCore.types.isSubmodelElementCollection(element)) continue; if (!aasCore.types.isSubmodelElementCollection(element)) continue;
const parsedAction = AIDParser.parseInterfaceMetadataProperty(element, endpointProtocol); const parsedAction = AIDParser.parseInterfaceMetadataAction(element, endpointProtocol);
if (parsedAction === null) continue; if (parsedAction === null) continue;
parsed.actions.push(parsedAction); 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; return parsed as InterfaceMetadata;
} }
@@ -136,22 +146,101 @@ export default class AIDParser {
public static parseInterfaceMetadataProperty(prop: aasCore.types.SubmodelElementCollection, endpointProtocol?: AvailableEndpoint): InterfaceProperty | null { public static parseInterfaceMetadataProperty(prop: aasCore.types.SubmodelElementCollection, endpointProtocol?: AvailableEndpoint): InterfaceProperty | null {
if (!aasCore.types.isSubmodelElementCollection(prop) || !prop.value) return 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 // This does not really matter but...
/* for (const key of ["title", "observeable", "type"]) { for (const key of ["title", "observeable", "type"]) {
const prop = AASStore.findElement(prop, element => (element as any).idShort?.toLocaleLowerCase() === key.toLocaleLowerCase()); const propProp = Traverser.findElement(prop, element => (element as any).idShort?.toLocaleLowerCase() === key.toLocaleLowerCase());
if (prop === null || !aasCore.types.isProperty(prop) || !prop.value) continue; // Not mandatory if (propProp === null || !aasCore.types.isProperty(propProp) || !propProp.value) continue;
else parsed[key] = prop.value; else parsed[key] = propProp.value;
} */; };
const parsedForms: InterfacePropertyForm[] = [];
const forms = Traverser.findElement(prop, element => (element as any).idShort?.toLocaleLowerCase() === "forms"); const forms = Traverser.findElement(prop, element => (element as any).idShort?.toLocaleLowerCase() === "forms");
if (forms === null || !aasCore.types.isSubmodelElementCollection(forms) || !forms.value) return null; // Mandatory if (forms === null || !aasCore.types.isSubmodelElementList(forms) || !forms.value) return null; // Mandatory
const parsedForm = AIDParser.parseInterfaceMetadataPropertyForm(forms, endpointProtocol); for (const form of forms.overValueOrEmpty()) {
if (parsedForm === null) return null; if (!aasCore.types.isSubmodelElementCollection(form)) continue;
// TODO const parsedForm = AIDParser.parseInterfaceMetadataForm(form, "property", endpointProtocol) as InterfacePropertyForm;
// Other values based on endpoint protocol if (parsedForm === null) continue;
return { ...parsed, forms: parsedForm } as InterfaceProperty; 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 * @param endpointProtocol Protocol used in the endpoint, so additional properties can be parsed
* @returns Parsed Form or null if not parsable * @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; if (!form.value) return null;
const parsed = {} as any; 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()); 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 if (prop === null || !aasCore.types.isProperty(prop) || !prop.value) return null; // Mandatory
else parsed[key] = prop.value; else parsed[key] = prop.value;
} }
// TODO if (!AIDParser.isValidFormOperation(parsed.op, element, endpointProtocol)) return null;
// Other values based on endpoint protocol
return parsed as InterfacePropertyForm; 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;
}
}
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 * Get the default protocol method for an operation
* @param prop SubmodelElementCollection of one Action * @param op Operation, e.g. "readproperty"
* @param endpointProtocol Protocol used in the endpoint, so additional properties can be parsed * @param endpoint
* @returns Parsed Action InterfaceMetadata or null if not parsable * @returns
* @alpha
*/ */
public static parseInterfaceMetadataAction(action: aasCore.types.SubmodelElementCollection, endpointProtocol?: AvailableEndpoint): InterfaceAction | null { private static getDefaultMethod(op: string, endpoint?: AvailableEndpoint): string | undefined {
// TODO const defaults: any = {
return null; "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"
}
};
/** op = op.toLocaleLowerCase();
* Parse a single Event InterfaceMetadata
* @param prop SubmodelElementCollection of one Event return defaults[op] === undefined ? defaults[op][endpoint ?? ""] ?? null : null;
* @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;
} }
} }
+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. 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 = { export type InterfaceProperty = {
title?: string; title?: string;
observable?: boolean; observable?: boolean;
forms: InterfacePropertyForm; forms: InterfacePropertyForm[];
type?: string; type: string;
// enum?: string[]; // enum?: string[];
const?: any; const?: any;
default?: any; default?: any;
@@ -66,95 +138,26 @@ export type InterfaceProperty = {
// maxItems?: number; // maxItems?: number;
} }
type FormOp = "readproperty" | /** @beta */
"writeproperty" | export type InterfaceAction = InterfaceActionSync | InterfaceActionAsync;
"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;
}
type InterfaceActionBase = { type InterfaceActionBase = {
title: string; title?: string;
type: string; type: string;
const?: boolean; forms: InterfaceActionForm[];
} }
type InterfaceActionSync = { type InterfaceActionSync = InterfaceActionBase & {
async: false;
} }
type InterfaceActionAsync = { type InterfaceActionAsync = {
async: true;
} }
/** @alpha */ /** @beta */
export type InterfaceEvent = { 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"; 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 type AvailableEndpoint = typeof availableEndpoints[number];
export function endpointAvailable(endpoint: string): AvailableEndpoint | null { export function endpointAvailable(endpoint: string): AvailableEndpoint | null {