Skip to content

Show filtered retain in the AlarmCondition sample (#845) - #893

Merged
romanett merged 1 commit into
masterfrom
romanett/opc-ua-sample-845-9f62ab
Sep 10, 2026
Merged

romanett merged 1 commit into
masterfrom
romanett/opc-ua-sample-845-9f62ab

Conversation

@romanett

Copy link
Copy Markdown
Contributor

Proposed changes

Adds a demonstration of filtered retain (OPC 10000-9, §B.1.4) to the Alarms & Conditions
quickstart. A fitting sample already existed, so this extends Workshop/AlarmCondition
rather than adding a new one.

Filtered retain lets a condition report one final event as it leaves the scope of a client's
where clause, so that client sees Retain go false even though the server itself still
retains the condition. Nothing in the samples showed it, and the alarm sample already had the
right shape for it: an operator's list which asks only for the alarms that need attention.

Server

  • Every alarm opts in with ConditionState.SupportsFilteredRetain. The flag is deliberately
    not part of the instance child hierarchy — Part 9 provides SupportsFilteredRetain on the
    ConditionType only — so it is set on the node rather than created from the type model,
    and a branch gets its own copy.
  • New FilteredRetainCapability startup task advertises the Property on the ConditionType
    node, which is where a client asks whether the server supports the concept at all. The
    standard address space ships it false, so a server which does support it has to say so.
  • SourceState.ConditionRefresh no longer overwrites the handle of the snapshot it replays
    with the source. This was a real defect for this feature: a monitored item resolves the
    condition behind an event through that handle, so nothing a refresh replayed could take
    part in filtered retain — it only began to work once an alarm reported a live event of its
    own. IsOwnCondition is the marker against replaying one source twice instead.

Client

  • The where clause now leaves out an alarm which is suppressed, shelved or out of service,
    which is what the sample README always described. That is what makes it a clause a
    condition falls out of while the condition itself carries on.
  • The clause is asked only of the alarms: SuppressedOrShelved is declared by
    AlarmConditionType, and an operand which resolves to nothing makes Equals answer null
    and the whole element false — so a bare SuppressedOrShelved == False silently drops every
    condition which is not an alarm, the OnlineState dialogs of this sample included.
  • A condition which reports Retain = false leaves the list, as ConditionChange.Removed,
    and the window drops the row. That covers both an alarm the plant and the operator are done
    with and the trailing event of one which left this client's filter; the two are
    deliberately indistinguishable to the client. Suppressing an alarm now takes it out of the
    list at once, instead of leaving a row which stands until the next Refresh.

Tests

  • Node manager — the ConditionType advertises the flag; a suppressed alarm is delivered
    one last time carrying SuppressedOrShelved = true (which no plain where clause evaluation
    could have let through) together with Retain = false, and is not delivered again until it
    comes back into scope. EventCapture gained an overload which takes a where clause next to
    the standard select clauses.
  • Client model — suppressing an alarm takes it out of the list without a condition
    refresh, and unsuppressing puts it back.
  • SeverityFilterKeepsTheLowerConditionsOut now allows a removal to carry a severity the
    filter rejects: dropping below the threshold is one of the ways out of the where clause.

Documentation: the sample README gained a Filtered retain section, its Things worth
trying
items 3 and 4 no longer tell the reader to press Refresh, and docs/TESTING.md and
the root README were updated.

Related Issues

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Enhancement (non-breaking change which adds functionality)
  • Test enhancement (non-breaking change to increase test coverage)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected, requires version increase of Nuget packages)
  • Documentation Update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc.
  • I have signed the CLA.
  • I ran tests locally with my changes, all passed.
  • I fixed all failing tests in the CI pipelines.
  • I fixed all introduced issues with CodeQL and LGTM.
  • I have added tests that prove my fix is effective or that my feature works and increased code coverage.
  • I have added necessary documentation (if appropriate).
  • Any dependent changes have been merged and published in downstream modules.

Further comments

One change is unrelated to #845 and worth a separate look. master did not build against
the 2.0.312 packages this branch pins: the node manager generator emits a parameterless
constructor which chains with a literal null, and that was ambiguous with the
RuntimeNodeSets sample's own three argument constructor (CS0121), which left
SampleNodeManagers.Tests unbuildable. Workshop/RuntimeNodeSets/Server/SiteNodeManager.cs
gains a fourth optional parameter to break the tie. Happy to split it out or drop it if it is
already being fixed elsewhere.

Behaviour change worth flagging: the condition list no longer keeps Retain = false rows
greyed out — they leave the list, which is what Part 9 §5.5.2 asks of a client and what makes
filtered retain visible at all. Branches are still shown next to the current state and toned
down.

Local test results, all tiers:

Tier Result
0 — Configuration 247 passed
1 — Server smoke 126 passed
1.5 — Node managers 205 passed, 1 skipped
1.7 — Client models 447 passed, 3 skipped
2 — Client smoke 65 passed

Full-solution build is clean across all target frameworks.

🤖 Generated with Claude Code

Part 9 B.1.4 lets a condition report one final event as it leaves the
scope of a client's where clause, so that client sees Retain go false
even though the server still retains the condition. Nothing in the
samples demonstrated it. The alarm sample is where it belongs, and it
already had the shape for it: an operator's list which asks only for the
alarms that need attention.

Server
- Every alarm opts in with ConditionState.SupportsFilteredRetain. The
  flag is not part of the instance child hierarchy - Part 9 provides
  SupportsFilteredRetain on the ConditionType only - so it is set on the
  node rather than created from the type model, and a branch gets a copy.
- FilteredRetainCapability advertises the Property on the ConditionType
  node, which is where a client asks whether the server supports the
  concept. The standard address space ships it false, so a server which
  does support it has to say so.
- SourceState.ConditionRefresh no longer overwrites the handle of the
  snapshot it replays with the source. A monitored item resolves the
  condition behind an event through that handle, so nothing a refresh
  replayed could take part in filtered retain; it only started working
  once an alarm reported a live event of its own. IsOwnCondition is the
  marker against replaying one source twice instead.

Client
- The where clause now leaves out an alarm which is suppressed, shelved
  or out of service, which is what the sample README always described.
  That makes it a clause a condition falls out of while the condition
  itself carries on - the case filtered retain exists for.
- The clause is asked only of the alarms. SuppressedOrShelved is declared
  by AlarmConditionType, and an operand which resolves to nothing makes
  Equals answer null and the element false, so a bare
  SuppressedOrShelved == False silently drops every condition which is
  not an alarm, the OnlineState dialogs of the sample included.
- A condition which reports Retain = false leaves the list, as
  ConditionChange.Removed, and the window drops the row. That covers both
  an alarm the plant and the operator are done with and the trailing
  event of one which left this client's filter; the two are deliberately
  indistinguishable. Suppressing an alarm now takes it out of the list at
  once instead of leaving a row which stands until the next Refresh.

Tests
- Node manager: the ConditionType advertises the flag; a suppressed alarm
  is delivered one last time carrying SuppressedOrShelved = true, which
  no plain where clause evaluation could have let through, together with
  Retain = false - and is not delivered again until it comes back into
  scope. EventCapture gained an overload which takes a where clause next
  to the standard select clauses.
- Client model: suppressing an alarm takes it out of the list without a
  condition refresh, and unsuppressing puts it back.
- SeverityFilterKeepsTheLowerConditionsOut allows a removal to carry a
  severity the filter rejects: dropping below the threshold is one of the
  ways out of the where clause.

Also fixes an unrelated build break on this branch: the 2.0.312 node
manager generator emits a parameterless constructor which chains with a
literal null, which was ambiguous with the RuntimeNodeSets sample's own
three argument constructor and left SampleNodeManagers.Tests unbuildable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@romanett
romanett merged commit 93c9e42 into master Sep 10, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Filtered Retain Sample

1 participant