diff --git a/README.md b/README.md index 11f694e7..dfe1ffc0 100644 --- a/README.md +++ b/README.md @@ -247,13 +247,13 @@ Mappings are persisted as Managed Objects and can be easily changed, deleted or In addition to using plain properties of the source payload, you can apply functions on the payload properties. This covers a scenario where a device name should be a combination of a generic name and an external device Id. Complex mapping expressions are supported by using [JSONata](https://jsonata.org). \ In this case the following function could be used: -```$join([device_name, _DEVICE_IDENT_])```. +```$join([device_name, id])```. Further example for JSONata expressions are: * to convert a UNIX timestamp to ISO date format use: $fromMillis($number(deviceTimestamp)) * to join substring starting at position 5 of property txt with device - identifier use: $join([$substring(txt,5), "-", _DEVICE_IDENT_]) + identifier use: $join([$substring(txt,5), "-", id]) >**_NOTE:_** > * escape properties with special characters with `. The property @@ -398,9 +398,8 @@ Connected devices send their data using an external device identifier, e.g. IMEI #### Define templates and substitutions for source and target payload In the second wizard step, shown on the screenshot below the mapping is further defined: -1. Editing the source template directly or use a snooped template by pressing button ```<-```, arrow left -2. Editing the target template directly or use a sample template by pressing button ```->```, arrow right -3. Adding substitutions +1. Editing the source template directly +2. Editing the target template directly

@@ -408,11 +407,10 @@ In the second wizard step, shown on the screenshot below the mapping is further


-In order to define a substitution ( substitute values in the target payload with values extracted at runtime from the source payload), the UI offers the following features: -1. Add mapping (button with "+" sign) -2. Show & Select already defined substitutions (button with skip symbol). A selected substitution is colored and can be deleted by pressing the button with "-" sign -3. Delete mapping (button with one "-" sign), the selected substitution is deleted -4. Delete all mappings (button with two "--" signs). In this case the substitution to define the deviceIdentifier is automatically added again. This is the case when a template topic contains a wildcard, either "+"- single level or "#" - multi level +In order to define a substitution (a substitution substitutes values in the target payload with values extracted at runtime from the source payload), the UI offers the following feaoptionstures: +1. Add new substitution by pressing button "Add substitution". Further details for the substitution can be defined in the next modal dialog. See as well the next paragraph. +2. Update an existing substitution, by selecting the substitution in the table of substitutions in the lower section of the wizard. Then press button "Update substitution" +3. Delete an existing substitution, by pressing the button with the red minus

@@ -420,20 +418,29 @@ In order to define a substitution ( substitute values in the target payload with
To define a new substitution the following steps have to be performed: -1. Select a property in the source JSON payload by click on the respective property. Then the JSONpath is appears in the field with the label ```Evaluate expression on source``` -2. Select a property in the target JSON payload by click on the respective property. Then the JSONpath is appears in the field with the label ```Substitute in target``` -3. Select ```Expand Array``` if the result of the source expression is an array and you want to generate any of the following substitutions: - * ```multi-device-single-value``` - * ```multi-device-multi-value``` - * ```single-device-multi-value```\ +1. Select a property in the source JSON payload by click on the respective property. Then the JSONpath is appears in the field with the label ```Evaluate Expression on Source``` +1. Select a property in the target JSON payload by click on the respective property. Then the JSONpath is appears in the field with the label ```Evaluate Expression on Target``` +>**_NOTE:_** Use the same JSONata + expressions as in the source template. In addition you can use $ to merge the + result of the source expression with the existing target template. Special care is + required since this can overwrite mandatory Cumulocity attributes, e.g. source.id. This can result in API calls that are rejected by the Cumulocity backend! + +3. Press the button "Add substitution". In the next modal dialog the following details can be specified: + 1. Select option ```Expand Array``` if the result of the source expression is an array and you want to generate any of the following substitutions: + * ```multi-device-single-value``` + * ```multi-device-multi-value``` + * ```single-device-multi-value```\ Otherwise an extracted array is treated as a single value, see [Different type of substitutions](#different-type-of-substitutions). -4. Select a repair strategy that determines how the mapping is applied: - * ```DEFAULT```: Map the extracted values to the attribute addressed on right side - * ```USE_FIRST_VALUE_OF_ARRAY```: When the left side of the mapping returns an array, only use the 1. item in the array and map this to the right side - * ```USE_LAST_VALUE_OF_ARRAY```: When the left side of the mapping returns an array, only use the last item in the array and map this to the right side - * ```REMOVE_IF_MISSING```: When the left side of the mapping returns no result (not NULL), then delete the attribute (that is addressed in mapping) in the target on the right side. This avoids empty attribute, e.d. ```airsensor: undefined``` - * ```REMOVE_IF_NULL```: When the left side of the mapping returns ```null```, then delete the attribute (that is addressed in mapping) in the target on the right side. This avoids empty attribute, e.d. ```airsensor: undefined``` -5. Press the add button with the ```+``` sign, to add the substitution to the list of substitutions. + 1. Select option ```Resolve to externalId``` if you want to resolve system Cumulocity Id to externalId using externalIdType. This can onlybe used for OUTBOUND mappings. + 1. Select a ```Reapir Strategy``` that determines how the mapping is applied: + * ```DEFAULT```: Map the extracted values to the attribute addressed on right side + * ```USE_FIRST_VALUE_OF_ARRAY```: When the left side of the mapping returns an array, only use the 1. item in the array and map this to the right side + * ```USE_LAST_VALUE_OF_ARRAY```: When the left side of the mapping returns an array, only use the last item in the array and map this to the right side + * ```REMOVE_IF_MISSING```: When the left side of the mapping returns no result (not NULL), then delete the attribute (that is addressed in mapping) in the target on the right side. This avoids empty attribute, e.d. ```airsensor: undefined``` + * ```REMOVE_IF_NULL```: When the left side of the mapping returns ```null```, then delete the attribute (that is addressed in mapping) in the target on the right side. This avoids empty attribute, e.d. ```airsensor: undefined``` +

+ +


>**_NOTE:_** When adding a new substitution the following two consistency rules are checked: @@ -441,9 +448,9 @@ To define a new substitution the following steps have to be performed: >2. If the new substitution defines the device identifier, it is checked if another substitution already withe the same proprty exists. If so, a modal dialog appears and asks for confirmation to overwrite the existing substitution. -To avoid inconsistent JSON being sent to the Cumulocity API schemas are defined for all target payloads (Measurement, Event, Alarm, Inventory). The schemas validate if reqiured properties are defined and if the time is in the correct format. +To avoid inconsistent JSON being sent to the Cumulocity API the defined target tmeplate are validated with schemas. These are defined for all target payloads (Measurement, Event, Alarm, Inventory). The schemas validate if reqiured properties are defined and if the time is in the correct format. -In the sample below, e.g. a warning is shown since the required property ```c8y_IsDevice``` is missing in the payload. +In the sample below, e.g. a warning is shown since the required property ```source.id``` is missing in the payload.

diff --git a/mqtt-mapping-service/src/main/java/mqtt/mapping/core/C8YAgent.java b/mqtt-mapping-service/src/main/java/mqtt/mapping/core/C8YAgent.java index 4dd99851..e7d3d93f 100644 --- a/mqtt-mapping-service/src/main/java/mqtt/mapping/core/C8YAgent.java +++ b/mqtt-mapping-service/src/main/java/mqtt/mapping/core/C8YAgent.java @@ -463,6 +463,9 @@ public ManagedObjectRepresentation upsertDevice(ID identity, ProcessingContext { statusMapping.put(ms.ident, ms); }); + } else { + statusMapping = new HashMap(); } if (!statusMapping.containsKey(MappingStatus.IDENT_UNSPECIFIED_MAPPING)) { statusMapping.put(MappingStatus.IDENT_UNSPECIFIED_MAPPING, MappingStatus.UNSPECIFIED_MAPPING_STATUS); @@ -128,7 +130,7 @@ private void initializeMappingStatus() { public void initializeMappingComponent(MappingServiceRepresentation mappingServiceRepresentation) { this.mappingServiceRepresentation = mappingServiceRepresentation; - initializeMappingStatus(); + initializeMappingStatus(false); } public void sendStatusMapping() { @@ -144,7 +146,8 @@ public void sendStatusMapping() { updateMor.setAttrs(service); this.inventoryApi.update(updateMor); } else { - log.debug("Ignoring mapping monitoring: {}, intialized: {}", statusMapping.values().size(), intialized); + log.debug("Ignoring mapping monitoring: {}, initialized: {}", statusMapping.values().size(), + intialized); } }); } @@ -180,12 +183,6 @@ public List getMappingStatus() { return new ArrayList(statusMapping.values()); } - public List resetMappingStatus() { - ArrayList msl = new ArrayList(statusMapping.values()); - msl.forEach(ms -> ms.reset()); - return msl; - } - public void saveMappings(List mappings) { subscriptionsService.runForTenant(tenant, () -> { mappings.forEach(m -> { @@ -243,7 +240,8 @@ public List getMappings() { return result; } - public Mapping updateMapping(Mapping mapping, boolean allowUpdateWhenActive) throws Exception { + public Mapping updateMapping(Mapping mapping, boolean allowUpdateWhenActive, boolean ignoreValidation) + throws Exception { // test id the mapping is active, we don't delete or modify active mappings MutableObject exception = new MutableObject(null); Mapping result = subscriptionsService.callForTenant(tenant, () -> { @@ -255,7 +253,7 @@ public Mapping updateMapping(Mapping mapping, boolean allowUpdateWhenActive) thr // mapping is deactivated and we can delete it List mappings = getMappings(); List errors = MappingRepresentation.isMappingValid(mappings, mapping); - if (errors.size() == 0) { + if (errors.size() == 0 || ignoreValidation) { MappingRepresentation mr = new MappingRepresentation(); mapping.lastUpdate = System.currentTimeMillis(); mr.setType(MappingRepresentation.MQTT_MAPPING_TYPE); @@ -263,6 +261,7 @@ public Mapping updateMapping(Mapping mapping, boolean allowUpdateWhenActive) thr mr.setId(mapping.id); ManagedObjectRepresentation mor = toManagedObject(mr); mor.setId(GId.asGId(mapping.id)); + mor.setName(mapping.name); inventoryApi.update(mor); return mapping; } else { @@ -301,7 +300,7 @@ public Mapping createMapping(Mapping mapping) { mr.getC8yMQTTMapping().setId(mapping.id); mor = toManagedObject(mr); mor.setId(GId.asGId(mapping.id)); - + mor.setName(mapping.name); inventoryApi.update(mor); log.info("Created mapping: {}", mor); return mapping; @@ -422,12 +421,15 @@ public void setActivationMapping(String id, Boolean active) throws Exception { log.info("Setting active: {} got mapping: {}", id, active); Mapping mapping = getMapping(id); mapping.setActive(active); - if (Direction.INBOUND.equals(mapping.direction)) { + if (Direction.INBOUND.equals(mapping.direction)) { // step 2. retrieve collected snoopedTemplates mapping.setSnoopedTemplates(getCacheMappingInbound().get(id).getSnoopedTemplates()); } // step 3. update mapping in inventory - updateMapping(mapping, true); + // don't validate mapping when setting active = false, this allows to remove + // mappings that are not working + boolean ignoreValidation = !active; + updateMapping(mapping, true, ignoreValidation); // step 4. delete mapping from update cache removeDirtyMapping(mapping); // step 5. update caches @@ -446,7 +448,7 @@ public void cleanDirtyMappings() throws Exception { for (Mapping mapping : dirtyMappings) { log.info("Found mapping to be saved: {}, {}", mapping.id, mapping.snoopStatus); // no reload required - updateMapping(mapping, true); + updateMapping(mapping, true, false); } // reset dirtySet dirtyMappings = new HashSet(); diff --git a/mqtt-mapping-service/src/main/java/mqtt/mapping/model/API.java b/mqtt-mapping-service/src/main/java/mqtt/mapping/model/API.java index 7a2a8afe..6c279e4a 100644 --- a/mqtt-mapping-service/src/main/java/mqtt/mapping/model/API.java +++ b/mqtt-mapping-service/src/main/java/mqtt/mapping/model/API.java @@ -30,7 +30,7 @@ public enum API { ALARM("ALARM", "source.id", "alarms"), EVENT("EVENT", "source.id", "events"), MEASUREMENT("MEASUREMENT", "source.id", "measurements"), - INVENTORY("INVENTORY", "_DEVICE_IDENT_", "managedObjects"), + INVENTORY("INVENTORY", "id", "managedObjects"), OPERATION("OPERATION", "deviceId", "operations"), EMPTY("NN", "nn", "nn"), diff --git a/mqtt-mapping-service/src/main/java/mqtt/mapping/model/Mapping.java b/mqtt-mapping-service/src/main/java/mqtt/mapping/model/Mapping.java index f49c9602..57baed60 100644 --- a/mqtt-mapping-service/src/main/java/mqtt/mapping/model/Mapping.java +++ b/mqtt-mapping-service/src/main/java/mqtt/mapping/model/Mapping.java @@ -41,6 +41,9 @@ @ToString(exclude = { "source", "target", "snoopedTemplates" }) public class Mapping implements Serializable { + public static String TOKEN_TOPIC_LEVEL = "_TOPIC_LEVEL_"; + + public static String TIME = "time"; public static int SNOOP_TEMPLATES_MAX = 5; public static String SPLIT_TOPIC_REGEXP = "((?<=/)|(?=/))"; public static Mapping UNSPECIFIED_MAPPING; diff --git a/mqtt-mapping-service/src/main/java/mqtt/mapping/model/MappingSubstitution.java b/mqtt-mapping-service/src/main/java/mqtt/mapping/model/MappingSubstitution.java index 8f8bd5d1..af90683b 100644 --- a/mqtt-mapping-service/src/main/java/mqtt/mapping/model/MappingSubstitution.java +++ b/mqtt-mapping-service/src/main/java/mqtt/mapping/model/MappingSubstitution.java @@ -32,6 +32,8 @@ import javax.validation.constraints.NotNull; import java.io.Serializable; +import java.util.List; +import java.util.Map; @Getter @ToString() @@ -57,18 +59,26 @@ public SubstituteValue(JsonNode value, TYPE type, RepairStrategy repair) { } public Object typedValue() { - Object result; DocumentContext dc; switch (type) { case OBJECT: + Map ro = null; + if (value != null && !value.isNull()) { + dc = JsonPath.parse(value.toString()); + ro = dc.read("$"); + } else { + ro= null; + } + return ro; case ARRAY: + List> ra = null; if (value != null && !value.isNull()) { dc = JsonPath.parse(value.toString()); - result = dc.read("$"); + ra = dc.read("$"); } else { - result= value; + ra= null; } - return result; + return ra; case IGNORE: return null; case NUMBER: diff --git a/mqtt-mapping-service/src/main/java/mqtt/mapping/processor/inbound/BasePayloadProcessor.java b/mqtt-mapping-service/src/main/java/mqtt/mapping/processor/inbound/BasePayloadProcessor.java index 44be94d5..188b8b18 100644 --- a/mqtt-mapping-service/src/main/java/mqtt/mapping/processor/inbound/BasePayloadProcessor.java +++ b/mqtt-mapping-service/src/main/java/mqtt/mapping/processor/inbound/BasePayloadProcessor.java @@ -73,11 +73,6 @@ public BasePayloadProcessor(ObjectMapper objectMapper, MQTTClient mqttClient, C8 @Autowired SysHandler sysHandler; - public static String TOKEN_DEVICE_TOPIC = "_DEVICE_IDENT_"; - public static String TOKEN_TOPIC_LEVEL = "_TOPIC_LEVEL_"; - - public static final String TIME = "time"; - public abstract ProcessingContext deserializePayload(ProcessingContext context, MqttMessage mqttMessage) throws IOException; @@ -136,7 +131,7 @@ public ProcessingContext substituteInTargetAndSend(ProcessingContext conte } if (!mapping.targetAPI.equals(API.INVENTORY)) { - if (pathTarget.equals(deviceIdentifierMapped2PathTarget2)) { + if (pathTarget.equals(deviceIdentifierMapped2PathTarget2) && mapping.mapDeviceIdentifier) { ExternalIDRepresentation sourceId = c8yAgent.resolveExternalId2GlobalId( new ID(mapping.externalIdType, substituteValue.typedValue().toString()), context); @@ -162,8 +157,9 @@ public ProcessingContext substituteInTargetAndSend(ProcessingContext conte context.getCurrentRequest().setError(e); } } else if (sourceId == null && context.isSendPayload()) { - throw new RuntimeException("External id " + substituteValue.typedValue().toString() + " for type " - + mapping.externalIdType + " not found!"); + throw new RuntimeException( + "External id " + substituteValue.typedValue().toString() + " for type " + + mapping.externalIdType + " not found!"); } else if (sourceId == null) { substituteValue.value = null; } else { @@ -222,23 +218,37 @@ public ProcessingContext substituteInTargetAndSend(ProcessingContext conte public void substituteValueInObject(MappingType type, SubstituteValue sub, DocumentContext jsonObject, String keys) throws JSONException { boolean subValueMissing = sub.value == null; - boolean subValueNull = (sub.value == null) || ( sub.value != null && sub.value.isNull()); - // variant where the default strategy for PROCESSOR_EXTENSION is REMOVE_IF_MISSING - // if ((sub.repairStrategy.equals(RepairStrategy.REMOVE_IF_MISSING) && subValueMissing) || + boolean subValueNull = (sub.value == null) || (sub.value != null && sub.value.isNull()); + // variant where the default strategy for PROCESSOR_EXTENSION is + // REMOVE_IF_MISSING + // if ((sub.repairStrategy.equals(RepairStrategy.REMOVE_IF_MISSING) && + // subValueMissing) || // (sub.repairStrategy.equals(RepairStrategy.REMOVE_IF_NULL) && subValueNull) || - // ((type.equals(MappingType.PROCESSOR_EXTENSION) || type.equals(MappingType.PROTOBUF_STATIC)) - // && (subValueMissing || subValueNull))) - if ((sub.repairStrategy.equals(RepairStrategy.REMOVE_IF_MISSING) && subValueMissing) || - (sub.repairStrategy.equals(RepairStrategy.REMOVE_IF_NULL) && subValueNull)) { - jsonObject.delete(keys); - } else if (sub.repairStrategy.equals(RepairStrategy.CREATE_IF_MISSING) ) { - boolean pathIsNested = keys.contains(".") || keys.contains("[") ; - if (pathIsNested) { - throw new JSONException ("Can only create new nodes ion the root level!"); + // ((type.equals(MappingType.PROCESSOR_EXTENSION) || + // type.equals(MappingType.PROTOBUF_STATIC)) + // && (subValueMissing || subValueNull))) + + if ("$".equals(keys)) { + Object replacement = sub.typedValue(); + if (replacement instanceof Map) { + Map rm = (Map) replacement; + for (Map.Entry entry : rm.entrySet()) { + jsonObject.put("$",entry.getKey(), entry.getValue()); + } } - jsonObject.put("$", keys, sub.typedValue()); } else { - jsonObject.set(keys, sub.typedValue()); + if ((sub.repairStrategy.equals(RepairStrategy.REMOVE_IF_MISSING) && subValueMissing) || + (sub.repairStrategy.equals(RepairStrategy.REMOVE_IF_NULL) && subValueNull)) { + jsonObject.delete(keys); + } else if (sub.repairStrategy.equals(RepairStrategy.CREATE_IF_MISSING)) { + boolean pathIsNested = keys.contains(".") || keys.contains("["); + if (pathIsNested) { + throw new JSONException("Can only create new nodes ion the root level!"); + } + jsonObject.put("$", keys, sub.typedValue()); + } else { + jsonObject.set(keys, sub.typedValue()); + } } } diff --git a/mqtt-mapping-service/src/main/java/mqtt/mapping/processor/inbound/JSONProcessor.java b/mqtt-mapping-service/src/main/java/mqtt/mapping/processor/inbound/JSONProcessor.java index 4d0c9c40..7540daf2 100644 --- a/mqtt-mapping-service/src/main/java/mqtt/mapping/processor/inbound/JSONProcessor.java +++ b/mqtt-mapping-service/src/main/java/mqtt/mapping/processor/inbound/JSONProcessor.java @@ -54,7 +54,7 @@ @Service public class JSONProcessor extends BasePayloadProcessor { - public JSONProcessor ( ObjectMapper objectMapper, MQTTClient mqttClient, C8YAgent c8yAgent){ + public JSONProcessor(ObjectMapper objectMapper, MQTTClient mqttClient, C8YAgent c8yAgent) { super(objectMapper, mqttClient, c8yAgent); } @@ -81,11 +81,11 @@ public void extractFromSource(ProcessingContext context) List splitTopicAsList = Mapping.splitTopicExcludingSeparatorAsList(context.getTopic()); splitTopicAsList.forEach(s -> topicLevels.add(s)); if (payloadJsonNode instanceof ObjectNode) { - ((ObjectNode) payloadJsonNode).set(TOKEN_TOPIC_LEVEL, topicLevels); + ((ObjectNode) payloadJsonNode).set(Mapping.TOKEN_TOPIC_LEVEL, topicLevels); } else { log.warn("Parsing this message as JSONArray, no elements from the topic level can be used!"); } - String payload = payloadJsonNode.toPrettyString(); + String payload = payloadJsonNode.toPrettyString(); log.info("Patched payload: {}", payload); boolean substitutionTimeExists = false; @@ -127,18 +127,25 @@ public void extractFromSource(ProcessingContext context) } else if (jn.isNumber()) { postProcessingCacheEntry .add(new SubstituteValue(jn, TYPE.NUMBER, substitution.repairStrategy)); + } else if (jn.isArray()) { + postProcessingCacheEntry + .add(new SubstituteValue(jn, TYPE.ARRAY, substitution.repairStrategy)); } else { - log.warn("Since result is not textual or number it is ignored: {}", - jn.asText()); + // log.warn("Since result is not textual or number it is ignored: {}", + // jn.asText()); + postProcessingCacheEntry + .add(new SubstituteValue(jn, TYPE.OBJECT, substitution.repairStrategy)); } } context.addCardinality(substitution.pathTarget, extractedSourceContent.size()); postProcessingCache.put(substitution.pathTarget, postProcessingCacheEntry); } else { - // treat this extracted enry as single value, no MULTI_VALUE or MULTI_DEVICE substitution + // treat this extracted enry as single value, no MULTI_VALUE or MULTI_DEVICE + // substitution context.addCardinality(substitution.pathTarget, 1); postProcessingCacheEntry - .add(new SubstituteValue(extractedSourceContent, TYPE.ARRAY, substitution.repairStrategy)); + .add(new SubstituteValue(extractedSourceContent, TYPE.ARRAY, + substitution.repairStrategy)); postProcessingCache.put(substitution.pathTarget, postProcessingCacheEntry); } } else if (extractedSourceContent.isTextual()) { @@ -165,18 +172,18 @@ public void extractFromSource(ProcessingContext context) } } - if (substitution.pathTarget.equals(TIME)) { + if (substitution.pathTarget.equals(Mapping.TIME)) { substitutionTimeExists = true; } } // no substitution for the time property exists, then use the system time if (!substitutionTimeExists && mapping.targetAPI != API.INVENTORY && mapping.targetAPI != API.OPERATION) { - List postProcessingCacheEntry = postProcessingCache.getOrDefault(TIME, + List postProcessingCacheEntry = postProcessingCache.getOrDefault(Mapping.TIME, new ArrayList()); postProcessingCacheEntry.add( new SubstituteValue(new TextNode(new DateTime().toString()), TYPE.TEXTUAL, RepairStrategy.DEFAULT)); - postProcessingCache.put(TIME, postProcessingCacheEntry); + postProcessingCache.put(Mapping.TIME, postProcessingCacheEntry); } } diff --git a/mqtt-mapping-service/src/main/java/mqtt/mapping/processor/outbound/BasePayloadProcessorOutbound.java b/mqtt-mapping-service/src/main/java/mqtt/mapping/processor/outbound/BasePayloadProcessorOutbound.java index 7bea3629..8a3a9972 100644 --- a/mqtt-mapping-service/src/main/java/mqtt/mapping/processor/outbound/BasePayloadProcessorOutbound.java +++ b/mqtt-mapping-service/src/main/java/mqtt/mapping/processor/outbound/BasePayloadProcessorOutbound.java @@ -21,9 +21,7 @@ package mqtt.mapping.processor.outbound; -import com.cumulocity.model.idtype.GId; import com.cumulocity.rest.representation.AbstractExtensibleRepresentation; -import com.cumulocity.rest.representation.identity.ExternalIDRepresentation; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.TextNode; import com.jayway.jsonpath.DocumentContext; @@ -72,9 +70,6 @@ public BasePayloadProcessorOutbound(ObjectMapper objectMapper, MQTTClient mqttCl @Autowired SysHandler sysHandler; - public static String TOKEN_DEVICE_TOPIC = "_DEVICE_IDENT_"; - public static String TOKEN_TOPIC_LEVEL = "_TOPIC_LEVEL_"; - public static final String TIME = "time"; public abstract ProcessingContext deserializePayload(ProcessingContext context, C8YMessage c8yMessage) @@ -99,7 +94,7 @@ public ProcessingContext substituteInTargetAndSend(ProcessingContext conte * is required in the payload for a substitution */ List splitTopicExAsList = Mapping.splitTopicExcludingSeparatorAsList(context.getTopic()); - payloadTarget.set(TOKEN_TOPIC_LEVEL, splitTopicExAsList); + payloadTarget.set(Mapping.TOKEN_TOPIC_LEVEL, splitTopicExAsList); String deviceSource = "undefined"; @@ -115,7 +110,7 @@ public ProcessingContext substituteInTargetAndSend(ProcessingContext conte * step 4 prepare target payload for sending to mqttBroker */ if (!mapping.targetAPI.equals(API.INVENTORY)) { - List topicLevels = payloadTarget.read(TOKEN_TOPIC_LEVEL); + List topicLevels = payloadTarget.read(Mapping.TOKEN_TOPIC_LEVEL); if (topicLevels != null && topicLevels.size() > 0) { // now merge the replaced topic levels MutableInt c = new MutableInt(0); @@ -139,7 +134,7 @@ public ProcessingContext substituteInTargetAndSend(ProcessingContext conte } AbstractExtensibleRepresentation attocRequest = null; // remove TOPIC_LEVEL - payloadTarget.delete(TOKEN_TOPIC_LEVEL); + payloadTarget.delete(Mapping.TOKEN_TOPIC_LEVEL); var newPredecessor = context.addRequest( new C8YRequest(predecessor, RequestMethod.POST, deviceSource, mapping.externalIdType, payloadTarget.jsonString(), diff --git a/mqtt-mapping-service/src/main/java/mqtt/mapping/rest/MQTTMappingRestController.java b/mqtt-mapping-service/src/main/java/mqtt/mapping/rest/MQTTMappingRestController.java index 281cf562..310c361b 100644 --- a/mqtt-mapping-service/src/main/java/mqtt/mapping/rest/MQTTMappingRestController.java +++ b/mqtt-mapping-service/src/main/java/mqtt/mapping/rest/MQTTMappingRestController.java @@ -204,7 +204,7 @@ public ResponseEntity runOperation(@Valid @RequestBody ServiceOperat } else if (operation.getOperation().equals(Operation.REFRESH_STATUS_MAPPING)) { mappingComponent.sendStatusMapping(); } else if (operation.getOperation().equals(Operation.RESET_STATUS_MAPPING)) { - mappingComponent.resetMappingStatus(); + mappingComponent.initializeMappingStatus(true); } else if (operation.getOperation().equals(Operation.RELOAD_EXTENSIONS)) { c8yAgent.reloadExtensions(); } else if (operation.getOperation().equals(Operation.ACTIVATE_MAPPING)) { @@ -319,7 +319,7 @@ else if (ex instanceof JsonProcessingException) public ResponseEntity updateMapping(@PathVariable String id, @Valid @RequestBody Mapping mapping) { try { log.info("Update mapping: {}, {}", mapping, id); - mapping = mappingComponent.updateMapping(mapping, false); + mapping = mappingComponent.updateMapping(mapping, false, false); if (Direction.OUTBOUND.equals(mapping.direction)) { mappingComponent.rebuildMappingOutboundCache(); } else { diff --git a/mqtt-mapping-ui/mqtt-mapping/package.json b/mqtt-mapping-ui/mqtt-mapping/package.json index 6c324c3b..d08cef2e 100644 --- a/mqtt-mapping-ui/mqtt-mapping/package.json +++ b/mqtt-mapping-ui/mqtt-mapping/package.json @@ -1,6 +1,6 @@ { "name": "mqtt-mapping", - "version": "3.3.1", + "version": "3.3.2", "description": "Cumulocity plugin to map custom JSON payloads to C8Y payloads.The lugin support both directions: inbound/outbound.", "repository": { "type": "git", diff --git a/mqtt-mapping-ui/mqtt-mapping/src/mqtt-configuration/broker-configuration.component.html b/mqtt-mapping-ui/mqtt-mapping/src/mqtt-configuration/broker-configuration.component.html index 06bbe532..440e92f5 100644 --- a/mqtt-mapping-ui/mqtt-mapping/src/mqtt-configuration/broker-configuration.component.html +++ b/mqtt-mapping-ui/mqtt-mapping/src/mqtt-configuration/broker-configuration.component.html @@ -367,4 +367,28 @@

+
+
+
+

+ {{ "Mapping Monitoring" | translate }} +

+
+
+ +
+
+
diff --git a/mqtt-mapping-ui/mqtt-mapping/src/mqtt-configuration/broker-configuration.component.ts b/mqtt-mapping-ui/mqtt-mapping/src/mqtt-configuration/broker-configuration.component.ts index fa18cd87..b8e80188 100644 --- a/mqtt-mapping-ui/mqtt-mapping/src/mqtt-configuration/broker-configuration.component.ts +++ b/mqtt-mapping-ui/mqtt-mapping/src/mqtt-configuration/broker-configuration.component.ts @@ -228,17 +228,28 @@ export class BokerConfigurationComponent implements OnInit { } private async disconnectFromMQTT() { - const response = await this.configurationService.runOperation( + const res = await this.configurationService.runOperation( Operation.DISCONNECT ); - console.log("Details disconnectFromMQTT", response); - if (response.status < 300) { + console.log("Details disconnectFromMQTT", res); + if (res.status < 300) { this.alertservice.success(gettext("Successfully disconnected")); } else { this.alertservice.danger(gettext("Failed to disconnect")); } } + public async resetMonitoring() { + const res = await this.configurationService.runOperation( + Operation.RESET_STATUS_MAPPING + ); + if (res.status < 300) { + this.alertservice.success(gettext("Successfully rreset")); + } else { + this.alertservice.danger(gettext("Failed to rest statistic.")); + } + } + ngOnDestroy(): void { console.log("Stop subscription"); this.configurationService.unsubscribeFromMonitoringChannel( diff --git a/mqtt-mapping-ui/mqtt-mapping/src/mqtt-configuration/broker-configuration.service.ts b/mqtt-mapping-ui/mqtt-mapping/src/mqtt-configuration/broker-configuration.service.ts index 1622539d..a357eb38 100644 --- a/mqtt-mapping-ui/mqtt-mapping/src/mqtt-configuration/broker-configuration.service.ts +++ b/mqtt-mapping-ui/mqtt-mapping/src/mqtt-configuration/broker-configuration.service.ts @@ -58,6 +58,7 @@ export class BrokerConfigurationService { status: Status.NOT_READY, }); private _currentServiceStatus = this.serviceStatus.asObservable(); + private _feature: Feature; private realtime: Realtime; async initializeMQTTAgent(): Promise { @@ -158,14 +159,16 @@ export class BrokerConfigurationService { } async getFeatures(): Promise { - const response = await this.client.fetch( - `${BASE_URL}/${PATH_FEATURE_ENDPOINT}`, - { - method: "GET", - } - ); - const result = await response.json(); - return result; + if (!this._feature) { + const response = await this.client.fetch( + `${BASE_URL}/${PATH_FEATURE_ENDPOINT}`, + { + method: "GET", + } + ); + this._feature = await response.json(); + } + return this._feature; } async subscribeMonitoringChannel(): Promise { @@ -180,8 +183,8 @@ export class BrokerConfigurationService { ); } - unsubscribeFromMonitoringChannel(subscription: object): object { - return this.realtime.unsubscribe(subscription); + unsubscribeFromMonitoringChannel(subscription: object) { + this.realtime.unsubscribe(subscription); } private updateStatus(p: object): void { diff --git a/mqtt-mapping-ui/mqtt-mapping/src/mqtt-mapping/core/c8y-agent.service.ts b/mqtt-mapping-ui/mqtt-mapping/src/mqtt-mapping/core/c8y-agent.service.ts index 301cdd82..c5183c36 100644 --- a/mqtt-mapping-ui/mqtt-mapping/src/mqtt-mapping/core/c8y-agent.service.ts +++ b/mqtt-mapping-ui/mqtt-mapping/src/mqtt-mapping/core/c8y-agent.service.ts @@ -124,7 +124,15 @@ export class C8YAgent { identity: IExternalIdentity, context: ProcessingContext ): Promise { - let deviceId: string = await this.resolveExternalId2GlobalId(identity, context); + let deviceId: string; + try { + deviceId = await this.resolveExternalId2GlobalId(identity, context); + } catch (e) { + console.log( + `External id ${identity.externalId} doesn't exist! Just return original id ${identity.externalId} ` + ); + } + let currentRequest = context.requests[context.requests.length - 1].request; let device: Partial = { ...currentRequest, @@ -132,14 +140,17 @@ export class C8YAgent { c8y_mqttMapping_TestDevice: {}, com_cumulocity_model_Agent: {}, }; - + // remove device identifier + if (deviceId) { + device.id = deviceId; const response: IResult = await this.inventory.update( device, context - ); - return response.data; - } else { + ); + return response.data; + } else { + delete device[API.INVENTORY.identifier]; const response: IResult = await this.inventory.create( device, context @@ -160,13 +171,11 @@ export class C8YAgent { identity: IExternalIdentity, context: ProcessingContext ): Promise { - try { - const { data, res } = await this.identity.resolveExternalId2GlobalId(identity, context); - return data.managedObject.id as string; - } catch (e) { - console.log(`External id ${identity.externalId} doesn't exist! Just return original id ${identity.externalId} `); - return identity.externalId; - } + const { data, res } = await this.identity.resolveExternalId2GlobalId( + identity, + context + ); + return data.managedObject.id as string; } async resolveGlobalId2ExternalId( @@ -174,12 +183,11 @@ export class C8YAgent { externalIdType: string, context: ProcessingContext ): Promise { - try { - const data = await this.identity.resolveGlobalId2ExternalId(identity, externalIdType, context); - return data.managedObject.id as string; - } catch (e) { - console.log(`External id ${identity}, ${externalIdType} doesn't exist! Just return original id ${identity}`); - return identity; - } + const data = await this.identity.resolveGlobalId2ExternalId( + identity, + externalIdType, + context + ); + return data.managedObject.id as string; } } diff --git a/mqtt-mapping-ui/mqtt-mapping/src/mqtt-mapping/edit/edit-substitution-modal.component.ts b/mqtt-mapping-ui/mqtt-mapping/src/mqtt-mapping/edit/edit-substitution-modal.component.ts index fec4e30b..3989db4c 100644 --- a/mqtt-mapping-ui/mqtt-mapping/src/mqtt-mapping/edit/edit-substitution-modal.component.ts +++ b/mqtt-mapping-ui/mqtt-mapping/src/mqtt-mapping/edit/edit-substitution-modal.component.ts @@ -156,7 +156,7 @@ import { definesDeviceIdentifier } from "../../shared/util"; @@ -392,6 +391,7 @@ editorTestingRequestTemplateEmitter " [mapping]="getCurrentMapping(true)" + (testResult)="updateTestResult($event)" [stepperConfiguration]="stepperConfiguration" > JSONata in your expressions:
    @@ -220,7 +222,7 @@ export class MappingStepperComponent implements OnInit {
  1. to join substring starting at position 5 of property txt with device - identifier use: $join([$substring(txt,5), "-", _DEVICE_IDENT_])
  2. + identifier use: $join([$substring(txt,5), "-", id])
  3. function chaining using ~ is not supported, instead use function notation. The expression Account.Product.(Price * Quantity) ~> $sum() becomes $sum(Account.Product.(Price * Quantity))
  4. @@ -235,7 +237,6 @@ export class MappingStepperComponent implements OnInit { }, expressionProperties: { "templateOptions.class": (model) => { - //console.log("Logging class:", t) if ( model.currentSubstitution.pathSource == "" && model.stepperConfiguration.allowDefiningSubstitutions @@ -270,6 +271,10 @@ export class MappingStepperComponent implements OnInit { .value ); }, + description: `Use the same JSONata + expressions as in the source template. In addition you can use $ to merge the + result of the source expression with the existing target template. Special care is + required since this can overwrite mandatory Cumulocity attributes, e.g. source.id. This can result in API calls that are rejected by the Cumulocity backend!`, required: false, }, expressionProperties: { @@ -336,7 +341,7 @@ export class MappingStepperComponent implements OnInit { className: "col-lg-5 col-lg-offset-1 text-monospace font-smaller column-right-border", key: "currentSubstitution.sourceExpression.result", - type: "input-custom", + type: "textarea-custom", wrappers: ["custom-form-field"], templateOptions: { class: "input-sm", @@ -344,15 +349,18 @@ export class MappingStepperComponent implements OnInit { readonly: true, }, expressionProperties: { - "templateOptions.label": (label) => + "templateOptions.label": (model) => `Result Type [${this.templateModel.currentSubstitution.sourceExpression.resultType}]`, + "templateOptions.value": (model) => { + return `${this.templateModel.currentSubstitution.sourceExpression.result}`; + }, }, }, { className: "col-lg-5 text-monospace font-smaller column-left-border", key: "currentSubstitution.targetExpression.result", - type: "input-custom", + type: "textarea-custom", wrappers: ["custom-form-field"], templateOptions: { class: "input-sm", @@ -360,8 +368,11 @@ export class MappingStepperComponent implements OnInit { readonly: true, }, expressionProperties: { - "templateOptions.label": (label) => + "templateOptions.label": (model) => `Result Type [${this.templateModel.currentSubstitution.targetExpression.resultType}]`, + "templateOptions.value": (model) => { + return `${this.templateModel.currentSubstitution.targetExpression.result}`; + }, }, }, ], @@ -515,7 +526,7 @@ export class MappingStepperComponent implements OnInit { const definesDI = definesDeviceIdentifier( this.mapping.targetAPI, - this.templateModel, + this.templateModel.currentSubstitution, this.mapping.direction ); if (definesDI) { @@ -526,6 +537,11 @@ export class MappingStepperComponent implements OnInit { ` defined s in the previous step.`; this.templateModel.currentSubstitution.targetExpression.severity = "text-info"; + } else if (path == "$") { + this.templateModel.currentSubstitution.targetExpression.msgTxt = `By specifying "$" you selected the root of the target + template and this rersults in merging the source expression with the target template.`; + this.templateModel.currentSubstitution.targetExpression.severity = + "text-warning"; } } catch (error) { console.log("Error evaluating target expression: ", error); @@ -543,11 +559,11 @@ export class MappingStepperComponent implements OnInit { public getCurrentMapping(patched: boolean): Mapping { return { ...this.mapping, - source: this.reduceSourceTemplate( + source: reduceSourceTemplate( this.editorSource ? this.editorSource.get() : {}, patched - ), //remove dummy field "_DEVICE_IDENT_", array "_TOPIC_LEVEL_" since it should not be stored - target: this.reduceTargetTemplate(this.editorTarget?.get(), patched), //remove dummy field "_DEVICE_IDENT_", since it should not be stored + ), //remove array "_TOPIC_LEVEL_" since it should not be stored + target: reduceTargetTemplate(this.editorTarget?.get(), patched), //remove pachted attributes, since it should not be stored lastUpdate: Date.now(), }; } @@ -558,15 +574,17 @@ export class MappingStepperComponent implements OnInit { async onSampleTargetTemplatesButton() { if (this.stepperConfiguration.direction == Direction.INBOUND) { - this.templateTarget = this.expandC8YTemplate( - JSON.parse(SAMPLE_TEMPLATES_C8Y[this.mapping.targetAPI]) + this.templateTarget = expandC8YTemplate( + JSON.parse(SAMPLE_TEMPLATES_C8Y[this.mapping.targetAPI]), + this.mapping ); } else { let levels: String[] = splitTopicExcludingSeparator( this.mapping.templateTopicSample ); - this.templateTarget = this.expandExternalTemplate( + this.templateTarget = expandExternalTemplate( JSON.parse(getExternalTemplate(this.mapping)), + this.mapping, levels ); } @@ -660,13 +678,15 @@ export class MappingStepperComponent implements OnInit { this.mapping.templateTopicSample ); if (this.stepperConfiguration.direction == Direction.INBOUND) { - this.templateSource = this.expandExternalTemplate( + this.templateSource = expandExternalTemplate( this.templateSource, + this.mapping, levels ); } else { - this.templateSource = this.expandC8YTemplate( - this.templateSource + this.templateSource = expandC8YTemplate( + this.templateSource, + this.mapping ); } this.onSampleTargetTemplatesButton(); @@ -714,19 +734,23 @@ export class MappingStepperComponent implements OnInit { if (this.stepperConfiguration.editorMode == EditorMode.CREATE) { if (this.stepperConfiguration.direction == Direction.INBOUND) { - this.templateSource = this.expandExternalTemplate( + this.templateSource = expandExternalTemplate( JSON.parse(getExternalTemplate(this.mapping)), + this.mapping, levels ); - this.templateTarget = this.expandC8YTemplate( - JSON.parse(SAMPLE_TEMPLATES_C8Y[this.mapping.targetAPI]) + this.templateTarget = expandC8YTemplate( + JSON.parse(SAMPLE_TEMPLATES_C8Y[this.mapping.targetAPI]), + this.mapping ); } else { - this.templateSource = this.expandC8YTemplate( - JSON.parse(SAMPLE_TEMPLATES_C8Y[this.mapping.targetAPI]) + this.templateSource = expandC8YTemplate( + JSON.parse(SAMPLE_TEMPLATES_C8Y[this.mapping.targetAPI]), + this.mapping ); - this.templateTarget = this.expandExternalTemplate( + this.templateTarget = expandExternalTemplate( JSON.parse(getExternalTemplate(this.mapping)), + this.mapping, levels ); } @@ -737,19 +761,23 @@ export class MappingStepperComponent implements OnInit { ); } else { if (this.stepperConfiguration.direction == Direction.INBOUND) { - this.templateSource = this.expandExternalTemplate( + this.templateSource = expandExternalTemplate( JSON.parse(this.mapping.source), + this.mapping, levels ); - this.templateTarget = this.expandC8YTemplate( - JSON.parse(this.mapping.target) + this.templateTarget = expandC8YTemplate( + JSON.parse(this.mapping.target), + this.mapping ); } else { - this.templateSource = this.expandC8YTemplate( - JSON.parse(this.mapping.source) + this.templateSource = expandC8YTemplate( + JSON.parse(this.mapping.source), + this.mapping ); - this.templateTarget = this.expandExternalTemplate( + this.templateTarget = expandExternalTemplate( JSON.parse(this.mapping.target), + this.mapping, levels ); } @@ -774,12 +802,16 @@ export class MappingStepperComponent implements OnInit { ); } if (this.stepperConfiguration.direction == Direction.INBOUND) { - this.templateSource = this.expandExternalTemplate( + this.templateSource = expandExternalTemplate( this.templateSource, + this.mapping, splitTopicExcludingSeparator(this.mapping.templateTopicSample) ); } else { - this.templateSource = this.expandC8YTemplate(this.templateSource); + this.templateSource = expandC8YTemplate( + this.templateSource, + this.mapping + ); } this.mapping.snoopStatus = SnoopStatus.STOPPED; this.snoopedTemplateCounter++; @@ -789,6 +821,10 @@ export class MappingStepperComponent implements OnInit { this.templateTarget = templateTarget; } + async updateTestResult(result) { + this.mapping.tested = result; + } + public onAddSubstitution() { if (this.isSubstitutionValid()) { this.templateModel.currentSubstitution.expandArray = false; @@ -901,40 +937,6 @@ export class MappingStepperComponent implements OnInit { } } - private expandExternalTemplate(t: object, levels: String[]): object { - if (Array.isArray(t)) { - return t; - } else { - return { - ...t, - _TOPIC_LEVEL_: levels, - }; - } - } - - private expandC8YTemplate(t: object): object { - if (this.mapping.targetAPI == API.INVENTORY.name) { - return { - ...t, - _DEVICE_IDENT_: "909090", - }; - } else { - return t; - } - } - - private reduceSourceTemplate(t: object, patched: boolean): string { - if (!patched) delete t[TOKEN_TOPIC_LEVEL]; - let tt = JSON.stringify(t); - return tt; - } - - private reduceTargetTemplate(t: object, patched: boolean): string { - if (!patched) delete t[TOKEN_DEVICE_TOPIC]; - let tt = JSON.stringify(t); - return tt; - } - public onTemplateChanged(templateTarget: any): void { this.editorTarget.set(templateTarget); } diff --git a/mqtt-mapping-ui/mqtt-mapping/src/mqtt-mapping/step-three/mapping-testing.component.ts b/mqtt-mapping-ui/mqtt-mapping/src/mqtt-mapping/step-three/mapping-testing.component.ts index 0610b6d7..50afdb5b 100644 --- a/mqtt-mapping-ui/mqtt-mapping/src/mqtt-mapping/step-three/mapping-testing.component.ts +++ b/mqtt-mapping-ui/mqtt-mapping/src/mqtt-mapping/step-three/mapping-testing.component.ts @@ -24,6 +24,7 @@ import { EventEmitter, Input, OnInit, + Output, ViewChild, ViewEncapsulation, } from "@angular/core"; @@ -46,6 +47,7 @@ import { StepperConfiguration } from "../step-main/stepper-model"; }) export class MappingStepTestingComponent implements OnInit { @Input() mapping: Mapping; + @Output() testResult: EventEmitter = new EventEmitter(); @Input() stepperConfiguration: StepperConfiguration; @Input() editorTestingRequestTemplateEmitter: EventEmitter; @@ -134,7 +136,13 @@ export class MappingStepTestingComponent implements OnInit { false ); this.testingModel.results = testProcessingContext.requests; - if (testProcessingContext.errors.length > 0) { + const errors = []; + testProcessingContext.requests?.forEach((r) => { + if (r?.error) { + errors.push(r?.error); + } + }); + if (testProcessingContext.errors.length > 0 || errors.length > 0) { this.alertService.warning("Test tranformation was not successful!"); testProcessingContext.errors.forEach((msg) => { this.alertService.danger(msg); @@ -151,15 +159,23 @@ export class MappingStepTestingComponent implements OnInit { true ); this.testingModel.results = testProcessingContext.requests; - if (testProcessingContext.errors.length > 0) { + const errors = []; + testProcessingContext.requests?.forEach((r) => { + if (r?.error) { + errors.push(r?.error); + } + }); + if (testProcessingContext.errors.length > 0 || errors.length > 0) { this.alertService.warning("Test tranformation was not successful!"); testProcessingContext.errors.forEach((msg) => { this.alertService.danger(msg); }); + this.testResult.emit(false); } else { this.alertService.info( `Sending tranformation was successful: ${testProcessingContext.requests[0].response.id}` ); + this.testResult.emit(true); //console.log("RES", testProcessingContext.requests[0].response); } this.onNextTestResult(); diff --git a/mqtt-mapping-ui/mqtt-mapping/src/mqtt-monitoring/shared/monitoring.service.ts b/mqtt-mapping-ui/mqtt-mapping/src/mqtt-monitoring/shared/monitoring.service.ts index f2574990..03271067 100644 --- a/mqtt-mapping-ui/mqtt-mapping/src/mqtt-monitoring/shared/monitoring.service.ts +++ b/mqtt-mapping-ui/mqtt-mapping/src/mqtt-monitoring/shared/monitoring.service.ts @@ -55,8 +55,8 @@ export class MonitoringService { ); } - unsubscribeFromMonitoringChannel(subscription: object): object { - return this.realtime.unsubscribe(subscription); + unsubscribeFromMonitoringChannel(subscription: object) { + this.realtime.unsubscribe(subscription); } private updateStatus(p: object): void { diff --git a/mqtt-mapping-ui/mqtt-mapping/src/mqtt-testing-devices/testing.module.ts b/mqtt-mapping-ui/mqtt-mapping/src/mqtt-testing-devices/testing.module.ts index 3a8279aa..589329a5 100644 --- a/mqtt-mapping-ui/mqtt-mapping/src/mqtt-testing-devices/testing.module.ts +++ b/mqtt-mapping-ui/mqtt-mapping/src/mqtt-testing-devices/testing.module.ts @@ -43,6 +43,8 @@ import { TypeHeaderCellRendererComponent } from "./grid/type-data-grid-column/ty providers: [ hookRoute({ path: "sag-ps-pkg-mqtt-mapping/testing", + component: TestingComponent, + }), ], }) diff --git a/mqtt-mapping-ui/mqtt-mapping/src/shared/mapping.model.ts b/mqtt-mapping-ui/mqtt-mapping/src/shared/mapping.model.ts index d5ea5c96..c9479ab5 100644 --- a/mqtt-mapping-ui/mqtt-mapping/src/shared/mapping.model.ts +++ b/mqtt-mapping-ui/mqtt-mapping/src/shared/mapping.model.ts @@ -153,7 +153,7 @@ export const API = { }, INVENTORY: { name: "INVENTORY", - identifier: "_DEVICE_IDENT_", + identifier: "id", notificationFilter: "managedObjects", }, OPERATION: { diff --git a/mqtt-mapping-ui/mqtt-mapping/src/shared/util.ts b/mqtt-mapping-ui/mqtt-mapping/src/shared/util.ts index 5de9bce7..9f1074e0 100644 --- a/mqtt-mapping-ui/mqtt-mapping/src/shared/util.ts +++ b/mqtt-mapping-ui/mqtt-mapping/src/shared/util.ts @@ -95,7 +95,8 @@ export const SAMPLE_TEMPLATES_EXTERNAL = { }`, INVENTORY: `{ \"name\": \"Vibration Sensor\", - \"type\": \"maker_Vibration_Sensor\" + \"type\": \"maker_Vibration_Sensor\", + \"id\": \"909090\" }`, OPERATION: `{ \"deviceId\": \"909090\", @@ -237,7 +238,7 @@ export const SCHEMA_INVENTORY = { $id: "http://example.com/root.json", type: "object", title: "INVENTORY", - required: ["c8y_IsDevice", "type", "name"], + required: ["c8y_IsDevice", "type", "name", "id"], properties: { c8y_IsDevice: { $id: "#/properties/c8y_IsDevice", @@ -255,6 +256,11 @@ export const SCHEMA_INVENTORY = { type: "string", title: "Name of the device.", }, + id: { + $id: "#/properties/id", + type: "string", + title: "Cumulocity id of the device.", + }, }, }; @@ -289,7 +295,6 @@ export const SCHEMA_PAYLOAD = { required: [], }; -export const TOKEN_DEVICE_TOPIC = "_DEVICE_IDENT_"; export const TOKEN_TOPIC_LEVEL = "_TOPIC_LEVEL_"; export const TIME = "time"; @@ -904,3 +909,40 @@ export function cloneSubstitution( resolve2ExternalId: sub.resolve2ExternalId, }; } + +export function expandExternalTemplate( + t: object, + m: Mapping, + levels: String[] +): object { + if (Array.isArray(t)) { + return t; + } else { + return { + ...t, + _TOPIC_LEVEL_: levels, + }; + } +} + +export function expandC8YTemplate(t: object, m: Mapping): object { + if (m.targetAPI == API.INVENTORY.name) { + return { + ...t, + id: "909090", + }; + } else { + return t; + } +} + +export function reduceSourceTemplate(t: object, patched: boolean): string { + if (!patched) delete t[TOKEN_TOPIC_LEVEL]; + let tt = JSON.stringify(t); + return tt; +} + +export function reduceTargetTemplate(t: object, patched: boolean): string { + let tt = JSON.stringify(t); + return tt; +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 102461aa..f354c0d7 100644 --- a/pom.xml +++ b/pom.xml @@ -38,7 +38,7 @@ UTF-8 - 3.3.1 + 3.3.2 1018.0.220 1.18.24 11 diff --git a/resources/image/Generic_MQTT_MappingTemplate_EditModal.png b/resources/image/Generic_MQTT_MappingTemplate_EditModal.png new file mode 100644 index 00000000..b525276e Binary files /dev/null and b/resources/image/Generic_MQTT_MappingTemplate_EditModal.png differ diff --git a/resources/script/mapping/sampleMapping/SampleMappings_08.pdf b/resources/script/mapping/sampleMapping/SampleMappings_08.pdf deleted file mode 100644 index 4a7272a6..00000000 Binary files a/resources/script/mapping/sampleMapping/SampleMappings_08.pdf and /dev/null differ diff --git a/resources/script/mapping/sampleMapping/SampleMappings_08.xlsx b/resources/script/mapping/sampleMapping/SampleMappings_08.xlsx deleted file mode 100644 index 4bb3056e..00000000 Binary files a/resources/script/mapping/sampleMapping/SampleMappings_08.xlsx and /dev/null differ diff --git a/resources/script/mapping/sampleMapping/SampleMappings_09.pdf b/resources/script/mapping/sampleMapping/SampleMappings_09.pdf new file mode 100644 index 00000000..5685cb98 Binary files /dev/null and b/resources/script/mapping/sampleMapping/SampleMappings_09.pdf differ diff --git a/resources/script/mapping/sampleMapping/SampleMappings_09.xlsx b/resources/script/mapping/sampleMapping/SampleMappings_09.xlsx new file mode 100644 index 00000000..069869cb Binary files /dev/null and b/resources/script/mapping/sampleMapping/SampleMappings_09.xlsx differ diff --git a/resources/script/mapping/sampleMapping/sampleMappings_08.json b/resources/script/mapping/sampleMapping/sampleMappings_09.json similarity index 86% rename from resources/script/mapping/sampleMapping/sampleMappings_08.json rename to resources/script/mapping/sampleMapping/sampleMappings_09.json index a952b382..64eb0206 100644 --- a/resources/script/mapping/sampleMapping/sampleMappings_08.json +++ b/resources/script/mapping/sampleMapping/sampleMappings_09.json @@ -5,7 +5,7 @@ "ident": "627EA012-9D07-4E60-B0AC-0D830C30F0E3", "tested": false, "mapDeviceIdentifier": true, - "active": true, + "active": false, "autoAckOperation": true, "targetAPI": "MEASUREMENT", "source": "{\"value\":100}", @@ -45,7 +45,7 @@ ], "updateExistingDevice": false, "mappingType": "JSON", - "lastUpdate": 1697997974250, + "lastUpdate": 1698758504453, "name": "Mapping - 1", "snoopedTemplates": [], "createNonExistingDevice": true, @@ -59,7 +59,7 @@ "ident": "05241eba-e0c5-4c77-a723-15511dee8709", "tested": false, "mapDeviceIdentifier": true, - "active": true, + "active": false, "autoAckOperation": true, "targetAPI": "MEASUREMENT", "source": "{\"mea\":[{\"tid\":\"uuid_01\",\"psid\":\"Crest\",\"devicePath\":\"path01_80_X03_VVB001StatusB_Crest\",\"values\":[{\"value\":4.6,\"timestamp\":1648562285347}]},{\"tid\":\"uuid_02\",\"psid\":\"Crest\",\"devicePath\":\"path01_80_X03_VVB001StatusB_Crest\",\"values\":[{\"value\":5.6,\"timestamp\":1648563285347}]}]}", @@ -92,7 +92,7 @@ ], "updateExistingDevice": false, "mappingType": "JSON", - "lastUpdate": 1697998478652, + "lastUpdate": 1698758510127, "name": "Mapping - 2", "snoopedTemplates": [], "createNonExistingDevice": true, @@ -102,39 +102,42 @@ }, { "snoopStatus": "NONE", - "extension": null, "templateTopicSample": "device/express/berlin_01", "ident": "38c5ebbd-990c-4eeb-b556-75109b37c904", - "tested": false, + "tested": true, "mapDeviceIdentifier": true, - "active": true, + "active": false, "autoAckOperation": true, "targetAPI": "INVENTORY", "source": "{\"line\":\"Bus-Berlin-Rom\",\"operator\":\"EuroBus\",\"customFragment\":{\"customFragmentValue\":\"Express\"},\"capacity\":64,\"customArray\":[\"ArrayValue1\",\"ArrayValue2\"],\"customType\":\"type_International\"}", - "target": "{\"c8y_IsDevice\":{},\"name\":\"Vibration Sensor\",\"type\":\"maker_Vibration_Sensor\",\"capacity\":77}", + "target": "{\"c8y_IsDevice\":{},\"name\":\"Vibration Sensor\",\"type\":\"maker_Vibration_Sensor\",\"capacity\":77,\"id\":\"909090\"}", "externalIdType": "c8y_Serial", "templateTopic": "device/express/+", "qos": "AT_LEAST_ONCE", "substitutions": [ { + "resolve2ExternalId": false, "pathSource": "_TOPIC_LEVEL_[2]", - "pathTarget": "_DEVICE_IDENT_", + "pathTarget": "id", "repairStrategy": "DEFAULT", "expandArray": false }, { + "resolve2ExternalId": false, "pathSource": "capacity", "pathTarget": "capacity", "repairStrategy": "DEFAULT", "expandArray": false }, { + "resolve2ExternalId": false, "pathSource": "customType", "pathTarget": "type", "repairStrategy": "DEFAULT", "expandArray": false }, { + "resolve2ExternalId": false, "pathSource": "operator&\"-\"&line", "pathTarget": "name", "repairStrategy": "DEFAULT", @@ -143,7 +146,7 @@ ], "updateExistingDevice": false, "mappingType": "JSON", - "lastUpdate": 1669530606363, + "lastUpdate": 1698758553960, "name": "Mapping - 3", "snoopedTemplates": [], "createNonExistingDevice": false, @@ -157,7 +160,7 @@ "ident": "9109c667-16a3-486f-babf-9d01afdf1d2b", "tested": false, "mapDeviceIdentifier": true, - "active": true, + "active": false, "autoAckOperation": true, "targetAPI": "EVENT", "source": "{\"msg_type\":\"c8y_BusStopEvent\",\"txt\":\"Bus stopped at petrol station today!\",\"td\":\"2022-09-08T16:21:53.389+02:00\",\"ts\":\"1665473038000\"}", @@ -197,7 +200,7 @@ ], "updateExistingDevice": false, "mappingType": "JSON", - "lastUpdate": 1693234426310, + "lastUpdate": 1698758515764, "name": "Mapping - 4", "snoopedTemplates": [], "createNonExistingDevice": true, @@ -209,36 +212,40 @@ "snoopStatus": "NONE", "templateTopicSample": "measurement/berlin_01/gazoline", "ident": "2b295739-c1c4-4c2c-ae9e-2f04dc8313f6", - "tested": false, + "tested": true, "mapDeviceIdentifier": true, - "active": true, + "active": false, "autoAckOperation": true, "targetAPI": "MEASUREMENT", "source": "{\"fuel\":65,\"ts\":\"2022-08-05T00:14:49.389+02:00\",\"mea\":\"c8y_FuelMeasurement\"}", - "externalIdType": "c8y_Serial", "target": "{\"c8y_FuelMeasurement\":{\"Tank\":{\"value\":110,\"unit\":\"l\"}},\"time\":\"2022-08-05T00:14:49.389+02:00\",\"source\":{\"id\":\"909090\"},\"type\":\"c8y_FuelMeasurement\"}", + "externalIdType": "c8y_Serial", "templateTopic": "measurement/+/gazoline", "qos": "AT_LEAST_ONCE", "substitutions": [ { + "resolve2ExternalId": false, "pathSource": "_TOPIC_LEVEL_[1]", "pathTarget": "source.id", "repairStrategy": "DEFAULT", "expandArray": false }, { + "resolve2ExternalId": false, "pathSource": "mea", "pathTarget": "type", "repairStrategy": "DEFAULT", "expandArray": false }, { + "resolve2ExternalId": false, "pathSource": "$now()", "pathTarget": "time", "repairStrategy": "DEFAULT", "expandArray": false }, { + "resolve2ExternalId": false, "pathSource": "fuel*3.78541", "pathTarget": "c8y_FuelMeasurement.Tank.value", "repairStrategy": "DEFAULT", @@ -246,13 +253,13 @@ } ], "updateExistingDevice": false, - "lastUpdate": 1667758218993, "mappingType": "JSON", + "lastUpdate": 1698758518128, "name": "Mapping - 5", "snoopedTemplates": [], "createNonExistingDevice": true, - "subscriptionTopic": "measurement/#", "id": "1657071508", + "subscriptionTopic": "measurement/#", "direction": "INBOUND" }, { @@ -261,11 +268,11 @@ "ident": "e87a10ad-c223-4bd7-bd91-9df23e2bd3cf", "tested": false, "mapDeviceIdentifier": true, - "active": true, + "active": false, "autoAckOperation": true, "targetAPI": "INVENTORY", "source": "{\"device\":[\"d1_id\",\"d2_id\"],\"types\":{\"type_A\":\"type_A\",\"type_B\":\"type_B\"},\"used_name\":[\"Pressure_d1\",\"Pressure_d2\"]}", - "target": "{\"c8y_IsDevice\":{},\"name\":\"Vibration Sensor\",\"type\":\"maker_Vibration_Sensor\"}", + "target": "{\"c8y_IsDevice\":{},\"name\":\"Vibration Sensor\",\"type\":\"maker_Vibration_Sensor\",\"id\":\"909090\"}", "externalIdType": "c8y_Serial", "templateTopic": "multiarray/devices", "qos": "AT_LEAST_ONCE", @@ -273,7 +280,7 @@ { "resolve2ExternalId": false, "pathSource": "device", - "pathTarget": "_DEVICE_IDENT_", + "pathTarget": "id", "repairStrategy": "DEFAULT", "expandArray": true }, @@ -294,7 +301,7 @@ ], "updateExistingDevice": false, "mappingType": "JSON", - "lastUpdate": 1693233101650, + "lastUpdate": 1698758556679, "name": "Mapping - 6", "snoopedTemplates": [], "createNonExistingDevice": false, @@ -308,7 +315,7 @@ "ident": "c35424fc-aa6e-42f9-8434-66444e68d2aa", "tested": false, "mapDeviceIdentifier": true, - "active": true, + "active": false, "autoAckOperation": true, "targetAPI": "MEASUREMENT", "source": "[{\"tid\":\"5e4bac9f-b47a-499e-8601-68fc16a9847c\",\"psid\":\"Crest\",\"devicePath\":\"c2818e07-4c09-42f0-ba24-ddb712573ab5_AL1352_192168221_80_X03_VVB001StatusB_Crest\",\"processDataUnit\":\"20\",\"values\":[{\"value\":4.6,\"timestamp\":1648562285347}]},{\"tid\":\"5e4bac9f-b47a-499e-8601-68fc16a9847c\",\"psid\":\"Crest\",\"devicePath\":\"c2818e07-4c09-42f0-ba24-ddb712573ab5_AL1352_192168221_80_X03_VVB001StatusB_Crest\",\"processDataUnit\":\"20\",\"values\":[{\"value\":5.6,\"timestamp\":1648562285347}]}]", @@ -341,7 +348,7 @@ ], "updateExistingDevice": true, "mappingType": "JSON", - "lastUpdate": 1698005167190, + "lastUpdate": 1698758523515, "name": "Mapping - 7", "snoopedTemplates": [], "createNonExistingDevice": true, @@ -355,7 +362,7 @@ "ident": "def10a25-d11e-4201-b454-48b54ba8cd5e", "tested": false, "mapDeviceIdentifier": true, - "active": true, + "active": false, "autoAckOperation": true, "targetAPI": "EVENT", "source": "{\"msg_type\":\"c8y_BusStopEvent\",\"txt\":\"Bus stopped at petrol station today!\",\"td\":\"2022-09-08T16:21:53.389+02:00\",\"model\":{\"name\":\"MAN e-Bus\"}}", @@ -365,30 +372,35 @@ "qos": "AT_LEAST_ONCE", "substitutions": [ { + "resolve2ExternalId": false, "pathSource": "_TOPIC_LEVEL_[1]", "pathTarget": "source.id", "repairStrategy": "DEFAULT", "expandArray": false }, { + "resolve2ExternalId": false, "pathSource": "txt", "pathTarget": "text", "repairStrategy": "DEFAULT", "expandArray": false }, { + "resolve2ExternalId": false, "pathSource": "msg_type", "pathTarget": "type", "repairStrategy": "DEFAULT", "expandArray": false }, { + "resolve2ExternalId": false, "pathSource": "$now()", "pathTarget": "time", "repairStrategy": "DEFAULT", "expandArray": false }, { + "resolve2ExternalId": false, "pathSource": "model", "pathTarget": "customProperties", "repairStrategy": "REMOVE_IF_MISSING", @@ -397,7 +409,7 @@ ], "updateExistingDevice": false, "mappingType": "JSON", - "lastUpdate": 1688650122303, + "lastUpdate": 1698758529166, "name": "Mapping - 8", "snoopedTemplates": [], "createNonExistingDevice": true, @@ -411,7 +423,7 @@ "ident": "15699050-545d-41d1-a523-acf6de778739", "tested": false, "mapDeviceIdentifier": true, - "active": true, + "active": false, "autoAckOperation": true, "targetAPI": "MEASUREMENT", "source": "{\"fuel\":65,\"oil\":4.5,\"ts\":\"2022-08-05T00:14:49.389+02:00\",\"mea\":\"c8y_FuelMeasurement\"}", @@ -458,7 +470,7 @@ ], "updateExistingDevice": false, "mappingType": "JSON", - "lastUpdate": 1698005734224, + "lastUpdate": 1698758531880, "name": "Mapping - 9", "snoopedTemplates": [], "createNonExistingDevice": true, @@ -472,7 +484,7 @@ "ident": "5c5d2ea9-b0ce-4237-ad23-be5b393ca14c", "tested": false, "mapDeviceIdentifier": true, - "active": true, + "active": false, "autoAckOperation": true, "targetAPI": "EVENT", "source": "{}", @@ -483,7 +495,7 @@ "substitutions": [], "updateExistingDevice": false, "mappingType": "GENERIC_BINARY", - "lastUpdate": 1698005911395, + "lastUpdate": 1698758543942, "name": "Mapping - 13", "snoopedTemplates": [ "{\"message\":\"57652077696c6c20616c77617973206170707265636961746520746865206e6578742072656c656173652031302e313721\"}", @@ -502,7 +514,7 @@ "ident": "ef19e1db-fd9a-481c-aa83-ee000eb2ed5e", "tested": false, "mapDeviceIdentifier": true, - "active": true, + "active": false, "autoAckOperation": true, "targetAPI": "OPERATION", "source": "{\"text\":\"Special operation\"}", @@ -528,7 +540,7 @@ ], "updateExistingDevice": false, "mappingType": "JSON", - "lastUpdate": 1698009557391, + "lastUpdate": 1698758547242, "name": "Mapping - 14", "snoopedTemplates": [], "createNonExistingDevice": true, @@ -542,7 +554,7 @@ "ident": "df794b94-7921-42de-b612-aa7951b1410b", "tested": true, "mapDeviceIdentifier": true, - "active": true, + "active": false, "autoAckOperation": true, "targetAPI": "EVENT", "source": "{\"message\":\"5a75207370c3a47420303821\"}", @@ -575,7 +587,7 @@ ], "updateExistingDevice": false, "mappingType": "GENERIC_BINARY", - "lastUpdate": 1698007842317, + "lastUpdate": 1698758549690, "name": "Mapping - 15", "snoopedTemplates": [ "{\"message\":\"5a75207370c3a47420303821\"}", @@ -598,18 +610,20 @@ "autoAckOperation": true, "targetAPI": "INVENTORY", "source": "{\"customType\":\"type_Overnight\"}", - "target": "{\"type\":\"type\"}", + "target": "{\"type\":\"type\",\"id\":\"909090\"}", "externalIdType": "c8y_Serial", "templateTopic": "device/update/+", "qos": "AT_LEAST_ONCE", "substitutions": [ { + "resolve2ExternalId": false, "pathSource": "_TOPIC_LEVEL_[2]", - "pathTarget": "_DEVICE_IDENT_", + "pathTarget": "id", "repairStrategy": "DEFAULT", "expandArray": false }, { + "resolve2ExternalId": false, "pathSource": "customType", "pathTarget": "type", "repairStrategy": "DEFAULT", @@ -618,7 +632,7 @@ ], "updateExistingDevice": true, "mappingType": "JSON", - "lastUpdate": 1667839346115, + "lastUpdate": 1698587756129, "name": "Mapping - 17", "snoopedTemplates": [], "createNonExistingDevice": false, @@ -775,7 +789,7 @@ "ident": "a8dfc9d7-65f6-4e43-b1db-3029b7efe236", "tested": false, "mapDeviceIdentifier": true, - "active": true, + "active": false, "autoAckOperation": true, "targetAPI": "MEASUREMENT", "source": "{\"Measurementname\":\"Airsensor\",\"Seriesname\":\"Humidity\",\"value\":10,\"unit\":\"%\"}", @@ -785,30 +799,35 @@ "qos": "AT_LEAST_ONCE", "substitutions": [ { + "resolve2ExternalId": false, "pathSource": "_TOPIC_LEVEL_[1]", "pathTarget": "source.id", "repairStrategy": "DEFAULT", "expandArray": false }, { + "resolve2ExternalId": false, "pathSource": "Measurementname & \"_type\"", "pathTarget": "type", "repairStrategy": "DEFAULT", "expandArray": false }, { + "resolve2ExternalId": false, "pathSource": "Measurementname = \"Airsensor\" ? {Seriesname:{\"value\": value, \"unit\": unit}} : null", "pathTarget": "Airsensor", "repairStrategy": "REMOVE_IF_NULL", "expandArray": false }, { + "resolve2ExternalId": false, "pathSource": "Measurementname = \"Liquidsensor\" ? {Seriesname:{\"value\": value, \"unit\": unit}} : null", "pathTarget": "Liquidsensor", "repairStrategy": "REMOVE_IF_NULL", "expandArray": false }, { + "resolve2ExternalId": false, "pathSource": "$now()", "pathTarget": "time", "repairStrategy": "DEFAULT", @@ -817,7 +836,7 @@ ], "updateExistingDevice": false, "mappingType": "JSON", - "lastUpdate": 1689013437492, + "lastUpdate": 1698758564836, "name": "Mapping - 23", "snoopedTemplates": [], "createNonExistingDevice": true, @@ -831,7 +850,7 @@ "ident": "3612278e-be55-4531-a878-03563ddd3f3b", "tested": false, "mapDeviceIdentifier": true, - "active": true, + "active": false, "autoAckOperation": true, "targetAPI": "ALARM", "source": "{\"msg_type\":\"c8y_FlatTireAlarm\",\"tx\":\"Left rear tire loses air!\",\"bus_id\":\"berlin_01\"}", @@ -864,7 +883,7 @@ ], "updateExistingDevice": false, "mappingType": "JSON", - "lastUpdate": 1698050412756, + "lastUpdate": 1698758561670, "name": "Mapping - 24", "snoopedTemplates": [], "createNonExistingDevice": true, @@ -883,7 +902,7 @@ "ident": "464f3925-883c-41ee-b885-f39cc338202b", "tested": false, "mapDeviceIdentifier": true, - "active": true, + "active": false, "autoAckOperation": true, "targetAPI": "MEASUREMENT", "source": "{}", @@ -894,7 +913,7 @@ "substitutions": [], "updateExistingDevice": false, "mappingType": "PROCESSOR_EXTENSION", - "lastUpdate": 1690318558279, + "lastUpdate": 1698758570476, "name": "Mapping - 25", "snoopedTemplates": [], "createNonExistingDevice": false, @@ -902,6 +921,100 @@ "subscriptionTopic": "measurementExt", "direction": "INBOUND" }, + { + "snoopStatus": "NONE", + "templateTopicSample": "v2/things/berlin_01", + "ident": "44ed5048-84c0-4e2d-9b56-a76b3c18083b", + "tested": true, + "mapDeviceIdentifier": true, + "active": false, + "autoAckOperation": true, + "targetAPI": "MEASUREMENT", + "source": "{\"values\":[{\"key\":\"velocidad_cabezal\",\"value\":136.34},{\"key\":\"temperature\",\"value\":25}]}", + "target": "{\"time\":\"2022-08-05T00:14:49.389+02:00\",\"source\":{\"id\":\"909090\"},\"type\":\"c8y_FlexibleMeasurement\"}", + "externalIdType": "c8y_Serial", + "templateTopic": "v2/things/+", + "qos": "AT_LEAST_ONCE", + "substitutions": [ + { + "resolve2ExternalId": false, + "pathSource": "_TOPIC_LEVEL_[2]", + "pathTarget": "source.id", + "repairStrategy": "DEFAULT", + "expandArray": false + }, + { + "resolve2ExternalId": false, + "pathSource": "$now()", + "pathTarget": "time", + "repairStrategy": "DEFAULT", + "expandArray": false + }, + { + "resolve2ExternalId": false, + "pathSource": "values{key: {'Measurement':{'value':value, 'key': 'U'}}}", + "pathTarget": "$", + "repairStrategy": "DEFAULT", + "expandArray": false + } + ], + "updateExistingDevice": false, + "mappingType": "JSON", + "lastUpdate": 1698758616335, + "name": "Mapping - 26", + "snoopedTemplates": [], + "createNonExistingDevice": false, + "id": "6659380200", + "subscriptionTopic": "v2/things/#", + "direction": "INBOUND" + }, + { + "snoopStatus": "NONE", + "templateTopicSample": "v3/things/berlin_01", + "ident": "1d7b0142-5dae-4e17-bcbb-07d6465c95f2", + "tested": true, + "mapDeviceIdentifier": true, + "active": false, + "autoAckOperation": true, + "targetAPI": "MEASUREMENT", + "source": "{\"values\":[{\"key\":\"velocidad_cabezal\",\"value\":136.34},{\"key\":\"temperature\",\"value\":25}]}", + "target": "{\"time\":\"2022-08-05T00:14:49.389+02:00\",\"source\":{\"id\":\"909090\"},\"type\":\"c8y_FlexibleMeasurement\"}", + "externalIdType": "c8y_Serial", + "templateTopic": "v3/things/+", + "qos": "AT_LEAST_ONCE", + "substitutions": [ + { + "resolve2ExternalId": false, + "pathSource": "_TOPIC_LEVEL_[2]", + "pathTarget": "source.id", + "repairStrategy": "DEFAULT", + "expandArray": false + }, + { + "resolve2ExternalId": false, + "pathSource": "$now()", + "pathTarget": "time", + "repairStrategy": "DEFAULT", + "expandArray": false + }, + { + "resolve2ExternalId": false, + "pathSource": "values[].{key: {'Measurement':{'value':value, 'key': 'U'}}}", + "pathTarget": "$", + "repairStrategy": "DEFAULT", + "expandArray": true + } + ], + "updateExistingDevice": false, + "mappingType": "JSON", + "lastUpdate": 1698758614100, + "name": "Mapping - 27", + "snoopedTemplates": [], + "createNonExistingDevice": false, + "id": "9759386026", + "subscriptionTopic": "v3/things/#", + "direction": "INBOUND" +}, { "templateTopicSample": "evt/outbound/berlin_01", "ident": "7eaad46b-6e2e-47b0-ad96-73f042d4945d", @@ -954,7 +1067,7 @@ "direction": "OUTBOUND", "snoopStatus": "NONE", "mapDeviceIdentifier": true, - "active": true, + "active": false, "targetAPI": "EVENT", "filterOutbound": "bus_event", "publishTopic": "evt/outbound/#", @@ -962,7 +1075,7 @@ "templateTopic": "evt/outbound/+", "updateExistingDevice": false, "mappingType": "JSON", - "lastUpdate": 1698055678488, + "lastUpdate": 1698473992759, "name": "Mapping - 51" }, { @@ -1003,7 +1116,7 @@ "direction": "OUTBOUND", "snoopStatus": "NONE", "mapDeviceIdentifier": true, - "active": true, + "active": false, "targetAPI": "OPERATION", "filterOutbound": "bus_opp", "publishTopic": "opp/outbound/#", @@ -1011,7 +1124,7 @@ "templateTopic": "", "updateExistingDevice": false, "mappingType": "JSON", - "lastUpdate": 1698057693638, + "lastUpdate": 1698474014437, "name": "Mapping - 52" } -] \ No newline at end of file +]