Skip to content

API reference

Client for the BACnet/IP REST proxy (https://github.com/stblassitude/bacnet-ip-rest-proxy).

Client

Client(
    url: str,
    token: str | None = None,
    *,
    timeout: float = 30.0,
    verify: bool | str = True,
    retry: RetryPolicy = RetryPolicy(),
    concurrency: int = 10,
    transport: AsyncBaseTransport | None = None,
)

A client of one BACnet/IP REST proxy, playing the part of a bacpypes3 Application: read_property() and write_property() take the same arguments as bacpypes3's, except that the device is addressed by the proxy's device id (a configured alias, or a hostname/IP[:port]) instead of a BACnet Address.

Where the proxy works differently from BACnet, so does this class:

  • Errors are raised, never returned (see errors.py).
  • list_objects() returns the proxy's cached object catalog, including each object's name and the caller's access to it, in one request.
  • There is no ReadPropertyMultiple. read_properties() issues one HTTP request per property instead, concurrently, and reports each one's outcome separately.
  • Every request goes through the proxy's authorization, based on token. A client acting for different users needs one Client per user token.
  • Transient failures are retried (see RetryPolicy and is_transient()), writes included: a WriteProperty of the same value at the same priority is idempotent.

Use as an async context manager, or call aclose() when done.

url is the proxy's base URL, without the /api/v1 path.

token is sent as a bearer token: one of the proxy's configured tokens, or a JWT it accepts (e.g. a user's OIDC access token).

timeout is the default per-request timeout in seconds; each call can override it. verify is passed to httpx: False to accept a self-signed certificate, or the path of a CA bundle.

concurrency bounds how many requests this client has in flight at once, across all calls. The proxy serializes what it sends to the device itself; this keeps a large read_properties() from opening hundreds of connections to the proxy. A request waiting out a retry delay doesn't hold a slot.

transport replaces httpx's network transport, e.g. with an httpx.MockTransport in tests.

list_devices async

list_devices(*, timeout: float | None = None) -> list[str]

The device ids (aliases) configured on the proxy.

read_device async

read_device(
    device: str, *, timeout: float | None = None
) -> dict[str, Any]

A device's identity summary, as the proxy's DeviceInfo JSON: instance, object-name, vendor-name, model-name, access.

list_objects async

list_objects(
    device: str, *, timeout: float | None = None
) -> list[ObjectSummary]

Every object on the device the caller may read, from the proxy's cache (refreshed proxy-side, by default every 60s). Objects the caller may not read are left out; if that is all of them, this raises ForbiddenError.

read_object async

read_object(
    device: str,
    objid: ObjectIdentifierLike,
    *,
    timeout: float | None = None,
) -> dict[str, Any]

A summary of one object, read by the proxy in a single ReadPropertyMultiple: the proxy's ObjectInfo JSON, keyed by property name (object-name, present-value, description, units, status-flags), plus access. Properties the object doesn't have are left out.

read_property async

read_property(
    device: str,
    objid: ObjectIdentifierLike,
    prop: str,
    array_index: int | None = None,
    *,
    timeout: float | None = None,
) -> Any

Read one property (bacpypes3: read_property(address, objid, prop, array_index)) and return its value, decoded as described in values.py. prop is the property name, e.g. "present-value", or "property(N)" for one the proxy has no name for.

read_properties async

read_properties(
    device: str,
    requests: Iterable[PropertyRequest],
    *,
    timeout: float | None = None,
) -> list[Any]

Read several properties concurrently, one request each, and return a list in the same order holding each value or, for a read that failed, its ProxyError. Unlike a BACnet ReadPropertyMultiple this is not a single transaction: each value is read at a slightly different moment, and each read succeeds or fails on its own.

write_property async

write_property(
    device: str,
    objid: ObjectIdentifierLike,
    prop: str,
    value: Any,
    priority: int | None = None,
    *,
    timeout: float | None = None,
) -> None

Write one property (bacpypes3: write_property(address, objid, prop, value, array_index, priority), minus array_index, which the proxy's write endpoint doesn't take).

value uses the same JSON shapes a read returns. None relinquishes priority on a commandable property instead of writing a value. priority (1-16) applies to commandable properties only; the proxy uses 16 if it's omitted.

RetryPolicy dataclass

RetryPolicy(
    attempts: int = 5,
    base_delay: float = 1.0,
    max_delay: float = 8.0,
)

How often, and how patiently, a transient failure is retried.

attempts counts the first try, so RetryPolicy(attempts=1) disables retrying. The delay before retry n (1-based) is base_delay * 2**(n-1), capped at max_delay: 1+2+4+8 = 15s in total with the defaults.

BACnetError

BACnetError(
    status_code: int,
    message: str,
    error_class: int | None = None,
    error_code: int | None = None,
)

Bases: ProxyStatusError

502: the device answered with a BACnet Error, Reject or Abort.

BACnetTimeoutError

BACnetTimeoutError(
    status_code: int,
    message: str,
    error_class: int | None = None,
    error_code: int | None = None,
)

Bases: ProxyStatusError

504: the device did not respond in time.

BadRequestError

BadRequestError(
    status_code: int,
    message: str,
    error_class: int | None = None,
    error_code: int | None = None,
)

Bases: ProxyStatusError

400: the request was malformed (e.g. an unknown object type name).

CommunicationError

Bases: ProxyError

The proxy could not be reached, or its reply could not be decoded (e.g. an empty or truncated 200 body). The underlying exception is chained as cause.

ForbiddenError

ForbiddenError(
    status_code: int,
    message: str,
    error_class: int | None = None,
    error_code: int | None = None,
)

Bases: ProxyStatusError

403: the credentials were accepted, but the proxy's authorization rules deny this request -- e.g. a write to a variable the token only has readonly access to.

NotFoundError

NotFoundError(
    status_code: int,
    message: str,
    error_class: int | None = None,
    error_code: int | None = None,
)

Bases: ProxyStatusError

404: unknown device, object or property.

ProxyError

Bases: Exception

Base class: a request to the proxy did not succeed.

ProxyStatusError

ProxyStatusError(
    status_code: int,
    message: str,
    error_class: int | None = None,
    error_code: int | None = None,
)

Bases: ProxyError

The proxy answered with an HTTP error status.

is_unknown_object_or_property property

is_unknown_object_or_property: bool

Whether this means the object or property doesn't exist. The proxy says so either with a 404, when it can tell without asking the device, or with a 502 carrying the device's unknown-object or unknown-property Error_Code.

UnauthorizedError

UnauthorizedError(
    status_code: int,
    message: str,
    error_class: int | None = None,
    error_code: int | None = None,
)

Bases: ProxyStatusError

401: the proxy did not accept the credentials (missing, invalid or expired token).

ValueRejectedError

ValueRejectedError(
    status_code: int,
    message: str,
    error_class: int | None = None,
    error_code: int | None = None,
)

Bases: ProxyStatusError

422: the device rejected the value written (e.g. out of range).

Access

Bases: StrEnum

The caller's effective right on an object, according to the proxy's authorization rules -- a proxy concept with no BACnet equivalent. It says nothing about whether the device will accept a write.

ObjectIdentifier

Bases: NamedTuple

A BACnetObjectIdentifier: object type and instance number.

Like bacpypes3's ObjectIdentifier this is a 2-tuple, so objtype, instance = objid works the same way, and it parses bacpypes3's "analog-value,1" string form. Unlike bacpypes3, the type is kept as the string the proxy uses on the wire (e.g. "analog-value", or "object-type(N)" for a type without a name) rather than an ObjectType enumeration -- the client has no local object-type registry, the proxy does all of that.

parse classmethod

parse(
    value: ObjectIdentifier | tuple[str, int] | str,
) -> Self

Accepts an ObjectIdentifier, a (type, instance) tuple, or a string "type,instance" (bacpypes3's form) or "type:instance".

from_json classmethod

from_json(data: dict[str, Any]) -> Self

From the proxy's {"type", "instance"} JSON shape.

ObjectSummary dataclass

ObjectSummary(
    objid: ObjectIdentifier,
    name: str | None = None,
    description: str | None = None,
    units: Any = None,
    access: Access | None = None,
)

One entry of Client.list_objects(): an object on the device with the metadata the proxy caches for it. There is no bacpypes3 equivalent; with plain BACnet this takes a walk of the device's object-list plus a read of each object's properties.

is_transient

is_transient(exc: ProxyError) -> bool

Whether a failed request is worth retrying: the proxy couldn't be reached or sent a garbled reply, the device timed out, or the device answered with an error other than "no such object/property". Any other status (400/401/403/404/422) is a property of the request itself and would fail again the same way.

decode_value

decode_value(
    object_type: str,
    prop: str,
    raw: Any,
    array_index: int | None = None,
) -> Any

Decode a property value as received from the proxy.

encode_value

encode_value(value: Any) -> Any

Encode a value for a write. A non-finite float becomes the string the proxy expects, since JSON has no NaN or Infinity.