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 5961fc46e56b CAMEL-25626: camel-milo - deliver a data change with a 
Bad status even when its value is null
5961fc46e56b is described below

commit 5961fc46e56b43cf29a022fb7e93a46041315753
Author: Serdar Gökay <[email protected]>
AuthorDate: Fri Oct 16 17:59:44 2026 +0300

    CAMEL-25626: camel-milo - deliver a data change with a Bad status even when 
its value is null
    
    [CAMEL-25626](https://issues.apache.org/jira/browse/CAMEL-25626).
    
    With the default `omitNullValues=true`, the `milo-client` consumer dropped 
every data change whose value was null, regardless of its status code. OPC UA 
Part 4 requires a server to send a null value together with a Bad status, so 
dropping null values drops every Bad status: a sensor failure, a removed node 
or a monitored item that could not be created (#27780, #27906) never reached 
the route unless `omitNullValues=false` was set.
    
    Why this matters in automation: a route that feeds a historian, an MES, a 
UNS topic or an alarm evaluates the last value it received. When the quality of 
a tag goes Bad and the route never hears about it, the stale value keeps being 
treated as current, and the plant acts on a number that nobody is measuring any 
more. Quality propagation is the first thing an OT integration is expected to 
get right; the component should deliver it by default.
    
    Closes #27908
    
    Co-authored-by: Claude Fable 5.1 <[email protected]>
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../camel/catalog/components/milo-client.json      |  2 +-
 .../camel/catalog/docs/milo-client-component.adoc  | 16 +++++++--
 .../camel/component/milo/client/milo-client.json   |  2 +-
 .../src/main/docs/milo-client-component.adoc       | 16 +++++++--
 .../component/milo/client/MiloClientConsumer.java  | 13 ++++++--
 .../component/milo/client/MiloClientEndpoint.java  |  3 +-
 .../component/milo/MonitorItemLifecycleTest.java   | 39 +++++++++++++++++-----
 .../milo/MonitorItemMultiConnectionsTest.java      | 10 +++---
 .../camel/component/milo/MonitorItemTest.java      | 38 +++++++++++----------
 .../camel/component/milo/MonitorNodesTest.java     | 29 +++++++++++++++-
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    | 13 +++++++-
 .../dsl/MiloClientEndpointBuilderFactory.java      | 18 ++++++----
 12 files changed, 152 insertions(+), 47 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 8b3ac0766cbb..8b32e5c1d6ff 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
@@ -70,7 +70,7 @@
     "method": { "index": 8, "kind": "parameter", "displayName": "Method", 
"group": "common", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "description": "The method definition (see Method ID)" },
     "monitorFilterType": { "index": 9, "kind": "parameter", "displayName": 
"Monitor Filter Type", "group": "common", "label": "", "required": false, 
"type": "enum", "javaType": 
"org.apache.camel.component.milo.client.MonitorFilterType", "enum": [ 
"dataChangeFilter" ], "deprecated": false, "autowired": false, "secret": false, 
"configurationClass": 
"org.apache.camel.component.milo.client.MonitorFilterConfiguration", 
"configurationField": "monitorFilterConfiguration", "description": "Monito [...]
     "node": { "index": 10, "kind": "parameter", "displayName": "Node", 
"group": "common", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "description": "The node definition (see Node ID)" },
-    "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" },
+    "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 a data change whose 
value is null and whose status is Good. A data change with a Bad or Uncertain 
status is always delivered." },
     "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" },
     "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)" },
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 e04f72f92737..046501c41d7d 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
@@ -149,8 +149,20 @@ 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.
+The consumer delivers one exchange with the bad status code for such a node, 
and the `CamelMiloNodeId` header names it.
+
+=== Status codes and null values
+
+A data change with a Bad or Uncertain status is always delivered, even when 
its value is null; `omitNullValues` only
+omits a null value with a Good status. This includes the first notification of 
a monitored item: it carries the current
+value of the node, at startup and again after a reconnect, so it can be a Bad 
status such as `Bad_WaitingForInitialData`
+or the status of a `milo-server` item that was not written yet. To skip such a 
notification, a route filters on its
+status code (`getStatusCode()` of the `DataValue` body); a filter on 
`isGood()` would also drop the Bad status of a
+failed sensor.
+
+The default `omitNullValues=true` also omits a null value with a Good status 
that follows a Bad status, so the route
+does not learn that the node recovered. Set `omitNullValues=false` for a node 
whose value can be null, such as a
+`String`.
 
 === Method ID
 
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 8b3ac0766cbb..8b32e5c1d6ff 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
@@ -70,7 +70,7 @@
     "method": { "index": 8, "kind": "parameter", "displayName": "Method", 
"group": "common", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "description": "The method definition (see Method ID)" },
     "monitorFilterType": { "index": 9, "kind": "parameter", "displayName": 
"Monitor Filter Type", "group": "common", "label": "", "required": false, 
"type": "enum", "javaType": 
"org.apache.camel.component.milo.client.MonitorFilterType", "enum": [ 
"dataChangeFilter" ], "deprecated": false, "autowired": false, "secret": false, 
"configurationClass": 
"org.apache.camel.component.milo.client.MonitorFilterConfiguration", 
"configurationField": "monitorFilterConfiguration", "description": "Monito [...]
     "node": { "index": 10, "kind": "parameter", "displayName": "Node", 
"group": "common", "label": "", "required": false, "type": "string", 
"javaType": "java.lang.String", "deprecated": false, "autowired": false, 
"secret": false, "description": "The node definition (see Node ID)" },
-    "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" },
+    "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 a data change whose 
value is null and whose status is Good. A data change with a Bad or Uncertain 
status is always delivered." },
     "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" },
     "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)" },
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 e04f72f92737..046501c41d7d 100644
--- a/components/camel-milo/src/main/docs/milo-client-component.adoc
+++ b/components/camel-milo/src/main/docs/milo-client-component.adoc
@@ -149,8 +149,20 @@ 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.
+The consumer delivers one exchange with the bad status code for such a node, 
and the `CamelMiloNodeId` header names it.
+
+=== Status codes and null values
+
+A data change with a Bad or Uncertain status is always delivered, even when 
its value is null; `omitNullValues` only
+omits a null value with a Good status. This includes the first notification of 
a monitored item: it carries the current
+value of the node, at startup and again after a reconnect, so it can be a Bad 
status such as `Bad_WaitingForInitialData`
+or the status of a `milo-server` item that was not written yet. To skip such a 
notification, a route filters on its
+status code (`getStatusCode()` of the `DataValue` body); a filter on 
`isGood()` would also drop the Bad status of a
+failed sensor.
+
+The default `omitNullValues=true` also omits a null value with a Good status 
that follows a Bad status, so the route
+does not learn that the node recovered. Set `omitNullValues=false` for a node 
whose value can be null, such as a
+`String`.
 
 === Method ID
 
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 a6b6e0c2cd1c..59e0c90aabc1 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
@@ -116,8 +116,8 @@ public class MiloClientConsumer extends DefaultConsumer {
     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)) {
-            LOG.debug("Handle item update omitted due to null values (see 
omitNullValues parameter)");
+        if (omitNullValues && isNullValueWithGoodStatus(value)) {
+            LOG.debug("Handle item update omitted due to a null value with a 
Good status (see omitNullValues parameter)");
             return;
         }
 
@@ -131,6 +131,15 @@ public class MiloClientConsumer extends DefaultConsumer {
         }
     }
 
+    /**
+     * The data change that omitNullValues omits: a null value with a Good 
status. A data change with a Bad or Uncertain
+     * status is delivered even when its value is null.
+     */
+    private static boolean isNullValueWithGoodStatus(final DataValue value) {
+        return value == null
+                || (value.getValue().getValue() == null && 
value.getStatusCode().isGood());
+    }
+
     private void mapToMessage(final DataValue value, final Message message) {
         if (value != null) {
             Messages.fillFromDataValue(value, message);
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 752e4adb114d..afc7cc34ec2f 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
@@ -91,7 +91,8 @@ public class MiloClientEndpoint extends DefaultEndpoint {
     private boolean defaultAwaitWrites;
 
     /**
-     * Omit notifications in case of null values
+     * Omit a data change whose value is null and whose status is Good. A data 
change with a Bad or Uncertain status is
+     * always delivered.
      */
     @UriParam(defaultValue = "true")
     private boolean omitNullValues = true;
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
index b5f50b241691..cec8822d6eb0 100644
--- 
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
@@ -40,6 +40,7 @@ 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.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
@@ -77,15 +78,13 @@ public class MonitorItemLifecycleTest extends 
AbstractMiloServerTest {
             = 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";
+            = MILO_CLIENT + 
NodeIds.nodeValue(MiloServerComponent.DEFAULT_NAMESPACE_URI, "unknownItem");
 
     private static final String MILO_CLIENT_UNKNOWN_ITEM_WRITE_BACK
-            = MILO_CLIENT + 
NodeIds.nodeValue(MiloServerComponent.DEFAULT_NAMESPACE_URI, 
"unknownItemWriteBack")
-              + "&omitNullValues=false";
+            = MILO_CLIENT + 
NodeIds.nodeValue(MiloServerComponent.DEFAULT_NAMESPACE_URI, 
"unknownItemWriteBack");
 
     private static final String MILO_CLIENT_ITEMS_1_2
-            = MILO_CLIENT_BASE + "&omitNullValues=false&nodes=RAW(" + 
NODE_ITEM_1 + "," + NODE_ITEM_2 + ")";
+            = MILO_CLIENT_BASE + "&nodes=RAW(" + NODE_ITEM_1 + "," + 
NODE_ITEM_2 + ")";
 
     private static final String MOCK_ITEM_1 = "mock:item1";
 
@@ -126,10 +125,10 @@ public class MonitorItemLifecycleTest extends 
AbstractMiloServerTest {
                 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_1)).routeId("item1").autoStartup(false).to(MOCK_ITEM_1);
                 
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);
 
@@ -145,9 +144,11 @@ public class MonitorItemLifecycleTest extends 
AbstractMiloServerTest {
 
     @Test
     void restartedConsumerReceivesEachValueOnce() throws Exception {
+        producer1.sendBody("Foo");
+
         item1Endpoint.expectedMessageCount(1);
         testBody(item1Endpoint.message(0), assertGoodValue("Foo"));
-        producer1.sendBody("Foo");
+        context.getRouteController().startRoute("item1");
         item1Endpoint.assertIsSatisfied();
 
         // the other consumers keep the shared connection open
@@ -180,12 +181,34 @@ public class MonitorItemLifecycleTest extends 
AbstractMiloServerTest {
         unknownItemEndpoint.assertIsSatisfied();
 
         // the other consumers of the connection are not affected
+        producer1.sendBody("Foo");
+
         item1Endpoint.expectedMessageCount(1);
         testBody(item1Endpoint.message(0), assertGoodValue("Foo"));
-        producer1.sendBody("Foo");
+        context.getRouteController().startRoute("item1");
         item1Endpoint.assertIsSatisfied();
     }
 
+    @Test
+    void itemThatWasNotWrittenYetReportsABadStatusAtStartup() throws Exception 
{
+        // the first notification carries the current value of the item, which 
is a Bad status without a value until
+        // the item is written; the consumer delivers it like any other Bad 
status
+        item2Endpoint.expectedMessageCount(1);
+        item2Endpoint.expectedHeaderReceived(MiloConstants.HEADER_NODE_ID, 
NODE_ITEM_2);
+        testBody(item2Endpoint.message(0), value -> {
+            assertTrue(value.getStatusCode().isBad(), 
value.getStatusCode().toString());
+            assertNull(value.getValue().getValue());
+        });
+        context.getRouteController().startRoute("item2");
+        item2Endpoint.assertIsSatisfied();
+
+        item2Endpoint.reset();
+        item2Endpoint.expectedMessageCount(1);
+        testBody(item2Endpoint.message(0), assertGoodValue("Bar"));
+        producer2.sendBody("Bar");
+        item2Endpoint.assertIsSatisfied();
+    }
+
     @Test
     @Timeout(value = 60, unit = TimeUnit.SECONDS)
     void badStatusIsDeliveredWithoutHoldingTheConnectionLock() throws 
Exception {
diff --git 
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemMultiConnectionsTest.java
 
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemMultiConnectionsTest.java
index a673c6d3bf0e..152582e3bb00 100644
--- 
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemMultiConnectionsTest.java
+++ 
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemMultiConnectionsTest.java
@@ -86,9 +86,10 @@ public class MonitorItemMultiConnectionsTest extends 
AbstractMiloServerTest {
             public void configure() {
                 from(DIRECT_START_1).to(MILO_SERVER_ITEM_1);
 
-                from(resolve(MILO_CLIENT_ITEM_C1_1)).to(MOCK_TEST_1);
-                from(resolve(MILO_CLIENT_ITEM_C2_1)).to(MOCK_TEST_2);
-                from(resolve(MILO_CLIENT_ITEM_C3_1)).to(MOCK_TEST_3);
+                // the consumers deliver the current value while they start, 
so the test starts these routes
+                
from(resolve(MILO_CLIENT_ITEM_C1_1)).autoStartup(false).to(MOCK_TEST_1);
+                
from(resolve(MILO_CLIENT_ITEM_C2_1)).autoStartup(false).to(MOCK_TEST_2);
+                
from(resolve(MILO_CLIENT_ITEM_C3_1)).autoStartup(false).to(MOCK_TEST_3);
             }
         };
     }
@@ -110,8 +111,9 @@ public class MonitorItemMultiConnectionsTest extends 
AbstractMiloServerTest {
         this.test3Endpoint.setExpectedCount(0);
         this.test3Endpoint.setSleepForEmptyTest(5_000);
 
-        // set server value
+        // set server value, then start the consumers
         this.producer1.sendBody("Foo");
+        context.getRouteController().startAllRoutes();
 
         // assert
         MockEndpoint.assertIsSatisfied(context);
diff --git 
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemTest.java
 
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemTest.java
index 07509dea2348..3181f2457a35 100644
--- 
a/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemTest.java
+++ 
b/components/camel-milo/src/test/java/org/apache/camel/component/milo/MonitorItemTest.java
@@ -108,10 +108,11 @@ public class MonitorItemTest extends 
AbstractMiloServerTest {
             public void configure() {
                 from(DIRECT_START_1).to(MILO_SERVER_ITEM_1);
 
-                from(resolve(MILO_CLIENT_ITEM_C1_1)).to(MOCK_TEST_1);
-                from(resolve(MILO_CLIENT_ITEM_C1_2)).to(MOCK_TEST_2);
-                from(resolve(MILO_CLIENT_ITEM_C1_3)).to(MOCK_TEST_3);
-                from(resolve(MILO_CLIENT_ITEM_C2_1)).to(MOCK_TEST_4);
+                // the consumers deliver the current value while they start, 
so the test starts these routes
+                
from(resolve(MILO_CLIENT_ITEM_C1_1)).autoStartup(false).to(MOCK_TEST_1);
+                
from(resolve(MILO_CLIENT_ITEM_C1_2)).autoStartup(false).to(MOCK_TEST_2);
+                
from(resolve(MILO_CLIENT_ITEM_C1_3)).autoStartup(false).to(MOCK_TEST_3);
+                
from(resolve(MILO_CLIENT_ITEM_C2_1)).autoStartup(false).to(MOCK_TEST_4);
             }
         };
     }
@@ -162,13 +163,13 @@ public class MonitorItemTest extends 
AbstractMiloServerTest {
 
         /*
          * test3Endpoint is related to client C1_3 with: 
requestedPublishingInterval=2000, samplingInterval=1000 from
-         * default, queueSize=4. It should get 1 (or 2 depending on exact 
timing) of the first 3 messages because of
-         * samplingInterval and an update pause of 2 seconds after they are 
sent. From the rest 16 messages, sent in
-         * about 3 seconds, only 3 or 4 should be sampled. So minimum is 4, 
maximum is
-         * 6 messages
+         * default, queueSize=4. It should get Foo, the value when it starts, 
and 1 (or 2 depending on exact timing) of
+         * the next 3 messages because of samplingInterval and an update pause 
of 2 seconds after they are sent. From
+         * the rest 16 messages, sent in about 3 seconds, only 3 or 4 should 
be sampled. So minimum is 5, maximum is
+         * 7 messages
          */
         test3Endpoint.reset();
-        test3Endpoint.setMinimumExpectedMessageCount(4);    // One or 2 of the 
first 3, plus 3 or 4 of the remaining 16
+        test3Endpoint.setMinimumExpectedMessageCount(5);    // Foo, one or 2 
of the next 3, plus 3 or 4 of the remaining 16
         test3Endpoint.setAssertPeriod(3000);
 
         /*
@@ -183,19 +184,20 @@ public class MonitorItemTest extends 
AbstractMiloServerTest {
         test4Endpoint.setMinimumExpectedMessageCount(12);    // the first 3, 
plus at least 3*3 more from rest (if they fall to 3 periods)
         test4Endpoint.setAssertPeriod(3000);
 
-        // Allow time for OPC UA client-server connection to establish
-        await().pollDelay(1, TimeUnit.SECONDS).untilAsserted(() -> {
-            // Connection should be established
-        });
-
         // Debug: Check if server is configured correctly
         LOG.info("Server Port: {}", this.getServerPort());
         LOG.info("Client URI resolved: {}", resolve(MILO_CLIENT_ITEM_C1_1));
 
-        // set server values sent faster than requestedPublishingInterval so 
they must be put to queue
+        // the first value is written before the consumers start, so it is the 
first notification of each client
         this.producer1.sendBody("Foo");
-        await().pollDelay(time, TimeUnit.MILLISECONDS).untilAsserted(() -> {
+        context.getRouteController().startAllRoutes();
+
+        // Allow time for OPC UA client-server connection to establish
+        await().pollDelay(1, TimeUnit.SECONDS).untilAsserted(() -> {
+            // Connection should be established
         });
+
+        // set server values sent faster than requestedPublishingInterval so 
they must be put to queue
         this.producer1.sendBody("Bar");
         await().pollDelay(time, TimeUnit.MILLISECONDS).untilAsserted(() -> {
         });
@@ -241,9 +243,9 @@ public class MonitorItemTest extends AbstractMiloServerTest 
{
         Exchange last = receivedExchanges.get(receivedExchanges.size() - 1);
         assertGoodValue("Done").accept((DataValue) last.getIn().getBody());
 
-        // The third client should get no more than 6 messages
+        // The third client should get no more than 7 messages
         int count3 = this.test3Endpoint.getReceivedCounter();
-        assertTrue(count3 <= 6, "Not enough messages have been dropped, but 
should because of queueSize");
+        assertTrue(count3 <= 7, "Not enough messages have been dropped, but 
should because of queueSize");
 
         // The fourth client should get no more than 14 messages, because it 
is very sensitive on the timing we allow 1 more
         int count4 = this.test4Endpoint.getReceivedCounter();
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
index 18460c74164a..cef04fa5f63c 100644
--- 
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
@@ -34,9 +34,11 @@ 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.eclipse.milo.opcua.stack.core.types.builtin.Variant;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 
 /**
@@ -61,7 +63,7 @@ public class MonitorNodesTest extends AbstractMiloServerTest {
     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 + ")";
+            = MILO_CLIENT + "&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 + ")";
@@ -144,6 +146,31 @@ public class MonitorNodesTest extends 
AbstractMiloServerTest {
         unknownNodeEndpoint.assertIsSatisfied();
     }
 
+    @Test
+    void notGoodStatusWithNullValueIsDeliveredByDefault() throws Exception {
+        // a null value with a Good status is omitted; a Bad or Uncertain 
status with a null value is delivered
+        template.sendBody("milo-server:node1", new 
DataValue(Variant.NULL_VALUE, StatusCode.GOOD, null));
+        template.sendBody("milo-server:node2", new DataValue(new 
StatusCode(StatusCodes.Uncertain_LastUsableValue)));
+
+        unknownNodeEndpoint.expectedMessageCount(2);
+        unknownNodeEndpoint.setAssertPeriod(1000);
+        context.getRouteController().startRoute("unknownNode");
+        unknownNodeEndpoint.assertIsSatisfied();
+
+        final Map<String, DataValue> values = 
valuesByNode(unknownNodeEndpoint);
+        assertEquals(Set.of(UNKNOWN_NODE, NODE_2), values.keySet());
+        assertEquals(new StatusCode(StatusCodes.Bad_NodeIdUnknown), 
values.get(UNKNOWN_NODE).getStatusCode());
+        assertEquals(new StatusCode(StatusCodes.Uncertain_LastUsableValue), 
values.get(NODE_2).getStatusCode());
+        assertNull(values.get(NODE_2).getValue().getValue());
+
+        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();
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 739fad1e7cb8..0814687b4a00 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
@@ -1188,10 +1188,21 @@ support it keep their existing projection. The `camel 
semantic audit --json` dis
 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 - a Bad or Uncertain status is delivered by default
+
+A `milo-client` consumer now delivers a data change with a Bad or Uncertain 
status by default, even when its value is
+null; previously the default `omitNullValues=true` dropped it. 
`omitNullValues` now only omits a null value with a Good
+status. A route may therefore receive a Bad status with a null value, also as 
the first notification of a monitored
+item at startup and after a reconnect, for example `Bad_WaitingForInitialData` 
or the status of a `milo-server` item
+that was not written yet. A route that uses the value must check 
`getStatusCode()` of the `DataValue` body first:
+Camel's type converter returns `0` or `false` for a null value that is 
converted to a primitive type such as `double`
+or `boolean`. To skip the startup notification, filter on its status code; a 
filter on `isGood()` would also drop the
+Bad status of a failed sensor.
+
 === 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
+exist, received nothing. Now it receives one `DataValue` with the bad status 
code. 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.
 
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 fdde075f0dce..079691e01dc6 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
@@ -271,7 +271,8 @@ public interface MiloClientEndpointBuilderFactory {
             return this;
         }
         /**
-         * Omit notifications in case of null values.
+         * Omit a data change whose value is null and whose status is Good. A
+         * data change with a Bad or Uncertain status is always delivered.
          * 
          * The option is a: <code>boolean</code> type.
          * 
@@ -286,7 +287,8 @@ public interface MiloClientEndpointBuilderFactory {
             return this;
         }
         /**
-         * Omit notifications in case of null values.
+         * Omit a data change whose value is null and whose status is Good. A
+         * data change with a Bad or Uncertain status is always delivered.
          * 
          * The option will be converted to a <code>boolean</code> type.
          * 
@@ -1153,7 +1155,8 @@ public interface MiloClientEndpointBuilderFactory {
             return this;
         }
         /**
-         * Omit notifications in case of null values.
+         * Omit a data change whose value is null and whose status is Good. A
+         * data change with a Bad or Uncertain status is always delivered.
          * 
          * The option is a: <code>boolean</code> type.
          * 
@@ -1168,7 +1171,8 @@ public interface MiloClientEndpointBuilderFactory {
             return this;
         }
         /**
-         * Omit notifications in case of null values.
+         * Omit a data change whose value is null and whose status is Good. A
+         * data change with a Bad or Uncertain status is always delivered.
          * 
          * The option will be converted to a <code>boolean</code> type.
          * 
@@ -1949,7 +1953,8 @@ public interface MiloClientEndpointBuilderFactory {
             return this;
         }
         /**
-         * Omit notifications in case of null values.
+         * Omit a data change whose value is null and whose status is Good. A
+         * data change with a Bad or Uncertain status is always delivered.
          * 
          * The option is a: <code>boolean</code> type.
          * 
@@ -1964,7 +1969,8 @@ public interface MiloClientEndpointBuilderFactory {
             return this;
         }
         /**
-         * Omit notifications in case of null values.
+         * Omit a data change whose value is null and whose status is Good. A
+         * data change with a Bad or Uncertain status is always delivered.
          * 
          * The option will be converted to a <code>boolean</code> type.
          * 

Reply via email to