SiLA Endpoints
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.
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)
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]
whereT
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()
.