Tutorial

Implementing the observable property

Implementing the observable property for the door state.

Creating the implementation class

To use the feature "door controller" and the just created subscription method, we need to implement it in a sub-class: the implementation class. We create a file called door_controller.py in the connector folder (you should see a __init__.py and __main__.py as well as the greeting_provider.py file there already)

├── src
  ├── connector
    ├── feature
      ├── door_controller
        ├── __init__.py
        ├── door_controller_base.py
    ├── __init__.py
    ├── __main__.py
    ├── door_controller.py

In door_controller.py we initialize our DoorController class.

door_controller.py
from .features.door_controller import DoorControllerBase


class DoorController(DoorControllerBase):
    def __init__(self):
        super().__init__()
        self._door_open_change_event.set()

Code explanation

In the first line, the base class we created before is imported. Then, we define a class called DoorController that inherits from the DoorControllerBase class. In the __init__ method, we call the __init__ method of the base class and set the event we created there. This initialises the DoorController and the change event for the _door_open property is activated. We will use this in our implementation of the subscribe_door_open method.

Implementation of the subscribe_door_open method

While in the base class, we defined the name, kind, parameters, type hints, and documentation of the method, the implementation class contains the logic of the method:

async def subscribe_door_open(self):
    yield self._door_open
    self._door_open_change_event.clear()
    while True:
        await self._door_open_change_event.wait()
        self._door_open_change_event.clear()
        yield self._door_open

Code explanation

Every time we call this method we want to directly get the current door state back. That's why first we yield the state and then clear the event (it might be already cleared at this point but if not we need to clear it). while True creates an endless loop, which means as long as we stay subscribed to this function, this loop repeats. Within the loop, we have 4 steps:

  1. Wait for the event we defined previously. As this event is set during the initialisation, the first time this loop is called, we move on to the second step.
  2. This line clears the event, so we can reuse it.
  3. We yield the _door_open property which is set by other methods that update the state of the door like the open or close method.
  4. We go back to step 1 and wait for the _door_open event to be triggered again.

Registering the feature

Open the __init__.py file in the connector directory:

├── src
  ├── connector
    ├── feature
    ├── __init__.py
    ├── __main__.py
    ├── door_controller.py

Import the DoorController feature and register it with the connector application using the app.register() method. Within this file you can also add and update the connector information, such as name, type, description, version, and the vendor url. The connector information is not just visible to the client, but also provides valuable information for auto-discovery as it is also part of the mDNS message that is broadcasted via zeroconf/bonjour in your local network. That makes it very easy to find, ultimately enabling plug 'n play!

__init__.py
from unitelabs.cdk import Connector

from .door_controller import DoorController


async def create_app():
    """Creates the connector application"""
    app = Connector(
        {
            "sila_server": {
                "name": "Thermocycler",
                "type": "Thermocycler",
                "description": "Control the temperature of your samples with this thermocycler.",
                "version": "0.1.0",
                "vendor_url": "https://unitelabs.io/",
            }
        }
    )

    app.register(DoorController())
    return app

Environmental variables

The connector application uses environmental variables to determine the IP and port it should run on. Each connector also requires a unique identifier, so they don't get mixed up by a client. You can create a .env file in the source directory to define theses variables. The .env file is loaded on connector start.

├── src
  ├── connector
  ├── .env
  ├── .gitignore
  ├── LICENSE
  ├── ...

Define the UUID, HOST, and PORT and you're ready to go. The cloud server endpoint environmental variables are set to default as we won't be needing them for this tutorial yet.

.env
CLOUD_SERVER_ENDPOINT__ENDPOINT = localhost:443
CLOUD_SERVER_ENDPOINT__SECURE = True
SILA_SERVER__UUID = a1c7147d-a11f-4ac5-8518-af4f62ac6e2d
SILA_SERVER__HOST = 0.0.0.0
SILA_SERVER__PORT = 50051

Testing

Congrats, you have implemented your first observable property! Let's test it by running:

$ poetry run connector start

You can interact with the connector with the SiLA Browser to see if the subscription method works. The SiLA Browser should automatically detect your connector. Since we initialise the door in a closed state(_door_open=false, see __init__ method in the base class), we expect "No" as an answer in the SiLA Browser as soon as we run the subscription method.

How the observable property should look like in the sila browser


Copyright © 2024