OPC-UA Server¶
Normal can serve its point database over OPC-UA, so that SCADA systems, historians and analytics tools can read live values, browse the equipment model, pull history and command points without going through the REST or gRPC APIs.
This is a server, not a driver: it exposes the points Normal has already collected from BACnet, Modbus, Fox and the rest. It does not read from OPC-UA devices.
The server runs as its own process alongside the rest of Normal. A fault in the OPC-UA stack cannot take the BACnet and Modbus drivers down with it, and the port is only opened when you turn it on.
Enabling the server¶
The server is off by default, because it opens a network port. Turn it on under Protocols > OPC-UA > Settings by setting Enabled, and the service starts within a few seconds — no restart of Normal is needed.
Clients then connect to:
opc.tcp://<your-normal-host>:4840/
Protocols > OPC-UA shows what the server is configured to do: whether it is enabled, the endpoint URL it advertises to clients, which namespaces it publishes, and its authentication and write posture. It flags an advertised hostname that clients are unlikely to reach — see the warning below.
If a client discovers the server but will not connect
OPC-UA clients call GetEndpoints and then reconnect to the URL the
server returns, rather than to the address you typed. If that URL is not
reachable from the client, the connection fails after discovery succeeds —
usually with an unhelpful error.
The default is the system hostname, which is correct for a normal install: Normal runs with host networking, because BACnet/IP requires it, so that is the machine's own name.
Set Advertised Hostname when it is not:
- Normal is running with Docker bridge networking, where the hostname is the container ID and resolves nowhere else. The server logs a warning at startup when it detects this.
- Clients reach Normal by a different name or address than the machine knows itself by — through NAT, or on another DNS domain.
Check what is actually being advertised, without needing credentials:
python3 examples/opc/endpoints.py --url opc.tcp://your-host:4840/
Namespaces¶
The same data is published twice, under two namespace URIs. Clients should resolve these by URI; the numeric index is assigned at startup and is not stable across versions.
| Namespace URI | Contents |
|---|---|
urn:normal:points |
The raw point database. Devices and containers appear as Objects, points as Variables. The NodeId is the point UUID. |
urn:normal:equipment |
The semantic model. One Object per equipment instance, referencing its member points. |
Equipment members are referenced, not copied: there is one node per point and one place its value is written. A SCADA client can browse the flat tag list while an analytics client browses typed equipment, and both read the same node. If you have no use for the model, turn off Equipment Namespace and only the points are published.
How the point tree is built¶
A point's parent folder is resolved in this order:
parent_uuid, when it names a container point — durable and unambiguous, so it wins where it exists.parent_name, which Normal populates far more widely. If exactly one container carries that name the point goes under it; otherwise a folder is synthesized for the name.- Otherwise the point sits at the namespace root.
Points carrying neither end up in a flat list at the root, which is hard on tree widgets past a few thousand nodes. Imported and synthetic data often looks like this — modelling the site fixes the browse experience as a side effect.
Values and quality¶
NodeIds are point UUIDs, which survive renames and re-discovery.
Normal already knows when a point is stale, railed or not reporting, and
OPC-UA has a vocabulary for exactly that, so quality is translated rather than
everything reading Good:
| Normal | OPC-UA StatusCode |
|---|---|
| Communications error | BadNoCommunication |
| Never reported | BadWaitingForInitialData |
| Dead | BadNoCommunication |
| Out of range (hard) | BadOutOfRange |
| Sentinel value | BadSensorFailure |
| Stale, intermittent | UncertainLastUsableValue |
| Railed, flatlined, noisy, spiking, non-monotonic | UncertainSensorNotAccurate |
| Insufficient data | UncertainInitialValue |
Where a point carries several flags, the most severe wins.
Client software that treats anything other than Good as a failure will show
gaps where Normal is telling you something more specific. That is the intended
behaviour: a stale value reported as Good is worse than one reported as
Uncertain.
History¶
Point variables advertise Historizing and the HistoryRead access bit, and
history is served straight from Normal's time series.
- ReadRawModified returns raw samples, with continuation points for paging.
- ReadProcessed aggregates server-side. This is the one worth using: a client asking for hourly averages over a month gets a few hundred values instead of every raw sample.
Supported aggregates are Average, Minimum, Maximum, Total, Count, Start and
End. Anything else — TimeAverage, Interpolative, the quality aggregates —
is reported as unsupported rather than quietly substituted, because returning a
plain average where a time-weighted one was requested is a wrong number, not an
approximation.
Two limits are worth knowing:
- Raw retention is a rolling window (7 days by default). A client asking for a year gets what exists, with no way to distinguish "never recorded" from "aged out".
- Sub-second timestamps are reported at millisecond resolution.
Writing points¶
Writes are off by default. Turn on Enable Writes to allow them.
An OPC-UA write does not go straight to the device. It opens a command in Normal's command service, exactly as a write from the console or the API would, so priority, guardrails and revert-on-expiry all still apply, and the write appears in the command log with the API key that made it.
Because OPC-UA carries no priority or duration of its own, both come from settings: Write Priority (default 10) and Write Duration (default one hour, after which the command expires and the point reverts).
Writes always require an authenticated session with a read/write scope. Anonymous sessions can never write, whatever these settings say.
Authentication¶
Clients authenticate with a Normal API key, created under Settings > API
Keys: the client_id is the username and the client_secret the password.
The token's scopes decide what the session may do, so there is no second permission model to keep in sync with the console:
| Scope | OPC-UA access |
|---|---|
normalgw.hpl.v*.readonly |
browse, read, subscribe, history |
normalgw.hpl.v*.readwrite |
the above, plus writes |
| anonymous | reads only, and only while Allow Anonymous is on |
Credentials are held in memory and re-checked every five minutes (Credential Revalidation Interval). This is what makes revocation work: deleting the API key or rotating its secret makes the next check fail and the session loses its identity. Until that check, a revoked key keeps the access it already had — set a shorter interval if that matters to you.
Credentials require an encrypted endpoint
The server refuses username/password authentication on the unencrypted endpoint. With no message security the identity token is not encrypted, so accepting it there would put your client secret on the wire in clear. If your client can only do unencrypted connections, it can only be anonymous — and therefore read-only.
Security¶
By default the server offers both an encrypted and an unencrypted endpoint, and allows anonymous reads. That combination gets clients connected, but it is not what you want on an untrusted network.
To lock the server down:
| Setting | Set to | Effect |
|---|---|---|
| Allow Insecure Endpoint | off | Withdraws the unencrypted endpoint entirely |
| Allow Anonymous | off | Every session must present an API key |
| Trust All Client Certificates | off (default) | Unknown client certificates must be approved by an administrator |
Three encrypted policies are offered: Basic256Sha256,
Aes128_Sha256_RsaOaep and Aes256_Sha256_RsaPss, all SignAndEncrypt.
Basic128Rsa15 and Basic256 are not offered at any setting — both are
deprecated by the OPC Foundation as insecure.
Certificates¶
On first start the server generates a self-signed certificate under
/var/nf/opcua/pki, which lives on the Normal volume and survives upgrades.
When a client connects with a certificate the server has not seen, the
connection is rejected and the certificate is written to
/var/nf/opcua/pki/rejected/. To trust it, move it into
/var/nf/opcua/pki/trusted/:
docker exec -it nf sh -c \
'mv /var/nf/opcua/pki/rejected/*.der /var/nf/opcua/pki/trusted/'
The client will connect on its next attempt. This is a deliberate speed bump: it means an administrator sees every peer once. Trust All Client Certificates skips it, which is convenient during bringup and unwise afterwards, since any peer presenting any certificate is then accepted.
Settings reference¶
All settings live under Protocols > OPC-UA > Settings. Changing any of them restarts the OPC-UA server only; the rest of Normal is unaffected.
| Setting | Default | Purpose |
|---|---|---|
| Enabled | off | Master switch |
| Port | 4840 | TCP port for opc.tcp |
| Advertised Hostname | system hostname | What clients are told to reconnect to |
| Bind Address | 0.0.0.0 |
Local listen address |
| Layer | (union) | Point layer to expose |
| Equipment Namespace | on | Publish the equipment model |
| Allow Anonymous | on | Permit sessions with no credentials |
| Allow Insecure Endpoint | on | Offer an endpoint with no security |
| Security Policies | all three | Encrypted policies to offer |
| Allow Sign Without Encryption | off | Also offer Sign-only endpoints |
| Trust All Client Certificates | off | Skip certificate review |
| Check Certificate Validity Dates | on | Enforce certificate validity windows |
| Credential Revalidation Interval | 300s | How often held credentials are re-checked |
| Enable Writes | off | Allow commanding points |
| Write Priority | 10 | Command priority for OPC-UA writes |
| Write Duration | 3600s | How long a write holds before reverting |
| Load Page Size | 1000 | Points fetched per request at startup |
Connecting a client¶
Any OPC-UA client works. For a first look, UaExpert (free, with registration) and the open-source opcua-client both browse the address space and plot history.
For scripted access, the nf-sdk repository has worked examples under
examples/opc covering browsing, reading, subscriptions, history and writes.
pip3 install asyncua
python3 examples/opc/browse.py --url opc.tcp://localhost:4840/
Limitations¶
- The address space is built at startup. Points added or removed in Normal appear after the OPC-UA server restarts. Value changes are live.
- Equipment objects are untyped. They are
BaseObjectTypewith the Normal type name as a property, rather than generated OPC-UA ObjectTypes. - History is read-only.
HistoryUpdateis not supported. - Engineering units are display strings, not UNECE Recommendation 20 unit codes, so they are human-readable but not machine-comparable.
- Authorization is service-level. A read/write key can command any point; there is no way to scope a key to part of the building.