Skip to content

Master Slave Operation 1.0

Calin Crisan edited this page Apr 15, 2020 · 16 revisions

Masters And Slaves

A qToggle device may act as a master for other slave qToggle devices. The master controls these slave devices and allows accessing them through its own API functions. A slave device can act as a master for other devices. Complex chained master-slave configurations can be obtained in this way.

Special API functions are supported by the master for listing, adding and removing slaves.

Slave devices are identified on the master by their names. The master must be prepared for a slave to change its name at any time.

Authentication of all API requests from master to slaves must be done by supplying tokens computed using the device's admin username and its corresponding password.

Master Device Constraints

Master devices must expose the "master" flag via the flags device attribute.

Master devices must support real date/time.

HTTP Status Codes

When the master calls API functions on a slave, the following status codes must be used in case of communication errors:

  • 502 Bad Gateway

    This status is used to indicate an invalid response from the slave device. The following values are defined for the error field:

    • invalid device - the device doesn't (properly) implement the qToggle protocol
    • connection refused - connection refused on specified port
    • unreachable - the host/network is unreachable or any other network-related error
    • other error: <error> - any other unexpected error
  • 503 Service Unavailable

    This status is used to indicate that the device is currently offline. The error field value must be device offline (see Offline Devices).

  • 504 Gateway Timeout

    This status is used to indicate a timeout when communicating with the slave device. The error field value must be device timeout. The timeout used when waiting for the slave device's response is up to the implementation.

API Call Forwarding

All API requests made to master at locations starting with /devices/{name}/forward, with some exceptions listed below, must be synchronously forwarded to the device named name, to the corresponding API function path. In fact, the master should act as an HTTP proxy for the slave devices. For example, GET /device/mydev1/forward/device on master should be equivalent to a call to GET /device on the slave named mydev1.

For offline devices, requests to /devices/{name}/forward/device, /devices/{name}/forward/webhooks and /devices/{name}/forward/reverse must be intercepted by the master. In these cases, when receiving GET requests to these paths, the master must use cached values to provide a response to the client, if all cached field values are available. Upon receiving a PATCH request to these paths, the master must update the cached values accordingly and mark the changes for Offline Devices Provisioning.

Requests to unknown (unregistered) slave devices must be responded with a 404 Not Found and an error field set to no such device.

Slave Ports

All ports from slave devices must be exposed directly by the master. Slave port identifiers exposed by thr master must have the slave device name prepended to them, followed by a dot. For example, port gpio1 on slave device mydev1 would be exposed by the master under the id mydev1.gpio1. From the API point of view, the port should behave just like any regular port attached directly to the master.

After adding a slave to the master, all mechanisms on master must take the slave and its ports into account.

The master must keep a cache of all attributes of slave ports, including their values, so that they are available even when a slave device is offline.

Attributes

Slave port attributes must be made available on master. They must be kept synchronized between the master and the slave. The following attributes must however be treated separately:

id

The id attribute must be exposed by the master as indicated above.

tag

The tag attribute should never be synchronized with the slave. A master-only value should be kept for this attribute.

expression

The expression attribute must be handled separately, on master, as described in Expressions.

online

The online attribute must be exposed by master and must take into account the online status of the corresponding slave device. The value of the port attribute, as exposed by the master, must be:

  • false for:

    • a disabled port
    • a port whose value is expired
    • a port whose value never expires but belongs to an offline slave device
  • the value reported by the slave port itself, or true if the port itself does not expose the online attribute, in any other case

last_sync

The last_sync attribute must be exposed by master and represents the moment, expressed as seconds since Epoch, when the last value was received for the port; 0 should be used when this information is not available.

The master should not generate port-update events when last_sync changes, since the information can be deduced by the consumer from the value-change event.

This number attribute is not modifiable.

validity

The expires attribute must be exposed by master and represents the number of seconds after which a port value is considered expired. The special 0 value signifies that the port value never expires, and should be used as a default by the master.

The master must use last_sync in conjunction with expires to determine if a port value has expired or not. A port whose value has expired must have its online attribute set to false.

This number attribute is modifiable. Valid values go from 0 to 2147483647.

provisioning

The provisioning attribute must be exposed by master and represents the list of names of all port attributes (or value) that have been marked for provisioning (see Offline Devices Provisioning).

This list of string attribute is not modifiable.

Expressions

Setting a slave port-dependent value expression on a master port should work as expected, given that slave ports are treated by master as any other local port. Setting a master port-dependent value expression on a slave port must also be possible, but the expression itself must never be forwarded to the slave, but rather stored and processed on the master device, given that the slave has no knowledge of the master.

The master should keep track of both a master-side and a slave-side expression for each slave port that supports expressions. Both attributes could be set at the same time and they could (and most likely will) have different, unrelated expressions, one evaluated by the master and the other evaluated by the slave.

The slave-side expression attribute of the slave device, if present, must be exposed by master under the name device_expression. The master-side expression attribute of the slave device, if present, must be exposed by the master under the name expression.

Moreover, in order to support complex master-slave chainings, the master should prepend the string device_ to any additional slave attribute that starts with device_ and ends with _expression.

Transform expressions are transparent to the master-slave mechanism.

Sequences

The sequences must be forwarded and executed directly on the slave device. There is no way for the master to know about the state of the sequences running on its slaves, therefore the slave ports should be treated all the time by the master as executing no current sequence.

If however the port in question has a master expression, the request must be rejected with 400 Bad Request and an error field set to "port with expression".

Slave Events

There are three ways in which the master can be notified about slave events:

  • the listening mechanism
  • device polling
  • webhooks

Regardless of the way events reach the master, they must be treated as any other locally generated event, with the except of a slave generated device-update event, which must be translated into a slave-device-update event.

For slave devices that support listening, when the listen slave parameter is true, the master must use a listening client to get informed of the slave events. When the poll_interval slave parameter is greater than 0, the master must use a polling client to get informed of the slave events.

The master device must cache slave device and port attributes, so that these details are available even when a slave is offline.

Device Polling

When polling is enabled (poll_interval is greater than 0), the master must call GET /device on the slave at fixed intervals, indicated by the poll_interval slave parameter, to detect attribute changes.

Similarly, the master must then call GET /ports on the slave at fixed intervals, indicated by the poll_interval slave parameter, to detect port changes.

The master must then locally generate events corresponding to any additions, removals and changes detected after polling.

Webhooks

Slaves can be configured to send events to the master by leveraging the webhooks functionality. The master should not attempt to automatically configure the webhooks mechanism on its slaves. It must however expect events to be pushed by slaves via HTTP requests at POST /devices/{name}/events. These events must be treated locally just like any other slave event.

The master must however ignore events transmitted via webhooks by slave devices that are disabled. It must also ignore events transmitted via webhooks by slave devices that are registered with listening or polling mechanisms enabled.

For each slave device that supports webhooks, the master must keep a local cache with its webhooks parameters. The master may regularly poll the slave device to update the local cache with the new parameters.

At the end of the Offline Devices Provisioning procedure, in the absence of any webhooks provisioning parameters, the master must query devices that support webhooks and update its local cached parameters.

Reverse API Calls

For each slave device that supports reverse API calls, the master must keep a local cache with its reverse API calls parameters. The master may regularly poll the slave device to update the local cache with the new parameters.

At the end of the Offline Devices Provisioning procedure, in the absence of any reverse API calls provisioning parameters, the master must query devices that support reverse API calls and update its local cached parameters.

Disabled Devices

A slave device can be explicitly disabled and reenabled afterwards using the PATCH /devices/{name} API function. The enabled parameter of a slave device indicates whether the slave device is active on the master or not.

The internal mechanisms of the master should treat inactive slaves just as if they weren't present at all. No requests should be forwarded to disabled slaves; instead, the request should be responded with 404 Not Found and an error field set to "no such device". When it comes to ports, enabling and disabling a slave device should have the same effect as adding and removing it.

Offline Devices

The master should keep an internal online state for each of its registered slaves. It should continuously monitor its slaves and update the internal online state accordingly, triggering the corresponding events.

For slaves with listening enabled, the master must use the listening mechanism to detect offline devices: a slave device not responding correctly to a listen request within the established listen timeout (plus a guard interval left to the implementation) is considered offline.

For slaves with polling enabled, the master must use the Device Polling mechanism to detect offline devices: a slave device not responding correctly to a polling request within a timeout interval (left to the implementation) is considered offline.

Permanently Offline Devices

If both listening and polling are disabled, the device is considered permanently offline. Devices that are permanently offline usually employ the Webhooks mechanism to send events to the master, whenever they need to. Permanently offline devices must however be reachable at least when they are added to the master.

Offline Devices Provisioning

The master must implement a way to provision offline devices with new values for:

  • device attributes
  • webhooks parameters (for devices that implement webhooks)
  • reverse API calls parameters (for devices that implement reverse API calls)
  • port attributes
  • port values

Internal provisioning flags should be used to mark device/port attributes, port values or any other parameters that have been changed via master during the offline period.

As soon as the device gets back online, the master must immediately provision it with the new values, issuing corresponding requests to the device. For Permanently Offline Devices, as soon as the master receives a request to POST /devices/{name}/events from the offline slave, it can assume the device is reachable for a limited period of time; the master must then immediately provision it with the new values, issuing corresponding requests to the device with the updated values.

In case of slave API call errors, the master may employ a retry mechanism. The provisioning flags should be cleared when the master succeeds pushing the provisioning values to the slave, or upon reaching a certain number of retries.

When receiving events carrying new attribute values or port values from a slave, values associated locally with a provisioning flag that is set must be ignored by the master.

The provisioning field of a slave device contains the list of all device attributes that are marked for provisioning. In addition to the attributes, the presence of values "webhooks" and "reverse" indicate the provisioning status of webhooks and reverse API call parameters, respectively.

An additional provisioning attribute must be exposed by each slave port through master. The attribute is a list of strings containing the name of each port attribute that has been marked for provisioning. In addition to the attributes, the presence of "value" indicates the provisioning status of the port value.

Device Names

Slave devices are uniquely identified on master by their name. The master must not have two or more devices with the same name, at the same time. The master must also not have a slave having the same name as its own.

When adding a new slave, the master must first fetch its attributes and refuse the addition if the name conditions above are not met.

Upon receiving events from an existing slave, the master must check for name changes. If the name has been changed, it must update all internal references to the device's name (e.g. port identifiers, persisted data). If however, the new name is a duplicate, the slave must be disabled.

Slave Parameters

A slave device is defined on the master by the following parameters:

  • name - the device name (a reported by the slave device)
  • scheme determines the transport protocol used and can be either http or https; for devices that don't support SSL, the only accepted value should be http
  • host is the client's host (HTTP server in this case); this can be either an IP address or a DNS domain name
  • port - is the client's TCP port
  • path - the base path at which the qToggle API functions are available (without any trailing slash)
  • admin_password - the admin access level password admin (recommended to be kept as a sha256 hash)
  • poll_interval - the polling interval, in seconds (see Device Polling). Valid values range from 0 to 86400. 0 signifies that polling is disabled
  • listen - whether to enable the listening mechanism for this device or not

Event Types

slave-device-update

A slave-device-update event is triggered by the master whenever:

  • the slave parameters kept by the master are changed
  • the device online status changes as described in Offline Devices
  • the attributes of a slave device are updated

This event must however not be triggered when a slave device is renamed. Instead, the slave-device-remove event must be triggered for the device with the old name, followed by a slave-device-add event, with the new name.

The master should also avoid triggering this event when last_sync is updated.

This event has the same parameters as an element of the list returned by the GET /devices API function call.

This event type requires admin access level.

slave-device-add

A slave-device-add event is triggered by the master when a new slave device has been added using POST /devices. This event might be triggered by a master device at startup, when loading slaves; for slaves that are already present on a client, the client should handle slave-device-add events just like a slave-device-update event.

This event has the same parameters as an element of the list returned by the GET /devices API function call.

This event type requires admin access level.

slave-device-remove

A slave-device-remove event is triggered by the master when a slave device has been removed using DELETE /devices/{name}. This event has the following parameters:

{
    "name": string
}

This event type requires admin access level.

API Functions

All valid API requests made to URIs starting with /devices/{name}/forward must be forwarded synchronously to the slave device with the given name. The response must be relayed back to the client.

GET /devices

Returns the list of slave devices registered to this master device.

This API function requires admin access level.

Response has the following body:

[
    {
        "name": string,
        "enabled": boolean,
        "scheme": string,
        "host": string,
        "port": number,
        "path": string,
        "poll_interval": number,
        "listen_enabled": boolean,
        "online": boolean,
        "last_sync": number,
        "provisioning": string[]
        "attrs": {...}
     },
     ...
]

Each record in the list represents a slave device.

  • online represents the online state of the slave device (see Offline Devices)
  • last_sync represents the moment, expressed as seconds since Epoch, of the last successful communication with the slave device; if no communication has been done, this must be 0
  • provisioning - is a list of attribute names and other parameters that are marked for provisioning (see Offline Devices Provisioning)
  • attrs represents the cached slave device attributes, as returned by the GET /device API call

For the rest of the fields, see Parameters.

The response body may contain JSON References.

Status codes:

  • 200 OK - the API call was successful

Example:

Client's request:

GET /devices HTTP/1.1
Host: device.example.com
Connection: close
Cache-Control: no-cache
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c3IiOiJhZG1pbiIsImlhdCI6MTUxNjIzOTAyMn0.l2uO5g3viMWLQu2s7KJ0zZI5Cn-Cpk5i7am9vv2JcJ0

Device's response:

HTTP/1.1 200 OK
Connection: close
Cache-Control: no-cache
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c3IiOiJhZG1pbiIsImlhdCI6MTUxNjIzOTAyM30.If1_Cu-WRZ3qzICahiA5flU7o4lR1RGMhF9HYeBHpKM
Content-Type: application/json; charset=utf-8
Content-Length: n

[
    {
        "name": "kitchen_device",
        "enabled": true,
        "scheme": "http",
        "host": "192.168.1.4",
        "port": 80,
        "path": "/",
        "poll_interval": 0,
        "listen_enabled": true,
        "online": true,
        "last_sync": 1524599833,
        "provisioning": ["frequency", "webhooks"],
        "attrs": {
            "name": "kitchen_device",
            "description": "Kitchen Device",
            "version": "3.14.15f",
            "api_version": "1.0",
            "firmware": true,
            "listen": true,
            "webhooks": true,
            "reverse": false,
            "ssl": false,
            "frequency": "40",
            "sleep_timeout": "300",
            "definitions": {
                "frequency": {
                    "description": "The operating frequency of the device",
                    "type": "number",
                    "modifiable": true,
                    "unit": "MHz",
                    "choices": [
                        "20",
                        "40",
                        "80"
                    ]
                },
                "sleep_timeout": {
                    "description": "Interval of inactivity after which device enters sleep mode",
                    "type": "number",
                    "modifiable": true,
                    "unit": "seconds",
                    "min": 1,
                    "max": 3600,
                    "integer": true
                }
            }
        }
    },
    {
        "name": "garage_device",
        "enabled": true,
        "scheme": "http",
        "host": "192.168.1.9",
        "port": 8080,
        "path": "/qtoggle",
        "poll_interval": 3600,
        "listen_enabled": false,
        "online": false,
        "last_sync": 0,
        "provisioning": [],
        "attrs": {
            "name": "garage_device",
            "description": "Garage Device",
            "version": "3.14.16g",
            "api_version": "1.0",
            "firmware": true,
            "listen": true,
            "webhooks": false,
            "reverse": false,
            "ssl": false,
            "frequency": "80",
            "sleep_timeout": "300",
            "definitions": {
                "frequency": {
                    "description": "The operating frequency of the device",
                    "type": "number",
                    "modifiable": true,
                    "unit": "MHz",
                    "choices": [
                        "20",
                        "40",
                        "80"
                    ]
                },
                "sleep_timeout": {
                    "description": "Interval of inactivity after which device enters sleep mode",
                    "type": "number",
                    "modifiable": true,
                    "unit": "seconds",
                    "min": 1,
                    "max": 3600,
                    "integer": true
                }
            }
        }
    }
]

POST /devices

Adds a new slave device to this master device.

A request to GET /device must be issued by the master on the new slave device to test the connectivity and to retrieve the device attributes.

The slave device must be reachable when added to the master. The device must always be added as enabled.

This API function requires admin access level.

Request has the following body:

{
    "scheme": string,
    "host": string,
    "port": 8080,
    "path": string,
    "admin_password": string,
    "poll_interval": number,
    "listen_enabled": boolean
}

Fields poll_interval and listen_enabled are optional. If not supplied, the master must check the slave device flags attribute and, if slave device indicates listening support, the master must enable the listening mechanism; otherwise, it should enable polling with a default interval of choice.

For more details on the meaning of each field, see Parameters.

Response has the same body as one of the list elements in the response of the GET /devices API function call, representing the attributes of the newly added device.

The response body may contain JSON References.

Status codes:

  • 201 Created - the API call was successful
  • 400 Bad Request - error field values:
    • duplicate device - a device with this name or these parameters already exists
    • forbidden - authentication/authorization problems
    • no listen support - listen_enabled was set to true but device does not support listening
    • listening and polling - both listening and polling are enabled (poll_interval is greater than 0 and listen_enabled is true)
    • invalid field: poll_interval - poll_interval is not an integer between 0 and 86400

Example:

Client's request:

POST /devices HTTP/1.1
Host: device.example.com
Connection: close
Cache-Control: no-cache
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c3IiOiJhZG1pbiIsImlhdCI6MTUxNjIzOTAyMn0.l2uO5g3viMWLQu2s7KJ0zZI5Cn-Cpk5i7am9vv2JcJ0
Content-Type: application/json; charset=utf-8
Content-Length: n

{
    "scheme": "http",
    "host": "192.168.1.4",
    "port": 80,
    "path": "/",
    "admin_password": "test1234",
    "poll_interval": 60,
    "listen_enabled": false
}

Device's response:

HTTP/1.1 201 Created
Connection: close
Cache-Control: no-cache
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c3IiOiJhZG1pbiIsImlhdCI6MTUxNjIzOTAyM30.If1_Cu-WRZ3qzICahiA5flU7o4lR1RGMhF9HYeBHpKM
Content-Type: application/json; charset=utf-8
Content-Length: n

{
    "name": "kitchen_device",
    "enabled": true,
    "scheme": "http",
    "host": "192.168.1.4",
    "port": 80,
    "path": "/",
    "poll_interval": 0,
    "listen_enabled": true,
    "online": true,
    "last_sync": 1524599833,
    "attrs": {
        "name": "kitchen_device",
        "description": "Kitchen Device",
        "version": "3.14.15f",
        "api_version": "1.0",
        "firmware": true,
        "listen": true,
        "webhooks": false,
        "reverse": false,
        "ssl": false,
        "frequency": "40",
        "sleep_timeout": "300",
        "definitions": {
            "frequency": {
                "description": "The operating frequency of the device",
                "type": "number",
                "modifiable": true,
                "unit": "MHz",
                "choices": [
                    "20",
                    "40",
                    "80"
                ]
            },
            "sleep_timeout": {
                "description": "Interval of inactivity after which device enters sleep mode",
                "type": "number",
                "modifiable": true,
                "unit": "seconds",
                "min": 1,
                "max": 3600,
                "integer": true
            }
        }
    }
}

PATCH /devices/{name}

Updates a slave device on this master device.

This API function requires admin access level.

Request has the following body:

{
    "enabled": boolean,
    "poll_interval": number,
    "listen_enabled": boolean
}

All fields are optional. It is an error to have both polling and listen enabled.

For more details on the meaning of each field, see Parameters.

Response has no body.

Status codes:

  • 204 No Content - the API call was successful
  • 400 Bad Request - error field values:
    • invalid field: poll_interval - poll_interval is not an integer between 0 and 86400
    • no listen support - listen_enabled was set to true but device does not support listening
    • listening and polling - both listening and polling are enabled (poll_interval is greater than 0 and listen_enabled is true)
  • 404 Not Found - error field values:
    • no such device - a slave device with the given name is not present on this master

Example:

Client's request:

PATCH /devices/kitchen_device HTTP/1.1
Host: device.example.com
Connection: close
Cache-Control: no-cache
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c3IiOiJhZG1pbiIsImlhdCI6MTUxNjIzOTAyMn0.l2uO5g3viMWLQu2s7KJ0zZI5Cn-Cpk5i7am9vv2JcJ0
Content-Type: application/json; charset=utf-8
Content-Length: n

{
    "enabled": true
}

Device's response:

HTTP/1.1 204 No Content
Connection: close
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c3IiOiJhZG1pbiIsImlhdCI6MTUxNjIzOTAyM30.If1_Cu-WRZ3qzICahiA5flU7o4lR1RGMhF9HYeBHpKM

DELETE /devices/{name}

Removes a slave device from this master device.

This API function requires admin access level.

Path has the following arguments:

  • name specifies the name of the slave device to be removed

Request has no body.

Response has no body.

Status codes:

  • 204 No Content - the API call was successful
  • 404 Not Found - error field values:
    • no such device - a slave device with the given name is not present on this master

Example:

Client's request:

DELETE /devices/kitchen_device HTTP/1.1
Host: device.example.com
Connection: close
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c3IiOiJhZG1pbiIsImlhdCI6MTUxNjIzOTAyMn0.l2uO5g3viMWLQu2s7KJ0zZI5Cn-Cpk5i7am9vv2JcJ0

Device's response:

HTTP/1.1 204 No Content
Connection: close
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c3IiOiJhZG1pbiIsImlhdCI6MTUxNjIzOTAyM30.If1_Cu-WRZ3qzICahiA5flU7o4lR1RGMhF9HYeBHpKM

POST /devices/{name}/events

Receives a slave event and treats it locally. This API call is intended to be used with slave device webhooks.

Validation of the authentication token for this API function must be made by computing the signature using the slave's admin_password; the expected ori claim must be "device" and the usr claim must be ignored.

The response to this API call should not contain any authentication token.

Path has the following arguments:

  • name specifies the name of the slave device sending events

Request has the following body:

{
    "type": string,
    "params": any
}

Fields type and params represent the type and the parameters of the event.

The request body may contain JSON References.

Response has no body.

Status codes:

  • 204 No Content - the API call was successful
  • 400 Bad Request - error field values:
    • polling enabled - device has polling enabled
    • listening enabled - device has listening enabled
  • 404 Not Found - error field values:
    • no such device - a slave device with the given name is not present on this master or is disabled

Example:

Client's request:

POST /devices/kitchen_device/events HTTP/1.1
Host: device.example.com
Connection: close
Cache-Control: no-cache
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c3IiOiJhZG1pbiIsImlhdCI6MTUxNjIzOTAyMn0.l2uO5g3viMWLQu2s7KJ0zZI5Cn-Cpk5i7am9vv2JcJ0
Content-Type: application/json; charset=utf-8
Content-Length: n

{
    "type": "value-change",
    "params": {
        "id": "gpio0",
        "value": true
    }
}

Device's response:

HTTP/1.1 204 No Content
Connection: close
Clone this wiki locally