AVEVA PI Web API
The aveva-pi connector reads and writes a PI System through its
PI Web API REST interface. It is the first
tedge-dot connector for a server rather than a fieldbus device:
- A point is a PI point (by tag name or path), an AF attribute (by path) or any stream (by WebId). WebIds are looked up once per connection.
- One request reads all of a device’s due points, however many there are.
- By default, values are pushed over the PI Web API Channels WebSocket and keep their PI timestamps.
- After an outage or a restart, the gap is filled from the PI archive, so Cumulocity gets the readings it missed, in order.
- Writable points can be changed from the cloud.
What you’ll build: one device, pi-demo, that receives four values from the demo’s PI Web
API simulator (aveva-pi-sim, plain HTTP on host port 8480) and writes a temperature setpoint
back to it.
AVEVA, PI System and PI Web API are trademarks of AVEVA Group Limited or its subsidiaries. tedge-dot is not affiliated with or endorsed by AVEVA.
Run the demo
Section titled “Run the demo”With thin-edge.io and a tedge-dot package installed:
-
Start the simulator:
Terminal window docker compose -f /usr/share/tedge-dot/demo/docker-compose.yaml up -d aveva-pi-sim -
Enable the demo config. The service picks it up within a few seconds:
Terminal window sudo install -m 0640 -o tedge -g tedge /usr/share/tedge-dot/demo/aveva-pi.toml /etc/tedge/plugins/ot/aveva-pi.toml -
Watch the values arrive.
pi-demoappears in Cumulocity as a child device of the gateway:Terminal window tedge mqtt sub 'te/device/pi-demo/#'
Read and write the simulator with no broker. Use tedge-dot from a package, or
cargo run --manifest-path impl/rust/Cargo.toml -- in its place:
docker compose -f demo/docker-compose.yaml up -d --wait aveva-pi-simtedge-dot read -c demo/config/aveva-pi.toml -p sinusoidtedge-dot read -c demo/config/aveva-pi.toml -p pump02_statetedge-dot write -c demo/config/aveva-pi.toml -d pi-demo -p setpoint --value 72.5docker compose -f demo/docker-compose.yaml downEach command logs a warning that the base URL is plain HTTP. Only the simulator should be reached that way.
The device
Section titled “The device”A device is one PI Web API endpoint:
[[device]]name = "pi-demo"type = "aveva-pi-server"protocol_address = { base_url = "http://127.0.0.1:8480/piwebapi", data_server = "PISRV01", af_database = '\\AFSRV01\Plant' }demo/config/aveva-pi.toml on GitHub| Key | Required | Notes |
|---|---|---|
base_url | yes | The PI Web API root, such as https://pi.example.com/piwebapi. |
data_server | for tag points | The PI Data Archive that bare tag names live in. |
af_database | no | \\<AF server>\<database>, the prefix of relative attribute paths. |
delivery | no | How values arrive: see below. |
Any [connection] key, such as auth or tls, can be overridden per device. Several devices
may share one base_url, and then they share one connection pool.
delivery decides how readable points are delivered:
delivery | How values arrive | Timestamp |
|---|---|---|
"channel" (default) | Pushed over a Channels WebSocket as PI archives them, with backfill after an outage. | The PI time of each value. |
"recorded" | The archive is polled every sampling_interval (else poll_interval), from where the last poll stopped. | The PI time of each value. |
"snapshot" | The current value is read at every poll_interval, as the community plugin did. | The read time; the PI time is in the sample’s addr.timestamp. |
If the server refuses the Channels WebSocket while its REST requests work, the device falls back
to recorded until the next reconnect and logs the refusal once.
Points
Section titled “Points”# Pushed over Channels, with their PI timestamps.[[device.point]]id = "sinusoid"name = "Sinusoid"datatype = "float64"address = { tag = "SINUSOID" }
[[device.point]]id = "reactor_temp"name = "Reactor temperature"datatype = "float32"address = { tag = "REACTOR01.TEMP" }
[[device.point]]id = "pump02_state"name = "Pump 02 state"datatype = "string"address = { tag = "PUMP02.STATE" }
[[device.point]]id = "discharge_pressure"name = "Pump 01 discharge pressure"datatype = "float64"address = { path = 'Line1\Pump01|Discharge Pressure' }
# A device parameter: writable from the cloud.[[device.point]]id = "setpoint"name = "Temperature setpoint"datatype = "float64"unit = "degC"access = "read_write"address = { tag = "SETPOINT" }meta.parameter = { key = "control.setpoint" }demo/config/aveva-pi.toml on GitHubEach point has exactly one address key:
| Address | Kind |
|---|---|
{ tag = "SINUSOID" } | A PI point on the device’s data_server. |
{ path = '\\PISRV01\SINUSOID' } | A PI point by its full path. |
{ path = '\\AF\Plant\Line1\Pump01|Discharge Pressure' } | An AF attribute by its full path. |
{ path = 'Line1\Pump01|Discharge Pressure' } | An AF attribute, relative to af_database. |
{ web_id = "F1DP…" } | Any stream, by WebId. It isn’t looked up. |
Use TOML literal strings ('…') for paths, so the backslashes stay as written. A tag that the
server doesn’t have makes that point bad with not found: <path>, without taking the device
down, and it is looked up again every resolve_retry (5 minutes by default).
- Datatypes. Every point needs a
datatype. Usefloat32orfloat64for PI Float points,int16orint32for Int points, andstringfor String and Timestamp points. - Digital states. A PI digital point has a code and a name from its state set, for example
1andRunning.int32gives the code, which becomes a measurement series.stringgives the name, which is useful for a device parameter, an event or an alarm, but is not a measurement. For both, configure two points with the sametag. - Units. A point without a
unittakes its engineering unit from PI. - Quality. A value that PI marks as not good, or a system state such as
I/O Timeout, arrives as abadsample with PI’s state name as the error. Questionable, substituted and annotated values stay good and are flagged in the sample’saddr.flags.
Connecting to a real PI Web API server
Section titled “Connecting to a real PI Web API server”A real PI Web API server is HTTPS and needs credentials. The packaged
/etc/tedge/plugins/ot/aveva-pi.toml has no devices and lists every option as a comment.
-
Put the secret in a file that only the
tedgeuser can read. The connector never reads a password or token from the config itself:Terminal window sudo install -d -m 0700 -o tedge -g tedge /etc/tedge/plugins/ot/secretsprintf '%s' 'the-password' | sudo install -m 0600 -o tedge -g tedge /dev/stdin /etc/tedge/plugins/ot/secrets/aveva-pi.password -
Name the method and the file in
[connection]:/etc/tedge/plugins/ot/aveva-pi.toml [connection]auth = { method = "basic", username = 'CORP\pi-reader', password_file = "/etc/tedge/plugins/ot/secrets/aveva-pi.password" }tls = { ca_file = "/etc/tedge/plugins/ot/certs/pi-ca.pem" } -
Add a
[[device]]with anhttps://base_url, and points as above.
auth.method | Keys | Notes |
|---|---|---|
basic | username, password_file | A domain user is written 'DOMAIN\user'. |
bearer | token_file | The file is read again after a 401, so an external job can rotate the token. |
certificate | cert_file, key_file | A PEM client certificate and key (mutual TLS). |
Kerberos and NTLM aren’t supported.
- Server certificate. It is checked against the system trust store, plus the CA bundle in
tls.ca_file(intedge-dot-c, the bundle replaces the system store). The certificate needs a subject alternative name for the host inbase_url.tls.insecure_skip_verify = trueturns the check off and logs a warning on every connect. - Plain HTTP. An
http://base_urlis accepted only without authentication. With anyauth.methodexceptnone, the config is refused, so credentials are never sent unencrypted. - Local-only settings. The secret file keys,
tlsandstate_dircan’t be changed by a management command from the cloud.
Outages and backfill
Section titled “Outages and backfill”With channel or recorded delivery, the connector keeps a cursor for each point: the time of
the last value it delivered. The cursors are written to
/var/lib/tedge-dot/aveva-pi/<service>/<device>.json (state_dir), so they survive a restart.
After an outage or a restart, the connector reads the archive from the cursor onwards, delivers
those values in time order, and then continues with the live ones. The catch-up is limited by
backfill:
[connection]backfill = { max_age = "1h", max_events = 10000 } # per point; backfill = false turns it offA longer gap is logged once, with the point and the time range that was skipped. The reporting policy applies to replayed values on their PI time, so a backfilled hour is reduced as the live stream would have been.
Writing to PI
Section titled “Writing to PI”A point with access = "read_write" (or "write") can be written. The demo’s setpoint is also
a device parameter, so it can be changed from the
Cumulocity Parameters tab.
- A write sends the value with the server’s time.
- For a digital point, write either the state name or its code. A state that isn’t in the point’s state set fails without a request.
update_option(default"Replace") andbuffer_option(default"BufferIfPossible") in[connection]set how PI stores the value.- A write is never retried. If PI refuses it, for example with a 403 because the account may only read, the command fails with PI’s error message and the device stays connected.
Migrating from the community plugin
Section titled “Migrating from the community plugin”If you run the community thin-edge-aveva-pi-connector,
a script turns its pi_config.json and datapoints.json into a connector config. The
measurements land on a child device with a different type, so read the
migration guide before you switch.