UniteLabs

UniteLabs SDK & REST API

Access connectors programmatically — discover services, explore their modules and actions, and subscribe to live data using the UniteLabs SDK or the REST API.

The UniteLabs SDK and REST API expose the same connector model: services (connectors), modules (features), and actions (properties, sensors, and controls). Each section below shows both approaches side by side.

This guide shows how to discover connectors and call each type of action. For a complete automation-script walkthrough, including deck setup and liquid handling, continue with Calling a Connector.

Prerequisites

  1. Access to the UniteLabs platform
  2. An environment with an installed SDK — see SDK installation
  3. A connector deployed and connected to your tenant (the thermocycler demo works for these examples)

Authentication

The UniteLabs SDK handles authentication automatically using environment variables. For the REST API you need a Bearer token.

The SDK reads credentials from environment variables. Set them in your .env file:

.env
BASE_URL=your-tenant-base-url
AUTH_URL=your-authentication-url
CLIENT_ID=your-client-id
CLIENT_SECRET=your-client-secret

Then instantiate the client — no token handling required:

from unitelabs.sdk import AsyncApiClient

client = AsyncApiClient()

List connectors

Retrieve all connectors connected to your tenant.

services = await client.list_services()
print(services)
Terminal
(Thermocycler(client=..., id='daa46515-49bc-4a7d-944f-369732edde2e', name='Thermocycler'),)

Get a specific connector

Look up a connector by name or ID.

By name:

thermocycler = await client.get_service_by_name(name="Thermocycler")
print(thermocycler.id)
print(thermocycler.name)

By ID:

thermocycler = await client.get_service(service_id="daa46515-49bc-4a7d-944f-369732edde2e")

Explore modules (features)

A connector's modules are its features — logical groupings of related actions.

print(thermocycler.modules.keys())
Terminal
dict_keys(['sila_service', 'temperature_controller', 'door_controller'])

Explore actions

Each module exposes actions: the individual properties, sensors, and controls you can call.

print(thermocycler.temperature_controller.actions.keys())
Terminal
dict_keys(['get_target_temperature', 'subscribe_current_temperature', 'set_target_temperature'])

Check the type of an action (PROPERTY, SENSOR, or CONTROL):

print(thermocycler.temperature_controller.get_target_temperature.type)
# PROPERTY

Every action has one of three types. The type determines how you consume it, and each maps to an underlying SiLA interaction:

TypeWhat it doesHow you consume itSiLA interaction
PROPERTYReads a single value and completesCall it and await the valueUnobservable Property
SENSORStreams values as they changeSubscribe and read each valueObservable Property
CONTROLTriggers a change, with optional parametersCall it and await the resultUnobservable Command
CONTROLTriggers a change and reports progress while it runsCall it and await the result, or subscribe to follow its updatesObservable Command

The three sections below show how to consume each type. For the concept behind action types see Action, and for the cross-surface mapping see the Terminology table.


Read a property

A property (PROPERTY) reads a single value and completes. Properties take no parameters, and the SDK method name is prefixed with get_.

target = await thermocycler.temperature_controller.get_target_temperature()
print(target)  # e.g. 42

Read a sensor

A sensor (SENSOR) streams values that change over time. Subscribing returns a subscription object: await the call to get it, then open it as an async context manager. The SDK method name is prefixed with subscribe_.

Subscribe and read each value as it changes:

subscription = await thermocycler.temperature_controller.subscribe_current_temperature()
async with subscription:
    async for temperature in subscription:
        print(temperature)  # prints each new reading as it arrives

A sensor emits only when its value changes, so iteration blocks until the next update. Leaving the async with block closes the stream. You can also break out of the loop or use asyncio cancellation.

If you only need a single snapshot, take the first value and stop. This is how you read a live value that has no property of its own: the actual current temperature, for instance, since the property only holds the target.

subscription = await thermocycler.temperature_controller.subscribe_current_temperature()
async with subscription:
    temperature = await anext(subscription)
    print(temperature)  # the next reading, e.g. 41.7

See Subscription for lifecycle, reliability, and cancellation.


Execute a control

A control (CONTROL) triggers a change on the instrument, optionally with parameters, and returns its result once the execution finishes. Unlike properties and sensors, a control's method carries no prefix: it is the action name itself.

Call the control and await its result. Pass parameters as keyword arguments:

await thermocycler.temperature_controller.set_target_temperature(target_temperature=42)

The call resolves when the instrument reports the command finished, and returns the final response (or None if the control has no response).

Observe a long-running control

Short-running and long-running controls are called the same way; the distinction only matters when you want to watch a long one while it runs. Some controls run long enough to report progress and intermediate responses before they finish. These are observable commands, for example a door that streams its opening status. The generated action method returns only the final result and discards those updates. To observe them, create the subscription yourself and iterate the raw events:

open_door = thermocycler.door_controller.open_door
subscription = await open_door.client.create_subscription(action_id=open_door.id, parameters={})
async with subscription:
    async for event, value in subscription:
        if event == "error":
            raise value
        if event == "response":
            print("done:", value)     # final result
        else:
            print("update:", value)   # progress or intermediate result

This is a low-level escape hatch. For a plain call-and-wait, use the action method directly as shown above. For controls that take parameters, pass them under the names defined in the action's parameter schema.


Next steps

  • Execute commands: read properties and call commands on the instrument from a script: Calling a Connector
  • Liquid handling & robots: additional SDK packages for deck building and liquid transfers: Operate