Files
UA-.NETStandard/docs/AlarmsAndConditions.md

598 lines
24 KiB
Markdown

# Alarms and Conditions (OPC UA Part 9)
This guide describes the OPC UA Part 9 *Alarms & Conditions* support in
this stack: how to build alarm-aware servers, how to consume alarm events
on the client, and how the new helpers (latched alarms, alarm groups,
suppression engine, alarm metrics, streaming alarm records) fit together.
For the formal model, see
[OPC UA Part 9 — Alarms and Conditions](https://reference.opcfoundation.org/specs/OPC-10000-9/full).
- [Quick reference](#quick-reference)
- [Server side](#server-side)
- [Creating an alarm](#creating-an-alarm)
- [Driving state from your process](#driving-state-from-your-process)
- [Latched alarms](#latched-alarms)
- [Re-alarming](#re-alarming)
- [Audible alarms and silencing](#audible-alarms-and-silencing)
- [Suppression, out-of-service, shelving](#suppression-out-of-service-shelving)
- [Alarm groups and first-in-group](#alarm-groups-and-first-in-group)
- [Alarm metrics (rate tracking)](#alarm-metrics-rate-tracking)
- [Vetoing alarm operations](#vetoing-alarm-operations)
- [Audit events](#audit-events)
- [Client side](#client-side)
- [`AlarmClient` — typed operations](#alarmclient--typed-operations)
- [Subscribing to alarms with `IAsyncEnumerable`](#subscribing-to-alarms-with-iasyncenumerable)
- [Typed alarm records](#typed-alarm-records)
- [Per-record event filters](#per-record-event-filters)
- [Dialog conditions](#dialog-conditions)
- [`ConditionRefresh`](#conditionrefresh)
- [Reference](#reference)
## Quick reference
| Concern | Server entry point | Client entry point |
|---|---|---|
| Create an alarm | `new AlarmConditionState(telemetry, parent); alarm.Create(...)` | n/a |
| Acknowledge / Confirm | `AcknowledgeableConditionState.OnAcknowledgeCalled` (auto-wired) | `AlarmClient.AcknowledgeAsync` / `ConfirmAsync` |
| Silence | `AlarmConditionState.SetSilenceState` | `AlarmClient.SilenceAsync` |
| Suppress / Unsuppress | `SetSuppressedState` | `AlarmClient.SuppressAsync` / `UnsuppressAsync` |
| Out-of-service | `SetOutOfServiceState` | `AlarmClient.RemoveFromServiceAsync` / `PlaceInServiceAsync` |
| Latched + Reset | `SetLatchedState` + auto from `SetActiveState` | `AlarmClient.ResetAsync` |
| Shelve / Unshelve | `SetShelvingState` | `AlarmClient.TimedShelveAsync` / `OneShotShelveAsync` / `UnshelveAsync` |
| Re-alarm | `ProcessReAlarm` | n/a (server timer) |
| Alarm groups | `AlarmGroup`, `AlarmSuppressionEngine` | `AlarmClient.GetGroupMembershipsAsync` |
| Alarm rate | `AlarmRateTracker` | read `AlarmMetricsType` attributes |
| Refresh state | `Server.ConditionRefresh` (server-driven) | `AlarmClient.ConditionRefreshAsync` / `ConditionRefresh2Async` |
| Stream alarm events | n/a | `IStreamingSubscription.SubscribeAlarmsAsync` |
| Decode raw event fields | n/a | `EventRecordDecoderRegistry.Default.Decode` |
| Build an event filter | n/a | `{Type}Record.EventFilters.Build(registry?)` |
`AlarmClient` is obtained from any `ISession` and a telemetry context;
internally it delegates every Part 9 method call to the matching
source-generated `*TypeClient` proxy (so there is exactly one place
that knows each method NodeId — the generator):
```csharp
AlarmClient alarms = session.GetAlarmClient(telemetry);
```
For dependency-injected hosts use the
[`builder.AddAlarms()`](DependencyInjection.md#alarms-and-conditions)
extension and resolve `AlarmClientFactory` (which threads the
host's telemetry into the client for you):
```csharp
var factory = sp.GetRequiredService<AlarmClientFactory>();
AlarmClient alarms = factory.Create(session);
```
If you already have a typed handle to a specific condition node you
can also construct a proxy directly — `AlarmClient` is a convenience
façade over these:
```csharp
await new AlarmConditionTypeClient(session, conditionId, telemetry)
.AcknowledgeAsync(eventId, comment);
```
## Server side
### Creating an alarm
`AlarmConditionState` is the central server-side state type for all
Part 9 alarms. It is *source-generated* from the standard NodeSet and
extended with hand-written behavior. The behavior partial file
(`src/Opc.Ua.Core.Types/State/AlarmConditionState.Methods.cs`) wires
every Part 9 method handler during `OnAfterCreate`, so as soon as you
populate the optional state nodes the corresponding methods are
callable.
```csharp
var alarm = new AlarmConditionState(telemetry, parent);
alarm.Create(
context,
nodeId: NodeId.Null, // server-assigned
browseName: new QualifiedName("MyAlarm"),
displayName: null,
assignNodeIds: true);
alarm.SetEnableState(context, enabled: true);
alarm.SetSeverity(context, EventSeverity.High);
alarm.SetActiveState(context, active: true);
alarm.ReportEvent(context, alarm);
```
The optional state nodes are created the way you would expect on a
generated `AlarmConditionState`: assign to `alarm.SilenceState`,
`alarm.OutOfServiceState`, `alarm.LatchedState`, etc. before calling
`Create`. The quickstart reference server creates these via
`AlarmConditionTypeHolder.Initialize` —
`samples/Quickstarts.Servers/Alarms/AlarmHolders/`.
### Driving state from your process
All state transitions go through typed setters that:
- update the `TwoStateVariableState` value + `Id`
- stamp `TransitionTime`
- recompute `EffectiveDisplayName` and the composite `SuppressedOrShelved` flag
- clear `ChangeMasks` so the next publish cycle sees the new state
| Setter | Notes |
|---|---|
| `SetEnableState(context, enabled)` | Inherited from `ConditionState`. Disabling clears `Retain` per Part 9 §5.5.2. |
| `SetSeverity(context, severity)` | Records `LastSeverity` before updating. |
| `SetActiveState(context, active)` | On *true*: if `LatchedState` is present, sets it true; if `SilenceState` is present and silenced, clears it. On *false*: if shelved as `OneShotShelve`, unshelves. |
| `SetSuppressedState(context, suppressed)` | Updates `SuppressedOrShelved` taking `OutOfServiceState` and `ShelvingState` into account. |
| `SetOutOfServiceState(context, outOfService)` | Sets `SuppressedOrShelved` true when out of service (Part 9 §5.8.2). |
| `SetShelvingState(context, shelved, oneShot, shelvingTime)` | Drives the `ShelvedStateMachineState`, runs the unshelve timer, computes `UnshelveTime`. |
| `SetLatchedState(context, latched)` | Direct latched-state setter; usually called automatically from `SetActiveState`. |
| `SetSilenceState(context, silenced)` | Direct silence-state setter; usually called automatically on activation / re-alarm. |
### Latched alarms
If you populate `alarm.LatchedState`, the alarm becomes a *latching
alarm* (Part 9 §4.8). The semantics:
- `SetActiveState(context, true)` — also sets `LatchedState = true`.
- `SetActiveState(context, false)` — `ActiveState` reflects the real
process state, but `LatchedState` stays `true`.
- `Reset` (server-side method, auto-wired) — clears `LatchedState`.
The `Reset` method validates **all** preconditions before accepting:
enabled, not active, acknowledged, confirmed (if `ConfirmedState` is
present). Any other state returns `Bad_InvalidState`.
Latched alarms are *retained* (`Retain = true`) for as long as
`LatchedState.Id` is `true`, so a client refresh sees them.
### Re-alarming
A re-alarm reminder fires when an alarm has been active and
unacknowledged longer than `ReAlarmTime`. The state type provides a
helper rather than an automatic timer (so the host owns scheduling and
event generation):
```csharp
// In your re-alarm scheduler (e.g. an external Timer)
if (alarm.IsReAlarmEnabled && alarm.ActiveState.Id.Value
&& alarm.AckedState?.Id.Value != true)
{
alarm.ProcessReAlarm(context);
}
// On deactivation / acknowledge:
alarm.ResetReAlarmRepeatCount(context);
```
`ProcessReAlarm`:
- clears `AckedState` (forces re-acknowledgement)
- clears `SilenceState` (audible annunciation resumes)
- increments `ReAlarmRepeatCount`
- calls `ReportStateChange` so the new event is published
### Audible alarms and silencing
If `AudibleEnabled = true` and `AudibleSound` is populated, the
`UpdateAudibleState` helper takes care of clearing the silence state
when the alarm activates (so the next activation is audible again):
```csharp
ByteString sound = LoadWavFile();
alarm.UpdateAudibleState(context, active: true, soundData: sound);
```
`SilenceAsync` from the client (or the `Silence` method handler on the
server) sets `SilenceState.Id = true`.
### Suppression, out-of-service, shelving
All three states contribute to the `SuppressedOrShelved` boolean. The
state-type setters keep that flag in sync — clearing one does **not**
clear `SuppressedOrShelved` while the others are still active:
```csharp
alarm.SetSuppressedState(context, true); // SuppressedOrShelved = true
alarm.SetOutOfServiceState(context, true); // SuppressedOrShelved stays true
alarm.SetSuppressedState(context, false); // SuppressedOrShelved stays true
alarm.SetOutOfServiceState(context, false); // SuppressedOrShelved = false
```
### Alarm groups and first-in-group
`src/Opc.Ua.Server/Alarms/AlarmGroup.cs` wraps a generated
`AlarmGroupState` and provides typed add/remove/enumerate:
```csharp
var group = new AlarmGroup(motorAlarmGroupState);
group.AddMember(motorHighTempAlarm);
group.AddMember(motorLowOilAlarm);
foreach (NodeId id in group.GetMemberIds(context))
{
// ...
}
```
`AlarmSuppressionEngine` centralizes both the **AlarmSuppressionGroup**
pattern and the **FirstInGroup** pattern. Register on startup, call
`Evaluate` from your simulation/process loop, and the engine routes
suppression to the right alarm members:
```csharp
using var engine = new AlarmSuppressionEngine();
engine.RegisterSuppressionGroup(
suppressionGroup: motorShutdownGroup,
suppressionSource: () => motorIsShutDown.Value,
alarmMembers: new[] { motorHighTempAlarm, motorLowOilAlarm });
engine.RegisterFirstInGroupAlarm(
firstAlarm: masterTripAlarm,
group: tripGroup,
otherMembers: dependentTripAlarms);
// In your periodic update:
engine.Evaluate(context);
// On master trip activation:
engine.OnFirstInGroupActiveChanged(context, masterTripAlarm, tripGroup,
firstActive: true);
```
The first `Evaluate` call always applies the current state, so
clients see a coherent suppression state immediately after
registration — there is no edge required.
> **Live demo:** The `samples/Quickstarts.Servers/Alarms/
> AlarmNodeManager.cs` reference implementation wires this up
> end-to-end. It exposes a `/Alarms/AnalogGroup` (`AlarmGroupType`)
> containing every analog-source alarm and a writable
> `/Alarms/MaintenanceMode` boolean. Writing `true` to
> MaintenanceMode runs `AlarmSuppressionEngine.Evaluate(...)` and
> suppresses every group member; writing `false` clears
> suppression on the next evaluation. The reference server's node
> manager itself derives from `AsyncCustomNodeManager`, so the same
> file is a worked example of porting a Part 9 demo to the modern
> async base class.
### Alarm metrics (rate tracking)
`AlarmRateTracker` records activations into a sliding window and
exposes `CurrentAlarmRate` / `MaximumAlarmRate` suitable for surfacing
through an `AlarmMetricsType` instance:
```csharp
var tracker = new AlarmRateTracker(TimeSpan.FromMinutes(1));
alarm.OnSilenceRequested = (ctx, a) =>
{
tracker.RecordActivation();
return ServiceResult.Good;
};
// Periodically push to AlarmMetrics:
metrics.CurrentAlarmRate.Value = tracker.CurrentAlarmRate;
metrics.MaximumAlarmRate.Value = tracker.MaximumAlarmRate;
```
### Vetoing alarm operations
Each Part 9 alarm method has an *optional* delegate that runs **before**
the default state transition. Returning a `Bad` status from the
delegate aborts the operation; returning `Good` (or `null`) lets the
default behavior run:
| Delegate | Triggered by |
|---|---|
| `alarm.OnSilenceRequested` | `Silence` method |
| `alarm.OnSuppressRequested(suppressing: bool)` | `Suppress` / `Unsuppress` |
| `alarm.OnOutOfServiceRequested(outOfService: bool)` | `RemoveFromService` / `PlaceInService` |
| `alarm.OnResetRequested` | `Reset` (latched alarms) |
| `alarm.OnShelve` | `OneShotShelve` / `TimedShelve` / `Unshelve` (existing) |
```csharp
alarm.OnResetRequested = (ctx, a) =>
{
if (!hardwareDiagnosticPassed)
{
return new ServiceResult(StatusCodes.BadUserAccessDenied,
"Hardware diagnostic must pass before reset.");
}
return ServiceResult.Good;
};
```
### Audit events
Every Part 9 alarm method generates the spec-mandated audit event
type automatically. You do not have to call `ReportEvent` for the
audit event — it happens inside the method handler when
`AreEventsMonitored` is true:
| Method | Audit event type |
|---|---|
| `Silence` | `AuditConditionSilenceEventType` |
| `Suppress` / `Unsuppress` / `*2` | `AuditConditionSuppressionEventType` |
| `RemoveFromService` / `PlaceInService` / `*2` | `AuditConditionOutOfServiceEventType` |
| `Reset` / `Reset2` | `AuditConditionResetEventType` |
| Existing: `Acknowledge` / `Confirm` | `AuditConditionAcknowledgeEventType` / `AuditConditionConfirmEventType` |
| Existing: shelving | `AuditConditionShelvingEventType` |
## Client side
### `AlarmClient` — typed operations
`AlarmClient` is the strongly-typed client API for Part 9 methods.
Each method delegates to the matching source-generated
`*TypeClient` proxy (`ConditionTypeClient`,
`AcknowledgeableConditionTypeClient`,
`AlarmConditionTypeClient`, `DialogConditionTypeClient`,
`ShelvedStateMachineTypeClient`), passing the caller-supplied
`conditionId` as the proxy's `ObjectId`. This honours the Part 9
§5.5.4 idiom (`ConditionId` is acceptable as `ObjectId`) and keeps
exactly one source of truth — the generated proxy — for every
method NodeId and argument shape.
```csharp
AlarmClient alarms = session.GetAlarmClient(telemetry);
// ConditionType methods
await alarms.EnableAsync(conditionId);
await alarms.DisableAsync(conditionId);
await alarms.AddCommentAsync(conditionId, eventId,
new LocalizedText("en", "Looks like flow sensor drift"));
await alarms.ConditionRefreshAsync(subscriptionId);
await alarms.ConditionRefresh2Async(subscriptionId, monitoredItemId);
// AcknowledgeableConditionType
await alarms.AcknowledgeAsync(conditionId, eventId,
new LocalizedText("en", "Operator review complete"));
await alarms.ConfirmAsync(conditionId, eventId,
new LocalizedText("en", "Maintenance verified"));
// AlarmConditionType
await alarms.SilenceAsync(conditionId);
await alarms.SuppressAsync(conditionId,
comment: new LocalizedText("en", "Routine maintenance"));
await alarms.UnsuppressAsync(conditionId);
await alarms.RemoveFromServiceAsync(conditionId);
await alarms.PlaceInServiceAsync(conditionId);
await alarms.ResetAsync(conditionId); // latched-alarm reset
await alarms.TimedShelveAsync(conditionId, shelvingTime: 30000); // 30s
await alarms.OneShotShelveAsync(conditionId);
await alarms.UnshelveAsync(conditionId);
ArrayOf<NodeId> groups = await alarms.GetGroupMembershipsAsync(conditionId);
```
The `*Async(... comment ...)` overloads automatically pick the
spec-defined `*2` method when a non-empty comment is supplied
(`Suppress2`, `Unsuppress2`, `RemoveFromService2`, `PlaceInService2`,
`Reset2`).
### Subscribing to alarms with `IAsyncEnumerable`
Alarm events flow through the [streaming subscription
API](Subscriptions.md#streaming-subscriptions). The
`AlarmStreamExtensions.SubscribeAlarmsAsync` extension returns
strongly-typed records:
```csharp
ManagedSession session = ...;
IStreamingSubscription streaming = session.DefaultStreaming;
await foreach (ConditionTypeRecord record in streaming
.SubscribeAlarmsAsync(notifierId: ObjectIds.Server, ct: ct)
.ConfigureAwait(false))
{
switch (record)
{
case ExclusiveLimitAlarmTypeRecord limit:
Console.WriteLine($"{limit.SourceName} limit-state-id={limit.LimitState}");
break;
case AlarmConditionTypeRecord alarm when alarm.ActiveStateId == true:
await alarms.AcknowledgeAsync(alarm.ConditionId,
alarm.EventId,
new LocalizedText("en", "Auto-ack")).ConfigureAwait(false);
break;
case DialogConditionTypeRecord dialog:
// Pick a response index from dialog.ResponseOptionSet
await alarms.RespondAsync(dialog.ConditionId,
selectedResponse: 0).ConfigureAwait(false);
break;
}
}
```
State-machine waits compose naturally with
`TakeUntilAsync` / `WithTimeoutAsync`:
```csharp
// Wait for myAlarm to clear, or 5 minutes — whichever comes first.
await streaming.SubscribeAlarmsAsync(ObjectIds.Server)
.TakeUntilAsync(r =>
r is AlarmConditionTypeRecord a && a.ConditionId == myAlarmId &&
a.ActiveStateId == false)
.WithTimeoutAsync(TimeSpan.FromMinutes(5))
.LastAsync(ct);
```
### Typed alarm records
Alarm and condition records are **source-generated** by the
`Opc.Ua.SourceGeneration` analyzer (see the `EventRecordGenerator`).
For every `ObjectType` whose base-type chain ends at `BaseEventType`,
the generator emits a `partial record {Type}Record` deriving from the
record of its parent type, exposing one init-only property per
declared field. The standard NodeSet produces the following hierarchy
(abridged — only the most commonly observed types are shown):
```
EventRecord (anchor; hand-written)
└── BaseEventTypeRecord (i=2041)
└── ConditionTypeRecord (i=2782)
├── DialogConditionTypeRecord (i=2830)
└── AcknowledgeableConditionTypeRecord
└── AlarmConditionTypeRecord
├── LimitAlarmTypeRecord
│ ├── ExclusiveLimitAlarmTypeRecord
│ └── NonExclusiveLimitAlarmTypeRecord
├── DiscreteAlarmTypeRecord
│ └── OffNormalAlarmTypeRecord
│ └── CertificateExpirationAlarmTypeRecord
└── DiscrepancyAlarmTypeRecord
```
Vendor models that derive from any of these types **automatically**
get their own `*TypeRecord` deriving from the closest standard
ancestor — no checked-in code, no manual class definitions. Add a
hand-written `partial record VibrationAlarmTypeRecord` in your project
to extend the generated declaration with computed properties or
custom helpers.
The decoder upgrades the record type based on which fields are
populated in the event. A simple `switch` on the record type gives you
the right field set:
```csharp
EventRecord? record = EventRecordDecoderRegistry.Default.Decode(eventFields);
if (record is CertificateExpirationAlarmTypeRecord cert)
{
Console.WriteLine($"Cert {cert.CertificateType} expires {cert.ExpirationDate}");
}
```
The shared `ConditionTypeRecord.ConditionId` property is a hand-written
alias for `SourceNode` — Part 9 of the OPC UA specification defines
the "ConditionId" as the NodeId of the condition object that fired
the event, which is reported through the `SourceNode` event field.
### Source-generated decoders + `EventRecordDecoderRegistry`
Every record emitted by the `EventRecordGenerator` also exposes a
nested `static class Decoder` with a positional `StandardFields`
table and a `Decode(IReadOnlyList<Variant>)` method that populates
own + inherited init-only properties. A per-file
`Register{ModelPrefix}Decoders(this EventRecordDecoderRegistry)`
extension registers every emitted decoder with a caller-supplied
registry.
The process-wide `EventRecordDecoderRegistry.Default` ships with the
standard UA model pre-registered. It routes by the event's
`EventType` field and walks the OPC UA event-type hierarchy through
an optional `SuperTypeResolver` when the exact type is not
registered. Vendor models register their generated extensions via
`Register{Prefix}Decoders` or
`CreateChildScope().Register{Prefix}Decoders()` for test isolation.
```csharp
// EventType-keyed dispatch backed by source-generated decoders:
EventRecord? rec = EventRecordDecoderRegistry.Default.Decode(eventFields);
// Vendor scenario — register extra decoders on an app-scoped child:
var app = EventRecordDecoderRegistry.Default
.CreateChildScope()
.RegisterMyVendorDecoders();
EventRecord? vendorRec = app.Decode(eventFields);
```
### Per-record event filters
Every generated `{Type}Record` exposes a nested
`static class EventFilters` alongside its `Decoder` block. The
`Build(registry?)` factory produces an `EventFilter` whose where
clause restricts events to `OfType({recordTypeId})` and whose
select clauses come from the supplied registry's composed
`StandardFields` (defaults to
`EventRecordDecoderRegistry.Default`). The returned filter pairs
cleanly with `EventRecordDecoderRegistry.Decode` — the registry
remaps the composed positions to each decoder's own layout
before invoking it, so vendor models extend transparently.
```csharp
// Filter for alarm events:
EventFilter filter = AlarmConditionTypeRecord.EventFilters.Build();
// Or for any condition:
EventFilter f2 = ConditionTypeRecord.EventFilters.Build();
// Or for dialog events:
EventFilter f3 = DialogConditionTypeRecord.EventFilters.Build();
// Or for a specific subtype:
EventFilter f4 = CertificateExpirationAlarmTypeRecord.EventFilters.Build();
// Vendor scenario — pass a child registry so the filter superset
// includes the vendor model's fields:
var app = EventRecordDecoderRegistry.Default
.CreateChildScope()
.RegisterMyVendorDecoders();
EventFilter vendorFilter = VibrationAlarmTypeRecord.EventFilters.Build(app);
```
The same registry should be passed to `Subscribe*Async`
(through the `registry:` parameter) so the streaming side decodes
through the registry that built the filter.
### Dialog conditions
A `DialogConditionType` event arrives as a
`DialogConditionTypeRecord`. The `Respond` and `Respond2` methods on
`IDialogConditionOperations` close out the dialog. The decoded record
exposes the prompt and available response option set so the caller
can pick an index:
```csharp
await foreach (DialogConditionTypeRecord dialog in streaming.SubscribeDialogsAsync(notifierId))
{
Console.WriteLine($"Prompt: {dialog.Prompt}");
LocalizedText[] options = dialog.ResponseOptionSet ?? Array.Empty<LocalizedText>();
Console.WriteLine($"Options: {string.Join(", ", options)}");
// Pick whichever option matches your scenario.
int selectedIndex = 0;
await alarms.Respond2Async(dialog.ConditionId, selectedIndex,
new LocalizedText("en", "Approved by operator-1")).ConfigureAwait(false);
}
```
The `OkResponse` / `CancelResponse` / `DefaultResponse` properties on
the *server-side* `DialogConditionType` (Part 9 §5.6.2) carry their
canonical indices for clients to read separately via Read service if
the application needs them; they are not surfaced in the standard
`DialogConditionTypeRecord` (which only carries the dialog prompt +
active state).
### `ConditionRefresh`
Both Part 9 refresh methods are available:
```csharp
// Refresh all conditions for the subscription
await alarms.ConditionRefreshAsync(subscriptionId);
// Refresh just one monitored item's conditions (Part 9 §5.5.8)
await alarms.ConditionRefresh2Async(subscriptionId, monitoredItemId);
```
The classic `ISubscription.ConditionRefreshAsync` on the streaming
subscription is also available — they are equivalent when the
`AlarmClient` and the `IStreamingSubscription` are bound to the same
session.
## Reference
- [OPC UA Part 9 — Alarms and Conditions](https://reference.opcfoundation.org/specs/OPC-10000-9/full)
- [IEC 62682](https://webstore.iec.ch/publication/61256) — Management of alarm systems for the process industries
- [ISA 18.2](https://www.isa.org/products/ansi-isa-18-2-2016-management-of-alarm-systems-) — Management of Alarm Systems for the Process Industries
- [Subscriptions and Monitored Items Service Set](Subscriptions.md) — `IStreamingSubscription` and the V2 subscription engine
- [State Machines](StateMachines.md) — generic Part 16 state-machine API used by
`AlarmClient.GetShelvingStateAsync` / `ObserveShelvingTransitionsAsync`
- [Model Change Tracking](ModelChangeTracking.md) — client cache invalidation on address-space changes
- Source: `src/Opc.Ua.Server/Alarms/`, `src/Opc.Ua.Client/Alarms/`,
`src/Opc.Ua.Core.Types/State/AlarmConditionState.Methods.cs`
- Reference client sample: `samples/ConsoleReferenceClient/AlarmClientSample.cs`
- Conformance tests: `tests/Opc.Ua.History.Tests/AlarmsAndConditions*.cs`