This is an automated email from the ASF dual-hosted git repository. tenthe pushed a commit to branch 4690-deprecate-legacy-plc4x-s7-and-modbus-adapters in repository https://gitbox.apache.org/repos/asf/streampipes.git
commit 177787060ef3a0ca292c2533d093370fd51dca6a Author: Philipp Zehnder <[email protected]> AuthorDate: Tue Jul 7 13:15:20 2026 +0000 feat(#4690): migrate PLC4X S7 to generic adapter --- .../connectors/plc/PlcConnectorsModuleExport.java | 2 + .../Plc4xS7ToGenericAdapterMigration.java | 231 +++++++++++++++++++++ .../Plc4xS7ToGenericAdapterMigrationTest.java | 200 ++++++++++++++++++ 3 files changed, 433 insertions(+) diff --git a/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/PlcConnectorsModuleExport.java b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/PlcConnectorsModuleExport.java index 6f53414919..73ca2d664f 100644 --- a/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/PlcConnectorsModuleExport.java +++ b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/PlcConnectorsModuleExport.java @@ -26,6 +26,7 @@ import org.apache.streampipes.extensions.api.pe.IStreamPipesPipelineElement; import org.apache.streampipes.extensions.connectors.plc.adapter.GenericAdapterGenerator; import org.apache.streampipes.extensions.connectors.plc.adapter.migration.Plc4xModbusAdapterMigrationV1; import org.apache.streampipes.extensions.connectors.plc.adapter.migration.Plc4xS7AdapterMigrationV1; +import org.apache.streampipes.extensions.connectors.plc.adapter.migration.Plc4xS7ToGenericAdapterMigration; import org.apache.streampipes.extensions.connectors.plc.adapter.modbus.Plc4xModbusAdapter; import org.apache.streampipes.extensions.connectors.plc.adapter.s7.Plc4xS7Adapter; import org.apache.streampipes.extensions.connectors.plc.cache.SpCachedPlcConnectionManager; @@ -65,6 +66,7 @@ public class PlcConnectorsModuleExport implements IExtensionModuleExport { public List<IModelMigrator<?, ?>> migrators() { return List.of( new Plc4xS7AdapterMigrationV1(), + new Plc4xS7ToGenericAdapterMigration(), new Plc4xModbusAdapterMigrationV1() ); } diff --git a/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/migration/Plc4xS7ToGenericAdapterMigration.java b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/migration/Plc4xS7ToGenericAdapterMigration.java new file mode 100644 index 0000000000..78c86f9ef5 --- /dev/null +++ b/streampipes-extensions/streampipes-connectors-plc/src/main/java/org/apache/streampipes/extensions/connectors/plc/adapter/migration/Plc4xS7ToGenericAdapterMigration.java @@ -0,0 +1,231 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.streampipes.extensions.connectors.plc.adapter.migration; + +import org.apache.streampipes.extensions.api.extractor.IStaticPropertyExtractor; +import org.apache.streampipes.extensions.api.migration.IAdapterMigrator; +import org.apache.streampipes.extensions.connectors.plc.adapter.generic.config.AdapterConfigurationProvider; +import org.apache.streampipes.extensions.connectors.plc.adapter.generic.model.Plc4xLabels; +import org.apache.streampipes.model.connect.adapter.AdapterDescription; +import org.apache.streampipes.model.extensions.svcdiscovery.SpServiceTagPrefix; +import org.apache.streampipes.model.migration.MigrationResult; +import org.apache.streampipes.model.migration.ModelMigratorConfig; +import org.apache.streampipes.model.staticproperty.CollectionStaticProperty; +import org.apache.streampipes.model.staticproperty.Option; +import org.apache.streampipes.model.staticproperty.StaticProperty; +import org.apache.streampipes.model.staticproperty.StaticPropertyGroup; +import org.apache.streampipes.sdk.StaticProperties; +import org.apache.streampipes.sdk.extractor.StaticPropertyExtractor; +import org.apache.streampipes.sdk.helpers.Alternatives; +import org.apache.streampipes.sdk.helpers.CodeLanguage; +import org.apache.streampipes.sdk.helpers.Labels; +import org.apache.streampipes.sdk.utils.Datatypes; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class Plc4xS7ToGenericAdapterMigration implements IAdapterMigrator { + + private static final String OLD_APP_ID = "org.apache.streampipes.connect.iiot.adapters.plc4x.s7"; + static final String NEW_APP_ID = AdapterConfigurationProvider.ID + "s7"; + + private static final String PLC_IP = "plc_ip"; + private static final String PLC_POLLING_INTERVAL = "plc_polling_interval"; + private static final String PLC_NODES = "plc_nodes"; + private static final String PLC_NODE_NAME = "plc_node_name"; + private static final String PLC_NODE_RUNTIME_NAME = "plc_node_runtime_name"; + private static final String PLC_NODE_TYPE = "plc_node_type"; + private static final String PLC_NODE_INPUT_ALTERNATIVES = "plc_node_input_alternatives"; + private static final String PLC_NODE_INPUT_COLLECTION_ALTERNATIVE = "plc_node_input_collection_alternative"; + + @Override + public ModelMigratorConfig config() { + return new ModelMigratorConfig( + OLD_APP_ID, + SpServiceTagPrefix.ADAPTER, + 1, + 1 + ); + } + + @Override + public MigrationResult<AdapterDescription> migrate(AdapterDescription element, + IStaticPropertyExtractor extractor) throws RuntimeException { + var ipAddress = extractor.singleValueParameter(PLC_IP, String.class); + var pollingInterval = extractor.singleValueParameter(PLC_POLLING_INTERVAL, Integer.class); + + var splitAddress = SplitPlcAddress.from(ipAddress); + + element.setAppId(NEW_APP_ID); + element.setConfig(List.of( + StaticProperties.stringFreeTextProperty(Labels.withId(Plc4xLabels.PLC_IP), splitAddress.host()), + StaticProperties.integerFreeTextProperty(Labels.withId(Plc4xLabels.PLC_POLLING_INTERVAL), pollingInterval), + makeSupportedTransports(), + makeTransportMetadata(), + makeProtocolMetadata(splitAddress.queryParameters()), + makeCodeBlock(extractor) + )); + + return MigrationResult.success(element); + } + + private StaticProperty makeSupportedTransports() { + return StaticProperties.singleValueSelection( + Labels.withId(Plc4xLabels.SUPPORTED_TRANSPORTS), + List.of(new Option("tcp", true)) + ); + } + + private StaticProperty makeTransportMetadata() { + return StaticProperties.alternatives( + Labels.withId(Plc4xLabels.TRANSPORT_METADATA), + Alternatives.from( + Labels.withId(Plc4xLabels.REQUIRED_OPTIONS), + makeRuntimeResolvableGroup(Plc4xLabels.REQUIRED_GROUP_TRANSPORT), + true + ), + Alternatives.from( + Labels.withId(Plc4xLabels.ADVANCED_OPTIONS), + makeRuntimeResolvableGroup(Plc4xLabels.ADVANCED_GROUP_TRANSPORT) + ) + ); + } + + private StaticProperty makeRuntimeResolvableGroup(String internalName) { + var group = StaticProperties.runtimeResolvableGroupStaticProperty( + Labels.withId(internalName), + List.of(Plc4xLabels.SUPPORTED_TRANSPORTS) + ); + group.setStaticProperties(List.of()); + group.setHorizontalRendering(false); + return group; + } + + private StaticProperty makeProtocolMetadata(List<QueryParameter> queryParameters) { + return StaticProperties.alternatives( + Labels.withId(Plc4xLabels.PROTOCOL_METADATA), + Alternatives.from( + Labels.withId(Plc4xLabels.REQUIRED_OPTIONS), + makeProtocolGroup(Plc4xLabels.REQUIRED_GROUP_PROTOCOL, queryParameters), + true + ), + Alternatives.from( + Labels.withId(Plc4xLabels.ADVANCED_OPTIONS), + makeProtocolGroup(Plc4xLabels.ADVANCED_GROUP_PROTOCOL, List.of()) + ) + ); + } + + private StaticProperty makeProtocolGroup(String internalName, + List<QueryParameter> queryParameters) { + var group = StaticProperties.group( + Labels.withId(internalName), + false, + queryParameters.stream() + .map(this::makeProtocolParameter) + .toArray(StaticProperty[]::new) + ); + group.setHorizontalRendering(false); + return group; + } + + private StaticProperty makeProtocolParameter(QueryParameter queryParameter) { + var property = StaticProperties.freeTextProperty( + Labels.from(queryParameter.name(), queryParameter.name(), ""), + Datatypes.String + ); + property.setOptional(true); + property.setValue(queryParameter.value()); + return property; + } + + private StaticProperty makeCodeBlock(IStaticPropertyExtractor extractor) { + var codeBlock = StaticProperties.codeStaticProperty( + Labels.withId(Plc4xLabels.PLC_CODE_BLOCK), + CodeLanguage.None, + "" + ); + codeBlock.setValue(makeTags(extractor)); + return codeBlock; + } + + private String makeTags(IStaticPropertyExtractor extractor) { + var selectedAlternative = extractor.selectedAlternativeInternalId(PLC_NODE_INPUT_ALTERNATIVES); + if (Objects.equals(selectedAlternative, PLC_NODE_INPUT_COLLECTION_ALTERNATIVE)) { + var nodes = extractor.getStaticPropertyByName(PLC_NODES, CollectionStaticProperty.class); + return makeTagsFromCollection(nodes); + } else { + return extractor.codeblockValue(Plc4xLabels.PLC_CODE_BLOCK); + } + } + + private String makeTagsFromCollection(CollectionStaticProperty nodes) { + var tags = new ArrayList<String>(); + for (StaticProperty member : nodes.getMembers()) { + var memberExtractor = + StaticPropertyExtractor.from(((StaticPropertyGroup) member).getStaticProperties(), new ArrayList<>()); + tags.add("%s=%s:%s".formatted( + memberExtractor.textParameter(PLC_NODE_RUNTIME_NAME), + memberExtractor.textParameter(PLC_NODE_NAME), + memberExtractor.selectedSingleValue(PLC_NODE_TYPE, String.class) + .toUpperCase() + .replaceAll(" ", "_") + )); + } + return String.join(System.lineSeparator(), tags); + } + + private record SplitPlcAddress(String host, + List<QueryParameter> queryParameters) { + + static SplitPlcAddress from(String address) { + var splitAddress = address.split("\\?", 2); + var host = splitAddress[0]; + var queryParameters = splitAddress.length == 2 + ? parseQueryParameters(splitAddress[1]) + : List.<QueryParameter>of(); + + return new SplitPlcAddress(host, queryParameters); + } + + private static List<QueryParameter> parseQueryParameters(String query) { + if (query == null || query.isBlank()) { + return List.of(); + } + return Arrays.stream(query.split("&")) + .map(QueryParameter::from) + .filter(Objects::nonNull) + .toList(); + } + } + + private record QueryParameter(String name, + String value) { + + static QueryParameter from(String queryParameter) { + var splitParameter = queryParameter.split("=", 2); + if (splitParameter.length != 2 || splitParameter[0].isBlank()) { + return null; + } + return new QueryParameter(splitParameter[0], splitParameter[1]); + } + } +} diff --git a/streampipes-extensions/streampipes-connectors-plc/src/test/java/org/apache/streampipes/extensions/connectors/plc/adapter/migration/Plc4xS7ToGenericAdapterMigrationTest.java b/streampipes-extensions/streampipes-connectors-plc/src/test/java/org/apache/streampipes/extensions/connectors/plc/adapter/migration/Plc4xS7ToGenericAdapterMigrationTest.java new file mode 100644 index 0000000000..9909db9877 --- /dev/null +++ b/streampipes-extensions/streampipes-connectors-plc/src/test/java/org/apache/streampipes/extensions/connectors/plc/adapter/migration/Plc4xS7ToGenericAdapterMigrationTest.java @@ -0,0 +1,200 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.apache.streampipes.extensions.connectors.plc.adapter.migration; + +import org.apache.streampipes.extensions.connectors.plc.adapter.generic.model.Plc4xConnectionExtractor; +import org.apache.streampipes.extensions.connectors.plc.adapter.generic.model.Plc4xLabels; +import org.apache.streampipes.model.connect.adapter.AdapterDescription; +import org.apache.streampipes.model.staticproperty.Option; +import org.apache.streampipes.model.staticproperty.StaticProperty; +import org.apache.streampipes.sdk.StaticProperties; +import org.apache.streampipes.sdk.extractor.StaticPropertyExtractor; +import org.apache.streampipes.sdk.helpers.Alternatives; +import org.apache.streampipes.sdk.helpers.CodeLanguage; +import org.apache.streampipes.sdk.helpers.Labels; +import org.apache.streampipes.sdk.helpers.Options; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class Plc4xS7ToGenericAdapterMigrationTest { + + private static final String OLD_APP_ID = "org.apache.streampipes.connect.iiot.adapters.plc4x.s7"; + private static final String PLC_IP = "plc_ip"; + private static final String PLC_POLLING_INTERVAL = "plc_polling_interval"; + private static final String PLC_NODES = "plc_nodes"; + private static final String PLC_NODE_NAME = "plc_node_name"; + private static final String PLC_NODE_RUNTIME_NAME = "plc_node_runtime_name"; + private static final String PLC_NODE_TYPE = "plc_node_type"; + private static final String PLC_NODE_INPUT_ALTERNATIVES = "plc_node_input_alternatives"; + private static final String PLC_NODE_INPUT_CODE_BLOCK_ALTIVE = "plc_node_input_code_block_altive"; + private static final String PLC_NODE_INPUT_COLLECTION_ALTERNATIVE = "plc_node_input_collection_alternative"; + + private Plc4xS7ToGenericAdapterMigration migration; + + @BeforeEach + void setUp() { + this.migration = new Plc4xS7ToGenericAdapterMigration(); + } + + @Test + void migrateSimpleCollectionToGenericS7Adapter() { + var adapter = makeAdapter( + "192.168.34.56?remote-rack=0&remote-slot=3", + makeCollectionAlternative() + ); + + var result = migration.migrate(adapter, StaticPropertyExtractor.from(adapter.getConfig())); + var migratedAdapter = result.element(); + var settings = new Plc4xConnectionExtractor( + StaticPropertyExtractor.from(migratedAdapter.getConfig()), + "s7" + ).makeSettings(); + + assertEquals(Plc4xS7ToGenericAdapterMigration.NEW_APP_ID, migratedAdapter.getAppId()); + assertTrue(settings.connectionString().startsWith("s7-light:tcp://192.168.34.56?")); + assertTrue(Arrays.asList(settings.connectionString().split("\\?", 2)[1].split("&")) + .containsAll(List.of("remote-rack=0", "remote-slot=3"))); + assertEquals(1000, settings.pollingInterval()); + assertEquals("%I0.0:BOOL", settings.nodes().get("input")); + assertEquals("%Q0.4:TIME_OF_DAY", settings.nodes().get("output")); + } + + @Test + void migrateAdvancedCodeBlockToGenericS7Adapter() { + var codeBlock = """ + temperature=%I0.0:INT + pressure=%Q0.4:REAL + """; + var adapter = makeAdapter( + "192.168.34.56", + makeCodeBlockAlternative(codeBlock) + ); + + var result = migration.migrate(adapter, StaticPropertyExtractor.from(adapter.getConfig())); + var migratedAdapter = result.element(); + var settings = new Plc4xConnectionExtractor( + StaticPropertyExtractor.from(migratedAdapter.getConfig()), + "s7" + ).makeSettings(); + + assertEquals(Plc4xS7ToGenericAdapterMigration.NEW_APP_ID, migratedAdapter.getAppId()); + assertEquals("s7-light:tcp://192.168.34.56", settings.connectionString()); + assertEquals("%I0.0:INT", settings.nodes().get("temperature")); + assertEquals("%Q0.4:REAL", settings.nodes().get("pressure")); + } + + private AdapterDescription makeAdapter(StaticProperty nodeInputAlternative) { + return makeAdapter("192.168.34.56", nodeInputAlternative); + } + + private AdapterDescription makeAdapter(String ipAddress, + StaticProperty nodeInputAlternative) { + var adapter = new AdapterDescription(); + adapter.setAppId(OLD_APP_ID); + adapter.setVersion(1); + adapter.setConfig(List.of( + StaticProperties.stringFreeTextProperty(Labels.withId(PLC_IP), ipAddress), + StaticProperties.integerFreeTextProperty(Labels.withId(PLC_POLLING_INTERVAL), 1000), + nodeInputAlternative + )); + return adapter; + } + + private StaticProperty makeCollectionAlternative() { + var collection = StaticProperties.collection( + Labels.withId(PLC_NODES), + StaticProperties.stringFreeTextProperty(Labels.withId(PLC_NODE_RUNTIME_NAME)), + StaticProperties.stringFreeTextProperty(Labels.withId(PLC_NODE_NAME)), + StaticProperties.singleValueSelection( + Labels.withId(PLC_NODE_TYPE), + Options.from("Bool", "Time of day") + ) + ); + collection.setMembers(new ArrayList<>(List.of( + makeNode("input", "%I0.0", "Bool"), + makeNode("output", "%Q0.4", "Time of day") + ))); + + return StaticProperties.alternatives( + Labels.withId(PLC_NODE_INPUT_ALTERNATIVES), + Alternatives.from( + Labels.withId(PLC_NODE_INPUT_COLLECTION_ALTERNATIVE), + collection, + true + ), + Alternatives.from( + Labels.withId(PLC_NODE_INPUT_CODE_BLOCK_ALTIVE), + StaticProperties.codeStaticProperty( + Labels.withId(Plc4xLabels.PLC_CODE_BLOCK), + CodeLanguage.None, + "" + ) + ) + ); + } + + private StaticProperty makeCodeBlockAlternative(String codeBlockValue) { + var codeBlock = StaticProperties.codeStaticProperty( + Labels.withId(Plc4xLabels.PLC_CODE_BLOCK), + CodeLanguage.None, + "" + ); + codeBlock.setValue(codeBlockValue); + + return StaticProperties.alternatives( + Labels.withId(PLC_NODE_INPUT_ALTERNATIVES), + Alternatives.from( + Labels.withId(PLC_NODE_INPUT_COLLECTION_ALTERNATIVE), + StaticProperties.collection( + Labels.withId(PLC_NODES), + StaticProperties.stringFreeTextProperty(Labels.withId(PLC_NODE_RUNTIME_NAME)) + ) + ), + Alternatives.from( + Labels.withId(PLC_NODE_INPUT_CODE_BLOCK_ALTIVE), + codeBlock, + true + ) + ); + } + + private StaticProperty makeNode(String runtimeName, + String nodeName, + String type) { + return StaticProperties.group( + Labels.withId("node"), + StaticProperties.stringFreeTextProperty(Labels.withId(PLC_NODE_RUNTIME_NAME), runtimeName), + StaticProperties.stringFreeTextProperty(Labels.withId(PLC_NODE_NAME), nodeName), + StaticProperties.singleValueSelection( + Labels.withId(PLC_NODE_TYPE), + List.of( + new Option(type, true) + ) + ) + ); + } +}
