This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 900cfc883ab0 CAMEL-25566: camel-milo - monitor several nodes with one 
consumer (#27780)
900cfc883ab0 is described below

commit 900cfc883ab00e89ce414e729151e2b80777241b
Author: Serdar Gökay <[email protected]>
AuthorDate: Thu Oct 15 14:59:39 2026 +0300

    CAMEL-25566: camel-milo - monitor several nodes with one consumer (#27780)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Co-authored-by: Claude Fable 5.1 <[email protected]>
---
 .../camel/catalog/components/milo-client.json      |  52 +++--
 .../camel/catalog/docs/milo-client-component.adoc  |  25 ++
 .../milo/client/MiloClientEndpointConfigurer.java  |   3 +
 .../milo/client/MiloClientEndpointUriFactory.java  |   3 +-
 .../camel/component/milo/client/milo-client.json   |  52 +++--
 .../src/main/docs/milo-client-component.adoc       |  25 ++
 .../apache/camel/component/milo/MiloConstants.java |   5 +
 .../milo/client/MiloClientConnection.java          |  23 +-
 .../component/milo/client/MiloClientConsumer.java  |  53 ++++-
 .../component/milo/client/MiloClientEndpoint.java  |  14 ++
 .../milo/client/internal/SubscriptionManager.java  | 152 +++++++++----
 .../component/milo/MonitorItemLifecycleTest.java   | 252 +++++++++++++++++++++
 .../camel/component/milo/MonitorNodesTest.java     | 240 ++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |  13 ++
 .../dsl/MiloClientEndpointBuilderFactory.java      |  29 +++
 15 files changed, 836 insertions(+), 105 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-client.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-client.json
index 58b851de167e..f592a62caebd 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-client.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/milo-client.json
@@ -55,7 +55,8 @@
   },
   "headers": {
     "CamelMiloNodeIds": { "index": 0, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"List", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The node ids.", "constantName": 
"org.apache.camel.component.milo.MiloConstants#HEADER_NODE_IDS" },
-    "CamelMiloAwait": { "index": 1, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The await setting for writes.", 
"constantName": "org.apache.camel.component.milo.MiloConstants#HEADER_AWAIT" }
+    "CamelMiloAwait": { "index": 1, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The await setting for writes.", 
"constantName": "org.apache.camel.component.milo.MiloConstants#HEADER_AWAIT" },
+    "CamelMiloNodeId": { "index": 2, "kind": "header", "displayName": "", 
"group": "consumer", "label": "consumer", "required": false, "javaType": 
"String", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The node ID of the value, as configured in the 
node or nodes option. Not to be confused with CamelMiloNodeIds, the list of 
nodes that the producer reads.", "constantName": 
"org.apache.camel.component.milo.MiloConstants#HEADER_NODE_ID" }
   },
   "properties": {
     "endpointUri": { "index": 0, "kind": "path", "displayName": "Endpoint 
Uri", "group": "common", "label": "", "required": true, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "The OPC UA server 
endpoint" },
@@ -72,29 +73,30 @@
     "omitNullValues": { "index": 11, "kind": "parameter", "displayName": "Omit 
Null Values", "group": "common", "label": "", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": true, "description": "Omit notifications in 
case of null values" },
     "queueSize": { "index": 12, "kind": "parameter", "displayName": "Queue 
Size", "group": "common", "label": "", "required": false, "type": "integer", 
"javaType": "java.lang.Integer", "deprecated": false, "autowired": false, 
"secret": false, "description": "The queue size used for OPC UA subscriptions. 
If not set, a queue size of 1 is requested." },
     "samplingInterval": { "index": 13, "kind": "parameter", "displayName": 
"Sampling Interval", "group": "common", "label": "", "required": false, "type": 
"number", "javaType": "java.lang.Double", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": "1000.0", "description": "The sampling 
interval in milliseconds" },
-    "bridgeErrorHandler": { "index": 14, "kind": "parameter", "displayName": 
"Bridge Error Handler", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Allows for bridging the consumer to the 
Camel routing Error Handler, which mean any exceptions (if possible) occurred 
while the Camel consumer is trying to pickup incoming [...]
-    "exceptionHandler": { "index": 15, "kind": "parameter", "displayName": 
"Exception Handler", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "object", "javaType": 
"org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", 
"deprecated": false, "autowired": false, "secret": false, "description": "To 
let the consumer use a custom ExceptionHandler. Notice if the option 
bridgeErrorHandler is enabled then this option is not in use. By de [...]
-    "exchangePattern": { "index": 16, "kind": "parameter", "displayName": 
"Exchange Pattern", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "enum", "javaType": 
"org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], 
"deprecated": false, "autowired": false, "secret": false, "description": "Sets 
the exchange pattern when the consumer creates an exchange." },
-    "lazyStartProducer": { "index": 17, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produ [...]
-    "allowedSecurityPolicies": { "index": 18, "kind": "parameter", 
"displayName": "Allowed Security Policies", "group": "client", "label": 
"client", "required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "A set of allowed 
security policy URIs. Default is to accept all and use t [...]
-    "applicationName": { "index": 19, "kind": "parameter", "displayName": 
"Application Name", "group": "client", "label": "client", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": "Apache Camel adapter for 
Eclipse Milo", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The application name" },
-    "applicationUri": { "index": 20, "kind": "parameter", "displayName": 
"Application Uri", "group": "client", "label": "client", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": 
"http:\/\/camel.apache.org\/EclipseMilo\/Client", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The application URI" },
-    "channelLifetime": { "index": 21, "kind": "parameter", "displayName": 
"Channel Lifetime", "group": "client", "label": "client", "required": false, 
"type": "integer", "javaType": "java.lang.Long", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Channel lifetime in 
milliseconds" },
-    "keyAlias": { "index": 22, "kind": "parameter", "displayName": "Key 
Alias", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The name of the key in 
the keystore file" },
-    "keyPassword": { "index": 23, "kind": "parameter", "displayName": "Key 
Password", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The key password" },
-    "keyStorePassword": { "index": 24, "kind": "parameter", "displayName": 
"Key Store Password", "group": "client", "label": "client", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The keystore password" },
-    "keyStoreType": { "index": 25, "kind": "parameter", "displayName": "Key 
Store Type", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The key store type" },
-    "keyStoreUrl": { "index": 26, "kind": "parameter", "displayName": "Key 
Store Url", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The URL where the key 
should be loaded from" },
-    "maxPendingPublishRequests": { "index": 27, "kind": "parameter", 
"displayName": "Max Pending Publish Requests", "group": "client", "label": 
"client", "required": false, "type": "integer", "javaType": "java.lang.Long", 
"deprecated": false, "autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The maximum number of 
pending publish requests" },
-    "maxResponseMessageSize": { "index": 28, "kind": "parameter", 
"displayName": "Max Response Message Size", "group": "client", "label": 
"client", "required": false, "type": "integer", "javaType": "java.lang.Long", 
"deprecated": false, "autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The maximum number of 
bytes a response message may have" },
-    "overrideHost": { "index": 29, "kind": "parameter", "displayName": 
"Override Host", "group": "client", "label": "client", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Override the server 
reported endpoint host with the host from the endpoint URI." },
-    "overridePort": { "index": 30, "kind": "parameter", "displayName": 
"Override Port", "group": "client", "label": "client", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Override the server 
reported endpoint port with the port from the endpoint URI." },
-    "productUri": { "index": 31, "kind": "parameter", "displayName": "Product 
Uri", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": 
"http:\/\/camel.apache.org\/EclipseMilo", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The product URI" },
-    "requestedPublishingInterval": { "index": 32, "kind": "parameter", 
"displayName": "Requested Publishing Interval", "group": "client", "label": 
"client", "required": false, "type": "number", "javaType": "java.lang.Double", 
"deprecated": false, "autowired": false, "secret": false, "defaultValue": 
"1_000.0", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The requested publishing 
interval in m [...]
-    "requestTimeout": { "index": 33, "kind": "parameter", "displayName": 
"Request Timeout", "group": "client", "label": "client", "required": false, 
"type": "integer", "javaType": "java.lang.Long", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Request timeout in 
milliseconds" },
-    "sessionName": { "index": 34, "kind": "parameter", "displayName": "Session 
Name", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Session name" },
-    "sessionTimeout": { "index": 35, "kind": "parameter", "displayName": 
"Session Timeout", "group": "client", "label": "client", "required": false, 
"type": "integer", "javaType": "java.lang.Long", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Session timeout in 
milliseconds" },
-    "password": { "index": 36, "kind": "parameter", "displayName": "Password", 
"group": "security", "label": "security", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The password for 
authentication. Use this instead of embedding credentials in th [...]
-    "username": { "index": 37, "kind": "parameter", "displayName": "Username", 
"group": "security", "label": "security", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The username for 
authentication. Use this instead of embedding credentials in the endpoint URI 
when t [...]
+    "nodes": { "index": 14, "kind": "parameter", "displayName": "Nodes", 
"group": "consumer", "label": "consumer", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "description": "Comma-separated node definitions to monitor 
with one consumer, instead of node (see Node ID)" },
+    "bridgeErrorHandler": { "index": 15, "kind": "parameter", "displayName": 
"Bridge Error Handler", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Allows for bridging the consumer to the 
Camel routing Error Handler, which mean any exceptions (if possible) occurred 
while the Camel consumer is trying to pickup incoming [...]
+    "exceptionHandler": { "index": 16, "kind": "parameter", "displayName": 
"Exception Handler", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "object", "javaType": 
"org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", 
"deprecated": false, "autowired": false, "secret": false, "description": "To 
let the consumer use a custom ExceptionHandler. Notice if the option 
bridgeErrorHandler is enabled then this option is not in use. By de [...]
+    "exchangePattern": { "index": 17, "kind": "parameter", "displayName": 
"Exchange Pattern", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "enum", "javaType": 
"org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], 
"deprecated": false, "autowired": false, "secret": false, "description": "Sets 
the exchange pattern when the consumer creates an exchange." },
+    "lazyStartProducer": { "index": 18, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produ [...]
+    "allowedSecurityPolicies": { "index": 19, "kind": "parameter", 
"displayName": "Allowed Security Policies", "group": "client", "label": 
"client", "required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "A set of allowed 
security policy URIs. Default is to accept all and use t [...]
+    "applicationName": { "index": 20, "kind": "parameter", "displayName": 
"Application Name", "group": "client", "label": "client", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": "Apache Camel adapter for 
Eclipse Milo", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The application name" },
+    "applicationUri": { "index": 21, "kind": "parameter", "displayName": 
"Application Uri", "group": "client", "label": "client", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": 
"http:\/\/camel.apache.org\/EclipseMilo\/Client", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The application URI" },
+    "channelLifetime": { "index": 22, "kind": "parameter", "displayName": 
"Channel Lifetime", "group": "client", "label": "client", "required": false, 
"type": "integer", "javaType": "java.lang.Long", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Channel lifetime in 
milliseconds" },
+    "keyAlias": { "index": 23, "kind": "parameter", "displayName": "Key 
Alias", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The name of the key in 
the keystore file" },
+    "keyPassword": { "index": 24, "kind": "parameter", "displayName": "Key 
Password", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The key password" },
+    "keyStorePassword": { "index": 25, "kind": "parameter", "displayName": 
"Key Store Password", "group": "client", "label": "client", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The keystore password" },
+    "keyStoreType": { "index": 26, "kind": "parameter", "displayName": "Key 
Store Type", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The key store type" },
+    "keyStoreUrl": { "index": 27, "kind": "parameter", "displayName": "Key 
Store Url", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The URL where the key 
should be loaded from" },
+    "maxPendingPublishRequests": { "index": 28, "kind": "parameter", 
"displayName": "Max Pending Publish Requests", "group": "client", "label": 
"client", "required": false, "type": "integer", "javaType": "java.lang.Long", 
"deprecated": false, "autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The maximum number of 
pending publish requests" },
+    "maxResponseMessageSize": { "index": 29, "kind": "parameter", 
"displayName": "Max Response Message Size", "group": "client", "label": 
"client", "required": false, "type": "integer", "javaType": "java.lang.Long", 
"deprecated": false, "autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The maximum number of 
bytes a response message may have" },
+    "overrideHost": { "index": 30, "kind": "parameter", "displayName": 
"Override Host", "group": "client", "label": "client", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Override the server 
reported endpoint host with the host from the endpoint URI." },
+    "overridePort": { "index": 31, "kind": "parameter", "displayName": 
"Override Port", "group": "client", "label": "client", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Override the server 
reported endpoint port with the port from the endpoint URI." },
+    "productUri": { "index": 32, "kind": "parameter", "displayName": "Product 
Uri", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": 
"http:\/\/camel.apache.org\/EclipseMilo", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The product URI" },
+    "requestedPublishingInterval": { "index": 33, "kind": "parameter", 
"displayName": "Requested Publishing Interval", "group": "client", "label": 
"client", "required": false, "type": "number", "javaType": "java.lang.Double", 
"deprecated": false, "autowired": false, "secret": false, "defaultValue": 
"1_000.0", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The requested publishing 
interval in m [...]
+    "requestTimeout": { "index": 34, "kind": "parameter", "displayName": 
"Request Timeout", "group": "client", "label": "client", "required": false, 
"type": "integer", "javaType": "java.lang.Long", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Request timeout in 
milliseconds" },
+    "sessionName": { "index": 35, "kind": "parameter", "displayName": "Session 
Name", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Session name" },
+    "sessionTimeout": { "index": 36, "kind": "parameter", "displayName": 
"Session Timeout", "group": "client", "label": "client", "required": false, 
"type": "integer", "javaType": "java.lang.Long", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Session timeout in 
milliseconds" },
+    "password": { "index": 37, "kind": "parameter", "displayName": "Password", 
"group": "security", "label": "security", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The password for 
authentication. Use this instead of embedding credentials in th [...]
+    "username": { "index": 38, "kind": "parameter", "displayName": "Username", 
"group": "security", "label": "security", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The username for 
authentication. Use this instead of embedding credentials in the endpoint URI 
when t [...]
   }
 }
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc
index 3c5b1aecdb95..e04f72f92737 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/milo-client-component.adoc
@@ -127,6 +127,31 @@ However Camel allows to wrap the actual value inside 
`RAW(…)`, which makes esc
 
milo-client:opc.tcp://user:password@localhost:12345?node=RAW(nsu=http://foo.bar;s=foo/bar)
 ----
 
+=== Monitoring several nodes
+
+A consumer can monitor several nodes with the `nodes` option instead of `node` 
(the two options cannot be combined).
+The consumer creates the monitored items of all nodes together and deletes 
them together when it stops. Every value
+arrives in its own exchange, and the `CamelMiloNodeId` header holds the node 
ID as it was configured.
+
+The `nodes` option takes a comma-separated list of <<nodeid, Node IDs>>. The 
entries are trimmed, empty entries are
+skipped and a node that is listed twice is monitored once:
+
+----
+milo-client:opc.tcp://localhost:12345?nodes=RAW(ns=2;s=line1/temperature,ns=2;s=line1/pressure)
+----
+
+A string node ID that contains a comma cannot be used in `nodes`; monitor it 
with the `node` option instead. Long lists
+can come from a property placeholder, for example `+nodes=RAW({{tags}})+`.
+
+All nodes of the list share the `samplingInterval`, `queueSize` and 
`omitNullValues` options of the consumer and its
+monitor filter (`monitorFilterType`, `dataChangeFilter*`). Tags that need a 
different sampling interval or deadband
+need a consumer of their own.
+
+A node that does not exist on the server does not affect the other nodes. A 
node that cannot be monitored is
+reported once with its status code and is not retried; restarting the route or 
a rebuilt connection to the server creates the monitored items again.
+To see such nodes, set `omitNullValues=false`: the consumer then delivers one 
exchange with the bad status code for that
+node, and the `CamelMiloNodeId` header names it.
+
 === Method ID
 
 It is possible to perform methods calls on OPC UA nodes. If the parameter 
`method` is set to the Node ID of a method call (the node ID must be set to the 
parent object in this case),
diff --git 
a/components/camel-milo/src/generated/java/org/apache/camel/component/milo/client/MiloClientEndpointConfigurer.java
 
b/components/camel-milo/src/generated/java/org/apache/camel/component/milo/client/MiloClientEndpointConfigurer.java
index c415e53a4812..921685c5fb79 100644
--- 
a/components/camel-milo/src/generated/java/org/apache/camel/component/milo/client/MiloClientEndpointConfigurer.java
+++ 
b/components/camel-milo/src/generated/java/org/apache/camel/component/milo/client/MiloClientEndpointConfigurer.java
@@ -71,6 +71,7 @@ public class MiloClientEndpointConfigurer extends 
PropertyConfigurerSupport impl
         case "monitorfiltertype":
         case "monitorFilterType": 
target.getMonitorFilterConfiguration().setMonitorFilterType(property(camelContext,
 org.apache.camel.component.milo.client.MonitorFilterType.class, value)); 
return true;
         case "node": target.setNode(property(camelContext, 
java.lang.String.class, value)); return true;
+        case "nodes": target.setNodes(property(camelContext, 
java.lang.String.class, value)); return true;
         case "omitnullvalues":
         case "omitNullValues": target.setOmitNullValues(property(camelContext, 
boolean.class, value)); return true;
         case "overridehost":
@@ -148,6 +149,7 @@ public class MiloClientEndpointConfigurer extends 
PropertyConfigurerSupport impl
         case "monitorfiltertype":
         case "monitorFilterType": return 
org.apache.camel.component.milo.client.MonitorFilterType.class;
         case "node": return java.lang.String.class;
+        case "nodes": return java.lang.String.class;
         case "omitnullvalues":
         case "omitNullValues": return boolean.class;
         case "overridehost":
@@ -226,6 +228,7 @@ public class MiloClientEndpointConfigurer extends 
PropertyConfigurerSupport impl
         case "monitorfiltertype":
         case "monitorFilterType": return 
target.getMonitorFilterConfiguration().getMonitorFilterType();
         case "node": return target.getNode();
+        case "nodes": return target.getNodes();
         case "omitnullvalues":
         case "omitNullValues": return target.isOmitNullValues();
         case "overridehost":
diff --git 
a/components/camel-milo/src/generated/java/org/apache/camel/component/milo/client/MiloClientEndpointUriFactory.java
 
b/components/camel-milo/src/generated/java/org/apache/camel/component/milo/client/MiloClientEndpointUriFactory.java
index 15e5b54e0ab5..678e4ed402d7 100644
--- 
a/components/camel-milo/src/generated/java/org/apache/camel/component/milo/client/MiloClientEndpointUriFactory.java
+++ 
b/components/camel-milo/src/generated/java/org/apache/camel/component/milo/client/MiloClientEndpointUriFactory.java
@@ -24,7 +24,7 @@ public class MiloClientEndpointUriFactory extends 
org.apache.camel.support.compo
     private static final Set<String> ENDPOINT_IDENTITY_PROPERTY_NAMES;
     private static final Map<String, String> MULTI_VALUE_PREFIXES;
     static {
-        Set<String> props = new HashSet<>(38);
+        Set<String> props = new HashSet<>(39);
         props.add("allowedSecurityPolicies");
         props.add("applicationName");
         props.add("applicationUri");
@@ -51,6 +51,7 @@ public class MiloClientEndpointUriFactory extends 
org.apache.camel.support.compo
         props.add("method");
         props.add("monitorFilterType");
         props.add("node");
+        props.add("nodes");
         props.add("omitNullValues");
         props.add("overrideHost");
         props.add("overridePort");
diff --git 
a/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/client/milo-client.json
 
b/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/client/milo-client.json
index 58b851de167e..f592a62caebd 100644
--- 
a/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/client/milo-client.json
+++ 
b/components/camel-milo/src/generated/resources/META-INF/org/apache/camel/component/milo/client/milo-client.json
@@ -55,7 +55,8 @@
   },
   "headers": {
     "CamelMiloNodeIds": { "index": 0, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"List", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The node ids.", "constantName": 
"org.apache.camel.component.milo.MiloConstants#HEADER_NODE_IDS" },
-    "CamelMiloAwait": { "index": 1, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The await setting for writes.", 
"constantName": "org.apache.camel.component.milo.MiloConstants#HEADER_AWAIT" }
+    "CamelMiloAwait": { "index": 1, "kind": "header", "displayName": "", 
"group": "producer", "label": "producer", "required": false, "javaType": 
"Boolean", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The await setting for writes.", 
"constantName": "org.apache.camel.component.milo.MiloConstants#HEADER_AWAIT" },
+    "CamelMiloNodeId": { "index": 2, "kind": "header", "displayName": "", 
"group": "consumer", "label": "consumer", "required": false, "javaType": 
"String", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The node ID of the value, as configured in the 
node or nodes option. Not to be confused with CamelMiloNodeIds, the list of 
nodes that the producer reads.", "constantName": 
"org.apache.camel.component.milo.MiloConstants#HEADER_NODE_ID" }
   },
   "properties": {
     "endpointUri": { "index": 0, "kind": "path", "displayName": "Endpoint 
Uri", "group": "common", "label": "", "required": true, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "The OPC UA server 
endpoint" },
@@ -72,29 +73,30 @@
     "omitNullValues": { "index": 11, "kind": "parameter", "displayName": "Omit 
Null Values", "group": "common", "label": "", "required": false, "type": 
"boolean", "javaType": "boolean", "deprecated": false, "autowired": false, 
"secret": false, "defaultValue": true, "description": "Omit notifications in 
case of null values" },
     "queueSize": { "index": 12, "kind": "parameter", "displayName": "Queue 
Size", "group": "common", "label": "", "required": false, "type": "integer", 
"javaType": "java.lang.Integer", "deprecated": false, "autowired": false, 
"secret": false, "description": "The queue size used for OPC UA subscriptions. 
If not set, a queue size of 1 is requested." },
     "samplingInterval": { "index": 13, "kind": "parameter", "displayName": 
"Sampling Interval", "group": "common", "label": "", "required": false, "type": 
"number", "javaType": "java.lang.Double", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": "1000.0", "description": "The sampling 
interval in milliseconds" },
-    "bridgeErrorHandler": { "index": 14, "kind": "parameter", "displayName": 
"Bridge Error Handler", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Allows for bridging the consumer to the 
Camel routing Error Handler, which mean any exceptions (if possible) occurred 
while the Camel consumer is trying to pickup incoming [...]
-    "exceptionHandler": { "index": 15, "kind": "parameter", "displayName": 
"Exception Handler", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "object", "javaType": 
"org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", 
"deprecated": false, "autowired": false, "secret": false, "description": "To 
let the consumer use a custom ExceptionHandler. Notice if the option 
bridgeErrorHandler is enabled then this option is not in use. By de [...]
-    "exchangePattern": { "index": 16, "kind": "parameter", "displayName": 
"Exchange Pattern", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "enum", "javaType": 
"org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], 
"deprecated": false, "autowired": false, "secret": false, "description": "Sets 
the exchange pattern when the consumer creates an exchange." },
-    "lazyStartProducer": { "index": 17, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produ [...]
-    "allowedSecurityPolicies": { "index": 18, "kind": "parameter", 
"displayName": "Allowed Security Policies", "group": "client", "label": 
"client", "required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "A set of allowed 
security policy URIs. Default is to accept all and use t [...]
-    "applicationName": { "index": 19, "kind": "parameter", "displayName": 
"Application Name", "group": "client", "label": "client", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": "Apache Camel adapter for 
Eclipse Milo", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The application name" },
-    "applicationUri": { "index": 20, "kind": "parameter", "displayName": 
"Application Uri", "group": "client", "label": "client", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": 
"http:\/\/camel.apache.org\/EclipseMilo\/Client", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The application URI" },
-    "channelLifetime": { "index": 21, "kind": "parameter", "displayName": 
"Channel Lifetime", "group": "client", "label": "client", "required": false, 
"type": "integer", "javaType": "java.lang.Long", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Channel lifetime in 
milliseconds" },
-    "keyAlias": { "index": 22, "kind": "parameter", "displayName": "Key 
Alias", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The name of the key in 
the keystore file" },
-    "keyPassword": { "index": 23, "kind": "parameter", "displayName": "Key 
Password", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The key password" },
-    "keyStorePassword": { "index": 24, "kind": "parameter", "displayName": 
"Key Store Password", "group": "client", "label": "client", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The keystore password" },
-    "keyStoreType": { "index": 25, "kind": "parameter", "displayName": "Key 
Store Type", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The key store type" },
-    "keyStoreUrl": { "index": 26, "kind": "parameter", "displayName": "Key 
Store Url", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The URL where the key 
should be loaded from" },
-    "maxPendingPublishRequests": { "index": 27, "kind": "parameter", 
"displayName": "Max Pending Publish Requests", "group": "client", "label": 
"client", "required": false, "type": "integer", "javaType": "java.lang.Long", 
"deprecated": false, "autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The maximum number of 
pending publish requests" },
-    "maxResponseMessageSize": { "index": 28, "kind": "parameter", 
"displayName": "Max Response Message Size", "group": "client", "label": 
"client", "required": false, "type": "integer", "javaType": "java.lang.Long", 
"deprecated": false, "autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The maximum number of 
bytes a response message may have" },
-    "overrideHost": { "index": 29, "kind": "parameter", "displayName": 
"Override Host", "group": "client", "label": "client", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Override the server 
reported endpoint host with the host from the endpoint URI." },
-    "overridePort": { "index": 30, "kind": "parameter", "displayName": 
"Override Port", "group": "client", "label": "client", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Override the server 
reported endpoint port with the port from the endpoint URI." },
-    "productUri": { "index": 31, "kind": "parameter", "displayName": "Product 
Uri", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": 
"http:\/\/camel.apache.org\/EclipseMilo", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The product URI" },
-    "requestedPublishingInterval": { "index": 32, "kind": "parameter", 
"displayName": "Requested Publishing Interval", "group": "client", "label": 
"client", "required": false, "type": "number", "javaType": "java.lang.Double", 
"deprecated": false, "autowired": false, "secret": false, "defaultValue": 
"1_000.0", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The requested publishing 
interval in m [...]
-    "requestTimeout": { "index": 33, "kind": "parameter", "displayName": 
"Request Timeout", "group": "client", "label": "client", "required": false, 
"type": "integer", "javaType": "java.lang.Long", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Request timeout in 
milliseconds" },
-    "sessionName": { "index": 34, "kind": "parameter", "displayName": "Session 
Name", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Session name" },
-    "sessionTimeout": { "index": 35, "kind": "parameter", "displayName": 
"Session Timeout", "group": "client", "label": "client", "required": false, 
"type": "integer", "javaType": "java.lang.Long", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Session timeout in 
milliseconds" },
-    "password": { "index": 36, "kind": "parameter", "displayName": "Password", 
"group": "security", "label": "security", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The password for 
authentication. Use this instead of embedding credentials in th [...]
-    "username": { "index": 37, "kind": "parameter", "displayName": "Username", 
"group": "security", "label": "security", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The username for 
authentication. Use this instead of embedding credentials in the endpoint URI 
when t [...]
+    "nodes": { "index": 14, "kind": "parameter", "displayName": "Nodes", 
"group": "consumer", "label": "consumer", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "description": "Comma-separated node definitions to monitor 
with one consumer, instead of node (see Node ID)" },
+    "bridgeErrorHandler": { "index": 15, "kind": "parameter", "displayName": 
"Bridge Error Handler", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Allows for bridging the consumer to the 
Camel routing Error Handler, which mean any exceptions (if possible) occurred 
while the Camel consumer is trying to pickup incoming [...]
+    "exceptionHandler": { "index": 16, "kind": "parameter", "displayName": 
"Exception Handler", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "object", "javaType": 
"org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", 
"deprecated": false, "autowired": false, "secret": false, "description": "To 
let the consumer use a custom ExceptionHandler. Notice if the option 
bridgeErrorHandler is enabled then this option is not in use. By de [...]
+    "exchangePattern": { "index": 17, "kind": "parameter", "displayName": 
"Exchange Pattern", "group": "consumer (advanced)", "label": 
"consumer,advanced", "required": false, "type": "enum", "javaType": 
"org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], 
"deprecated": false, "autowired": false, "secret": false, "description": "Sets 
the exchange pattern when the consumer creates an exchange." },
+    "lazyStartProducer": { "index": 18, "kind": "parameter", "displayName": 
"Lazy Start Producer", "group": "producer (advanced)", "label": 
"producer,advanced", "required": false, "type": "boolean", "javaType": 
"boolean", "deprecated": false, "autowired": false, "secret": false, 
"defaultValue": false, "description": "Whether the producer should be started 
lazy (on the first message). By starting lazy you can use this to allow 
CamelContext and routes to startup in situations where a produ [...]
+    "allowedSecurityPolicies": { "index": 19, "kind": "parameter", 
"displayName": "Allowed Security Policies", "group": "client", "label": 
"client", "required": false, "type": "string", "javaType": "java.lang.String", 
"deprecated": false, "autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "A set of allowed 
security policy URIs. Default is to accept all and use t [...]
+    "applicationName": { "index": 20, "kind": "parameter", "displayName": 
"Application Name", "group": "client", "label": "client", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": "Apache Camel adapter for 
Eclipse Milo", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The application name" },
+    "applicationUri": { "index": 21, "kind": "parameter", "displayName": 
"Application Uri", "group": "client", "label": "client", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": false, "defaultValue": 
"http:\/\/camel.apache.org\/EclipseMilo\/Client", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The application URI" },
+    "channelLifetime": { "index": 22, "kind": "parameter", "displayName": 
"Channel Lifetime", "group": "client", "label": "client", "required": false, 
"type": "integer", "javaType": "java.lang.Long", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Channel lifetime in 
milliseconds" },
+    "keyAlias": { "index": 23, "kind": "parameter", "displayName": "Key 
Alias", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The name of the key in 
the keystore file" },
+    "keyPassword": { "index": 24, "kind": "parameter", "displayName": "Key 
Password", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The key password" },
+    "keyStorePassword": { "index": 25, "kind": "parameter", "displayName": 
"Key Store Password", "group": "client", "label": "client", "required": false, 
"type": "string", "javaType": "java.lang.String", "deprecated": false, 
"autowired": false, "secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The keystore password" },
+    "keyStoreType": { "index": 26, "kind": "parameter", "displayName": "Key 
Store Type", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The key store type" },
+    "keyStoreUrl": { "index": 27, "kind": "parameter", "displayName": "Key 
Store Url", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The URL where the key 
should be loaded from" },
+    "maxPendingPublishRequests": { "index": 28, "kind": "parameter", 
"displayName": "Max Pending Publish Requests", "group": "client", "label": 
"client", "required": false, "type": "integer", "javaType": "java.lang.Long", 
"deprecated": false, "autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The maximum number of 
pending publish requests" },
+    "maxResponseMessageSize": { "index": 29, "kind": "parameter", 
"displayName": "Max Response Message Size", "group": "client", "label": 
"client", "required": false, "type": "integer", "javaType": "java.lang.Long", 
"deprecated": false, "autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The maximum number of 
bytes a response message may have" },
+    "overrideHost": { "index": 30, "kind": "parameter", "displayName": 
"Override Host", "group": "client", "label": "client", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Override the server 
reported endpoint host with the host from the endpoint URI." },
+    "overridePort": { "index": 31, "kind": "parameter", "displayName": 
"Override Port", "group": "client", "label": "client", "required": false, 
"type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Override the server 
reported endpoint port with the port from the endpoint URI." },
+    "productUri": { "index": 32, "kind": "parameter", "displayName": "Product 
Uri", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "defaultValue": 
"http:\/\/camel.apache.org\/EclipseMilo", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The product URI" },
+    "requestedPublishingInterval": { "index": 33, "kind": "parameter", 
"displayName": "Requested Publishing Interval", "group": "client", "label": 
"client", "required": false, "type": "number", "javaType": "java.lang.Double", 
"deprecated": false, "autowired": false, "secret": false, "defaultValue": 
"1_000.0", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The requested publishing 
interval in m [...]
+    "requestTimeout": { "index": 34, "kind": "parameter", "displayName": 
"Request Timeout", "group": "client", "label": "client", "required": false, 
"type": "integer", "javaType": "java.lang.Long", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Request timeout in 
milliseconds" },
+    "sessionName": { "index": 35, "kind": "parameter", "displayName": "Session 
Name", "group": "client", "label": "client", "required": false, "type": 
"string", "javaType": "java.lang.String", "deprecated": false, "autowired": 
false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Session name" },
+    "sessionTimeout": { "index": 36, "kind": "parameter", "displayName": 
"Session Timeout", "group": "client", "label": "client", "required": false, 
"type": "integer", "javaType": "java.lang.Long", "deprecated": false, 
"autowired": false, "secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "Session timeout in 
milliseconds" },
+    "password": { "index": 37, "kind": "parameter", "displayName": "Password", 
"group": "security", "label": "security", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": true, "security": "secret", "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The password for 
authentication. Use this instead of embedding credentials in th [...]
+    "username": { "index": 38, "kind": "parameter", "displayName": "Username", 
"group": "security", "label": "security", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "configurationClass": 
"org.apache.camel.component.milo.client.MiloClientConfiguration", 
"configurationField": "configuration", "description": "The username for 
authentication. Use this instead of embedding credentials in the endpoint URI 
when t [...]
   }
 }
diff --git a/components/camel-milo/src/main/docs/milo-client-component.adoc 
b/components/camel-milo/src/main/docs/milo-client-component.adoc
index 3c5b1aecdb95..e04f72f92737 100644
--- a/components/camel-milo/src/main/docs/milo-client-component.adoc
+++ b/components/camel-milo/src/main/docs/milo-client-component.adoc
@@ -127,6 +127,31 @@ However Camel allows to wrap the actual value inside 
`RAW(…)`, which makes esc
 
milo-client:opc.tcp://user:password@localhost:12345?node=RAW(nsu=http://foo.bar;s=foo/bar)
 ----
 
+=== Monitoring several nodes
+
+A consumer can monitor several nodes with the `nodes` option instead of `node` 
(the two options cannot be combined).
+The consumer creates the monitored items of all nodes together and deletes 
them together when it stops. Every value
+arrives in its own exchange, and the `CamelMiloNodeId` header holds the node 
ID as it was configured.
+
+The `nodes` option takes a comma-separated list of <<nodeid, Node IDs>>. The 
entries are trimmed, empty entries are
+skipped and a node that is listed twice is monitored once:
+
+----
+milo-client:opc.tcp://localhost:12345?nodes=RAW(ns=2;s=line1/temperature,ns=2;s=line1/pressure)
+----
+
+A string node ID that contains a comma cannot be used in `nodes`; monitor it 
with the `node` option instead. Long lists
+can come from a property placeholder, for example `+nodes=RAW({{tags}})+`.
+
+All nodes of the list share the `samplingInterval`, `queueSize` and 
`omitNullValues` options of the consumer and its
+monitor filter (`monitorFilterType`, `dataChangeFilter*`). Tags that need a 
different sampling interval or deadband
+need a consumer of their own.
+
+A node that does not exist on the server does not affect the other nodes. A 
node that cannot be monitored is
+reported once with its status code and is not retried; restarting the route or 
a rebuilt connection to the server creates the monitored items again.
+To see such nodes, set `omitNullValues=false`: the consumer then delivers one 
exchange with the bad status code for that
+node, and the `CamelMiloNodeId` header names it.
+
 === Method ID
 
 It is possible to perform methods calls on OPC UA nodes. If the parameter 
`method` is set to the Node ID of a method call (the node ID must be set to the 
parent object in this case),
diff --git 
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/MiloConstants.java
 
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/MiloConstants.java
index b4c11dc57e24..82e27ac52757 100644
--- 
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/MiloConstants.java
+++ 
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/MiloConstants.java
@@ -30,6 +30,11 @@ public final class MiloConstants {
     @Metadata(label = "producer", description = "The \"await\" setting for 
writes.", javaType = "Boolean",
               applicableFor = SCHEME_CLIENT)
     public static final String HEADER_AWAIT = "CamelMiloAwait";
+    @Metadata(label = "consumer",
+              description = "The node ID of the value, as configured in the 
node or nodes option. Not to be confused with"
+                            + " CamelMiloNodeIds, the list of nodes that the 
producer reads.",
+              javaType = "String", applicableFor = SCHEME_CLIENT)
+    public static final String HEADER_NODE_ID = "CamelMiloNodeId";
 
     private MiloConstants() {
 
diff --git 
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConnection.java
 
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConnection.java
index b8222164a8f6..73fbe7ae6a20 100644
--- 
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConnection.java
+++ 
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConnection.java
@@ -16,6 +16,7 @@
  */
 package org.apache.camel.component.milo.client;
 
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.concurrent.CompletableFuture;
@@ -118,16 +119,28 @@ public class MiloClientConnection implements 
AutoCloseable {
             final ExpandedNodeId nodeId, Double samplingInterval, Integer 
queueSize,
             MonitorFilterConfiguration monitorFilterConfiguration, 
Consumer<DataValue> valueConsumer) {
 
-        requireNonNull(configuration);
         requireNonNull(valueConsumer);
 
+        return monitorValues(Collections.singletonMap(nodeId, valueConsumer), 
samplingInterval, queueSize,
+                monitorFilterConfiguration);
+    }
+
+    /**
+     * Monitors several nodes, each with its own value consumer. The monitored 
items of all nodes are created together,
+     * and the returned handle deletes them together.
+     */
+    public MonitorHandle monitorValues(
+            final Map<ExpandedNodeId, Consumer<DataValue>> valueConsumers, 
final Double samplingInterval,
+            final Integer queueSize, final MonitorFilterConfiguration 
monitorFilterConfiguration) {
+
+        requireNonNull(valueConsumers);
+
         checkInit();
 
-        final UInteger handle
-                = this.manager.registerItem(nodeId, samplingInterval, 
queueSize, valueConsumer,
-                        monitorFilterConfiguration);
+        final List<UInteger> handles
+                = this.manager.registerItems(valueConsumers, samplingInterval, 
queueSize, monitorFilterConfiguration);
 
-        return () -> MiloClientConnection.this.manager.unregisterItem(handle);
+        return () -> 
MiloClientConnection.this.manager.unregisterItems(handles);
     }
 
     public String getConnectionId() {
diff --git 
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConsumer.java
 
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConsumer.java
index 5f66a3a823fa..a6b6e0c2cd1c 100644
--- 
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConsumer.java
+++ 
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientConsumer.java
@@ -16,10 +16,16 @@
  */
 package org.apache.camel.component.milo.client;
 
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.Consumer;
+
 import org.apache.camel.Exchange;
 import org.apache.camel.Message;
 import org.apache.camel.Processor;
 import org.apache.camel.component.milo.Messages;
+import org.apache.camel.component.milo.MiloConstants;
 import 
org.apache.camel.component.milo.client.MiloClientConnection.MonitorHandle;
 import org.apache.camel.support.DefaultConsumer;
 import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
@@ -33,14 +39,15 @@ public class MiloClientConsumer extends DefaultConsumer {
 
     private MiloClientConnection connection;
     private MonitorHandle handle;
-    private ExpandedNodeId node;
     private Double samplingInterval;
     private Integer queueSize;
     private boolean omitNullValues;
 
     public MiloClientConsumer(final MiloClientEndpoint endpoint, final 
Processor processor) {
         super(endpoint, processor);
-        this.node = endpoint.getNodeId();
+        if (endpoint.getNode() != null && endpoint.getNodes() != null) {
+            throw new IllegalArgumentException("Configure either the node or 
the nodes option, not both");
+        }
         this.samplingInterval = endpoint.getSamplingInterval();
         this.queueSize = endpoint.getQueueSize();
         this.omitNullValues = endpoint.isOmitNullValues();
@@ -54,9 +61,44 @@ public class MiloClientConsumer extends DefaultConsumer {
     @Override
     protected void doStart() throws Exception {
         super.doStart();
+        final Map<ExpandedNodeId, Consumer<DataValue>> valueConsumers = new 
LinkedHashMap<>();
+        resolveNodes().forEach((nodeId, node) -> valueConsumers.put(nodeId, 
value -> handleValueUpdate(node, value)));
         this.connection = getEndpoint().createConnection();
-        this.handle = this.connection.monitorValue(this.node, 
this.samplingInterval, this.queueSize,
-                getEndpoint().getMonitorFilterConfiguration(), 
this::handleValueUpdate);
+        this.handle = this.connection.monitorValues(valueConsumers, 
this.samplingInterval, this.queueSize,
+                getEndpoint().getMonitorFilterConfiguration());
+    }
+
+    /**
+     * The node IDs of the node or nodes option, in the configured order and 
without duplicates. A node that is listed
+     * in several spellings is monitored once, the value is the first spelling 
of the node ID.
+     */
+    private Map<ExpandedNodeId, String> resolveNodes() {
+        final Map<ExpandedNodeId, String> result = new LinkedHashMap<>();
+
+        final String node = getEndpoint().getNode();
+        if (node != null) {
+            result.put(parseNodeId(node), node);
+            return result;
+        }
+
+        final String nodes = getEndpoint().getNodes();
+        if (nodes != null) {
+            Arrays.stream(nodes.split(",")).map(String::trim).filter(entry -> 
!entry.isEmpty())
+                    .forEach(entry -> result.putIfAbsent(parseNodeId(entry), 
entry));
+        }
+
+        if (result.isEmpty()) {
+            throw new IllegalArgumentException("No node to monitor, configure 
the node or the nodes option");
+        }
+        return result;
+    }
+
+    private static ExpandedNodeId parseNodeId(final String node) {
+        try {
+            return ExpandedNodeId.parse(node);
+        } catch (final Exception e) {
+            throw new IllegalArgumentException("Invalid node ID: " + node, e);
+        }
     }
 
     @Override
@@ -71,7 +113,7 @@ public class MiloClientConsumer extends DefaultConsumer {
         super.doStop();
     }
 
-    private void handleValueUpdate(final DataValue value) {
+    private void handleValueUpdate(final String node, final DataValue value) {
         LOG.debug("Handle item update - {} = {}", node, value);
 
         if (omitNullValues && (value == null || value.getValue() == null || 
value.getValue().getValue() == null)) {
@@ -82,6 +124,7 @@ public class MiloClientConsumer extends DefaultConsumer {
         final Exchange exchange = createExchange(true);
         try {
             mapToMessage(value, exchange.getMessage());
+            exchange.getMessage().setHeader(MiloConstants.HEADER_NODE_ID, 
node);
             getProcessor().process(exchange);
         } catch (final Exception e) {
             getExceptionHandler().handleException("Error processing exchange", 
e);
diff --git 
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientEndpoint.java
 
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientEndpoint.java
index 50e32811074a..752e4adb114d 100644
--- 
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientEndpoint.java
+++ 
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/MiloClientEndpoint.java
@@ -54,6 +54,12 @@ public class MiloClientEndpoint extends DefaultEndpoint {
     @UriParam
     private String node;
 
+    /**
+     * Comma-separated node definitions to monitor with one consumer, instead 
of node (see Node ID)
+     */
+    @UriParam(label = "consumer")
+    private String nodes;
+
     /**
      * The method definition (see Method ID)
      */
@@ -170,6 +176,14 @@ public class MiloClientEndpoint extends DefaultEndpoint {
         }
     }
 
+    public void setNodes(final String nodes) {
+        this.nodes = nodes;
+    }
+
+    public String getNodes() {
+        return nodes;
+    }
+
     ExpandedNodeId getMethodId() {
         if (this.method != null) {
             return ExpandedNodeId.parse(this.method);
diff --git 
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java
 
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java
index 65bd86614470..32957dfaf103 100644
--- 
a/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java
+++ 
b/components/camel-milo/src/main/java/org/apache/camel/component/milo/client/internal/SubscriptionManager.java
@@ -22,6 +22,7 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
@@ -51,6 +52,7 @@ import 
org.eclipse.milo.opcua.sdk.client.identity.AnonymousProvider;
 import org.eclipse.milo.opcua.sdk.client.identity.CompositeProvider;
 import org.eclipse.milo.opcua.sdk.client.identity.IdentityProvider;
 import org.eclipse.milo.opcua.sdk.client.identity.UsernameProvider;
+import 
org.eclipse.milo.opcua.sdk.client.subscriptions.MonitoredItemServiceOperationResult;
 import org.eclipse.milo.opcua.sdk.client.subscriptions.OpcUaMonitoredItem;
 import org.eclipse.milo.opcua.sdk.client.subscriptions.OpcUaSubscription;
 import org.eclipse.milo.opcua.stack.core.AttributeId;
@@ -160,6 +162,15 @@ public class SubscriptionManager {
         }
     }
 
+    /**
+     * A status value for a node that could not be monitored, delivered to its 
consumer outside of the lock
+     */
+    private record PendingValue(Consumer<DataValue> consumer, DataValue value) 
{
+        void deliver() {
+            consumer.accept(value);
+        }
+    }
+
     private class Connected {
         private OpcUaClient client;
         private final OpcUaSubscription manager;
@@ -175,10 +186,21 @@ public class SubscriptionManager {
             this.manager = manager;
         }
 
-        public void putSubscriptions(final Map<UInteger, Subscription> 
subscriptions) throws Exception {
+        /**
+         * Creates the monitored items of the subscriptions on the server.
+         *
+         * @param  subscriptions the subscriptions to create, by client handle
+         * @return               the bad status values of the nodes that could 
not be monitored, to be delivered to
+         *                       their consumers after the lock of the manager 
has been released
+         * @throws UaException   if the server rejected the 
CreateMonitoredItems service call, the connection has to be
+         *                       rebuilt
+         */
+        public List<PendingValue> putSubscriptions(final Map<UInteger, 
Subscription> subscriptions) throws Exception {
+
+            final List<PendingValue> pendingValues = new ArrayList<>();
 
             if (subscriptions.isEmpty()) {
-                return;
+                return pendingValues;
             }
 
             // convert to requests
@@ -193,7 +215,7 @@ public class SubscriptionManager {
                 final NodeId node = lookupNamespace(s.getNodeId()).get();
 
                 if (node == null) {
-                    handleSubscriptionError(new 
StatusCode(StatusCodes.Bad_InvalidArgument), entry.getKey(), s);
+                    handleSubscriptionError(new 
StatusCode(StatusCodes.Bad_InvalidArgument), entry.getKey(), s, pendingValues);
                 } else {
                     final ReadValueId itemId = new ReadValueId(node, 
AttributeId.Value.uid(), null, QualifiedName.NULL_VALUE);
                     final OpcUaMonitoredItem item = new 
OpcUaMonitoredItem(itemId, MonitoringMode.Reporting);
@@ -207,6 +229,8 @@ public class SubscriptionManager {
                     if (filter != null) {
                         item.setFilter(filter);
                     }
+                    // set the listener before the item is created, values 
without a listener are dropped
+                    item.setDataValueListener((monitoredItem, dataValue) -> 
s.getValueConsumer().accept(dataValue));
                     items.add(item);
                     // Keep track of which subscription this item belongs to
                     itemToClientHandle.put(item, entry.getKey());
@@ -219,54 +243,82 @@ public class SubscriptionManager {
                     this.manager.addMonitoredItem(item);
                 }
 
-                // Create monitored items on the server - this assigns client 
handles
-                this.manager.createMonitoredItems();
-
-                // Now set up data value listeners after items are created and 
have client handles
-                for (OpcUaMonitoredItem item : items) {
-                    UInteger originalClientHandle = 
itemToClientHandle.get(item);
-                    final Subscription s = 
subscriptions.get(originalClientHandle);
-                    if (s != null) {
-                        item.setDataValueListener((monitoredItem, dataValue) 
-> s.getValueConsumer().accept(dataValue));
+                // Create the monitored items of this call on the server
+                final List<MonitoredItemServiceOperationResult> results
+                        = 
this.manager.createMonitoredItems(itemToClientHandle::containsKey);
+
+                // the service call is partitioned, a rejected partition 
reports its service result for each of its items
+                StatusCode rejectedServiceResult = null;
+                final List<OpcUaMonitoredItem> failedItems = new ArrayList<>();
+                for (final MonitoredItemServiceOperationResult result : 
results) {
+                    final OpcUaMonitoredItem item = result.monitoredItem();
+                    final UInteger clientHandle = itemToClientHandle.get(item);
+                    if (result.isGood()) {
+                        this.goodSubscriptions.put(clientHandle, item);
+                    } else if (result.serviceResult().isBad()) {
+                        failedItems.add(item);
+                        rejectedServiceResult = result.serviceResult();
+                    } else {
+                        // reported once, the item is created again when the 
consumer restarts or the connection is rebuilt
+                        failedItems.add(item);
+                        final Subscription s = subscriptions.get(clientHandle);
+                        final StatusCode status = 
result.operationResult().orElse(result.serviceResult());
+                        LOG.warn("Monitored item for node {} could not be 
created: {}", s.getNodeId().toParseableString(),
+                                status);
+                        pendingValues.add(new 
PendingValue(s.getValueConsumer(), new DataValue(status)));
                     }
                 }
+
+                if (!failedItems.isEmpty()) {
+                    // remove the failed items from the subscription, they 
were not created on the server
+                    this.manager.removeMonitoredItems(failedItems);
+                    this.manager.deleteMonitoredItems();
+                }
+
+                if (rejectedServiceResult != null) {
+                    // the subscription is unusable, the caller tears the 
connection down and builds it up again
+                    throw new UaException(rejectedServiceResult);
+                }
             }
 
             if (!this.badSubscriptions.isEmpty()) {
                 SubscriptionManager.this.executor.schedule(this::resubscribe, 
SubscriptionManager.this.reconnectTimeout,
                         TimeUnit.MILLISECONDS);
             }
+
+            return pendingValues;
         }
 
-        private void handleSubscriptionError(final StatusCode statusCode, 
final UInteger clientHandle, final Subscription s) {
+        private void handleSubscriptionError(
+                final StatusCode statusCode, final UInteger clientHandle, 
final Subscription s,
+                final List<PendingValue> pendingValues) {
             this.badSubscriptions.put(clientHandle, s);
-            s.getValueConsumer().accept(new DataValue(statusCode));
+            pendingValues.add(new PendingValue(s.getValueConsumer(), new 
DataValue(statusCode)));
         }
 
         private void resubscribe() {
             final Map<UInteger, Subscription> subscriptions = new 
HashMap<>(this.badSubscriptions);
             this.badSubscriptions.clear();
             try {
-                putSubscriptions(subscriptions);
+                putSubscriptions(subscriptions).forEach(PendingValue::deliver);
             } catch (final Exception e) {
                 handleConnectionFailure(e);
             }
         }
 
-        public void activate(final UInteger clientHandle, final Subscription 
subscription) throws Exception {
-            putSubscriptions(Collections.singletonMap(clientHandle, 
subscription));
-        }
-
-        public void deactivate(final UInteger clientHandle) throws Exception {
-            final OpcUaMonitoredItem item = 
this.goodSubscriptions.remove(clientHandle);
-            if (item != null) {
-                // TODO is this migrated correctly?
-                this.manager.removeMonitoredItem(item);
-                //                this.manager.synchronizeMonitoredItems();
+        public void deactivate(final List<UInteger> clientHandles) throws 
Exception {
+            final List<OpcUaMonitoredItem> items = new 
ArrayList<>(clientHandles.size());
+            for (final UInteger clientHandle : clientHandles) {
+                final OpcUaMonitoredItem item = 
this.goodSubscriptions.remove(clientHandle);
+                if (item != null) {
+                    items.add(item);
+                } else {
+                    this.badSubscriptions.remove(clientHandle);
+                }
+            }
+            if (!items.isEmpty()) {
+                this.manager.removeMonitoredItems(items);
                 this.manager.deleteMonitoredItems();
-                //                
this.manager.deleteMonitoredItems(Collections.singletonList(item)).get();
-            } else {
-                this.badSubscriptions.remove(clientHandle);
             }
         }
 
@@ -670,6 +722,7 @@ public class SubscriptionManager {
     }
 
     private void performAndEvalConnect() {
+        final List<PendingValue> pendingValues = new ArrayList<>();
         try {
             final Connected connected = performConnect();
             LOG.debug("Connect call done");
@@ -682,7 +735,7 @@ public class SubscriptionManager {
 
                 try {
                     LOG.debug("Setting subscriptions: {}", 
this.subscriptions.size());
-                    connected.putSubscriptions(this.subscriptions);
+                    
pendingValues.addAll(connected.putSubscriptions(this.subscriptions));
 
                     LOG.debug("Update state : {} -> {}", this.connected, 
connected);
                     final Connected oldConnected = this.connected;
@@ -705,6 +758,9 @@ public class SubscriptionManager {
             LOG.info("Failed to connect", e);
             triggerReconnect(false);
         }
+
+        // deliver outside of the lock, the routes may use the connection
+        pendingValues.forEach(PendingValue::deliver);
     }
 
     private Connected performConnect() throws Exception {
@@ -955,35 +1011,43 @@ public class SubscriptionManager {
         return result;
     }
 
-    public UInteger registerItem(
-            final ExpandedNodeId nodeId, final Double samplingInterval, 
Integer queueSize,
-            final Consumer<DataValue> valueConsumer,
-            final MonitorFilterConfiguration monitorFilterConfiguration) {
+    public List<UInteger> registerItems(
+            final Map<ExpandedNodeId, Consumer<DataValue>> valueConsumers, 
final Double samplingInterval,
+            final Integer queueSize, final MonitorFilterConfiguration 
monitorFilterConfiguration) {
 
-        final UInteger clientHandle = 
Unsigned.uint(this.clientHandleCounter.incrementAndGet());
-        final Subscription subscription
-                = new Subscription(nodeId, samplingInterval, queueSize, 
valueConsumer, monitorFilterConfiguration);
+        final Map<UInteger, Subscription> subscriptions = new 
LinkedHashMap<>();
+        valueConsumers.forEach((nodeId, valueConsumer) -> subscriptions.put(
+                Unsigned.uint(this.clientHandleCounter.incrementAndGet()),
+                new Subscription(nodeId, samplingInterval, queueSize, 
valueConsumer, monitorFilterConfiguration)));
 
+        final List<PendingValue> pendingValues = new ArrayList<>();
         lock.lock();
         try {
-            this.subscriptions.put(clientHandle, subscription);
+            this.subscriptions.putAll(subscriptions);
 
-            whenConnected(connected -> {
-                connected.activate(clientHandle, subscription);
-            });
+            whenConnected(connected -> 
pendingValues.addAll(connected.putSubscriptions(subscriptions)));
         } finally {
             lock.unlock();
         }
 
-        return clientHandle;
+        // deliver outside of the lock, the routes may use the connection
+        pendingValues.forEach(PendingValue::deliver);
+
+        return new ArrayList<>(subscriptions.keySet());
     }
 
-    public void unregisterItem(final UInteger clientHandle) {
+    public void unregisterItems(final List<UInteger> clientHandles) {
         lock.lock();
         try {
-            if (this.subscriptions.remove(clientHandle) != null) {
+            final List<UInteger> removed = new 
ArrayList<>(clientHandles.size());
+            for (final UInteger clientHandle : clientHandles) {
+                if (this.subscriptions.remove(clientHandle) != null) {
+                    removed.add(clientHandle);
+                }
+            }
+            if (!removed.isEmpty()) {
                 whenConnected(connected -> {
-                    connected.deactivate(clientHandle);
+                    connected.deactivate(removed);
                 });
             }
         } finally {
diff --git 
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemLifecycleTest.java
 
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemLifecycleTest.java
new file mode 100644
index 000000000000..b5f50b241691
--- /dev/null
+++ 
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemLifecycleTest.java
@@ -0,0 +1,252 @@
+/*
+ * 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.camel.component.milo;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
+
+import org.apache.camel.EndpointInject;
+import org.apache.camel.Exchange;
+import org.apache.camel.Produce;
+import org.apache.camel.ProducerTemplate;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.milo.client.MiloClientConnection;
+import org.apache.camel.component.milo.client.MiloClientEndpoint;
+import org.apache.camel.component.milo.server.MiloServerComponent;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.eclipse.milo.opcua.sdk.client.OpcUaClient;
+import org.eclipse.milo.opcua.stack.core.StatusCodes;
+import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
+import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode;
+import org.eclipse.milo.opcua.stack.core.types.builtin.unsigned.UInteger;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import static org.awaitility.Awaitility.await;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Testing the monitored items of consumers sharing a connection, when a 
consumer is restarted, its node does not exist
+ * or the server rejects the CreateMonitoredItems service call
+ */
+public class MonitorItemLifecycleTest extends AbstractMiloServerTest {
+
+    private static final String DIRECT_START_1 = "direct:start1";
+
+    private static final String DIRECT_START_2 = "direct:start2";
+
+    private static final String MILO_SERVER_ITEM_1 = "milo-server:myitem1";
+
+    private static final String MILO_SERVER_ITEM_2 = "milo-server:myitem2";
+
+    private static final String NODE_ITEM_1 = "nsu=" + 
MiloServerComponent.DEFAULT_NAMESPACE_URI + ";s=myitem1";
+
+    private static final String NODE_ITEM_2 = "nsu=" + 
MiloServerComponent.DEFAULT_NAMESPACE_URI + ";s=myitem2";
+
+    private static final String NODE_UNKNOWN_ITEM_WRITE_BACK
+            = "nsu=" + MiloServerComponent.DEFAULT_NAMESPACE_URI + 
";s=unknownItemWriteBack";
+
+    // the client endpoints only differ in the node and the consumer options, 
so they share one connection
+    private static final String MILO_CLIENT_BASE = 
"milo-client:opc.tcp://foo:bar@localhost:@@port@@"
+                                                   + 
"?allowedSecurityPolicies=None&overrideHost=true"
+                                                   + 
"&samplingInterval=50&requestedPublishingInterval=100";
+
+    private static final String MILO_CLIENT = MILO_CLIENT_BASE + "&node=";
+
+    private static final String MILO_CLIENT_ITEM_1
+            = MILO_CLIENT + 
NodeIds.nodeValue(MiloServerComponent.DEFAULT_NAMESPACE_URI, "myitem1");
+
+    private static final String MILO_CLIENT_ITEM_2
+            = MILO_CLIENT + 
NodeIds.nodeValue(MiloServerComponent.DEFAULT_NAMESPACE_URI, "myitem2");
+
+    private static final String MILO_CLIENT_UNKNOWN_ITEM
+            = MILO_CLIENT + 
NodeIds.nodeValue(MiloServerComponent.DEFAULT_NAMESPACE_URI, "unknownItem")
+              + "&omitNullValues=false";
+
+    private static final String MILO_CLIENT_UNKNOWN_ITEM_WRITE_BACK
+            = MILO_CLIENT + 
NodeIds.nodeValue(MiloServerComponent.DEFAULT_NAMESPACE_URI, 
"unknownItemWriteBack")
+              + "&omitNullValues=false";
+
+    private static final String MILO_CLIENT_ITEMS_1_2
+            = MILO_CLIENT_BASE + "&omitNullValues=false&nodes=RAW(" + 
NODE_ITEM_1 + "," + NODE_ITEM_2 + ")";
+
+    private static final String MOCK_ITEM_1 = "mock:item1";
+
+    private static final String MOCK_ITEM_2 = "mock:item2";
+
+    private static final String MOCK_UNKNOWN_ITEM = "mock:unknownItem";
+
+    private static final String MOCK_WRITE_BACK = "mock:writeBack";
+
+    private static final String MOCK_ITEMS_1_2 = "mock:items12";
+
+    @EndpointInject(MOCK_ITEM_1)
+    protected MockEndpoint item1Endpoint;
+
+    @EndpointInject(MOCK_ITEM_2)
+    protected MockEndpoint item2Endpoint;
+
+    @EndpointInject(MOCK_UNKNOWN_ITEM)
+    protected MockEndpoint unknownItemEndpoint;
+
+    @EndpointInject(MOCK_WRITE_BACK)
+    protected MockEndpoint writeBackEndpoint;
+
+    @EndpointInject(MOCK_ITEMS_1_2)
+    protected MockEndpoint items12Endpoint;
+
+    @Produce(DIRECT_START_1)
+    protected ProducerTemplate producer1;
+
+    @Produce(DIRECT_START_2)
+    protected ProducerTemplate producer2;
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                from(DIRECT_START_1).to(MILO_SERVER_ITEM_1);
+                from(DIRECT_START_2).to(MILO_SERVER_ITEM_2);
+
+                
from(resolve(MILO_CLIENT_ITEM_1)).routeId("item1").to(MOCK_ITEM_1);
+                
from(resolve(MILO_CLIENT_UNKNOWN_ITEM)).routeId("unknownItem").to(MOCK_UNKNOWN_ITEM);
+
+                // the consumers deliver the current values while they start, 
so the tests start these routes
+                
from(resolve(MILO_CLIENT_ITEM_2)).routeId("item2").autoStartup(false).to(MOCK_ITEM_2);
+                
from(resolve(MILO_CLIENT_ITEMS_1_2)).routeId("items12").autoStartup(false).to(MOCK_ITEMS_1_2);
+
+                // the bad status of the unknown node is processed on another 
thread, which writes to the same connection
+                
from(resolve(MILO_CLIENT_UNKNOWN_ITEM_WRITE_BACK)).routeId("writeBack").autoStartup(false)
+                        .threads(1)
+                        .setBody(constant("Foo"))
+                        .to(resolve(MILO_CLIENT_ITEM_1 + 
"&defaultAwaitWrites=true"))
+                        .to(MOCK_WRITE_BACK);
+            }
+        };
+    }
+
+    @Test
+    void restartedConsumerReceivesEachValueOnce() throws Exception {
+        item1Endpoint.expectedMessageCount(1);
+        testBody(item1Endpoint.message(0), assertGoodValue("Foo"));
+        producer1.sendBody("Foo");
+        item1Endpoint.assertIsSatisfied();
+
+        // the other consumers keep the shared connection open
+        context.getRouteController().stopRoute("item1");
+
+        item1Endpoint.reset();
+        item1Endpoint.expectedMessageCount(0);
+        item1Endpoint.setAssertPeriod(1000);
+        producer1.sendBody("Bar");
+        item1Endpoint.assertIsSatisfied();
+
+        item1Endpoint.reset();
+        item1Endpoint.expectedMessageCount(1);
+        testBody(item1Endpoint.message(0), assertGoodValue("Bar"));
+        context.getRouteController().startRoute("item1");
+        item1Endpoint.assertIsSatisfied();
+
+        item1Endpoint.reset();
+        item1Endpoint.expectedMessageCount(1);
+        item1Endpoint.setAssertPeriod(1000);
+        testBody(item1Endpoint.message(0), assertGoodValue("Baz"));
+        producer1.sendBody("Baz");
+        item1Endpoint.assertIsSatisfied();
+    }
+
+    @Test
+    void unknownNodeReceivesBadStatus() throws Exception {
+        unknownItemEndpoint.expectedMinimumMessageCount(1);
+        testBody(unknownItemEndpoint.message(0), 
assertStatus(StatusCodes.Bad_NodeIdUnknown));
+        unknownItemEndpoint.assertIsSatisfied();
+
+        // the other consumers of the connection are not affected
+        item1Endpoint.expectedMessageCount(1);
+        testBody(item1Endpoint.message(0), assertGoodValue("Foo"));
+        producer1.sendBody("Foo");
+        item1Endpoint.assertIsSatisfied();
+    }
+
+    @Test
+    @Timeout(value = 60, unit = TimeUnit.SECONDS)
+    void badStatusIsDeliveredWithoutHoldingTheConnectionLock() throws 
Exception {
+        // the route of the bad status writes to the shared connection from 
another thread while the consumer starts
+        writeBackEndpoint.expectedMessageCount(1);
+        writeBackEndpoint.expectedHeaderReceived(MiloConstants.HEADER_NODE_ID, 
NODE_UNKNOWN_ITEM_WRITE_BACK);
+        testBody(writeBackEndpoint.message(0), StatusCode.class, status -> 
assertTrue(status.isGood(), status.toString()));
+
+        context.getRouteController().startRoute("writeBack");
+
+        writeBackEndpoint.assertIsSatisfied();
+    }
+
+    @Test
+    void failedCreateMonitoredItemsCallRebuildsTheConnection() throws 
Exception {
+        producer1.sendBody("Foo");
+        producer2.sendBody("Bar");
+
+        item2Endpoint.expectedMessageCount(1);
+        testBody(item2Endpoint.message(0), assertGoodValue("Bar"));
+        context.getRouteController().startRoute("item2");
+        item2Endpoint.assertIsSatisfied();
+
+        // the server deletes the subscription, so the next 
CreateMonitoredItems service call fails as a whole
+        final MiloClientEndpoint endpoint = 
context.getEndpoint(resolve(MILO_CLIENT_ITEM_1), MiloClientEndpoint.class);
+        final MiloClientConnection connection = endpoint.createConnection();
+        try {
+            final OpcUaClient client = connection.getOpcUaClient();
+            final UInteger subscriptionId = 
client.getSubscriptions().get(0).getSubscriptionId().orElseThrow();
+            
assertTrue(client.deleteSubscriptions(List.of(subscriptionId)).getResults()[0].isGood());
+        } finally {
+            endpoint.releaseConnection(connection);
+        }
+
+        // the connection is rebuilt and the new consumer receives the current 
values, not a bad status
+        items12Endpoint.expectedMessageCount(2);
+        items12Endpoint.allMessages().predicate(exchange -> 
exchange.getMessage().getBody(DataValue.class).getStatusCode()
+                .isGood());
+        context.getRouteController().startRoute("items12");
+        items12Endpoint.assertIsSatisfied();
+
+        producer1.sendBody("Baz");
+        await().atMost(10, TimeUnit.SECONDS).untilAsserted(() -> {
+            final Exchange last = lastExchange(items12Endpoint);
+            assertEquals(NODE_ITEM_1, 
last.getMessage().getHeader(MiloConstants.HEADER_NODE_ID, String.class));
+            
assertGoodValue("Baz").accept(last.getMessage().getBody(DataValue.class));
+        });
+
+        // the consumer that was running before keeps working on the rebuilt 
connection
+        producer2.sendBody("Qux");
+        await().atMost(10, TimeUnit.SECONDS)
+                .untilAsserted(() -> 
assertGoodValue("Qux").accept(lastExchange(item2Endpoint).getMessage()
+                        .getBody(DataValue.class)));
+    }
+
+    private static Exchange lastExchange(final MockEndpoint endpoint) {
+        final List<Exchange> exchanges = endpoint.getReceivedExchanges();
+        return exchanges.get(exchanges.size() - 1);
+    }
+
+    private static Consumer<DataValue> assertStatus(final long statusCode) {
+        return value -> assertEquals(new StatusCode(statusCode), 
value.getStatusCode());
+    }
+}
diff --git 
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorNodesTest.java
 
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorNodesTest.java
new file mode 100644
index 000000000000..18460c74164a
--- /dev/null
+++ 
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorNodesTest.java
@@ -0,0 +1,240 @@
+/*
+ * 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.camel.component.milo;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.camel.Consumer;
+import org.apache.camel.Endpoint;
+import org.apache.camel.EndpointInject;
+import org.apache.camel.Exchange;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.milo.client.MiloClientConnection;
+import org.apache.camel.component.milo.client.MiloClientEndpoint;
+import org.apache.camel.component.milo.server.MiloServerComponent;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.eclipse.milo.opcua.stack.core.StatusCodes;
+import org.eclipse.milo.opcua.stack.core.types.builtin.DataValue;
+import org.eclipse.milo.opcua.stack.core.types.builtin.ExpandedNodeId;
+import org.eclipse.milo.opcua.stack.core.types.builtin.StatusCode;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Testing one consumer that monitors several nodes with the nodes option
+ */
+public class MonitorNodesTest extends AbstractMiloServerTest {
+
+    private static final String NODE_1 = "nsu=" + 
MiloServerComponent.DEFAULT_NAMESPACE_URI + ";s=node1";
+
+    private static final String NODE_2 = "nsu=" + 
MiloServerComponent.DEFAULT_NAMESPACE_URI + ";s=node2";
+
+    private static final String UNKNOWN_NODE = "nsu=" + 
MiloServerComponent.DEFAULT_NAMESPACE_URI + ";s=unknownNode";
+
+    // the same node as NODE_1 with the default server index spelled out
+    private static final String NODE_1_WITH_SERVER_INDEX = "svr=0;" + NODE_1;
+
+    // the client endpoints only differ in the node options, so they share one 
connection
+    private static final String MILO_CLIENT = 
"milo-client:opc.tcp://foo:bar@localhost:@@port@@"
+                                              + 
"?allowedSecurityPolicies=None&overrideHost=true"
+                                              + 
"&samplingInterval=50&requestedPublishingInterval=100";
+
+    private static final String MILO_CLIENT_NODES = MILO_CLIENT + 
"&nodes=RAW(" + NODE_1 + "," + NODE_2 + ")";
+
+    private static final String MILO_CLIENT_UNKNOWN_NODE
+            = MILO_CLIENT + "&omitNullValues=false&nodes=RAW(" + NODE_1 + "," 
+ UNKNOWN_NODE + "," + NODE_2 + ")";
+
+    private static final String MILO_CLIENT_DUPLICATES
+            = MILO_CLIENT + "&nodes=RAW( " + NODE_1 + " ,, " + NODE_2 + " ," + 
NODE_1 + ")";
+
+    private static final String MILO_CLIENT_NODE = MILO_CLIENT + "&node=RAW(" 
+ NODE_1 + ")";
+
+    private static final String MILO_CLIENT_SAME_NODE
+            = MILO_CLIENT + "&nodes=RAW(" + NODE_1 + "," + 
NODE_1_WITH_SERVER_INDEX + ")";
+
+    @EndpointInject("mock:nodes")
+    protected MockEndpoint nodesEndpoint;
+
+    @EndpointInject("mock:unknownNode")
+    protected MockEndpoint unknownNodeEndpoint;
+
+    @EndpointInject("mock:duplicates")
+    protected MockEndpoint duplicatesEndpoint;
+
+    @EndpointInject("mock:node")
+    protected MockEndpoint nodeEndpoint;
+
+    @EndpointInject("mock:sameNode")
+    protected MockEndpoint sameNodeEndpoint;
+
+    @Override
+    protected RoutesBuilder createRouteBuilder() {
+        return new RouteBuilder() {
+            @Override
+            public void configure() {
+                // the consumers deliver the current values while they start, 
so each test starts its route
+                
from(resolve(MILO_CLIENT_NODES)).routeId("nodes").autoStartup(false).to("mock:nodes");
+                
from(resolve(MILO_CLIENT_UNKNOWN_NODE)).routeId("unknownNode").autoStartup(false).to("mock:unknownNode");
+                
from(resolve(MILO_CLIENT_DUPLICATES)).routeId("duplicates").autoStartup(false).to("mock:duplicates");
+                
from(resolve(MILO_CLIENT_NODE)).routeId("node").autoStartup(false).to("mock:node");
+                
from(resolve(MILO_CLIENT_SAME_NODE)).routeId("sameNode").autoStartup(false).to("mock:sameNode");
+            }
+        };
+    }
+
+    @Test
+    void consumerMonitorsSeveralNodes() throws Exception {
+        writeServerValues();
+
+        nodesEndpoint.expectedMessageCount(2);
+        context.getRouteController().startRoute("nodes");
+        nodesEndpoint.assertIsSatisfied();
+
+        final Map<String, DataValue> values = valuesByNode(nodesEndpoint);
+        assertEquals(Set.of(NODE_1, NODE_2), values.keySet());
+        assertGoodValue("Foo").accept(values.get(NODE_1));
+        assertGoodValue("Bar").accept(values.get(NODE_2));
+
+        nodesEndpoint.reset();
+        nodesEndpoint.expectedMessageCount(1);
+        nodesEndpoint.expectedHeaderReceived(MiloConstants.HEADER_NODE_ID, 
NODE_2);
+        testBody(nodesEndpoint.message(0), assertGoodValue("Baz"));
+        template.sendBody("milo-server:node2", "Baz");
+        nodesEndpoint.assertIsSatisfied();
+    }
+
+    @Test
+    void unknownNodeReceivesBadStatusAndTheOtherNodesKeepWorking() throws 
Exception {
+        writeServerValues();
+
+        unknownNodeEndpoint.expectedMessageCount(3);
+        context.getRouteController().startRoute("unknownNode");
+        unknownNodeEndpoint.assertIsSatisfied();
+
+        final Map<String, DataValue> values = 
valuesByNode(unknownNodeEndpoint);
+        assertEquals(Set.of(NODE_1, UNKNOWN_NODE, NODE_2), values.keySet());
+        assertEquals(new StatusCode(StatusCodes.Bad_NodeIdUnknown), 
values.get(UNKNOWN_NODE).getStatusCode());
+        assertGoodValue("Foo").accept(values.get(NODE_1));
+        assertGoodValue("Bar").accept(values.get(NODE_2));
+
+        unknownNodeEndpoint.reset();
+        unknownNodeEndpoint.expectedMessageCount(1);
+        
unknownNodeEndpoint.expectedHeaderReceived(MiloConstants.HEADER_NODE_ID, 
NODE_1);
+        testBody(unknownNodeEndpoint.message(0), assertGoodValue("Baz"));
+        template.sendBody("milo-server:node1", "Baz");
+        unknownNodeEndpoint.assertIsSatisfied();
+    }
+
+    @Test
+    void duplicateNodesAreMonitoredOnce() throws Exception {
+        writeServerValues();
+
+        // entries are trimmed and empty entries are skipped
+        duplicatesEndpoint.expectedMessageCount(2);
+        duplicatesEndpoint.setAssertPeriod(1000);
+        context.getRouteController().startRoute("duplicates");
+        duplicatesEndpoint.assertIsSatisfied();
+
+        final Map<String, DataValue> values = valuesByNode(duplicatesEndpoint);
+        assertEquals(Set.of(NODE_1, NODE_2), values.keySet());
+        assertGoodValue("Foo").accept(values.get(NODE_1));
+        assertGoodValue("Bar").accept(values.get(NODE_2));
+    }
+
+    @Test
+    void spellingsOfOneNodeAreMonitoredOnce() throws Exception {
+        // the entries are de-duplicated by the parsed node ID, not by their 
spelling
+        assertEquals(ExpandedNodeId.parse("ns=0;i=2258"), 
ExpandedNodeId.parse("i=2258"));
+        assertEquals(ExpandedNodeId.parse(NODE_1), 
ExpandedNodeId.parse(NODE_1_WITH_SERVER_INDEX));
+
+        writeServerValues();
+
+        // one exchange per value, the header carries the first spelling
+        sameNodeEndpoint.expectedMessageCount(1);
+        sameNodeEndpoint.setAssertPeriod(1000);
+        sameNodeEndpoint.expectedHeaderReceived(MiloConstants.HEADER_NODE_ID, 
NODE_1);
+        testBody(sameNodeEndpoint.message(0), assertGoodValue("Foo"));
+        context.getRouteController().startRoute("sameNode");
+        sameNodeEndpoint.assertIsSatisfied();
+
+        // and one monitored item on the server
+        final MiloClientEndpoint endpoint = 
context.getEndpoint(resolve(MILO_CLIENT_SAME_NODE), MiloClientEndpoint.class);
+        final MiloClientConnection connection = endpoint.createConnection();
+        try {
+            assertEquals(1, 
connection.getOpcUaClient().getSubscriptions().get(0).getMonitoredItems().size());
+        } finally {
+            endpoint.releaseConnection(connection);
+        }
+    }
+
+    @Test
+    void nodeAndNodesTogetherAreRejected() {
+        final Endpoint endpoint = context.getEndpoint(resolve(MILO_CLIENT_NODE 
+ "&nodes=RAW(" + NODE_2 + ")"));
+
+        assertThrows(IllegalArgumentException.class, () -> 
endpoint.createConsumer(exchange -> {
+        }));
+    }
+
+    @Test
+    void nodesWithoutNodeIdsAreRejected() throws Exception {
+        final Consumer consumer = context.getEndpoint(resolve(MILO_CLIENT + 
"&nodes=RAW( , )")).createConsumer(exchange -> {
+        });
+
+        assertThrows(IllegalArgumentException.class, consumer::start);
+    }
+
+    @Test
+    void invalidNodeIdIsNamed() throws Exception {
+        final Consumer consumer = context.getEndpoint(resolve(MILO_CLIENT + 
"&nodes=RAW(" + NODE_1 + ",ns=2;x=foo)"))
+                .createConsumer(exchange -> {
+                });
+
+        final IllegalArgumentException e = 
assertThrows(IllegalArgumentException.class, consumer::start);
+        assertEquals("Invalid node ID: ns=2;x=foo", e.getMessage());
+    }
+
+    @Test
+    void singleNodeConsumerSetsTheNodeHeader() throws Exception {
+        writeServerValues();
+
+        nodeEndpoint.expectedMessageCount(1);
+        nodeEndpoint.expectedHeaderReceived(MiloConstants.HEADER_NODE_ID, 
NODE_1);
+        testBody(nodeEndpoint.message(0), assertGoodValue("Foo"));
+        context.getRouteController().startRoute("node");
+        nodeEndpoint.assertIsSatisfied();
+    }
+
+    private void writeServerValues() {
+        // the server creates the nodes when the server endpoints start
+        template.sendBody("milo-server:node1", "Foo");
+        template.sendBody("milo-server:node2", "Bar");
+    }
+
+    private static Map<String, DataValue> valuesByNode(final MockEndpoint 
endpoint) {
+        final Map<String, DataValue> values = new HashMap<>();
+        for (final Exchange exchange : endpoint.getReceivedExchanges()) {
+            
values.put(exchange.getMessage().getHeader(MiloConstants.HEADER_NODE_ID, 
String.class),
+                    exchange.getMessage().getBody(DataValue.class));
+        }
+        return values;
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 5d1d302fc766..2e119511a5ee 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -797,3 +797,16 @@ Semantic audit backends can opt into the additive 
`inputType` extension. Readers
 support it keep their existing projection. The `camel semantic audit --json` 
display omits image
 base64 and reports metadata instead. Use `--output=<new-file>` to export the 
full returned JSON,
 with `--include-input` for case/bulk requests that need captured input. 
Existing files are not overwritten.
+
+=== camel-milo - creating and deleting the monitored items of a consumer
+
+Since 4.16, a `milo-client` consumer whose monitored item could not be 
created, for example because the node does not
+exist, received nothing. Now it receives one `DataValue` with the bad status 
code (when `omitNullValues=false`). The
+monitored item is created again when the route is restarted or when Camel 
rebuilds the connection to the server; unlike
+Camel 3.x, it is not retried every 10 seconds.
+
+The consumer now sets the `CamelMiloNodeId` header on every exchange, 
including a consumer with a single `node`.
+
+Stopping a consumer now deletes its monitored items. Previously they stayed on 
the server while another endpoint kept
+the shared connection open, so a restarted consumer received each value twice. 
While the server is unreachable, the
+stop waits until the client's next attempt to reconnect has failed.
diff --git 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloClientEndpointBuilderFactory.java
 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloClientEndpointBuilderFactory.java
index d6e3c02ca735..fdde075f0dce 100644
--- 
a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloClientEndpointBuilderFactory.java
+++ 
b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/MiloClientEndpointBuilderFactory.java
@@ -361,6 +361,21 @@ public interface MiloClientEndpointBuilderFactory {
             doSetProperty("samplingInterval", samplingInterval);
             return this;
         }
+        /**
+         * Comma-separated node definitions to monitor with one consumer,
+         * instead of node (see Node ID).
+         * 
+         * The option is a: <code>java.lang.String</code> type.
+         * 
+         * Group: consumer
+         * 
+         * @param nodes the value to set
+         * @return the dsl builder
+         */
+        default MiloClientEndpointConsumerBuilder nodes(String nodes) {
+            doSetProperty("nodes", nodes);
+            return this;
+        }
         /**
          * A set of allowed security policy URIs. Default is to accept all and
          * use the highest.
@@ -2542,6 +2557,20 @@ public interface MiloClientEndpointBuilderFactory {
         public String miloAwait() {
             return "CamelMiloAwait";
         }
+        /**
+         * The node ID of the value, as configured in the node or nodes option.
+         * Not to be confused with CamelMiloNodeIds, the list of nodes that the
+         * producer reads.
+         * 
+         * The option is a: {@code String} type.
+         * 
+         * Group: consumer
+         * 
+         * @return the name of the header {@code MiloNodeId}.
+         */
+        public String miloNodeId() {
+            return "CamelMiloNodeId";
+        }
     }
     static MiloClientEndpointBuilder endpointBuilder(String componentName, 
String path) {
         class MiloClientEndpointBuilderImpl extends AbstractEndpointBuilder 
implements MiloClientEndpointBuilder, AdvancedMiloClientEndpointBuilder {

Reply via email to