|
| 1 | +.. _tutorial_properties: |
| 2 | + |
| 3 | +Properties |
| 4 | +========================= |
| 5 | + |
| 6 | + |
| 7 | + |
| 8 | +Properties are values that can be read from and written to a Thing. They are used to represent the state of the Thing, such as its current temperature, brightness, or status. :ref:`wot_properties` are a key concept in the Web of Things standard. |
| 9 | + |
| 10 | +LabThings implements properties in a very similar way to the built-in Python `~builtins.property`. The key difference is that defining an attribute as a `.property` means that the property will be listed in the :ref:`gen_td` and exposed over HTTP. This is important for two reasons: |
| 11 | + |
| 12 | +* Only properties declared using `.property` (usually imported as ``lt.property``) can be accessed over HTTP. Regular attributes or properties using `builtins.property` are only available to your `.Thing` internally, except in some special cases. |
| 13 | +* Communication between `.Thing`\ s within a LabThings server should be done using a `.DirectThingClient` class. The purpose of `.DirectThingClient` is to provide the same interface as a `.ThingClient` over HTTP, so it will also only expose functionality described in the Thing Description. |
| 14 | + |
| 15 | +You can add properties to a `.Thing` by using `.property` (usually imported as ``lt.property``). |
| 16 | + |
| 17 | +Data properties |
| 18 | +------------------------- |
| 19 | + |
| 20 | +Data properties behave like variables: they simply store a value that is used by other code on the `.Thing`. They are defined similarly to fields in `dataclasses` or `pydantic` models: |
| 21 | + |
| 22 | +.. code-block:: python |
| 23 | +
|
| 24 | + import labthings_fastapi as lt |
| 25 | +
|
| 26 | + class MyThing(lt.Thing): |
| 27 | + my_property: int = lt.property(default=42) |
| 28 | +
|
| 29 | +The example above defines a property called `my_property` that has a default value of `42`. Note the type hint `int` which indicates that the property should hold an integer value. This is important, as the type will be enforced when the property is written to via HTTP, and it will appear in :ref:`gen_docs`. By default, this property may be read or written to by HTTP requests. If you want to make it read-only, you can set the `readonly` parameter to `True`: |
| 30 | + |
| 31 | +.. code-block:: python |
| 32 | +
|
| 33 | + class MyThing(lt.Thing): |
| 34 | + my_property: int = lt.property(default=42, readonly=True) |
| 35 | +
|
| 36 | +Note that the ``readonly`` parameter only affects *client* code, i.e. it may not be written to via HTTP requests or `.DirectThingClient` instances. However, the property can still be modified by the Thing's code, e.g. in response to an action or another property change as ``self.my_property = 100``. |
| 37 | + |
| 38 | +It is a good idea to make sure there is a docstring for your property. This will be used in the :ref:`gen_docs`, and it will help users understand what the property is for. You can add a docstring to the property by placing a string immediately after the property definition: |
| 39 | + |
| 40 | +.. code-block:: python |
| 41 | +
|
| 42 | + class MyThing(lt.Thing): |
| 43 | + my_property: int = lt.property(default=42, readonly=True) |
| 44 | + """A property that holds an integer value.""" |
| 45 | +
|
| 46 | +You don't need to include the type in the docstring, as it will be inferred from the type hint. However, you can include additional information about the property, such as its units or any constraints on its value. |
| 47 | + |
| 48 | +Data properties may be *observed*, which means notifications will be sent when the property is written to (see below). |
| 49 | + |
| 50 | +Functional properties |
| 51 | +------------------------- |
| 52 | + |
| 53 | +It is also possible to have properties that run code when they are read or written to. These are called functional properties, and they are defined using the `lt.FunctionalProperty` class. They might communicate with hardware (for example to read or write a setting on an instrument), or they might perform some computation based on other properties. They are defined with a decorator, very similarly to the built-in `property` function: |
| 54 | + |
| 55 | +.. code-block:: python |
| 56 | +
|
| 57 | + import labthings_fastapi as lt |
| 58 | +
|
| 59 | + class MyThing(lt.Thing): |
| 60 | + my_property: int = lt.property(default=42) |
| 61 | + """A property that holds an integer value.""" |
| 62 | +
|
| 63 | + @lt.property |
| 64 | + def twice_my_property(self) -> int: |
| 65 | + """Twice the value of my_property.""" |
| 66 | + return self.my_property * 2 |
| 67 | +
|
| 68 | +The example above defines a functional property called `twice_my_property` that returns twice the value of `my_property`. The type hint `-> int` indicates that the property should return an integer value. When this property is read via HTTP, the code in the method will be executed, and the result will be returned to the client. As with `property`, the docstring of the property is taken from the method's docstring, so you can include additional information about the property there. |
| 69 | + |
| 70 | +Functional properties may also have a "setter" method, which is called when the property is written to via HTTP. This allows you to perform some action when the property is set, such as updating a hardware setting or performing some computation. The setter method should take a single argument, which is the new value of the property: |
| 71 | + |
| 72 | +.. code-block:: python |
| 73 | +
|
| 74 | + import labthings_fastapi as lt |
| 75 | +
|
| 76 | + class MyThing(lt.Thing): |
| 77 | + my_property: int = lt.property(default=42) |
| 78 | + """A property that holds an integer value.""" |
| 79 | +
|
| 80 | + @lt.property |
| 81 | + def twice_my_property(self) -> int: |
| 82 | + """Twice the value of my_property.""" |
| 83 | + return self.my_property * 2 |
| 84 | +
|
| 85 | + @twice_my_property.setter |
| 86 | + def twice_my_property(self, value: int): |
| 87 | + """Set the value of twice_my_property.""" |
| 88 | + self.my_property = value // 2 |
| 89 | +
|
| 90 | +Adding a setter makes the property read-write (if only a getter is present, it must be read-only). |
| 91 | + |
| 92 | +It is possible to make a property read-only for clients by setting its ``readonly`` attribute: this has the same behaviour as for data properties. |
| 93 | + |
| 94 | +.. code-block:: python |
| 95 | +
|
| 96 | + import labthings_fastapi as lt |
| 97 | +
|
| 98 | + class MyThing(lt.Thing): |
| 99 | + my_property: int = lt.property(default=42) |
| 100 | + """A property that holds an integer value.""" |
| 101 | +
|
| 102 | + @lt.property |
| 103 | + def twice_my_property(self) -> int: |
| 104 | + """Twice the value of my_property.""" |
| 105 | + return self.my_property * 2 |
| 106 | +
|
| 107 | + @twice_my_property.setter |
| 108 | + def twice_my_property(self, value: int): |
| 109 | + """Set the value of twice_my_property.""" |
| 110 | + self.my_property = value // 2 |
| 111 | +
|
| 112 | + # Make the property read-only for clients |
| 113 | + twice_my_property.readonly = True |
| 114 | +
|
| 115 | +In the example above, ``twice_my_property`` may be set by code within ``MyThing`` but cannot be written to via HTTP requests or `.DirectThingClient` instances. |
| 116 | + |
| 117 | +Functional properties may not be observed, as they are not backed by a simple value. If you need to notify clients when the value changes, you can use a data property that is updated by the functional property. In the example above, ``my_property`` may be observed, while ``twice_my_property`` cannot be observed. It would be possible to observe changes in ``my_property`` and then query ``twice_my_property`` for its new value. |
| 118 | + |
| 119 | +HTTP interface |
| 120 | +-------------- |
| 121 | + |
| 122 | +LabThings is primarily controlled using HTTP. Mozilla have a good `Overview of HTTP`_ that is worth a read if you are unfamiliar with the concept of requests, or what ``GET`` and ``PUT`` mean. |
| 123 | + |
| 124 | +Each property in LabThings will be assigned a URL, which allows it to be read and (optionally) written to. The easiest way to explore this is in the interactive OpenAPI documentation, served by your LabThings server at ``/docs``\ . Properties can be read using a ``GET`` request and written using a ``PUT`` request. |
| 125 | + |
| 126 | +LabThings follows the `HTTP Protocol Binding`_ from the Web of Things standard. That's quite a detailed document: for a gentle introduction to HTTP and what a request means, see |
| 127 | + |
| 128 | +.. _`Overview of HTTP`: https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Overview |
| 129 | +.. _`HTTP Protocol Binding`: https://w3c.github.io/wot-binding-templates/bindings/protocols/http/index.html |
| 130 | + |
| 131 | +Observable properties |
| 132 | +------------------------- |
| 133 | + |
| 134 | +Properties can be made observable, which means that clients can subscribe to changes in the property's value. This is useful for properties that change frequently, such as sensor readings or instrument settings. In order for a property to be observable, LabThings must know whenever it changes. Currently, this means only data properties can be observed, as functional properties do not have a simple value that can be tracked. |
| 135 | + |
| 136 | +Properties are currently only observable via websockets: in the future, it may be possible to observe them from other `.Thing` instances or from other parts of the code. |
0 commit comments