bacnet-ip-rest-client¶
An async Python client for the BACnet/IP REST proxy. It is built on httpx.
from bacnet_ip_rest_client import Client, ProxyError
async with Client("https://bacnet-proxy.example.net", token) as client:
for obj in await client.list_objects("my-device"):
print(obj.objid, obj.name, obj.access)
temp = await client.read_property("my-device", "analog-input,1", "present-value")
slots = await client.read_property("my-device", "analog-value,5", "priority-array")
await client.write_property("my-device", "analog-value,5", "present-value", 21.5, priority=10)
await client.write_property("my-device", "analog-value,5", "present-value", None, priority=10) # relinquish
Authentication¶
Every request goes through the proxy's
authentication and authorization.
Pass token to Client to send it as Authorization: Bearer <token>. The
token can be one of the opaque tokens configured on the proxy, or a JWT it
accepts, such as a user's OIDC access token. Without a token, the proxy
only allows a request if its rules grant access without one, for example
by the client's IP address. A request the proxy denies raises
UnauthorizedError or ForbiddenError.
Relationship to bacpypes3¶
Client has the same role as a bacpypes3 Application, and
read_property() and write_property() take bacpypes3's arguments, with
some exceptions. It doesn't try to hide the ways an HTTP proxy works
differently from a BACnet/IP stack:
| bacpypes3 | this client | |
|---|---|---|
| Addressing a device | BACnet Address |
the proxy's device id: a configured alias, or a hostname/IP[:port] |
| Object identifiers | ObjectIdentifier with an ObjectType enum |
ObjectIdentifier(type: str, instance: int), also a 2-tuple; parses "analog-value,1" |
| Values | decoded into the property's BACnet datatype | JSON as the proxy sends it: enumerations as numbers, bit strings as lists of bools. REAL properties of analog objects are always float, including NaN/±Infinity |
| Errors | ErrorPDU/RejectPDU/AbortPDU, which derive from BaseException, and are sometimes returned instead of raised |
always raised, all derived from ProxyError(Exception), see below |
| Object discovery | read object-list, then each object's properties |
list_objects(): the proxy's cached catalog with names, units and access, in one request |
| ReadPropertyMultiple | one request, one transaction | none. read_properties() sends concurrent single reads, each with its own result |
| WriteProperty with array index | supported | not supported by the proxy |
| Authorization | none | a bearer token per Client. The proxy's rules decide what it may read and write (ObjectSummary.access) |
Errors¶
| Exception | Cause | Retried |
|---|---|---|
CommunicationError |
proxy unreachable, or a reply that couldn't be decoded | yes |
BadRequestError |
400, malformed request | no |
UnauthorizedError |
401, token not accepted | no |
ForbiddenError |
403, the proxy's authorization denies the request | no |
NotFoundError |
404, unknown device, object or property | no |
ValueRejectedError |
422, the device rejected the written value | no |
BACnetError |
502, BACnet Error/Reject/Abort from the device; error_class/error_code hold its numeric Error_Class/Error_Code |
yes, except unknown-object/unknown-property |
BACnetTimeoutError |
504, the device didn't answer | yes |
ProxyStatusError.is_unknown_object_or_property is true when the proxy
reports a missing object or property, whether it does so as a 404 or as a
502 from the device. Code that probes for optional properties, such as
priority-array, can check this one attribute.
Retries use exponential backoff as configured by RetryPolicy (default:
5 attempts, 1s doubling to at most 8s). Retry warnings are logged to the
bacnet_ip_rest_client logger. Writes are retried too, since writing the
same value at the same priority again has no further effect.
concurrency (default 10) caps the number of requests one Client has in
flight. The proxy serializes traffic to the device itself; this cap only
keeps a large read_properties() from opening hundreds of connections to
the proxy.