fix: wire DCL tag value delivery, alarm evaluation, and snapshot timestamps

Three runtime bugs fixed:
- DataConnectionActor: TagValueReceived/TagResolutionSucceeded/Failed not
  handled in any Become state — OPC UA values went to dead letters. Added
  initial read after subscribe to seed current values immediately.
- AlarmActor: ParseEvalConfig expected "attributeName"/"matchValue"/"min"/
  "max" keys but seed data uses "attribute"/"value"/"high"/"low". Added
  support for both conventions and !=prefix for not-equal matching.
- InstanceActor: snapshots reported all alarms (including unevaluated) with
  correct priorities and source timestamps instead of current UTC. Removed
  bogus Vibration template attribute that shadowed Speed's tag mapping.
This commit is contained in:
Joseph Doherty
2026-03-18 07:36:48 -04:00
parent 9c6e3c2e56
commit f063fb1ca3
3 changed files with 110 additions and 21 deletions

View File

@@ -153,6 +153,15 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
case WriteTagRequest req:
HandleWrite(req);
break;
case TagValueReceived tvr:
HandleTagValueReceived(tvr);
break;
case TagResolutionSucceeded trs:
HandleTagResolutionSucceeded(trs);
break;
case TagResolutionFailed trf:
HandleTagResolutionFailed(trf);
break;
case AdapterDisconnected:
HandleDisconnect();
break;
@@ -201,6 +210,13 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
// Allow unsubscribe even during reconnect (for cleanup on instance stop)
HandleUnsubscribe(req);
break;
case TagValueReceived:
// Ignore — stale callback from previous connection
break;
case TagResolutionSucceeded:
case TagResolutionFailed:
// Ignore — stale results from previous connection; ReSubscribeAll runs after reconnect
break;
case GetHealthReport:
ReplyWithHealthReport();
break;
@@ -313,6 +329,25 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
}
}
// Initial read — seed current values for all resolved tags so the Instance Actor
// doesn't stay Uncertain until the next OPC UA data change notification
foreach (var tagPath in instanceTags)
{
if (_unresolvedTags.Contains(tagPath)) continue;
try
{
var readResult = await _adapter.ReadAsync(tagPath);
if (readResult.Success && readResult.Value != null)
{
self.Tell(new TagValueReceived(tagPath, readResult.Value));
}
}
catch
{
// Best-effort — subscription will deliver subsequent changes
}
}
return new SubscribeTagsResponse(
request.CorrelationId, request.InstanceUniqueName, true, null, DateTimeOffset.UtcNow);
}).PipeTo(sender);
@@ -459,6 +494,28 @@ public class DataConnectionActor : UntypedActor, IWithStash, IWithTimers
// ── Internal message handlers for piped async results ──
private void HandleTagResolutionSucceeded(TagResolutionSucceeded msg)
{
if (_unresolvedTags.Remove(msg.TagPath))
{
_subscriptionIds[msg.TagPath] = msg.SubscriptionId;
_resolvedTags++;
_healthCollector.UpdateTagResolution(_connectionName, _totalSubscribed, _resolvedTags);
_log.Info("[{0}] Tag resolved: {1}", _connectionName, msg.TagPath);
}
if (_unresolvedTags.Count == 0)
{
Timers.Cancel("tag-resolution-retry");
}
}
private void HandleTagResolutionFailed(TagResolutionFailed msg)
{
_log.Debug("[{0}] Tag resolution still failing for {1}: {2}",
_connectionName, msg.TagPath, msg.Error);
}
private void HandleTagValueReceived(TagValueReceived msg)
{
// Fan out to all subscribed instances