Creating an API

Overview

The core of the API being created for the network clients is provided through the APIBase class. This reference describes the core APIBase class, together with example implementations (sub-classes) from the urest.example package.

The APIBase class delegates all network requests to the RESTServer class (described separately). During the marshalling of client requests by the RESTServer class, the path portion of the URI is extracted from the client. This path is expected to take the form of

/< noun >

with the ‘verb’ denoted by the HTTP methods. Each ‘noun’ is implemented by a class which inherits from APIBase. The ‘noun’ determines the resource of the request (i.e. derived class), and the ‘verb’ the action to be taken on that resource. Also important is the intent of the client as inferred from the underlying HTTP request, e.g. GET or PUT. Taken together the URI and the client intent are mapped to methods of APIBase as follows

Verb HTTP Method APIBase method
Get GET APIBase.get_state()
Set PUT APIBase.set_state()
Update POST APIBase.update_state()
Delete DELETE APIBase.delete_state()

Routing Requests

All nouns are registered with RESTServer in lowercase. Likewise all routing of API requests will also assume that the name of the noun to be used will be in lowercase. Thus, for instance, calling

app.register_noun('leD', SimpleLED(28))

or

app.register_noun('LED', SimpleLED(28))

will all route to the name noun with the canonical name led. Likewise the request

GET /lEd HTTP 1.1

or

GET /LED HTTP 1.1

will also route to the same noun with the canonical name led.

Note

All nouns are checked by RESTServer, and nouns will not be properly routed unless APIBase is an ancestor of the derived class. Thus, for example, the SimpleLED class above must also be a sub-class of APIBase for RESTServer to route the request properly.

Package and Class Structure

The APIBase class is an abstract base class and provides minimal services beyond defining the core interface (or protocol) for RESTServer. As outlined above, this interface is provided though the core class methods as shown below

API Package Structure

The API services for the network clients are then expected to be built from the abstract base class of APIBase. The urest library provides a number of examples in the urest.examples package, which illustrate some way of using the library. Many of these are also used by the code in the examples folder of the source repository. The classes of the urest.examples package are described later, but can be conceptually defined as follows

API Package Structure

Example Usage

Getting the Noun State

This example is based on the SimpleLED class as a minimal implementation of a noun controlling a GPIO pin, using the MicroPython Pin library. The full documentation for the class is detailed below, and the SimpleLED is available through the urest.examples package.

For the SimpleLED class, the exact pin being controlled is set during the object instantiation, via the class constructor. Assuming the RESTServer has been created as

app = RESTServer()

then a ‘noun’ led can be registered via the RESTServer.register_noun() method as

app.register_noun('led', SimpleLED(28))

This will also create an instance of the SimpleLED class, bound to the controlled GPIO pin Pin 28. Documentation for the micro-controller and platform being used will be need to determine suitable values for the GPIO pin numbering.

Once the ‘noun’ has been registered, then the state of GPIO Pin 28 can be found by the HTTP request

GET /led HTTP 1.1

On receiving the above request from the client, the RESTServer instance will call the relevant methods of the SimpleLED (as a sub-class of APIBase) as shown in Figure 1. Note that the ‘<< resource state>>’ returned by SimpleLED.get_state() is as a Python dict[str, Union[str, int]]. The full return of data to the client, including relevant HTTP headers, conversion to JSON, etc. is handeled by the RESTServer instance.

Figure 1: A Sequence Diagram for the Noun Get Request

Figure 1: A Sequence Diagram for the Noun Get Request of the Example

Setting the Noun State

Setting the state of the noun is a little more involved, as this will require the desired state of the noun to be sent to the server. A useful tool for testing purposes is the curl utility; available on most platforms.

Continuing the minimal example above, the command line

curl -X PUT -d '{"led":"0"}' -H "Content-Type: application/json" http://10.0.30.225/LED

will transmit something like the following HTTP request to RESTServer

PUT /LED HTTP/1.1

Host: 10.0.30.225
User-Agent: curl/7.81.0
Accept: */*
Content-Type: application/json
Content-Length: 11

{"led":"0"}

JSON is Required

The only format accepted by the RESTServer for the state of the nouns is JSON. The RESTServer will also only accept a sub-set of the JSON standard: notably assuming a single object, and a collection of key/value pairs.

Figure 2: A Sequence Diagram for the Noun Set Request

Figure 2: A Sequence Diagram for the Noun Set Request of the Example

The exact interpretation of the JSON << resource_state >> from the Client in Figure 2 is left to the implementation of the noun. The only internal guarantee provided by the library is the JSON will be parsed as far as possible and sent to SimpleLED.set_state(), in this case, as a Python dict[str, Union[str, int]]. For instance in the above example the key led is interpreted as referring to the noun, and the value 0 as the state value. In this case setting the GPIO Pin 28 to the value 0 (or off). A close correspondence between the name of the noun as referred to the API, and the name of the noun in the state list is strongly advised: but is not strictly required.

Tested Implementations

This version is written for MicroPython 3.4, and has been tested on:

  • Raspberry Pi Pico W

Classes

urest.api.base.APIBase

Define the Abstract Base Class for the nouns, used to structure the response from the server to the client.

This base class defines the minimum interface used in marshalling requests from the clients by the RESTServer class. The API defined by the server consists of ‘nouns’ representing the resources defined by the sub-classes of this base class, and the methods which act upon those resources (which map to the ‘verbs’ of the HTTP requests made to the RESTServer class).

Where data is returned to the client by the, e.g by the get_state method, JSON will ultimately be used as the encoding format. Sub-classes do not need to implement the saving of the object state to the client: however to assist they should be aware of the type of the each value returned as part of a key/value pair in the dictionary. Specifically the data representing the resource state is expected to be (coerced to) a dict[str, Union[str, int]] for all methods. This type implies that the only acceptable ‘key value’ for the dic is a Python string, and the ‘value’ itself is either a string or an integer.

When returning data to the client, the value of each entry in the dictionary will attempt to be inferred using the normal Python type library. If the type can be identified, then an appropriate JSON type of string or integer will be used as appropriate. If the type for the value of that dictionary entry cannot be determined, or cannot be coerced to an integer, then the value will be returned as a string. Note that string and integer are the only types returned to (and seen from) the client by sub-classes of APIBase.

Client Data is Handled ‘as is’

No further checking on the validity (or otherwise) of the content of the dictionary will be undertaken past this point. Anything that appear to be in a valid dictionary (of type dict[str, Union[str, int]]) will be returned to the client. It is the module consumers responsibility to ensure the returned data follows the form expected by those clients.

Similar data sent from the client will be passed to sub-classes of APIBase as a dictionary of type dict[str, Union[str, int]]. If the RESTServer class cannot coerce client data into this format it will be dropped and will not be passed onto sub-classes of APIBase. In this case an error will be returned to the client, and the methods of APIBase will not be called with the partial data.

Source code in urest/api/base.py
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
class APIBase:
    """Define the Abstract Base Class for the nouns, used to structure the
    response from the server to the client.

    This base class defines the minimum interface used in marshalling
    requests from the clients by the
    [`RESTServer`][urest.http.server.RESTServer] class. The API defined
    by the server consists of 'nouns' representing the resources defined
    by the sub-classes of this base class, and the methods which act
    upon those resources (which map to the 'verbs' of the HTTP requests
    made to the [`RESTServer`][urest.http.server.RESTServer] class).

    Where data is returned to the client by the, e.g by the `get_state`
    method, JSON will ultimately be used as the encoding format.
    Sub-classes do not need to implement the saving of the object state to
    the client: however to assist they should be aware of the type of
    the each `value` returned as part of a `key/value` pair in the
    dictionary. Specifically the data representing the resource state is
    expected to be (coerced to) a `dict[str, Union[str, int]]` for _all_
    methods. This type implies that the only acceptable 'key value' for the
    `dic` is a Python string, and the 'value' itself is either a string
    or an integer.

    When returning data to the client, the `value` of each entry in the
    dictionary will attempt to be inferred using the normal Python type
    library. If the type can be identified, then an appropriate JSON
    type of `string` or `integer` will be used as appropriate. If the
    type for the `value` of that dictionary entry cannot be determined,
    or cannot be coerced to an integer, then the value will be returned
    as a string. Note that `string` and `integer` are the _only_ types
    returned to (and seen from) the client by sub-classes of `APIBase`.

    !!! warning "Client Data is Handled 'as is'"
        No further checking on the validity (or otherwise)
        of the content of the dictionary will be undertaken past this
        point. Anything that appear to be in a valid dictionary (of type
        `dict[str, Union[str, int]]`) will be returned to the client. It
        is the module consumers responsibility to ensure the returned data
        follows the form expected by those clients.

        Similar data sent from the client will be passed to sub-classes
        of `APIBase` as a dictionary of type `dict[str, Union[str, int]]`.
        If the [`RESTServer`][urest.http.server.RESTServer] class cannot
        coerce client data into this format _it will be dropped_ and _will
        not_ be passed onto sub-classes of `APIBase`. In this case an error
        will be returned to the client, and the methods of `APIBase` _will
        not_ be called with the partial data.
    """

    ##
    ## Attributes
    ##

    _state_attributes: dict[str, Union[str, int]]
    """The current state and attributes of the resource."""

    ##
    ## Constructor
    ##

    def __init__(self) -> None:
        self._state_attributes = {"": 0}

    ##
    ## State Manipulation Methods
    ##

    def get_state(self) -> dict[str, Union[str, int]]:
        """Return the state of the resource, as defined by the sub-classes. By
        default this method will return the contents of the private
        `state_attributes` `Dictionary` to the client; assuming that
        `Dictionary` has been appropriately completed in the processing of the
        resource.

        Returns
        -------

        dict[str, Union[str, int]]
            A mapping of (key, value) pairs which defines the resource state to return to the
            client.

        """

        return self._state_attributes

    def set_state(self, state_attributes: dict[str, Union[str, int]]) -> None:
        """Set the full state of the resource.

        The exact mechanism for updating the internal state of the resource represented
        by sub-classes is implementation defined. By default this method expects the
        _full_ state to be represented by the `state_attributes` parameters; and by
        default the new state will be exactly as stated by the `Dictionary` passed in
        through `state_attributes`.

        Parameters
        ----------

        state_attributes: dict[str, Union[str, int]]
            A list of (key, value) pairs representing the _full_ state of the
            resource. No merging of state is undertaken, or attempted.

        """

        if state_attributes is not None and isinstance(state_attributes, dict):
            self._state_attributes = state_attributes
        else:
            self._state_attributes = {"": 0}

    def update_state(
        self,
        state_attributes: dict[str, Union[str, int]],
    ) -> None:
        """Update the state of the resource, using the 'key/value' pairs of the
        `Dictionary` in `state_attributes`.

        The exact mechanism for updating the internal state of the resource represented
        by sub-classes is implementation defined. By default this expects to update
        only part of the state through the use of the partial state defined in
        `state_attributes`. This defines a `Dictionary` of (key, value) pairs which
        can be interpreted by sub-classes to update the true internal state as needed.

        Parameters
        ----------

        state_attributes: dict[str, Union[str, int]]
            A list of (key, value) pairs representing the _partial_ state of the
            resource. The exact mechanism for merging this partial state if left
            to the implementation of the sub-classes.

        """

        if state_attributes is not None and isinstance(state_attributes, dict):
            for key in state_attributes:
                try:
                    self._state_attributes[key] = state_attributes[key]
                except KeyError:
                    self._state_attributes[key] = ""
        else:
            self._state_attributes = {"": 0}

    def delete_state(self) -> None:
        """Remove the internal state of the resource, essentially 'resetting'
        or re-initialising the object.

        The exact mechanism for returning the state to the defaults are
        left to the implementation. However it is expected that once
        this call completes the internal state will be _identical_ to
        that of the default constructor.
        """

        self._state_attributes = {"": 0}

Functions

delete_state
delete_state() -> None

Remove the internal state of the resource, essentially ‘resetting’ or re-initialising the object.

The exact mechanism for returning the state to the defaults are left to the implementation. However it is expected that once this call completes the internal state will be identical to that of the default constructor.

Source code in urest/api/base.py
188
189
190
191
192
193
194
195
196
197
198
def delete_state(self) -> None:
    """Remove the internal state of the resource, essentially 'resetting'
    or re-initialising the object.

    The exact mechanism for returning the state to the defaults are
    left to the implementation. However it is expected that once
    this call completes the internal state will be _identical_ to
    that of the default constructor.
    """

    self._state_attributes = {"": 0}
get_state
get_state() -> dict[str, Union[str, int]]

Return the state of the resource, as defined by the sub-classes. By default this method will return the contents of the private state_attributes Dictionary to the client; assuming that Dictionary has been appropriately completed in the processing of the resource.

Returns:
  • dict[str, Union[str, int]]

    A mapping of (key, value) pairs which defines the resource state to return to the client.

Source code in urest/api/base.py
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
def get_state(self) -> dict[str, Union[str, int]]:
    """Return the state of the resource, as defined by the sub-classes. By
    default this method will return the contents of the private
    `state_attributes` `Dictionary` to the client; assuming that
    `Dictionary` has been appropriately completed in the processing of the
    resource.

    Returns
    -------

    dict[str, Union[str, int]]
        A mapping of (key, value) pairs which defines the resource state to return to the
        client.

    """

    return self._state_attributes
set_state
set_state(
    state_attributes: dict[str, Union[str, int]]
) -> None

Set the full state of the resource.

The exact mechanism for updating the internal state of the resource represented by sub-classes is implementation defined. By default this method expects the full state to be represented by the state_attributes parameters; and by default the new state will be exactly as stated by the Dictionary passed in through state_attributes.

Parameters:
  • state_attributes (dict[str, Union[str, int]]) –

    A list of (key, value) pairs representing the full state of the resource. No merging of state is undertaken, or attempted.

Source code in urest/api/base.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
def set_state(self, state_attributes: dict[str, Union[str, int]]) -> None:
    """Set the full state of the resource.

    The exact mechanism for updating the internal state of the resource represented
    by sub-classes is implementation defined. By default this method expects the
    _full_ state to be represented by the `state_attributes` parameters; and by
    default the new state will be exactly as stated by the `Dictionary` passed in
    through `state_attributes`.

    Parameters
    ----------

    state_attributes: dict[str, Union[str, int]]
        A list of (key, value) pairs representing the _full_ state of the
        resource. No merging of state is undertaken, or attempted.

    """

    if state_attributes is not None and isinstance(state_attributes, dict):
        self._state_attributes = state_attributes
    else:
        self._state_attributes = {"": 0}
update_state
update_state(
    state_attributes: dict[str, Union[str, int]]
) -> None

Update the state of the resource, using the ‘key/value’ pairs of the Dictionary in state_attributes.

The exact mechanism for updating the internal state of the resource represented by sub-classes is implementation defined. By default this expects to update only part of the state through the use of the partial state defined in state_attributes. This defines a Dictionary of (key, value) pairs which can be interpreted by sub-classes to update the true internal state as needed.

Parameters:
  • state_attributes (dict[str, Union[str, int]]) –

    A list of (key, value) pairs representing the partial state of the resource. The exact mechanism for merging this partial state if left to the implementation of the sub-classes.

Source code in urest/api/base.py
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
def update_state(
    self,
    state_attributes: dict[str, Union[str, int]],
) -> None:
    """Update the state of the resource, using the 'key/value' pairs of the
    `Dictionary` in `state_attributes`.

    The exact mechanism for updating the internal state of the resource represented
    by sub-classes is implementation defined. By default this expects to update
    only part of the state through the use of the partial state defined in
    `state_attributes`. This defines a `Dictionary` of (key, value) pairs which
    can be interpreted by sub-classes to update the true internal state as needed.

    Parameters
    ----------

    state_attributes: dict[str, Union[str, int]]
        A list of (key, value) pairs representing the _partial_ state of the
        resource. The exact mechanism for merging this partial state if left
        to the implementation of the sub-classes.

    """

    if state_attributes is not None and isinstance(state_attributes, dict):
        for key in state_attributes:
            try:
                self._state_attributes[key] = state_attributes[key]
            except KeyError:
                self._state_attributes[key] = ""
    else:
        self._state_attributes = {"": 0}

urest.examples.echo.EchoServer

Bases: APIBase

An example of a ‘noun’ class which echo’s responses to the client. The main use of this library is as a test: it should work on both the Pico (W) boards and any ‘normal’ installation of Python. As such it provides very little functionality, but could in principle also be used as a the basis for a more useful test harness or stub.

For an example of the client part of the ‘echo server’, see the example echo_server.py. A full example of this class is also described in the Simple Echo Server How-To.

API

The ‘noun’ exposes the following internal state

JSON Key JSON Type Description
echo Integer Holds the current state of the resource, determined by the client

This state can be altered by the client sending either a 0 or 1 to the noun: which will record the last value seen from the client. This last value can then be returned to the client on request. Values greater than 1 will be treated as 1 for the purposes of setting the state.

Source code in urest/examples/echo.py
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
class EchoServer(APIBase):
    """An example of a 'noun' class which echo's responses to the client. The
    main use of this library is as a test: it should work on both the Pico (W)
    boards and any 'normal' installation of Python. As such it provides very
    little functionality, but could in principle also be used as a the basis
    for a more useful test harness or stub.

    For an example of the client part of the 'echo server', see the example
    [`echo_server.py`](https://github.com/dlove24/urest/blob/trunk/examples/echo_server.py).
    A full example of this class is also described in the
    [_Simple Echo Server_][a-simple-echo-server-windows-linux-and-mac] How-To.

    API
    ---

    The 'noun' exposes the following internal state

    | JSON Key | JSON Type | Description                                                       |
    |----------|-----------|-------------------------------------------------------------------|
    | `echo`   | `Integer` | Holds the current state of the resource, determined by the client |

    This state can be altered by the client sending either a `0` or `1` to the
    `noun`: which will record the last value seen from the client. This last
    value can then be returned to the client on request. Values greater than `1`
    will be treated as `1` for the purposes of setting the state.
    """

    def __init__(self) -> None:
        self._state = False
        self._state_attributes = {"echo": 0}

    def set_state(self, state_attributes: dict[str, Union[str, int]]) -> None:
        try:
            self._state_attributes["echo"] = state_attributes["echo"]

            if self._state_attributes["echo"] == 0:
                self._state = False
            else:
                self._state = True

        except KeyError:
            # On exception try to return to a known good
            # state
            self._state = False
            self._state_attributes["echo"] = 0

    def get_state(self) -> dict[str, Union[str, int]]:
        return {"echo": self._state}

    def delete_state(self) -> None:
        self._state = False
        self._state_attributes["echo"] = 0

    def update_state(
        self,
        state_attributes: dict[str, Union[str, int]],
    ) -> None:
        if self._state_attributes["echo"] == 0:
            self._state = True
            self._state_attributes["echo"] = 1
        else:
            self._state = False
            self._state_attributes["echo"] = 0

urest.examples.pwmled.PWMLED

Bases: APIBase

Controls an LED by PWM modulation, and gives a reasonably minimal example of a ‘noun’ class, which inherits from urest.api.base.APIBase. It also requires the Pin library from MicroPython: but should be able to be adapted to other GPIO libraries which provide a similar interface.

In contrast to the urest.examples.simpleled.SimpleLED class, the urest.examples.pwmled.PWMLED class shows the use of the ayncio create_tasks() hook within a urest.api.base.APIBase ‘noun’ to set off slow running tasks. This allows the state update to be returned to the network client via the API ‘immediately’ (at least subject to the other tasks outstanding and network conditions); without waiting for the actual internal state to complete. This is a much more realistic scenario for use in the control of external devices: especially devices such as motors which may take seconds (or longer) to obtain the correct state.

API

The ‘noun’ exposes the following internal state, with allowed values as 0 or 1 for both keys.

JSON Key JSON Type Description
actual Integer The current state of the controlled output
desired Integer The next state, if any, that the output is currently transitioning to

Since the class will not immediately set the desired state, but only once the full transition is complete, the client may not see any immediate changes in the get_state requests. Instead the full state table for the response, and interpretation, is as follows

Actual State Desired State Description
0 0 Output fully off
0 1 Output commanded on; currently turning from off to on
1 0 Output commanded off; currently turning from on to off
1 1 Output fully on
Source code in urest/examples/pwmled.py
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
class PWMLED(APIBase):
    """Controls an LED by PWM modulation, and gives a reasonably minimal
    example of a 'noun' class, which inherits from `urest.api.base.APIBase`. It
    also requires the `Pin` library from MicroPython: but should be able to be
    adapted to other GPIO libraries which provide a similar interface.

    In contrast to the `urest.examples.simpleled.SimpleLED` class, the
    `urest.examples.pwmled.PWMLED` class shows the use of the `ayncio`
    `create_tasks()` hook within a `urest.api.base.APIBase` 'noun' to set off slow
    running tasks. This allows the state update to be returned to the network
    client via the API 'immediately' (at least subject to the other tasks
    outstanding and network conditions); without waiting for the _actual_ internal
    state to complete. This is a much more realistic scenario for use in the
    control of external devices: especially devices such as motors which may take
    seconds (or longer) to obtain the correct state.

    API
    ---

    The 'noun' exposes the following internal state, with allowed values as `0`
    or `1` for both keys.

    | JSON Key  | JSON Type | Description                                                             |
    |-----------|-----------|-------------------------------------------------------------------------|
    | `actual`  | `Integer` | The _current_ state of the controlled output                            |
    | `desired` | `Integer` | The _next_ state, if any, that the output is currently transitioning to |

    Since the class will not immediately set the `desired` state, but only once
    the full transition is complete, the client may not see any immediate changes
    in the `get_state` requests. Instead the full state table for the response,
    and interpretation, is as follows

    | Actual State | Desired State | Description                                                  |
    |--------------|---------------|--------------------------------------------------------------|
    | 0            | 0             | Output fully `off`                                           |
    | 0            | 1             | Output commanded `on`; currently turning from `off` to `on`  |
    | 1            | 0             | Output commanded `off`; currently turning from `on` to `off` |
    | 1            | 1             | Output fully `on`                                            |
    """

    def __init__(self, pin: int) -> None:
        self._gpio = PWM(Pin(pin))
        self._gpio.duty_u16(0)
        self._gpio.freq(100)
        self._gpio_lock = asyncio.Lock()

        self._duty = 0

        self._state_attributes = {"desired": 0, "current": 0}

    async def _slow_on(self) -> None:
        # Wait for the GPIO lock if we need to
        await self._gpio_lock.acquire()

        # Increase the duty cycle from 0 to near the
        # maximum in steps lasting 1s. We will also
        # allow other co-routines to run whilst we
        # are waiting for the next step to take place

        self._duty = 0

        while self._duty < (PWM_LIMIT):
            print(f"duty on: {self._duty}")

            self._duty += PWM_STEP
            self._gpio.duty_u16(self._duty)

            await asyncio.sleep_ms(1000)  # type: ignore

        self._state_attributes["current"] = 1

        # Set the duty cycle to maximum before we leave,
        # and release the GPIO lock
        self._duty = 2**16
        self._gpio.duty_u16(self._duty)

        self._gpio_lock.release()

    async def _slow_off(self) -> None:
        # Wait for the GPIO lock if we need to
        await self._gpio_lock.acquire()

        # Decrease the duty cycle from the maximum to
        # near 0 in steps lasting 1s. We will also
        # allow other co-routines to run whilst we
        # are waiting for the next step to take place

        self._duty = 2**16

        while self._duty > PWM_STEP:
            print(f"duty off: {self._duty}")

            self._duty -= PWM_STEP
            self._gpio.duty_u16(self._duty)

            await asyncio.sleep_ms(1000)  # type: ignore

        self._state_attributes["current"] = 0

        # Set the duty cycle to 0 before we leave,
        # and release the GPIO lock
        self._duty = 0
        self._gpio.duty_u16(self._duty)

        self._gpio_lock.release()

    def set_state(self, state_attributes: dict[str, Union[str, int]]) -> None:
        try:
            loop = asyncio.get_event_loop()

            self._state_attributes["desired"] = state_attributes["desired"]

            if self._state_attributes["desired"] == 0:
                self._state_attributes["current"] = 1

                loop.create_task(self._slow_off())  # noqa: RUF006
            else:
                self._state_attributes["current"] = 0

                loop.create_task(self._slow_on())  # noqa: RUF006

        except KeyError:
            # On exception try to return to a known good
            # state
            self._gpio.duty_u16(0)
            self._duty = 0

            self._state_attributes["desired"] = 0
            self._state_attributes["current"] = 0

    def get_state(self) -> dict[str, Union[str, int]]:
        return self._state_attributes

    def delete_state(self) -> None:
        self._gpio.duty_u16(0)
        self._gpio.freq(100)
        self._duty = 0

        self._state_attributes["desired"] = 0
        self._state_attributes["current"] = 0

    def update_state(
        self,
        state_attributes: dict[str, Union[str, int]],
    ) -> None:
        loop = asyncio.get_event_loop()

        if self._state_attributes["desired"] == 0:
            self._state_attributes["desired"] = 1
            self._state_attributes["current"] = 0

            loop.create_task(self._slow_on())  # noqa: RUF006
        else:
            self._state_attributes["desired"] = 0
            self._state_attributes["current"] = 1

            loop.create_task(self._slow_off())  # noqa: RUF006

urest.examples.simpleled.SimpleLED

Bases: APIBase

A minimal example of a ‘noun’ class, which inherits from urest.api.base.APIBase. It also requires the Pin library from MicroPython: but should be able to be adapted to other GPIO libraries which provide a similar interface.

For an example of an application which uses this library, see the example led_control. This class is also referred to the Creating a Network Server How-To.

API

The ‘noun’ exposes the following internal state, with allowed values as 0 or 1 for both keys.

JSON Key JSON Type Description
led Integer The current state of the controlled output

The class will attempt to set the GPIO output (assumed to be an LED, but any digital output should work) to the commanded value immediately. Values equivalent to 0 for led will result in the output being set off (‘low’): any other integer value will be interpreted as 1 and set the input on (‘high’).

Source code in urest/examples/simpleled.py
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
class SimpleLED(APIBase):
    """A minimal example of a 'noun' class, which inherits from
    `urest.api.base.APIBase`. It also requires the `Pin` library from
    MicroPython: but should be able to be adapted to other GPIO libraries which
    provide a similar interface.

    For an example of an application which uses this library, see the example
    [`led_control`](https://github.com/dlove24/urest/blob/trunk/examples/led_control.py).
    This class is also referred to the
    [Creating a Network Server][creating-a-network-server] How-To.

    API
    ---

    The 'noun' exposes the following internal state, with allowed values as `0`
    or `1` for both keys.

    | JSON Key  | JSON Type | Description                                                          |
    |-----------|-----------|----------------------------------------------------------------------|
    | `led`  | `Integer` | The _current_ state of the controlled output                            |


    The class will attempt to set the GPIO output (assumed to be an LED, but any
    digital output should work) to the commanded value immediately. Values
    equivalent to `0` for `led` will result in the output being set `off`
    ('`low`'): any other integer value will be interpreted as `1` and set the
    input `on` ('`high`').
    """

    def __init__(self, pin: Pin) -> None:
        self._gpio = Pin(pin, Pin.OUT)
        self._gpio.off()

        self._state_attributes = {"led": 0}

    def set_state(self, state_attributes: dict[str, Union[str, int]]) -> None:
        try:
            self._state_attributes["led"] = state_attributes["led"]

            if self._state_attributes["led"] == 0:
                self._gpio.off()
            else:
                self._gpio.on()

        except KeyError:
            # On exception try to return to a known good
            # state
            self._gpio.off()
            self._state_attributes["led"] = 0

    def get_state(self) -> dict[str, Union[str, int]]:
        return {"led": self._gpio.value()}

    def delete_state(self) -> None:
        self._gpio.off()
        self._state_attributes["led"] = 0

    def update_state(
        self,
        state_attributes: dict[str, Union[str, int]],
    ) -> None:
        if self._state_attributes["led"] == 0:
            self._gpio.on()
            self._state_attributes["led"] = 1
        else:
            self._gpio.off()
            self._state_attributes["led"] = 0