From 57bb06bd1ebdbdb7bd6db09aa08077fb1ff89ff7 Mon Sep 17 00:00:00 2001 From: Matthias Krebs Date: Tue, 21 Oct 2025 12:38:10 +0200 Subject: [PATCH 1/4] implemented JSONata data transformation. --- CommHandler/CHANGELOG.md | 3 +- CommHandler/build.gradle | 1 + .../common/helper/JsonHelper.java | 27 +++++++++++ .../impl/MessageFilterHandlerImpl.java | 11 ++++- .../messaging/impl/SGrMessagingDevice.java | 20 +++++--- .../BearerTokenAuthenticator.java | 46 +++++++------------ .../rest/impl/SGrRestApiDevice.java | 20 ++++---- .../common/helper/JsonMapperTest.java | 31 ++++++++++++- 8 files changed, 110 insertions(+), 49 deletions(-) diff --git a/CommHandler/CHANGELOG.md b/CommHandler/CHANGELOG.md index c0ef67c4..e157db00 100644 --- a/CommHandler/CHANGELOG.md +++ b/CommHandler/CHANGELOG.md @@ -5,11 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Unreleased +## [unreleased] ## Added - client for SGr declaration library +- JSONata expressions as response query in REST / messaging data points and message filters ### Changed diff --git a/CommHandler/build.gradle b/CommHandler/build.gradle index 24d50e53..25c7607e 100644 --- a/CommHandler/build.gradle +++ b/CommHandler/build.gradle @@ -97,6 +97,7 @@ dependencies { api group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.18.4' implementation group: 'io.burt', name: 'jmespath-core', version: '0.6.0' implementation group: 'io.burt', name: 'jmespath-jackson', version: '0.6.0' + implementation group: 'com.dashjoin', name: 'jsonata', version: '0.9.8' implementation group: 'commons-io', name: 'commons-io', version: '2.15.1' implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.19.0' implementation group: 'jakarta.annotation', name: 'jakarta.annotation-api', version: '2.1.1' diff --git a/CommHandler/src/main/java/com/smartgridready/communicator/common/helper/JsonHelper.java b/CommHandler/src/main/java/com/smartgridready/communicator/common/helper/JsonHelper.java index d0223465..94ab882e 100644 --- a/CommHandler/src/main/java/com/smartgridready/communicator/common/helper/JsonHelper.java +++ b/CommHandler/src/main/java/com/smartgridready/communicator/common/helper/JsonHelper.java @@ -1,5 +1,6 @@ package com.smartgridready.communicator.common.helper; +import com.dashjoin.jsonata.Jsonata; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -55,6 +56,32 @@ public static Value parseJsonResponse(String jmesPath, String jsonResp) throws G JsonNode res = expression.search(jsonNode); return JsonValue.of(res); } + } catch (Exception e) { + throw new GenDriverException("Failed to parse JSON response", e); + } + } + + /** + * Evaluates a JSONata expression on a JSON string and returns as SGr value. + * @param jsonataExpression the JSONata expression + * @param jsonResp the JSON string + * @return an instance of {@link JsonValue} + * @throws GenDriverException when an error occurred during parsing + */ + public static Value parseJsonResponseWithJsonata(String jsonataExpression, String jsonResp) throws GenDriverException { + + ObjectMapper mapper = new ObjectMapper(); + + try { + if (jsonataExpression != null && !jsonataExpression.trim().isEmpty()) { + var expression = Jsonata.jsonata(jsonataExpression); + var jsonObj = mapper.readValue(jsonResp, Object.class); + var result = expression.evaluate(jsonObj); + return JsonValue.of(result); + } + + return JsonValue.of(mapper.readTree(jsonResp)); + } catch (IOException e) { throw new GenDriverException("Failed to parse JSON response", e); } diff --git a/CommHandler/src/main/java/com/smartgridready/communicator/messaging/impl/MessageFilterHandlerImpl.java b/CommHandler/src/main/java/com/smartgridready/communicator/messaging/impl/MessageFilterHandlerImpl.java index e5a430f4..62dfc85d 100644 --- a/CommHandler/src/main/java/com/smartgridready/communicator/messaging/impl/MessageFilterHandlerImpl.java +++ b/CommHandler/src/main/java/com/smartgridready/communicator/messaging/impl/MessageFilterHandlerImpl.java @@ -60,6 +60,14 @@ public boolean isFilterMatch(String payload) { } catch (GenDriverException e) { return false; // no match } + } else if (messageFilter.getJsonataFilter() != null) { + try { + var filter = messageFilter.getJsonataFilter(); + regexMatch = filter.getMatchesRegex(); + payloadValue = JsonHelper.parseJsonResponseWithJsonata(filter.getQuery(), payload); + } catch (GenDriverException e) { + return false; // no match + } } // regex matching for all filter types @@ -75,7 +83,8 @@ public void validate() throws OperationNotSupportedException { (messageFilter.getPlaintextFilter() != null) || (messageFilter.getRegexFilter() != null) || (messageFilter.getXpapathFilter() != null) || - (messageFilter.getJmespathFilter() != null) + (messageFilter.getJmespathFilter() != null) || + (messageFilter.getJsonataFilter() != null) ) { return; } diff --git a/CommHandler/src/main/java/com/smartgridready/communicator/messaging/impl/SGrMessagingDevice.java b/CommHandler/src/main/java/com/smartgridready/communicator/messaging/impl/SGrMessagingDevice.java index ef009cca..28604e2e 100644 --- a/CommHandler/src/main/java/com/smartgridready/communicator/messaging/impl/SGrMessagingDevice.java +++ b/CommHandler/src/main/java/com/smartgridready/communicator/messaging/impl/SGrMessagingDevice.java @@ -215,18 +215,21 @@ private Value getValueFromDevice(Map parameters, long timeoutMs, Value value; if (queryOpt.isPresent()) { ResponseQuery responseQuery = queryOpt.get(); - if (responseQuery.getQueryType() != null && ResponseQueryType.JMES_PATH_EXPRESSION == responseQuery.getQueryType()) { + if (responseQuery.getQueryType() == null) { + throw new GenDriverException("Response query type missing"); + } + if (ResponseQueryType.JMES_PATH_EXPRESSION == responseQuery.getQueryType()) { value = JsonHelper.parseJsonResponse(responseQuery.getQuery(), response); - } else if (responseQuery.getQueryType() != null && ResponseQueryType.JMES_PATH_MAPPING == responseQuery.getQueryType()) { + } else if (ResponseQueryType.JMES_PATH_MAPPING == responseQuery.getQueryType()) { value = JsonHelper.mapJsonResponse(responseQuery.getJmesPathMappings(), response); - } else if (responseQuery.getQueryType() != null && ResponseQueryType.X_PATH_EXPRESSION == responseQuery.getQueryType()) { + } else if (ResponseQueryType.X_PATH_EXPRESSION == responseQuery.getQueryType()) { value = XPathHelper.parseXmlResponse(responseQuery.getQuery(), response); - } else if (responseQuery.getQueryType() != null && ResponseQueryType.REGULAR_EXPRESSION == responseQuery.getQueryType()) { + } else if (ResponseQueryType.REGULAR_EXPRESSION == responseQuery.getQueryType()) { value = RegexHelper.query(responseQuery.getQuery(), response); - } else if (responseQuery.getQueryType() != null) { - throw new GenDriverException("Response query type " + responseQuery.getQueryType().name() + " not supported yet"); + } else if (ResponseQueryType.JSO_NATA_EXPRESSION == responseQuery.getQueryType()) { + value = JsonHelper.parseJsonResponseWithJsonata(responseQuery.getQuery(), response); } else { - throw new GenDriverException("Response query type missing"); + throw new GenDriverException("Response query type " + responseQuery.getQueryType().name() + " not supported yet"); } } else { // mapping device -> generic (only for plain string values) @@ -328,6 +331,9 @@ private void transformIncomingMessage( case REGULAR_EXPRESSION: value = RegexHelper.query(queryOpt.get().getQuery(), response); break; + case JSO_NATA_EXPRESSION: + value = JsonHelper.parseJsonResponseWithJsonata(queryOpt.get().getQuery(), response); + break; default: throw new GenDriverException("Response query type " + queryOpt.get().getQueryType().name() + " not supported yet"); } diff --git a/CommHandler/src/main/java/com/smartgridready/communicator/rest/http/authentication/BearerTokenAuthenticator.java b/CommHandler/src/main/java/com/smartgridready/communicator/rest/http/authentication/BearerTokenAuthenticator.java index f5deb3b7..4e6f7384 100644 --- a/CommHandler/src/main/java/com/smartgridready/communicator/rest/http/authentication/BearerTokenAuthenticator.java +++ b/CommHandler/src/main/java/com/smartgridready/communicator/rest/http/authentication/BearerTokenAuthenticator.java @@ -18,8 +18,6 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS package com.smartgridready.communicator.rest.http.authentication; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.smartgridready.driver.api.http.GenHttpResponse; import com.smartgridready.communicator.rest.http.client.RestServiceClient; import com.smartgridready.ns.v0.DeviceFrame; @@ -27,17 +25,15 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import com.smartgridready.ns.v0.ResponseQueryType; import com.smartgridready.ns.v0.RestApiInterfaceDescription; import com.smartgridready.ns.v0.RestApiServiceCall; +import com.smartgridready.communicator.common.helper.JsonHelper; import com.smartgridready.communicator.rest.exception.RestApiServiceCallException; +import com.smartgridready.driver.api.common.GenDriverException; import com.smartgridready.driver.api.http.GenHttpClientFactory; import com.smartgridready.communicator.rest.http.client.RestServiceClientUtils; -import io.burt.jmespath.Expression; -import io.burt.jmespath.JmesPath; -import io.burt.jmespath.jackson.JacksonRuntime; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; -import java.util.Optional; /** * Implements an HTTP authenticator using JWT bearer tokens. @@ -111,31 +107,21 @@ private void requestBearerToken(RestServiceClient restServiceClient) throws IOEx private String handleResponse(String result, RestApiServiceCall restApiServiceCall) throws IOException { - Optional queryOpt = Optional.ofNullable(restApiServiceCall.getResponseQuery()) - .filter(responseQuery -> responseQuery.getQueryType() != null - && responseQuery.getQueryType() == ResponseQueryType.JMES_PATH_EXPRESSION) - .map(ResponseQuery::getQuery); - - if (queryOpt.isPresent()) { - return parseResponse(result, queryOpt.get()); + if (restApiServiceCall.getResponseQuery() != null) { + ResponseQuery responseQuery = restApiServiceCall.getResponseQuery(); + if (responseQuery.getQueryType() != null) { + try { + if (ResponseQueryType.JMES_PATH_EXPRESSION == responseQuery.getQueryType()) { + return JsonHelper.parseJsonResponse(responseQuery.getQuery(), result).getString(); + } else if (ResponseQueryType.JSO_NATA_EXPRESSION == responseQuery.getQueryType()) { + return JsonHelper.parseJsonResponseWithJsonata(responseQuery.getQuery(), result).getString(); + } + } catch (GenDriverException e) { + throw new IOException("Failed to parse response", e); + } + } } + return result; } - - private String parseResponse(String jsonResp, String jmesPath) throws IOException { - - JmesPath path = new JacksonRuntime(); - Expression expression = path.compile(jmesPath); - - ObjectMapper mapper = new ObjectMapper(); - JsonNode jsonNode = mapper.readTree(jsonResp); - JsonNode res = expression.search(jsonNode); - - if (res != null) { - return res.asText(); - } - - LOG.error("Unable to extract bearer token from http response."); - return null; - } } diff --git a/CommHandler/src/main/java/com/smartgridready/communicator/rest/impl/SGrRestApiDevice.java b/CommHandler/src/main/java/com/smartgridready/communicator/rest/impl/SGrRestApiDevice.java index 2360c77b..c41a4307 100644 --- a/CommHandler/src/main/java/com/smartgridready/communicator/rest/impl/SGrRestApiDevice.java +++ b/CommHandler/src/main/java/com/smartgridready/communicator/rest/impl/SGrRestApiDevice.java @@ -228,14 +228,18 @@ private Value handleServiceResponse(RestApiServiceCall restApiServiceCall, Strin if (restApiServiceCall.getResponseQuery() != null) { ResponseQuery responseQuery = restApiServiceCall.getResponseQuery(); - if (responseQuery.getQueryType() != null && ResponseQueryType.JMES_PATH_EXPRESSION == responseQuery.getQueryType()) { - return JsonHelper.parseJsonResponse(responseQuery.getQuery(), response); - } else if (responseQuery.getQueryType() != null && ResponseQueryType.JMES_PATH_MAPPING == responseQuery.getQueryType()) { - return JsonHelper.mapJsonResponse(responseQuery.getJmesPathMappings(), response); - } else if (responseQuery.getQueryType() != null && ResponseQueryType.X_PATH_EXPRESSION == responseQuery.getQueryType()) { - return XPathHelper.parseXmlResponse(responseQuery.getQuery(), response); - } else if (responseQuery.getQueryType() != null && ResponseQueryType.REGULAR_EXPRESSION == responseQuery.getQueryType()) { - return RegexHelper.query(responseQuery.getQuery(), response); + if (responseQuery.getQueryType() != null) { + if (ResponseQueryType.JMES_PATH_EXPRESSION == responseQuery.getQueryType()) { + return JsonHelper.parseJsonResponse(responseQuery.getQuery(), response); + } else if (ResponseQueryType.JMES_PATH_MAPPING == responseQuery.getQueryType()) { + return JsonHelper.mapJsonResponse(responseQuery.getJmesPathMappings(), response); + } else if (ResponseQueryType.X_PATH_EXPRESSION == responseQuery.getQueryType()) { + return XPathHelper.parseXmlResponse(responseQuery.getQuery(), response); + } else if (ResponseQueryType.REGULAR_EXPRESSION == responseQuery.getQueryType()) { + return RegexHelper.query(responseQuery.getQuery(), response); + } else if (ResponseQueryType.JSO_NATA_EXPRESSION == responseQuery.getQueryType()) { + return JsonHelper.parseJsonResponseWithJsonata(responseQuery.getQuery(), response); + } } } diff --git a/CommHandler/src/test/java/com/smartgridready/communicator/common/helper/JsonMapperTest.java b/CommHandler/src/test/java/com/smartgridready/communicator/common/helper/JsonMapperTest.java index ac8cd44d..4c172217 100644 --- a/CommHandler/src/test/java/com/smartgridready/communicator/common/helper/JsonMapperTest.java +++ b/CommHandler/src/test/java/com/smartgridready/communicator/common/helper/JsonMapperTest.java @@ -2,7 +2,6 @@ import com.smartgridready.ns.v0.DeviceFrame; import com.smartgridready.ns.v0.JMESPathMapping; -import com.fasterxml.jackson.databind.JsonNode; import com.smartgridready.communicator.common.impl.SGrDeviceBase; import com.smartgridready.ns.v0.RestApiServiceCall; import io.vavr.Tuple3; @@ -55,6 +54,20 @@ void mapSwisspower() throws Exception { assertEquals(MAPPER.readTree(expectedJson).toString(), jsonResult); } + @Test + void mapSwisspower_Jsonata() throws Exception { + + String expression = "prices[].{\"start_timestamp\":start_timestamp,\"end_timestamp\":end_timestamp,\"integrated\":integrated[].{\"value\":value,\"unit\":unit,\"component\":component}}"; + + // map JSON + String jsonResponse = loadJson("TariffInSwisspower.json"); + String expectedJson = loadJson("TariffOutSwisspower_withTariffName.json"); + + String jsonResult = JsonHelper.parseJsonResponseWithJsonata(expression, jsonResponse).getJson().toString(); + LOG.debug("JSON result: {}", jsonResult); + assertEquals(MAPPER.readTree(expectedJson).toString(), jsonResult); + } + @Test void mapGroupeE() throws Exception { @@ -90,6 +103,20 @@ void mapGroupeE() throws Exception { assertEquals(MAPPER.readTree(expectedJson).toString(), jsonResult); } + @Test + void mapGroupeE_JSONata() throws Exception { + + String expression = "$.{\"start_timestamp\":start_timestamp,\"end_timestamp\":end_timestamp,\"integrated\":[{\"value\":vario_plus,\"unit\":unit}]}"; + + // map JSON + String jsonResponse = loadJson("TariffInGroupeE.json"); + String expectedJson = loadJson("TariffOutGroupeE.json"); + + String jsonResult = JsonHelper.parseJsonResponseWithJsonata(expression, jsonResponse).getJson().toString(); + LOG.debug("JSON result: {}", jsonResult); + assertEquals(MAPPER.readTree(expectedJson).toString(), jsonResult); + } + private static JMESPathMapping getJmesPathMapping(DeviceFrame deviceFrame) { var restApiConfigurationContent = deviceFrame @@ -108,7 +135,7 @@ private static JMESPathMapping getJmesPathMapping(DeviceFrame deviceFrame) { throw new IllegalArgumentException("Device Frame does not contain RestApiServiceCall"); } - private static String getJmesQueryExpression(DeviceFrame deviceFrame) { + private static String getQueryExpression(DeviceFrame deviceFrame) { var restApiConfigurationContent = deviceFrame .getInterfaceList() From 8b8dd789c90876cecee082ec72a562bd706e2be5 Mon Sep 17 00:00:00 2001 From: Matthias Krebs Date: Thu, 23 Oct 2025 10:03:56 +0200 Subject: [PATCH 2/4] refactored transformation using response query. implemented support for response/template query in write operations. --- CommHandler/CHANGELOG.md | 3 +- .../messaging/impl/SGrMessagingDevice.java | 181 ++++++++++-------- .../rest/impl/SGrRestApiDevice.java | 142 +++++++++----- 3 files changed, 205 insertions(+), 121 deletions(-) diff --git a/CommHandler/CHANGELOG.md b/CommHandler/CHANGELOG.md index e157db00..026117f7 100644 --- a/CommHandler/CHANGELOG.md +++ b/CommHandler/CHANGELOG.md @@ -11,10 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - client for SGr declaration library - JSONata expressions as response query in REST / messaging data points and message filters +- response or template query can be used in write operations ### Changed -- requires updated driver implementations +- requires updated driver implementations supporting driver API 2.4 ## [2.4.2] - 2025-10-09 diff --git a/CommHandler/src/main/java/com/smartgridready/communicator/messaging/impl/SGrMessagingDevice.java b/CommHandler/src/main/java/com/smartgridready/communicator/messaging/impl/SGrMessagingDevice.java index 28604e2e..b16cdb9c 100644 --- a/CommHandler/src/main/java/com/smartgridready/communicator/messaging/impl/SGrMessagingDevice.java +++ b/CommHandler/src/main/java/com/smartgridready/communicator/messaging/impl/SGrMessagingDevice.java @@ -51,6 +51,7 @@ public class SGrMessagingDevice extends SGrDeviceBase< MessagingFunctionalProfile, MessagingDataPoint> implements GenDeviceApi4Messaging { + @SuppressWarnings("unused") private static final Logger LOG = LoggerFactory.getLogger(SGrMessagingDevice.class); private static final long SYNC_READ_TIMEOUT_MSEC = 60000; @@ -208,33 +209,14 @@ private Value getValueFromDevice(Map parameters, long timeoutMs, .get() // returns Message .getPayload(); - Optional queryOpt = Optional.ofNullable(dataPoint.getMessagingDataPointConfiguration()) - .map(MessagingDataPointConfiguration::getInMessage) - .map(InMessage::getResponseQuery); - - Value value; - if (queryOpt.isPresent()) { - ResponseQuery responseQuery = queryOpt.get(); - if (responseQuery.getQueryType() == null) { - throw new GenDriverException("Response query type missing"); - } - if (ResponseQueryType.JMES_PATH_EXPRESSION == responseQuery.getQueryType()) { - value = JsonHelper.parseJsonResponse(responseQuery.getQuery(), response); - } else if (ResponseQueryType.JMES_PATH_MAPPING == responseQuery.getQueryType()) { - value = JsonHelper.mapJsonResponse(responseQuery.getJmesPathMappings(), response); - } else if (ResponseQueryType.X_PATH_EXPRESSION == responseQuery.getQueryType()) { - value = XPathHelper.parseXmlResponse(responseQuery.getQuery(), response); - } else if (ResponseQueryType.REGULAR_EXPRESSION == responseQuery.getQueryType()) { - value = RegexHelper.query(responseQuery.getQuery(), response); - } else if (ResponseQueryType.JSO_NATA_EXPRESSION == responseQuery.getQueryType()) { - value = JsonHelper.parseJsonResponseWithJsonata(responseQuery.getQuery(), response); - } else { - throw new GenDriverException("Response query type " + responseQuery.getQueryType().name() + " not supported yet"); - } - } else { - // mapping device -> generic (only for plain string values) - value = getMappedGenericValue(dataPoint.getMessagingDataPointConfiguration(), response); - } + + Value value = StringValue.of(response); + + // value transformation using responseQuery + value = getTransformedGenericValue(dataPoint.getMessagingDataPointConfiguration(), value); + + // mapping device -> generic (only for plain string values) + value = getMappedGenericValue(dataPoint.getMessagingDataPointConfiguration(), value); // unit conversion before returning to client return applyUnitConversion(dataPoint, value, SGrDeviceBase::multiply); @@ -267,10 +249,13 @@ public void setVal(String profileName, String dataPointName, Value value) value = applyUnitConversion(dataPoint, value, SGrDeviceBase::divide); // mapping generic -> device - String outValue = getMappedDeviceValue(dataPoint.getMessagingDataPointConfiguration(), value); + value = getMappedDeviceValue(dataPoint.getMessagingDataPointConfiguration(), value); + + // value transformation using templateQuery + value = getTransformedDeviceValue(dataPoint.getMessagingDataPointConfiguration(), value); // no regex here, string literal replacement is sufficient - outMessageTemplate = outMessageTemplate.replace("[[value]]", outValue); + outMessageTemplate = outMessageTemplate.replace("[[value]]", value.getString()); messagingClient.sendSync(outMessageTopic, Message.of(outMessageTemplate)); } @@ -311,38 +296,14 @@ private void transformIncomingMessage( String response = Optional.ofNullable(msgReceiveResult.get().getPayload()).orElse(""); - Optional queryOpt = Optional.ofNullable(dataPoint.getMessagingDataPointConfiguration()) - .map(MessagingDataPointConfiguration::getInMessage) - .map(InMessage::getResponseQuery); - try { - Value value; - if (queryOpt.isPresent()) { - switch (queryOpt.get().getQueryType()) { - case JMES_PATH_EXPRESSION: - value = JsonHelper.parseJsonResponse(queryOpt.get().getQuery(), response); - break; - case JMES_PATH_MAPPING: - value = JsonHelper.mapJsonResponse(queryOpt.get().getJmesPathMappings(), response); - break; - case X_PATH_EXPRESSION: - value = XPathHelper.parseXmlResponse(queryOpt.get().getQuery(), response); - break; - case REGULAR_EXPRESSION: - value = RegexHelper.query(queryOpt.get().getQuery(), response); - break; - case JSO_NATA_EXPRESSION: - value = JsonHelper.parseJsonResponseWithJsonata(queryOpt.get().getQuery(), response); - break; - default: - throw new GenDriverException("Response query type " + queryOpt.get().getQueryType().name() + " not supported yet"); - } - } else { - // mapping device -> generic (only for plain string values) - value = getMappedGenericValue(dataPoint.getMessagingDataPointConfiguration(), response); - } - LOG.debug("Received subscribed message on topic={}, filter={}, payload={}", - inMessageTopic, queryOpt.isPresent() ? queryOpt.get().getQuery() : "none", response); + Value value = StringValue.of(response); + + // value transformation using responseQuery + value = getTransformedGenericValue(dataPoint.getMessagingDataPointConfiguration(), value); + + // mapping device -> generic (only for plain string values) + value = getMappedGenericValue(dataPoint.getMessagingDataPointConfiguration(), value); // unit conversion before inserting into cache value = applyUnitConversion(dataPoint, value, SGrDeviceBase::multiply); @@ -431,40 +392,106 @@ private int countCacheRecords4Topic(String topic) { return count.get(); } - private static Value getMappedGenericValue(MessagingDataPointConfiguration dataPointConfiguration, String value) { - String mappedValue = value; + private static Value getMappedGenericValue(MessagingDataPointConfiguration dataPointConfiguration, Value value) { + Value mappedValue = value; List valueMappings = Optional.ofNullable(dataPointConfiguration) .map(MessagingDataPointConfiguration::getInMessage) .map(InMessage::getValueMapping) - .map(MessagingValueMapping::getMapping).orElse(Collections.emptyList()); - for (ValueMapping mapping: valueMappings) { - if (mappedValue.equals(mapping.getDeviceValue())) { - mappedValue = mapping.getGenericValue(); - break; - } + .map(MessagingValueMapping::getMapping) + .orElse(Collections.emptyList()); + + final String strVal = mappedValue.getString(); + Optional mappingOpt = valueMappings.stream() + .filter(m -> strVal.equals(m.getDeviceValue())) + .findFirst(); + if (mappingOpt.isPresent()) { + mappedValue = StringValue.of(mappingOpt.get().getGenericValue()); } - return StringValue.of(mappedValue); + return mappedValue; } - private static String getMappedDeviceValue(MessagingDataPointConfiguration dataPointConfiguration, Value value) { - String mappedValue = value.getString(); + private static Value getMappedDeviceValue(MessagingDataPointConfiguration dataPointConfiguration, Value value) { + Value mappedValue = value; List valueMappings = Optional.ofNullable(dataPointConfiguration) .map(MessagingDataPointConfiguration::getWriteCmdMessage) .map(OutMessage::getValueMapping) - .map(MessagingValueMapping::getMapping).orElse(Collections.emptyList()); - for (ValueMapping mapping: valueMappings) { - if (mappedValue.equals(mapping.getGenericValue())) { - mappedValue = mapping.getDeviceValue(); - break; - } + .map(MessagingValueMapping::getMapping) + .orElse(Collections.emptyList()); + + final String strVal = mappedValue.getString(); + Optional mappingOpt = valueMappings.stream() + .filter(m -> strVal.equals(m.getGenericValue())) + .findFirst(); + if (mappingOpt.isPresent()) { + mappedValue = StringValue.of(mappingOpt.get().getDeviceValue()); } return mappedValue; } + private static Value getTransformedGenericValue(MessagingDataPointConfiguration dataPointConfiguration, Value value) throws GenDriverException { + Value tmpValue = value; + + ResponseQuery responseQuery = Optional.ofNullable(dataPointConfiguration) + .map(MessagingDataPointConfiguration::getInMessage) + .map(InMessage::getResponseQuery) + .orElse(null); + + if (responseQuery != null) { + if (responseQuery.getQueryType() == null) { + throw new GenDriverException("Response query type missing"); + } + if (ResponseQueryType.JMES_PATH_EXPRESSION == responseQuery.getQueryType()) { + tmpValue = JsonHelper.parseJsonResponse(responseQuery.getQuery(), tmpValue.getString()); + } else if (ResponseQueryType.JMES_PATH_MAPPING == responseQuery.getQueryType()) { + tmpValue = JsonHelper.mapJsonResponse(responseQuery.getJmesPathMappings(), tmpValue.getString()); + } else if (ResponseQueryType.X_PATH_EXPRESSION == responseQuery.getQueryType()) { + tmpValue = XPathHelper.parseXmlResponse(responseQuery.getQuery(), tmpValue.getString()); + } else if (ResponseQueryType.REGULAR_EXPRESSION == responseQuery.getQueryType()) { + tmpValue = RegexHelper.query(responseQuery.getQuery(), tmpValue.getString()); + } else if (ResponseQueryType.JSO_NATA_EXPRESSION == responseQuery.getQueryType()) { + tmpValue = JsonHelper.parseJsonResponseWithJsonata(responseQuery.getQuery(), tmpValue.getString()); + } else { + throw new GenDriverException("Response query type " + responseQuery.getQueryType().name() + " not supported yet"); + } + } + + return tmpValue; + } + + private static Value getTransformedDeviceValue(MessagingDataPointConfiguration dataPointConfiguration, Value value) throws GenDriverException { + Value tmpValue = value; + + ResponseQuery templateQuery = Optional.ofNullable(dataPointConfiguration) + .map(MessagingDataPointConfiguration::getWriteCmdMessage) + .map(OutMessage::getTemplateQuery) + .orElse(null); + + if (templateQuery != null) { + if (templateQuery.getQueryType() == null) { + throw new GenDriverException("Template query type missing"); + } + if (ResponseQueryType.JMES_PATH_EXPRESSION == templateQuery.getQueryType()) { + tmpValue = JsonHelper.parseJsonResponse(templateQuery.getQuery(), tmpValue.getString()); + } else if (ResponseQueryType.JMES_PATH_MAPPING == templateQuery.getQueryType()) { + tmpValue = JsonHelper.mapJsonResponse(templateQuery.getJmesPathMappings(), tmpValue.getString()); + } else if (ResponseQueryType.X_PATH_EXPRESSION == templateQuery.getQueryType()) { + tmpValue = XPathHelper.parseXmlResponse(templateQuery.getQuery(), tmpValue.getString()); + } else if (ResponseQueryType.REGULAR_EXPRESSION == templateQuery.getQueryType()) { + tmpValue = RegexHelper.query(templateQuery.getQuery(), tmpValue.getString()); + } else if (ResponseQueryType.JSO_NATA_EXPRESSION == templateQuery.getQueryType()) { + tmpValue = JsonHelper.parseJsonResponseWithJsonata(templateQuery.getQuery(), tmpValue.getString()); + } else { + throw new GenDriverException("Template query type " + templateQuery.getQueryType().name() + " not supported yet"); + } + } + + return tmpValue; + } + private static String substituteParameterPlaceholders(String template, Map substitutions) { // this is for dynamic parameters String convertedTemplate = template; diff --git a/CommHandler/src/main/java/com/smartgridready/communicator/rest/impl/SGrRestApiDevice.java b/CommHandler/src/main/java/com/smartgridready/communicator/rest/impl/SGrRestApiDevice.java index c41a4307..abb4a54b 100644 --- a/CommHandler/src/main/java/com/smartgridready/communicator/rest/impl/SGrRestApiDevice.java +++ b/CommHandler/src/main/java/com/smartgridready/communicator/rest/impl/SGrRestApiDevice.java @@ -55,7 +55,9 @@ WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWIS import org.slf4j.LoggerFactory; import java.io.IOException; +import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Properties; @@ -163,14 +165,18 @@ private Value doReadWriteVal(RestApiDataPoint dataPoint, Value value, Properties RestApiServiceCall serviceCall = evaluateRestApiServiceCall(dpDescription, rwpDirection); if (value != null) { + // write operation checkOutOfRange(new Value[]{value}, dataPoint); value = applyUnitConversion(dataPoint, value, SGrDeviceBase::divide); - // substitute value mappings generic -> device - substitutions.put( - "value", - (serviceCall.getValueMapping() != null) ? getMappedDeviceValue(value.getString(), serviceCall.getValueMapping()) : value.getString() - ); + // value mappings generic -> device + value = getMappedDeviceValue(serviceCall, value); + + // value transformation using responseQuery + value = getTransformedDeviceValue(serviceCall, value); + + // substitute value + substitutions.put("value", value.getString()); } // substitute default values of dynamic request parameters if (null != dataPoint.getDataPoint().getParameterList()) { @@ -187,10 +193,19 @@ private Value doReadWriteVal(RestApiDataPoint dataPoint, Value value, Properties String response = handleServiceCall(restServiceClient, httpAuthenticator.isTokenRenewalSupported()); if (value == null) { - value = handleServiceResponse(serviceCall, response); + // read operation + value = StringValue.of(response); + + // value transformation using responseQuery + value = getTransformedGenericValue(serviceCall, value); + + // value mappings device -> generic + value = getMappedGenericValue(serviceCall, value); + return applyUnitConversion(dataPoint, value, SGrDeviceBase::multiply); } + // response of write operation return StringValue.of(response); } throw new GenDriverException("Missing 'restApiDataPointConfiguration' description in device description XML file"); @@ -224,33 +239,6 @@ private String handleServiceCall(RestServiceClient serviceClient, boolean tryTok } } - private Value handleServiceResponse(RestApiServiceCall restApiServiceCall, String response) throws GenDriverException { - - if (restApiServiceCall.getResponseQuery() != null) { - ResponseQuery responseQuery = restApiServiceCall.getResponseQuery(); - if (responseQuery.getQueryType() != null) { - if (ResponseQueryType.JMES_PATH_EXPRESSION == responseQuery.getQueryType()) { - return JsonHelper.parseJsonResponse(responseQuery.getQuery(), response); - } else if (ResponseQueryType.JMES_PATH_MAPPING == responseQuery.getQueryType()) { - return JsonHelper.mapJsonResponse(responseQuery.getJmesPathMappings(), response); - } else if (ResponseQueryType.X_PATH_EXPRESSION == responseQuery.getQueryType()) { - return XPathHelper.parseXmlResponse(responseQuery.getQuery(), response); - } else if (ResponseQueryType.REGULAR_EXPRESSION == responseQuery.getQueryType()) { - return RegexHelper.query(responseQuery.getQuery(), response); - } else if (ResponseQueryType.JSO_NATA_EXPRESSION == responseQuery.getQueryType()) { - return JsonHelper.parseJsonResponseWithJsonata(responseQuery.getQuery(), response); - } - } - } - - // return plain response - - // substitute value mappings device -> generic - return StringValue.of( - (restApiServiceCall.getValueMapping() != null) ? getMappedGenericValue(response, restApiServiceCall.getValueMapping()) : response - ); - } - private RestApiServiceCall evaluateRestApiServiceCall(RestApiDataPointConfiguration dataPointConfiguration, RwpDirections rwpDirections) throws GenDriverException { @@ -324,23 +312,91 @@ private RestApiInterfaceDescription getRestApiInterfaceDescription() { return getRestApiInterface().getRestApiInterfaceDescription(); } - private String getMappedDeviceValue(String genericValue, RestApiValueMapping valueMapping) { - for (ValueMapping mapping: valueMapping.getMapping()) { - if (genericValue.equals(mapping.getGenericValue())) { - return mapping.getDeviceValue(); + private Value getMappedDeviceValue(RestApiServiceCall serviceCall, Value value) { + Value mappedValue = value; + + List valueMappings = Optional.ofNullable(serviceCall.getValueMapping()) + .map(RestApiValueMapping::getMapping) + .orElse(Collections.emptyList()); + + final String strVal = mappedValue.getString(); + Optional mappingOpt = valueMappings.stream() + .filter(m -> strVal.equals(m.getGenericValue())) + .findFirst(); + if (mappingOpt.isPresent()) { + mappedValue = StringValue.of(mappingOpt.get().getDeviceValue()); + } + + return mappedValue; + } + + private Value getMappedGenericValue(RestApiServiceCall serviceCall, Value value) { + Value mappedValue = value; + + List valueMappings = Optional.ofNullable(serviceCall.getValueMapping()) + .map(RestApiValueMapping::getMapping) + .orElse(Collections.emptyList()); + + final String strVal = mappedValue.getString(); + Optional mappingOpt = valueMappings.stream() + .filter(m -> strVal.equals(m.getDeviceValue())) + .findFirst(); + if (mappingOpt.isPresent()) { + mappedValue = StringValue.of(mappingOpt.get().getGenericValue()); + } + + return mappedValue; + } + + private static Value getTransformedDeviceValue(RestApiServiceCall serviceCall, Value value) throws GenDriverException { + Value tmpValue = value; + + ResponseQuery templateQuery = serviceCall.getResponseQuery(); + if (templateQuery != null) { + if (templateQuery.getQueryType() == null) { + throw new GenDriverException("Template query type missing"); + } + if (ResponseQueryType.JMES_PATH_EXPRESSION == templateQuery.getQueryType()) { + tmpValue = JsonHelper.parseJsonResponse(templateQuery.getQuery(), tmpValue.getString()); + } else if (ResponseQueryType.JMES_PATH_MAPPING == templateQuery.getQueryType()) { + tmpValue = JsonHelper.mapJsonResponse(templateQuery.getJmesPathMappings(), tmpValue.getString()); + } else if (ResponseQueryType.X_PATH_EXPRESSION == templateQuery.getQueryType()) { + tmpValue = XPathHelper.parseXmlResponse(templateQuery.getQuery(), tmpValue.getString()); + } else if (ResponseQueryType.REGULAR_EXPRESSION == templateQuery.getQueryType()) { + tmpValue = RegexHelper.query(templateQuery.getQuery(), tmpValue.getString()); + } else if (ResponseQueryType.JSO_NATA_EXPRESSION == templateQuery.getQueryType()) { + tmpValue = JsonHelper.parseJsonResponseWithJsonata(templateQuery.getQuery(), tmpValue.getString()); + } else { + throw new GenDriverException("Template query type " + templateQuery.getQueryType().name() + " not supported yet"); } } - return genericValue; + return tmpValue; } - private String getMappedGenericValue(String deviceValue, RestApiValueMapping valueMapping) { - for (ValueMapping mapping: valueMapping.getMapping()) { - if (deviceValue.equals(mapping.getDeviceValue())) { - return mapping.getGenericValue(); + private static Value getTransformedGenericValue(RestApiServiceCall serviceCall, Value value) throws GenDriverException { + Value tmpValue = value; + + ResponseQuery responseQuery = serviceCall.getResponseQuery(); + if (responseQuery != null) { + if (responseQuery.getQueryType() == null) { + throw new GenDriverException("Response query type missing"); + } + if (ResponseQueryType.JMES_PATH_EXPRESSION == responseQuery.getQueryType()) { + tmpValue = JsonHelper.parseJsonResponse(responseQuery.getQuery(), tmpValue.getString()); + } else if (ResponseQueryType.JMES_PATH_MAPPING == responseQuery.getQueryType()) { + tmpValue = JsonHelper.mapJsonResponse(responseQuery.getJmesPathMappings(), tmpValue.getString()); + } else if (ResponseQueryType.X_PATH_EXPRESSION == responseQuery.getQueryType()) { + tmpValue = XPathHelper.parseXmlResponse(responseQuery.getQuery(), tmpValue.getString()); + } else if (ResponseQueryType.REGULAR_EXPRESSION == responseQuery.getQueryType()) { + tmpValue = RegexHelper.query(responseQuery.getQuery(), tmpValue.getString()); + } else if (ResponseQueryType.JSO_NATA_EXPRESSION == responseQuery.getQueryType()) { + tmpValue = JsonHelper.parseJsonResponseWithJsonata(responseQuery.getQuery(), tmpValue.getString()); + } else { + throw new GenDriverException("Response query type " + responseQuery.getQueryType().name() + " not supported yet"); } } - return deviceValue; + return tmpValue; } } From abdf2cb45e51d9b1e05d95de1746ea790936c1cc Mon Sep 17 00:00:00 2001 From: Matthias Krebs Date: Thu, 23 Oct 2025 10:39:06 +0200 Subject: [PATCH 3/4] uses JSOnata test EIDs instead of hard-coded expressions. --- .../common/helper/JsonMapperTest.java | 13 +- ...d_Dynamic_Tariffs_GroupeE_JSONata_V1.0.xml | 519 +++++++++++++++++ ...c_Tariffs_Swisspower_Test_JSONata_V1.0.xml | 550 ++++++++++++++++++ 3 files changed, 1080 insertions(+), 2 deletions(-) create mode 100644 CommHandler/src/test/resources/SGr_05_mmmm_dddd_Dynamic_Tariffs_GroupeE_JSONata_V1.0.xml create mode 100644 CommHandler/src/test/resources/SGr_05_mmmm_dddd_Dynamic_Tariffs_Swisspower_Test_JSONata_V1.0.xml diff --git a/CommHandler/src/test/java/com/smartgridready/communicator/common/helper/JsonMapperTest.java b/CommHandler/src/test/java/com/smartgridready/communicator/common/helper/JsonMapperTest.java index 4c172217..3248e360 100644 --- a/CommHandler/src/test/java/com/smartgridready/communicator/common/helper/JsonMapperTest.java +++ b/CommHandler/src/test/java/com/smartgridready/communicator/common/helper/JsonMapperTest.java @@ -14,6 +14,7 @@ import java.util.Properties; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; class JsonMapperTest extends JsonMapperTestBase { @@ -57,7 +58,11 @@ void mapSwisspower() throws Exception { @Test void mapSwisspower_Jsonata() throws Exception { - String expression = "prices[].{\"start_timestamp\":start_timestamp,\"end_timestamp\":end_timestamp,\"integrated\":integrated[].{\"value\":value,\"unit\":unit,\"component\":component}}"; + Tuple3, Properties> device = createDevice("SGr_05_mmmm_dddd_Dynamic_Tariffs_Swisspower_Test_JSONata_V1.0.xml"); + DeviceFrame deviceFrame = device._1; + + String expression = getQueryExpression(deviceFrame); + assertNotNull(expression); // map JSON String jsonResponse = loadJson("TariffInSwisspower.json"); @@ -106,7 +111,11 @@ void mapGroupeE() throws Exception { @Test void mapGroupeE_JSONata() throws Exception { - String expression = "$.{\"start_timestamp\":start_timestamp,\"end_timestamp\":end_timestamp,\"integrated\":[{\"value\":vario_plus,\"unit\":unit}]}"; + Tuple3, Properties> device = createDevice("SGr_05_mmmm_dddd_Dynamic_Tariffs_GroupeE_JSONata_V1.0.xml"); + DeviceFrame deviceFrame = device._1; + + String expression = getQueryExpression(deviceFrame); + assertNotNull(expression); // map JSON String jsonResponse = loadJson("TariffInGroupeE.json"); diff --git a/CommHandler/src/test/resources/SGr_05_mmmm_dddd_Dynamic_Tariffs_GroupeE_JSONata_V1.0.xml b/CommHandler/src/test/resources/SGr_05_mmmm_dddd_Dynamic_Tariffs_GroupeE_JSONata_V1.0.xml new file mode 100644 index 00000000..7df62697 --- /dev/null +++ b/CommHandler/src/test/resources/SGr_05_mmmm_dddd_Dynamic_Tariffs_GroupeE_JSONata_V1.0.xml @@ -0,0 +1,519 @@ + + + + Groupe E Dynamic Tariffs + Groupe E + 0 + + Published + + 1.0.0 + 2025-10-03 + SGr/mkr + Finalization + + + + + + The Vario tariff is a 15-minute dynamic day-ahead grid usage tariff.

+ +
    +
  • Data Update Frequency: Day-ahead
  • +
  • Time of Publication: until 18:00
  • +
  • Binding Character: no change
  • +
  • Non-dynamic Elements: Only the grid usage tariff is dynamic.
  • +
+ ]]> +
+ en + https://www.groupe-e.ch/de/strom/vario +
+ + + Der Vario-Tarif ist ein 15-min dynamischer day-ahead Netznutzungstarif.

+ +
    +
  • Häufigkeit der Datenaktualisierung: Day-ahead
  • +
  • Publikationszeitpunkt: bis spätestens 18:00 Uhr
  • +
  • Verbindlichkeit: keine Anpassung
  • +
  • Nicht-dynamische Elemente: Nur der Netznutzungstarif ist dynamisch.
  • +
+ ]]> +
+ de + https://www.groupe-e.ch/de/strom/vario +
+ DeviceInformation + false + 1.0.0 + - + GroupeEDynamicTariffs + 5 + + 1 + 0 + 0 + + Verified + + + Link zur technischen Dokumentation:

+ ]]> +
+ de + https://groupee.sharepoint.com/sites/MediaPoint/Supports%20publicitaires/02_Fiches%20produits/Smart%20meter/vario-integration-web-api-de.pdf?ga=1 +
+
+ + + tariff_name + + + + vario_grid + Vario Grid + + + vario_plus + Vario PLUS + + + dt_plus + Double PLUS + + + + vario_plus + + The actual tariff product to get + en + + + + Gewünschtes Tarif-Produkt + de + + + + + + + + URI + https://api.tariffs.groupe-e.ch + NoSecurityScheme + + + + + DynamicTariff + + 0 + DynamicTariff + Supplier + 5 + + 1 + 0 + 0 + + + + + The functional profile contains all the necessary information to retrieve a series of data points with information on tariff values every 15 minutes.

+

The prices are available on an online interface (WEB-API), which an energy management system will use to plan the operation of various devices.

+

+

This functional profile can only be provided by products with REST API interface.

+

The request for the tariffs defines the start time stamp and end time stamp of the interval the tariffs should be returned for. + Each entry in the resulting tariff list contains also the start time stamp and end time stamp of the sub intervals together with a list of entries with tariff unit and tariff value.

+ + The schema for the JSON created by the REST API is + +
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "Dynamic Tariffs",
+  "description": "JSON schema for the delivery of tariffs",
+  "type": "array",
+  "items": {
+    "type": "object",
+    "properties": {
+      "start_timestamp": {
+        "description": "Start time stamp of the tariff interval",
+        "type": "string"
+      },
+      "end_timestamp": {
+        "description": "End time stamp of the tariff interval",
+        "type": "string"
+      },
+      "integrated": {
+        "description": "List of tariffs in that interval",
+        "type": "array",
+        "items": {
+          "type": "object",
+          "properties": {
+            "component": {
+              "description": "The optional name of the tariff",
+              "type": "string"
+            },
+            "unit": {
+              "description": "The tariff unit for the price",
+              "type": "string"
+            },
+            "value": {
+              "description": "The tariff price value",
+              "type": "number"
+            }
+          },
+          "propertyNames": {
+            "type": "string",
+            "pattern": "^(component|unit|value)$"
+          },
+          "required": [
+            "unit",
+            "value"
+          ]
+        }
+      }
+    },
+    "propertyNames": {
+      "type": "string",
+      "pattern": "^(start_timestamp|end_timestamp|integrated)$"
+    },
+    "required": [
+      "start_timestamp",
+      "end_timestamp",
+      "integrated"
+    ]
+  }
+}
+
+ + An example JSON is: + +
+[
+    {
+        "start_timestamp": "2025-01-01T00:00:00+01:00",
+        "end_timestamp": "2025-01-01T00:15:00+01:00",
+        "integrated": [
+            {
+                "component": "work",
+                "unit": "CHF/kWh",
+                "value": 0.4925
+            }
+        ]
+    },
+    {
+        "start_timestamp": "2025-01-01T00:15:00+01:00",
+        "end_timestamp": "2025-01-01T00:30:00+01:00",
+        "integrated": [
+            {
+                "component": "work",
+                "unit": "CHF/kWh",
+                "value": 0.491133
+            }
+        ]
+    },
+    {
+        "start_timestamp": "2025-01-01T00:30:00+01:00",
+        "end_timestamp": "2025-01-01T00:45:00+01:00",
+        "integrated": [
+            {
+                "component": "work",
+                "unit": "CHF/kWh",
+                "value": 0.486722
+            }
+        ]
+    }
+]
+
+ ]]> +
+ en +
+ + + +

Das Funktionsprofil enthält alle nötigen Informationen, um eine Reihe von Datenpunkten mit Tarifinformationen alle 15 Minuten abzufragen.

+

Die Preise sind über ein Online-Interface (WEB-API) abrufbar, womit ein Energie-Management-System den Betrieb verschiedener Geräte zeitlich festlegt.

+

+

Dieses Funktionsprofil kann nur mit Produkten genutzt werden, die ein REST API unterstützen.

+

Die Tarif-Abfrage definiert den Start- und End-Zeitpunkt des Intervalls, für das die Tarife abgerufen werden. + Jedes Element der resultierenden Tarif-Liste enthält auch den Start- und End-Zeitpunkt der Sub-Intervalle, in Kombination mit einer Liste von Elementen mit Tarif-Einheit und -Preis.

+ + Das JSON-Schema des REST API ist + +
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "Dynamische Tarife",
+  "description": "JSON-Schema für den Abruf dynamischer Tarife",
+  "type": "array",
+  "items": {
+    "type": "object",
+    "properties": {
+      "start_timestamp": {
+        "description": "Start-Zeitpunkt des Tarif-Intervalls",
+        "type": "string"
+      },
+      "end_timestamp": {
+        "description": "End-Zeitpunkt des Tarif-Intervalls",
+        "type": "string"
+      },
+      "integrated": {
+        "description": "Liste der Tarife im Intervall",
+        "type": "array",
+        "items": {
+          "type": "object",
+          "properties": {
+            "component": {
+              "description": "Der optionale Tarif-Name",
+              "type": "string"
+            },
+            "unit": {
+              "description": "Die Einheit des Tarif-Preises",
+              "type": "string"
+            },
+            "value": {
+              "description": "Der Tarif-Preis",
+              "type": "number"
+            }
+          },
+          "propertyNames": {
+            "type": "string",
+            "pattern": "^(component|unit|value)$"
+          },
+          "required": [
+            "unit",
+            "value"
+          ]
+        }
+      }
+    },
+    "propertyNames": {
+      "type": "string",
+      "pattern": "^(start_timestamp|end_timestamp|integrated)$"
+    },
+    "required": [
+      "start_timestamp",
+      "end_timestamp",
+      "integrated"
+    ]
+  }
+}
+
+ + Ein JSON-Beispiel ist: + +
+[
+    {
+        "start_timestamp": "2025-01-01T00:00:00+01:00",
+        "end_timestamp": "2025-01-01T00:15:00+01:00",
+        "integrated": [
+            {
+                "component": "work",
+                "unit": "CHF/kWh",
+                "value": 0.4925
+            }
+        ]
+    },
+    {
+        "start_timestamp": "2025-01-01T00:15:00+01:00",
+        "end_timestamp": "2025-01-01T00:30:00+01:00",
+        "integrated": [
+            {
+                "component": "work",
+                "unit": "CHF/kWh",
+                "value": 0.491133
+            }
+        ]
+    },
+    {
+        "start_timestamp": "2025-01-01T00:30:00+01:00",
+        "end_timestamp": "2025-01-01T00:45:00+01:00",
+        "integrated": [
+            {
+                "component": "work",
+                "unit": "CHF/kWh",
+                "value": 0.486722
+            }
+        ]
+    }
+]
+
+ ]]> +
+ de +
+
+ + + + TariffSupply + R + + + + NONE + + + start_timestamp + + + + + + Begin of tariff periods to be queried. Format according ISO 8601: YYYY-MM-DDThh:mm:ssTZD. + + en + + + + + Beginn der abzurufenden Tarifperiode. Format gem. ISO 8601: YYYY-MM-DDThh:mm:ssTZD. + + de + + + + + end_timestamp + + + + + + End of tariff periods to be queried. Format according ISO 8601: YYYY-MM-DDThh:mm:ssTZD. + + en + + + + + Ende der abzurufenden Tarifperiode. Format gem. ISO 8601: YYYY-MM-DDThh:mm:ssTZD. + + de + + + + + + + Tariff list - description see above

+ Parameters are +
    +
  • start_timestamp - ISO8601 date/time - the start time stamp of the requested interval
  • +
  • end_timestamp - ISO8601 date/time - the end time stamp of the requested interval
  • +
+ The JMES queries for the elements of the result structure are: +
    +
  • start_timestamp: [*].start_timestamp +
    start time stamp of this interval
  • +
  • end_timestamp: [*].end_timestamp +
    end time stamp of this interval
  • +
  • component: [*].integrated[*].component +
    optional name of the tariff
  • +
  • unit: [*].integrated[*].unit +
    unit for the tariff
  • +
  • value: [*].integrated[*].value +
    price for the tariff
  • +
+ ]]> +
+ en +
+ + + Tarif-Liste - Beschreibung siehe oben

+ Parameter sind +
    +
  • start_timestamp - ISO8601 date/time - Start-Zeitpunkt des abzufragenden Intervalls
  • +
  • end_timestamp - ISO8601 date/time - End-Zeitpunkt des abzufragenden Intervalls
  • +
+ Die JMES-Queries der einzelnen Elemente der Ausgabe-Datenstruktur sind: +
    +
  • start_timestamp: [*].start_timestamp +
    Start-Zeitpunkt des Intervalls
  • +
  • end_timestamp: [*].end_timestamp +
    End-Zeitpunkt des Intervalls
  • +
  • component: [*].integrated[*].component +
    Optionaler Name des Tarifs
  • +
  • unit: [*].integrated[*].unit +
    Einheit des Tarifs
  • +
  • value: [*].integrated[*].value +
    Preis des Tarifs
  • +
+ ]]> +
+ de +
+ + + Maps the {{tariff_type}} property to match the integrated property required by SGr.

+

The component property is not supported.

+ ]]> +
+ en +
+
+ + JSON_object + + +
+ Accept + application/json +
+
+ GET + /v1/tariffs + + + start_timestamp + [[start_timestamp]] + + + end_timestamp + [[end_timestamp]] + + + + JSONataExpression + + + + +
+
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/CommHandler/src/test/resources/SGr_05_mmmm_dddd_Dynamic_Tariffs_Swisspower_Test_JSONata_V1.0.xml b/CommHandler/src/test/resources/SGr_05_mmmm_dddd_Dynamic_Tariffs_Swisspower_Test_JSONata_V1.0.xml new file mode 100644 index 00000000..f359728d --- /dev/null +++ b/CommHandler/src/test/resources/SGr_05_mmmm_dddd_Dynamic_Tariffs_Swisspower_Test_JSONata_V1.0.xml @@ -0,0 +1,550 @@ + + + + ESIT / Swisspower Dynamic Tariffs Test-API + ESIT / Swisspower + 0 + + Draft + + 1.0.0 + 2025-10-07 + SGr/mkr + Added dynamic parameters, prepare for publication + + + + + + Provides dynamic tariff prices of ESIT/Swisspower.

+

This EID is a template for actual provider tariffs!

+ ]]> +
+ en + https://www.dynamische-stromtarife.ch/ +
+ + + Stellt dynamische Strompreise von ESIT/Swisspower bereit.

+

Diese EID ist eine Vorlage für konkrete Stromanbieter-Tarife!

+ ]]> +
+ de + https://www.dynamische-stromtarife.ch/ +
+ DeviceInformation + false + 1.0.0 + - + SwisspowerDynamicTariffs + 5 + + 1 + 0 + 0 + + Verified +
+ + + tariff_type + + + + electricity + Electricity + + + grid + Grid + + + integrated + Electricity with grid fees included + + + dso + DSO + + + feed_in + Feed-in + + + + integrated + + The tariff type to get + en + + + + Gewünschter Tarif-Typ + de + + + + + metering_code + + + + + + Metering point identifier of the customer + en + + + + Identifikation des Messpunkts beim Kunden + de + + + + + token + + + + + + Access token for the web service + en + + + + Access-Token des Web-Service + de + + + + + + + + URI + https://esit-test.code-fabrik.ch + NoSecurityScheme + + + + + DynamicTariff + + 0 + DynamicTariff + Supplier + 5 + + 1 + 0 + 0 + + + + + The functional profile contains all the necessary information to retrieve a series of data points with information on tariff values every 15 minutes.

+

The prices are available on an online interface (WEB-API), which an energy management system will use to plan the operation of various devices.

+

+

This functional profile can only be provided by products with REST API interface.

+

The request for the tariffs defines the start time stamp and end time stamp of the interval the tariffs should be returned for. + Each entry in the resulting tariff list contains also the start time stamp and end time stamp of the sub intervals together with a list of entries with tariff unit and tariff value.

+ + The schema for the JSON created by the REST API is + +
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "Dynamic Tariffs",
+  "description": "JSON schema for the delivery of tariffs",
+  "type": "array",
+  "items": {
+    "type": "object",
+    "properties": {
+      "start_timestamp": {
+        "description": "Start time stamp of the tariff interval",
+        "type": "string"
+      },
+      "end_timestamp": {
+        "description": "End time stamp of the tariff interval",
+        "type": "string"
+      },
+      "integrated": {
+        "description": "List of tariffs in that interval",
+        "type": "array",
+        "items": {
+          "type": "object",
+          "properties": {
+            "component": {
+              "description": "The optional name of the tariff",
+              "type": "string"
+            },
+            "unit": {
+              "description": "The tariff unit for the price",
+              "type": "string"
+            },
+            "value": {
+              "description": "The tariff price value",
+              "type": "number"
+            }
+          },
+          "propertyNames": {
+            "type": "string",
+            "pattern": "^(component|unit|value)$"
+          },
+          "required": [
+            "unit",
+            "value"
+          ]
+        }
+      }
+    },
+    "propertyNames": {
+      "type": "string",
+      "pattern": "^(start_timestamp|end_timestamp|integrated)$"
+    },
+    "required": [
+      "start_timestamp",
+      "end_timestamp",
+      "integrated"
+    ]
+  }
+}
+
+ + An example JSON is: + +
+[
+    {
+        "start_timestamp": "2025-01-01T00:00:00+01:00",
+        "end_timestamp": "2025-01-01T00:15:00+01:00",
+        "integrated": [
+            {
+                "component": "work",
+                "unit": "CHF/kWh",
+                "value": 0.4925
+            }
+        ]
+    },
+    {
+        "start_timestamp": "2025-01-01T00:15:00+01:00",
+        "end_timestamp": "2025-01-01T00:30:00+01:00",
+        "integrated": [
+            {
+                "component": "work",
+                "unit": "CHF/kWh",
+                "value": 0.491133
+            }
+        ]
+    },
+    {
+        "start_timestamp": "2025-01-01T00:30:00+01:00",
+        "end_timestamp": "2025-01-01T00:45:00+01:00",
+        "integrated": [
+            {
+                "component": "work",
+                "unit": "CHF/kWh",
+                "value": 0.486722
+            }
+        ]
+    }
+]
+
+ ]]> +
+ en +
+ + + +

Das Funktionsprofil enthält alle nötigen Informationen, um eine Reihe von Datenpunkten mit Tarifinformationen alle 15 Minuten abzufragen.

+

Die Preise sind über ein Online-Interface (WEB-API) abrufbar, womit ein Energie-Management-System den Betrieb verschiedener Geräte zeitlich festlegt.

+

+

Dieses Funktionsprofil kann nur mit Produkten genutzt werden, die ein REST API unterstützen.

+

Die Tarif-Abfrage definiert den Start- und End-Zeitpunkt des Intervalls, für das die Tarife abgerufen werden. + Jedes Element der resultierenden Tarif-Liste enthält auch den Start- und End-Zeitpunkt der Sub-Intervalle, in Kombination mit einer Liste von Elementen mit Tarif-Einheit und -Preis.

+ + Das JSON-Schema des REST API ist + +
+{
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "title": "Dynamische Tarife",
+  "description": "JSON-Schema für den Abruf dynamischer Tarife",
+  "type": "array",
+  "items": {
+    "type": "object",
+    "properties": {
+      "start_timestamp": {
+        "description": "Start-Zeitpunkt des Tarif-Intervalls",
+        "type": "string"
+      },
+      "end_timestamp": {
+        "description": "End-Zeitpunkt des Tarif-Intervalls",
+        "type": "string"
+      },
+      "integrated": {
+        "description": "Liste der Tarife im Intervall",
+        "type": "array",
+        "items": {
+          "type": "object",
+          "properties": {
+            "component": {
+              "description": "Der optionale Tarif-Name",
+              "type": "string"
+            },
+            "unit": {
+              "description": "Die Einheit des Tarif-Preises",
+              "type": "string"
+            },
+            "value": {
+              "description": "Der Tarif-Preis",
+              "type": "number"
+            }
+          },
+          "propertyNames": {
+            "type": "string",
+            "pattern": "^(component|unit|value)$"
+          },
+          "required": [
+            "unit",
+            "value"
+          ]
+        }
+      }
+    },
+    "propertyNames": {
+      "type": "string",
+      "pattern": "^(start_timestamp|end_timestamp|integrated)$"
+    },
+    "required": [
+      "start_timestamp",
+      "end_timestamp",
+      "integrated"
+    ]
+  }
+}
+
+ + Ein JSON-Beispiel ist: + +
+[
+    {
+        "start_timestamp": "2025-01-01T00:00:00+01:00",
+        "end_timestamp": "2025-01-01T00:15:00+01:00",
+        "integrated": [
+            {
+                "component": "work",
+                "unit": "CHF/kWh",
+                "value": 0.4925
+            }
+        ]
+    },
+    {
+        "start_timestamp": "2025-01-01T00:15:00+01:00",
+        "end_timestamp": "2025-01-01T00:30:00+01:00",
+        "integrated": [
+            {
+                "component": "work",
+                "unit": "CHF/kWh",
+                "value": 0.491133
+            }
+        ]
+    },
+    {
+        "start_timestamp": "2025-01-01T00:30:00+01:00",
+        "end_timestamp": "2025-01-01T00:45:00+01:00",
+        "integrated": [
+            {
+                "component": "work",
+                "unit": "CHF/kWh",
+                "value": 0.486722
+            }
+        ]
+    }
+]
+
+ ]]> +
+ de +
+
+ + + + TariffSupply + R + + + + NONE + + + start_timestamp + + + + + + Begin of tariff periods to be queried. Format according ISO 8601: YYYY-MM-DDThh:mm:ssTZD. + + en + + + + + Beginn der abzurufenden Tarifperiode. Format gem. ISO 8601: YYYY-MM-DDThh:mm:ssTZD. + + de + + + + + end_timestamp + + + + + + End of tariff periods to be queried. Format according ISO 8601: YYYY-MM-DDThh:mm:ssTZD. + + en + + + + + Ende der abzurufenden Tarifperiode. Format gem. ISO 8601: YYYY-MM-DDThh:mm:ssTZD. + + de + + + + + + + Tariff list - description see above

+ Parameters are +
    +
  • start_timestamp - ISO8601 date/time - the start time stamp of the requested interval
  • +
  • end_timestamp - ISO8601 date/time - the end time stamp of the requested interval
  • +
+ The JMES queries for the elements of the result structure are: +
    +
  • start_timestamp: [*].start_timestamp +
    start time stamp of this interval
  • +
  • end_timestamp: [*].end_timestamp +
    end time stamp of this interval
  • +
  • component: [*].integrated[*].component +
    optional name of the tariff
  • +
  • unit: [*].integrated[*].unit +
    unit for the tariff
  • +
  • value: [*].integrated[*].value +
    price for the tariff
  • +
+ ]]> +
+ en +
+ + + Tarif-Liste - Beschreibung siehe oben

+ Parameter sind +
    +
  • start_timestamp - ISO8601 date/time - Start-Zeitpunkt des abzufragenden Intervalls
  • +
  • end_timestamp - ISO8601 date/time - End-Zeitpunkt des abzufragenden Intervalls
  • +
+ Die JMES-Queries der einzelnen Elemente der Ausgabe-Datenstruktur sind: +
    +
  • start_timestamp: [*].start_timestamp +
    Start-Zeitpunkt des Intervalls
  • +
  • end_timestamp: [*].end_timestamp +
    End-Zeitpunkt des Intervalls
  • +
  • component: [*].integrated[*].component +
    Optionaler Name des Tarifs
  • +
  • unit: [*].integrated[*].unit +
    Einheit des Tarifs
  • +
  • value: [*].integrated[*].value +
    Preis des Tarifs
  • +
+ ]]> +
+ de +
+ + + Maps the {{tariff_type}} property to match the integrated property required by SGr.

+ ]]> +
+ en +
+
+ + JSON_object + + +
+ Accept + application/json +
+
+ Authorization + Bearer {{token}} +
+
+ GET + /api/v1/metering_code + + + tariff_type + {{tariff_type}} + + + metering_code + {{metering_code}} + + + start_timestamp + [[start_timestamp]] + + + end_timestamp + [[end_timestamp]] + + + + JSONataExpression + + + + +
+
+
+
+
+
+
+
+
\ No newline at end of file From 1fe04ab938342551ea74f6dd1b6d96e0e69cae6e Mon Sep 17 00:00:00 2001 From: Matthias Krebs Date: Thu, 23 Oct 2025 10:56:06 +0200 Subject: [PATCH 4/4] updated changelog. --- CommHandler/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CommHandler/CHANGELOG.md b/CommHandler/CHANGELOG.md index 026117f7..919d721e 100644 --- a/CommHandler/CHANGELOG.md +++ b/CommHandler/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - requires updated driver implementations supporting driver API 2.4 +- requires `sgr-specification` 2.2 ## [2.4.2] - 2025-10-09