UniteLabs
Tutorial

SiLA Endpoints

UniteLabs CDK and the SiLA component library.

SiLA makes the distinction between Commands and Properties that commands may be parameterized, but properties may not. At UniteLabs we take a different philosophical approach to grouping functionality that distinguishes between Data Endpoints, which are used to return data to the user and Controls which have real-world physical device interactions associated with them. Data endpoints may be represented by observable or unobservable properties as well as unobservable commands, whereas Controls may only be represented by Commands. Before we dive into the differences between these four groups, let's first look at some definitions and learn about the shared attributes of SiLA Endpoints.

Definitions

Property

A Property is an attribute of the connector itself or the connected system, that is static. Properties return a single value immediately and cannot be subscribed. In some rare cases, Properties can contain values that change over time, since the categorization is automatically generated based on the SiLA interface of the connector. Examples of properties might be the system's serial number or calibration data.

Sensor

A Sensor returns a stream of responses consisting of intermediate responses and a final response. Sensors can be subscribed and provide values that are typically dynamic, such as temperature readings or the speed of a stirrer. Sensor are most commonly represented as observable properties.

Shared Attributes

The UniteLabs CDK offers function decorators to create SiLA Commands and Properties which auto-generate SiLA identifiers, display names, and descriptions for methods, their parameters, and their returns based on a function's typing and its docstring representation, ensuring that our SiLA client is strongly typed with a human-readable interface.

Identifiers and Display Names

Identifiers are auto-generated based on the method name. The method name get_server_name is turned into the property identifier "ServerName". Several prefixes are auto-detected and infer certain aspects of the method:

  • "get"-prefix is subtracted for unobservable properties
  • "subscribe"-prefix is subtracted for observable properties

When allowing the CDK to derive its own identifiers and display names, acronyms and abbreviations that are part of the identifier and display name will be turned into lowercase words. A method named "get_server_uuid" is transformed into "ServerUuid" (identifier) and "Server Uuid" (display name) respectively. However, in some cases, like the SiLA Service feature, identifier and display name are defined as "ServerUUID" and "Server UUID". If a display name is specified explicitly, the identifier is derived from it. Therefore, the decorator attributes must be set accordingly, for example:

@sila.UnobservableProperty(display_name="Server UUID")
async def get_server_uuid(self) -> str:
    ...

All UniteLabs CDK SiLA decorators accept identifier, display_name, and description as parameters for manually setting these values, rather than having them be derived. These decorators are an alternative to docstring-based declarations, which use directives in the form of

.. directive:: <description>
  :attr: <value>

For example instead of sila.Parameter:

.. parameter:: Description of parameter.
  :display_name: Parameter Name
  :identifier: ParameterName

Using both a decorator and a docstring description will preferentially return the decorator's content in the SiLA client. It should also be noted that IDEs rely on the docstring representations to generate hints while coding and the current decorator implementation does not update IDE docstrings based on decorator representations.

Future warning: The kit currently enforces a strong, PEP8-compliant method naming convention. Future releases may allow the definition of uppercase acronyms and abbreviations (e.g. "get_server_UUID") for increased usability.

Responses

Responses are inferred by the specified return type. Responses can be annotated with the sila.Response decorator or a .. return:: statement in the docstring.

While properties must only have one response, commands may have multiple responses. They can be added simply by adding multiple Response decorator. In this case, the method should return a tuple of all responses and the return statement looks like this:

@sila.UnobservableCommand()
@sila.Response(name="Answer")
@sila.Response(name="Random Fact")
async def generate_random_stuff(self) -> tuple[int, str]:
    """
    Creates random stuff.

    .. return:: Answer to the Ultimate Question of Life,
      the Universe, and Everything.
    .. return:: Some random fact to always keep in mind.
    """

    return 42, "Don't Panic!"

Return types must be valid SiLA Data Types. See the Complex Data Structures Guide for more information about Custom SiLA data types.

Parameters

Parameters can be inferred based on the type-hints provided in the function declaration. These type-hints must be combined with annotation of the parameters' human-readable name and description, which can be set using the sila.Parameter decorator or a .. parameter:: statement in the docstring. Parameters must be declared in the order that they are declared on the function.

@sila.UnobservableCommand()
@sila.Response(name="Random Number")
async def generate_random_seeded_number(self, seed: int) -> float:
    """
    Stably generate a random number.

    .. parameter:: Number used to initialize the pseudorandom number generator.
    .. return:: A randomly generated number.
    """

    import random

    random.seed(seed)

    return random.random()

Parameter types must be valid SiLA Data Types. See the Complex Data Structures Guide for more information about Custom SiLA data types.

Observable Properties

An observable property is a property that can be read at any time and that offers a subscription mechanism to observe any change of its value.

Add the ObservableProperty decorator to a method to turn it into an observable property.

@sila.ObservableProperty()
async def subscribe_random_numbers(self) -> sila.Stream[int]:
    """
    Stream of random numbers.
    """

    import asyncio
    import random

    while True:
        yield random.randint(0, 42)
        await asyncio.sleep(1)
Technical Hint: Python does not not allow return statements in async generators, therefore only yield is allowed.

Unobservable Properties

An unobservable property is a property that can be read at any time, but no subscription mechanism is provided to observe its changes.

Add the UnobservableProperty decorator to a method to turn it into an unobservable property.

@sila.UnobservableProperty()
async def get_random_number(self) -> int:
    """
    A single random number.
    """

    import random

    return random.randint(0, 42)

Unlike the observable property from the previous section, this unobservable property does not provide a stream of constantly updating values but only returns get_random_number once.

Unobservable Commands

An unobservable command is a command that triggers an action on the device and returns at most a single response. UnobservableCommands may act as both Data Endpoints and Controls, depending on their functionality.

Add the UnobservableCommand decorator to a method to turn it into an unobservable command.

@sila.UnobservableCommand()
@sila.Response(name="Success")
async def perform_action(self, name: str) -> bool:
    """
    Perform the action on the device identified by `name`.
    
    .. parameter: The action to be performed.
    .. return:: Whether or not the action was successful.
    """
    ...

Here we attempt to perform a named action and report whether or not that named action was successfully performed. The action does not report back any intermediary values and thus is unobservable.

As noted, unobservable commands must not necessarily act as controls, but may also be functions which have input parameters (which are not allowed for SiLA properties) and use those parameters to selectively get information from the device, i.e. extracting a single value from a dictionary based on its key.

@sila.UnobservableCommand()
@sila.Response(name="Value")
async def get_variable_value(self, variable: str) -> str:
    """
    Read out the variable value for the currently active configuration.
    
    .. parameter: The name of the variable in the active configuration
      to get the value for.
    .. return:: The variable value.
    """

    return self.active_config.get(variable, "")

Observable Commands

An observable command is a command that triggers and action on the device and can be subscribed. During the subscription, intermediate results can be returned that contain information on the command execution status, such as remaining execution time, progress and intermediate result data.

Add the ObservableCommand decorator to a method to turn it into an unobservable command.

@sila.ObservableCommand()
@sila.Response(name="Random Number")
@sila.IntermediateResponse(name="Current Iteration")
async def iterative_method(
    self, seed: int, iterations: int, *, status: sila.Status, intermediate: sila.Intermediate[int]
) -> float:
    """
    Creates a random number based on a `seed` after `iterations`.

    .. parameter:: Number used to initialize the pseudorandom number generator.
    .. parameter:: How many times to iterate before number generation.
    .. yield:: The current iteration.
    .. return:: A random number.
    """

    import asyncio
    import random

    random.seed(seed)

    total = iterations + 1
    for x in range(1, total):
        intermediate.send(x)
        status.update(progress=(x / total))
        await asyncio.sleep(1)

    return random.random()

Observable command functions may take the following keyword arguments:

  • status: sila.Status
  • intermediate: sila.Intermediate[T] where T is a valid SiLA data type

If the function signature contains these two arguments, one may then use the objects passed in to send status updates with status.update() or to send IntermediateResponse values via intermediate.send().


Copyright © 2025