diff --git a/Component-DataConnectionLayer.md b/Component-DataConnectionLayer.md
index 3a5886c..1a5033a 100644
--- a/Component-DataConnectionLayer.md
+++ b/Component-DataConnectionLayer.md
@@ -54,7 +54,7 @@ Additional protocols can be added by implementing this interface.
### Common Value Type
-Both protocols produce the same value tuple consumed by Instance Actors:
+Both protocols produce the same value tuple consumed by Instance Actors. Before the first value update arrives from the DCL, data-sourced attributes are held at **uncertain** quality by the Instance Actor (see Site Runtime — Initialization):
| Concept | ScadaLink Design | LmxProxy SDK (`Vtq`) |
|---|---|---|
diff --git a/Component-SiteRuntime.md b/Component-SiteRuntime.md
index e929017..e85ef78 100644
--- a/Component-SiteRuntime.md
+++ b/Component-SiteRuntime.md
@@ -87,9 +87,10 @@ Deployment Manager Singleton (Cluster Singleton)
### Initialization
1. Load all attribute values from the flattened configuration (static defaults).
-2. Register data source references with the Data Connection Layer for subscriptions.
-3. Create child Script Actors (one per script defined on the instance).
-4. Create child Alarm Actors (one per alarm defined on the instance).
+2. Set quality to **uncertain** for all attributes that have a data source reference. Static attributes (no data source reference) have quality **good**. The uncertain quality persists until the first value update arrives from the Data Connection Layer, distinguishing "not yet received" from "known good" or "connection lost."
+3. Register data source references with the Data Connection Layer for subscriptions.
+4. Create child Script Actors (one per script defined on the instance).
+5. Create child Alarm Actors (one per alarm defined on the instance).
### Attribute Value Updates
- Receives tag value updates from the Data Connection Layer for attributes with data source references.
diff --git a/HighLevelReqs.md b/HighLevelReqs.md
index f838f78..4f13f73 100644
--- a/HighLevelReqs.md
+++ b/HighLevelReqs.md
@@ -60,6 +60,7 @@
- Both protocols implement a **common interface** supporting: connect, subscribe to tag paths, receive value updates, and write values.
- Additional protocols can be added by implementing the common interface.
- The Data Connection Layer is a **clean data pipe** — it publishes tag value updates to Instance Actors but performs no evaluation of triggers or alarm conditions.
+- **Initial attribute quality**: Attributes bound to a data connection start with **uncertain** quality when the Instance Actor initializes. The quality remains uncertain until the first value update is received from the Data Connection Layer. This distinguishes "never received a value" from "received a known-good value" or "connection lost" (bad quality).
### 2.5 Scale
- Approximately **10 sites**.
diff --git a/src/ScadaLink.Commons/Messages/Instance/GetAttributeRequest.cs b/src/ScadaLink.Commons/Messages/Instance/GetAttributeRequest.cs
index 5632646..d0caaa8 100644
--- a/src/ScadaLink.Commons/Messages/Instance/GetAttributeRequest.cs
+++ b/src/ScadaLink.Commons/Messages/Instance/GetAttributeRequest.cs
@@ -11,7 +11,9 @@ public record GetAttributeRequest(
DateTimeOffset Timestamp);
///
-/// Response containing the current value of an attribute.
+/// Response containing the current value and quality of an attribute.
+/// Quality is "Good", "Bad", or "Uncertain".
+/// Data-sourced attributes start at "Uncertain" until the first DCL value update arrives.
///
public record GetAttributeResponse(
string CorrelationId,
@@ -19,4 +21,5 @@ public record GetAttributeResponse(
string AttributeName,
object? Value,
bool Found,
+ string Quality,
DateTimeOffset Timestamp);
diff --git a/src/ScadaLink.SiteRuntime/Actors/InstanceActor.cs b/src/ScadaLink.SiteRuntime/Actors/InstanceActor.cs
index 565d738..89be901 100644
--- a/src/ScadaLink.SiteRuntime/Actors/InstanceActor.cs
+++ b/src/ScadaLink.SiteRuntime/Actors/InstanceActor.cs
@@ -38,6 +38,7 @@ public class InstanceActor : ReceiveActor
private readonly SiteRuntimeOptions _options;
private readonly ILogger _logger;
private readonly Dictionary _attributes = new();
+ private readonly Dictionary _attributeQualities = new();
private readonly Dictionary _alarmStates = new();
private readonly Dictionary _scriptActors = new();
private readonly Dictionary _alarmActors = new();
@@ -75,11 +76,15 @@ public class InstanceActor : ReceiveActor
_configuration = JsonSerializer.Deserialize(configJson);
// Load default attribute values from the flattened configuration
+ // Data-sourced attributes start with Uncertain quality until the first DCL value arrives.
+ // Static attributes start with Good quality.
if (_configuration != null)
{
foreach (var attr in _configuration.Attributes)
{
_attributes[attr.CanonicalName] = attr.Value;
+ _attributeQualities[attr.CanonicalName] =
+ string.IsNullOrEmpty(attr.DataSourceReference) ? "Good" : "Uncertain";
}
}
@@ -169,12 +174,14 @@ public class InstanceActor : ReceiveActor
private void HandleGetAttribute(GetAttributeRequest request)
{
var found = _attributes.TryGetValue(request.AttributeName, out var value);
+ _attributeQualities.TryGetValue(request.AttributeName, out var quality);
Sender.Tell(new GetAttributeResponse(
request.CorrelationId,
_instanceUniqueName,
request.AttributeName,
value,
found,
+ quality ?? "Good",
DateTimeOffset.UtcNow));
}
@@ -249,6 +256,7 @@ public class InstanceActor : ReceiveActor
{
// WP-24: State mutation serialized through this actor
_attributes[changed.AttributeName] = changed.Value;
+ _attributeQualities[changed.AttributeName] = changed.Quality;
PublishAndNotifyChildren(changed);
}
@@ -345,7 +353,7 @@ public class InstanceActor : ReceiveActor
kvp.Key,
kvp.Key,
kvp.Value,
- "Good",
+ _attributeQualities.GetValueOrDefault(kvp.Key, "Good"),
DateTimeOffset.UtcNow)).ToList();
var alarmStates = _alarmStates.Select(kvp => new AlarmStateChanged(
diff --git a/tests/ScadaLink.SiteRuntime.Tests/Actors/InstanceActorTests.cs b/tests/ScadaLink.SiteRuntime.Tests/Actors/InstanceActorTests.cs
index 8d4511a..20b5254 100644
--- a/tests/ScadaLink.SiteRuntime.Tests/Actors/InstanceActorTests.cs
+++ b/tests/ScadaLink.SiteRuntime.Tests/Actors/InstanceActorTests.cs
@@ -222,4 +222,61 @@ public class InstanceActorTests : TestKit, IDisposable
var response = ExpectMsg();
Assert.Equal("98.6", response.Value?.ToString());
}
+
+ [Fact]
+ public void InstanceActor_DataSourcedAttribute_StartsWithUncertainQuality()
+ {
+ var config = new FlattenedConfiguration
+ {
+ InstanceUniqueName = "Pump1",
+ Attributes =
+ [
+ new ResolvedAttribute
+ {
+ CanonicalName = "Temperature",
+ Value = "0",
+ DataType = "Double",
+ DataSourceReference = "/Motor/Temperature",
+ BoundDataConnectionName = "OpcServer1"
+ }
+ ]
+ };
+
+ var actor = CreateInstanceActor("Pump1", config);
+
+ actor.Tell(new GetAttributeRequest(
+ "corr-quality-1", "Pump1", "Temperature", DateTimeOffset.UtcNow));
+
+ var response = ExpectMsg();
+ Assert.True(response.Found);
+ Assert.Equal("Uncertain", response.Quality);
+ }
+
+ [Fact]
+ public void InstanceActor_StaticAttribute_StartsWithGoodQuality()
+ {
+ var config = new FlattenedConfiguration
+ {
+ InstanceUniqueName = "Pump1",
+ Attributes =
+ [
+ new ResolvedAttribute
+ {
+ CanonicalName = "Label",
+ Value = "Main Pump",
+ DataType = "String"
+ // No DataSourceReference — static attribute
+ }
+ ]
+ };
+
+ var actor = CreateInstanceActor("Pump1", config);
+
+ actor.Tell(new GetAttributeRequest(
+ "corr-quality-2", "Pump1", "Label", DateTimeOffset.UtcNow));
+
+ var response = ExpectMsg();
+ Assert.True(response.Found);
+ Assert.Equal("Good", response.Quality);
+ }
}