Alarm events
EventNotificationReceived and the alarm-derived events (AlarmActivated, LimitAlarmExceeded, …) — the alarm pipeline. Severity-based routing via the Notifier component, acknowledgement endpoints, audit chains.
OPC UA distinguishes data changes (a value moved) from
events (something happened: a threshold was crossed, a
condition fired). Events arrive as
PhpOpcua\Client\Event\EventNotificationReceived. When the event
payload matches an alarm pattern, opcua-client additionally
dispatches one of the specific alarm events from
opcua-client/docs/observability/event-reference.md (e.g.
AlarmActivated, AlarmAcknowledged, LimitAlarmExceeded,
OffNormalAlarmTriggered).
All classes live in PhpOpcua\Client\Event\ (singular).
Note
Publish-loop driven. Like data events, alarm events only fire
while something is calling publish() on the subscription — in
managed mode that's the daemon with auto_publish: true.
Subscribing to events
Create an event monitored item on a notifier node (typically
Server / ns=0;i=2253):
$client = $this->opcua->connect();
$sub = $client->createSubscription(publishingInterval: 1000.0);
$client->createEventMonitoredItem(
$sub->subscriptionId,
'ns=0;i=2253', // Server node
[
'EventId', 'EventType', 'SourceName',
'Time', 'Message', 'Severity',
],
clientHandle: 100,
);
The 4-arg shape of createEventMonitoredItem is
($subscriptionId, $nodeId, $selectFields, $clientHandle).
EventNotificationReceived — field reference
opcua-client/src/Event/EventNotificationReceived.php:
| Field | Type | Meaning |
|---|---|---|
$client |
OpcUaClientInterface |
The live client instance |
$subscriptionId |
int | Server subscription id |
$sequenceNumber |
int | Publish sequence number |
$clientHandle |
int | The handle you assigned |
$eventFields |
array<string, mixed> |
{name => value} map of select fields |
The select-field names you supplied to createEventMonitoredItem
become the keys of $eventFields. Common keys:
| Key | Type / source |
|---|---|
EventId |
string (binary, bin2hex() for log lines) |
EventType |
NodeId string |
SourceName |
string |
Time |
DateTimeInterface or string |
Message |
LocalizedText or string |
Severity |
int (1–1000) |
EventNotificationReceiveddoes not flatten event fields into top-level properties — read them out of$eventFields.
Listener — record alarms
namespace App\EventListener;
use App\Message\RecordAlarm as RecordAlarmMessage;
use PhpOpcua\Client\Event\EventNotificationReceived;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Messenger\MessageBusInterface;
final class RecordAlarm
{
public function __construct(private MessageBusInterface $bus) {}
#[AsEventListener]
public function __invoke(EventNotificationReceived $event): void
{
$f = $event->eventFields;
$this->bus->dispatch(new RecordAlarmMessage(
clientHandle: $event->clientHandle,
eventId: isset($f['EventId']) ? bin2hex((string) $f['EventId']) : null,
eventType: isset($f['EventType']) ? (string) $f['EventType'] : null,
source: $f['SourceName'] ?? null,
severity: isset($f['Severity']) ? (int) $f['Severity'] : null,
message: (string) ($f['Message'] ?? ''),
occurredAt: $f['Time'] ?? null,
));
}
}
Handler persists to PlcAlarm Doctrine entity — see
Recipes · Alarm routing.
Severity-based routing via Notifier
When you want a derived alarm event with structured fields (so you
do not need to crack $eventFields open in every listener), listen
on the alarm-specific class AlarmActivated instead — it carries
typed fields:
AlarmActivated field |
Type | Meaning |
|---|---|---|
$client |
client | live client |
$subscriptionId |
int | subscription id |
$clientHandle |
int | handle |
$sourceName |
string | alarm source |
$severity |
int | 1–1000 |
$message |
string | alarm message |
framework:
notifier:
chatter_transports:
slack: '%env(SLACK_DSN)%'
texter_transports:
twilio: '%env(TWILIO_DSN)%'
channel_policy:
urgent: ['email', 'sms']
high: ['email', 'chat/slack']
low: ['email']
The Notification class:
namespace App\Notification;
use Symfony\Component\Notifier\Message\ChatMessage;
use Symfony\Component\Notifier\Message\EmailMessage;
use Symfony\Component\Notifier\Message\SmsMessage;
use Symfony\Component\Notifier\Notification\ChatNotificationInterface;
use Symfony\Component\Notifier\Notification\EmailNotificationInterface;
use Symfony\Component\Notifier\Notification\Notification;
use Symfony\Component\Notifier\Notification\SmsNotificationInterface;
use Symfony\Component\Notifier\Recipient\RecipientInterface;
final class PlcAlarmNotification extends Notification implements
EmailNotificationInterface,
ChatNotificationInterface,
SmsNotificationInterface
{
public function __construct(
public readonly string $source,
public readonly int $severity,
public readonly string $message,
) {
parent::__construct("PLC alarm — sev {$severity}");
$this->importance($this->mapImportance());
}
private function mapImportance(): string
{
return match (true) {
$this->severity >= 900 => Notification::IMPORTANCE_URGENT,
$this->severity >= 700 => Notification::IMPORTANCE_HIGH,
$this->severity >= 400 => Notification::IMPORTANCE_MEDIUM,
default => Notification::IMPORTANCE_LOW,
};
}
public function asEmailMessage(RecipientInterface $r, ?string $transport = null): ?EmailMessage
{
return EmailMessage::fromNotification($this, $r);
}
public function asChatMessage(RecipientInterface $r, ?string $transport = null): ?ChatMessage
{
return new ChatMessage("PLC alarm sev={$this->severity}: {$this->source} — {$this->message}");
}
public function asSmsMessage(RecipientInterface $r, ?string $transport = null): ?SmsMessage
{
return new SmsMessage($r->getPhone(), "PLC sev={$this->severity}: {$this->source}");
}
}
The listener:
namespace App\EventListener;
use App\Notification\PlcAlarmNotification;
use PhpOpcua\Client\Event\AlarmActivated;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\Notifier\NotifierInterface;
use Symfony\Component\Notifier\Recipient\Recipient;
final class RouteAlarmToOps
{
public function __construct(private NotifierInterface $notifier) {}
#[AsEventListener]
public function __invoke(AlarmActivated $event): void
{
if ($event->severity < 400) return;
$this->notifier->send(
new PlcAlarmNotification(
source: $event->sourceName,
severity: $event->severity,
message: $event->message,
),
new Recipient('[email protected]', '+15551234567'),
);
}
}
Notifier::send() consults the channel_policy and routes
based on the notification's importance.
Limit alarms
LimitAlarmExceeded is dispatched in addition to AlarmActivated
when the alarm is a LimitAlarmType variant. It carries
$limitState (HighHigh, High, Low, LowLow):
use PhpOpcua\Client\Event\LimitAlarmExceeded;
#[AsEventListener]
public function __invoke(LimitAlarmExceeded $event): void
{
if ($event->limitState === 'HighHigh') {
// stop the line
}
}
Acknowledgement endpoint
namespace App\Controller;
use App\Service\AlarmAcknowledgeService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\{JsonResponse, Request};
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
final class AlarmAckController extends AbstractController
{
public function __construct(private AlarmAcknowledgeService $svc) {}
#[Route('/api/alarms/{eventId}/ack', methods: ['POST'])]
#[IsGranted('ROLE_OPERATOR')]
public function ack(string $eventId, Request $request): JsonResponse
{
$comment = (string) ($request->toArray()['comment'] ?? '');
try {
$this->svc->acknowledge($eventId, $comment);
} catch (\RuntimeException $e) {
return $this->json(['error' => $e->getMessage()], 422);
}
return $this->json(['status' => 'acked']);
}
}
AlarmAcknowledgeService calls the OPC UA method via
OpcUaClientInterface::call() — see
Operations · Method calls.
Mercure broadcast for alarms
use PhpOpcua\Client\Event\AlarmActivated;
#[AsEventListener]
public function __invoke(AlarmActivated $event): void
{
$this->hub->publish(new Update(
topics: ['/plc/alarms'],
data: json_encode([
'source' => $event->sourceName,
'severity' => $event->severity,
'message' => $event->message,
]),
private: true,
));
}
The browser subscribes to the private topic (with auth) and updates in real time.
Filtering — server-side beats client-side
For a noisy plant emitting thousands of low-severity events,
push the filter to the server using an OPC UA event filter
in the request payload (or in your subscription-declaration
config). Filters drop events before they hit the wire —
your listener never sees them. See opcua-client event-filter
documentation for the supported predicates.
Where to read next
- Async listeners with Messenger — scaling rules.
- Recipes · Alarm routing — full pipeline.
- Integrations · Notifier — the routing surface in detail.