The RestAPI delivers access for 3rd Party developers to integrate with the SENZ smart thermostats.
To gain access to the API, first you must contact our support department to get access to a ClientID.
Request access to the API
To make your life easier when getting started developing against the API, we have implemented Swagger as our Endpoint and Model documentation tool.
This allows you to explore and test the API right in your browser, with our included test client.
The RestAPI uses OAuth2 and OpenID-Connect as the Authorization methods to the API.
OAuth 2 is an authorization framework that enables a service to grant 3rd party applications access to obtain limited access to a users account via a HTTP service.
This protocol allows third-party applications to request limited access to an HTTP service, either on behalf of a resource owner or by allowing the third-party application to obtain access on its own behalf.
Access is requested by the client, which can be a website, desktop application or a mobile application.
Grants:
describes a number of grants (“methods”) for a client application to acquire an access token.
Scopes:
provide a way to limit the amount of access that is granted to an access token. It is a lists of identifiers used to specify what access privileges are being requested.
Claims:
are name/value pairs that contain information about a user. Profile claims are included that ensures that the request for the user’s info was made using a token that was obtained with the profile scope.
ReturnURL:
parameter that informs login page where the user should be redirected once login is complete
Tokens are issued from the OAuth2 server to provide access to the system. Three types exist:
The implicit grant type is optimized for browser-based applications. Either for user authentication-only (both server-side and JavaScript applications), or authentication and access token requests (JavaScript applications).
In the implicit flow, all tokens are transmitted via the browser, and advanced features like refresh tokens are thus not allowed.
openid
restapi
Authorization code flow was originally specified by OAuth 2, and provides a way to retrieve tokens on a back-channel as opposed to the browser front-channel. It also support client authentication.
While this grant type is supported on its own, it is generally recommended you combine that with identity tokens which turns it into the so called hybrid flow. Hybrid flow gives you important extra features like signed protocol responses.
offline_access
- [Optionally required] Add this if you need to get a refresh tokenopenid
restapi
Hybrid flow is a combination of the implicit and authorization code flow - it uses combinations of multiple grant types, most typically code id_token
.
In hybrid flow the identity token is transmitted via the browser channel and contains the signed protocol response along with signatures for other artifacts like the authorization code. This mitigates a number of attacks that apply to the browser channel. After successful validation of the response, the back-channel is used to retrieve the access and refresh token.
This is the recommended flow for native applications that want to retrieve access tokens (and possibly refresh tokens as well) and is used for server-side web applications and native desktop/mobile applications.
offline_access
- [Optionally required] Add this if you need to get a refresh tokenopenid
restapi
Authorization Code | Implicit | Hybrid | |
---|---|---|---|
All tokens returned from authorization endpoint | |||
All tokens returned from token endpoint | |||
Tokens sent via user agent | |||
Client can be authenticated (e.g. using client secret) | |||
Can use refresh tokens | |||
Communication in one round trip | |||
Most communication server-to-server |
Flow | Response Types |
---|---|
Authorization Code | code |
Implicit | id_token |
Implicit | id_token token |
Hybrid | code id_token |
Hybrid | code token |
Hybrid | code id_token token |
Tables adapted from OpenID Connect 1.0 Core Specification.
As a 3rd party developer, you have two (2) webservices to talk to.
Here is an example scenario showing the communication between the components
Once the integrator has an access token it is possible to access the Rest API endpoints and to make changes and get information from the protected resource.
Here is an example scenario showing the communication between the components
Once the integrator has an access token it is possible to access the Rest API endpoints and to make changes and get information from the protected resource.
The RestAPI only supports json data. That means that it sends and receives the mimetype application/json
.
200
- Success
204
- No Content
400
- Bad Request
401
- Unauthorized
403
- Forbidden
404
- Not found
500
- Internal server error
The error codes are described more detailed in the error handling section.
All endpoints can send one of the following errors:
400
- Bad Request
401
- Unauthorized
403
- Forbidden
404
- Not found
429
- Too many requests
500
- Internal server error
The following status codes [400
, 500
] will return a json response.
{ "messages": [ "Description of the error that occured.", "Also this occured" ] }
The SENZ RestAPI supports multiple versions, which means that you as a developer, do not have to worry about
the release of new features or breaking changes.
New features added in the same API version will always be backwards
compatible and any breaking changes will be in a new major version of the api.
First version contains
GET
list for Thermostat model
GET
single for Thermostat and Account models
PUT
for Mode models
Burst and rate limits have been implemented to avoid overuse by 3rd party applications. This has been implemented to stop a single client from being able to reduce the quality of service for the rest of the clients, whether it is by accident or on purpose.
These limits are defined as a per user and per client basis. Which means a single user combined with a specific client id, has these specific limits. That means if a client ids user hits the limit, he is blocked out for the rest of that period, but it does not affect the other users of that client id.
Per second | 50 |
---|---|
Per 30 minutes | 750 |
Per 12 hours | 20,000 |
Per 7 days | 250,000 |
When the rate limits are hit, the RestAPI will return HttpCode 429 - Too many requests
.
If you hit the limit the response will be:
Status Code: 429 Retry-After: 58 Content: API calls quota exceeded! maximum admitted 2 per 1m.
If the request doesn't get rate limited then the longest period defined in the rules is used to compose the X-Rate-Limit headers, these headers are injected in the response:
X-Rate-Limit-Limit: the rate limit period (eg. 1m, 12h, 1d) X-Rate-Limit-Remaining: number of request remaining X-Rate-Limit-Reset: UTC date time (ISO 8601) when the limits resets
A client can parse the X-Rate-Limit-Reset like this:
DateTime resetDate = DateTime.ParseExact(resetHeader, "o", DateTimeFormatInfo.InvariantInfo);
A thermostat has three operating modes; Auto, Manual and Hold. In Auto mode, the thermostat uses its schedule to adjust the temperature during the day.
When the thermostat is set to Operating Mode Manual, the thermostats schedule is ignored. Hold mode sets thermostat to specific temperature for period of time,
specified in HoldUntil field.
The Thermostat Updates from the API when in Manual mode function as usual.
In this section we will cover how to use the different modes, and what input you should provide to get the expected result.
ModeAuto
the thermostat will use the schedule to define the temperature it should be set to.
{ "serialNumber": "00000000", }
ModeHold
, you temporarily set the temperature so it doesn't follow the current schedule.
Temperature
or it will try to use the last used value it had in this mode.
If the HoldUntil
is not supplied, it will temperature for next 2 hours. Once the HoldSetPointDateTime
expires it will
return to ModeAuto
.
HoldUntil
with a specific date and time, as long as it is within the following 23 hours.
{ "serialNumber": "00000000", "temperature": 10, "holdUntil": "2019-07-02T09:12:48.119Z", "temperatureType": 0 }
ModeAuto
on its own.
Temperature
to set the thermostats target temperature.
{ "serialNumber": "00000000", "temperature": 15, "temperatureType": 0 }
The required date and time format is ISO-8601 and its based on UTC. All timestamps must be in 24-hour format.
yyyy-MM-ddTHH:mm:ssZDate
yyyy-MM-ddTime
HH:mm
Date and time: 2018-09-14T07:58:51Z 2018-09-14T09:58:51+02:00 Date: 2017-12-18 2018-09-14 Time: 11:05 23:30
Change notifications functionality was implemented utilizing SignalR technologgery and should be used for minimising an amount of requests to RestAPI. Currently we support following notification types:
typeid | type | id |
---|---|---|
1 | UserAccount | UserAccount id |
2 | Thermostat | Thermostat Serialnumber |
Client side for handling notifications is technology agnostic, and some basic examples for creating SignalR clients can be found here.
Also need to remember that for authorization it is using OAuth2 and OpenID-Connect in the same way as RestAPI.
[{"type":1,"id":"34","timeStamp":"2018-10-04T11:51:27Z"}, {"type":2,"id":"223414","timeStamp":"2018-10-04T11:52:27Z"}]
The following plain JS/jQuery code sample shows how to create a simple SignalR client, establish connection and subscribe/unsubscripted for certain notification types. And for authorization it uses oidc-client-js library.
var oidcConfig = { authority: authorityUrl, // OAuth2 authority url. client_id: "js", // client id that will be used for authorization. redirect_uri: redirectUrl, //Redirect url, should be matched with one defined for the specified client. response_type: "id_token token", //Authorization response type scope: "openid profile restapi", //Authorization scope automaticSilentRenew: true, filterProtocolClaims: true, nonce: "N" + Math.random() + "" + Date.now(), loadUserInfo: true }; var manager = new Oidc.UserManager(oidcConfig) var token; manager.signinRedirectCallback().then(function (user) { console.logger("signed in", user); token = user.access_token; }).catch(function (err) { console.logger(err); }); //Builds SignalR connection. SignalR connection hub is called - "changenotifications". //So full connection url should always be "baseUrl/changenotifications". var connection = new signalR.HubConnectionBuilder().withUrl(baseUrl/changenotifications+ "?token=" + token).build(); //Start new connection. connection.start().then(() => { console.logger('Connection started!'); }) .catch(err => console.error(err.toString())); }); //Close connection. //If notification tracking is not needed anymore then connection should be closed, and it's safe to close connection without unsubscribing from what is currently tracking. connection.stop() .then(() => { console.logger('Connection stopped!'); }) .catch(err => console.error(err.toString())); }); //Subscribe for notifications after connections starts. var notificationTypes = ["1","2"] //Subscribe for all kind of notifications. connection .invoke("Subscribe", notificationTypes).then(() => { console.logger('Subscribed for notifications'); }) .catch(err => console.error(err.toString())); }); //Unsubscribe from notifications. Please remember that amount of "unsubscribe" calls should be equal to amount of "subscribe" calls connection .invoke("Unsubscribe", notificationTypes).then(() => { console.logger('Unsubscribed from notifications'); }) .catch(err => console.error(err.toString())); }); //Handle notifications. The only callback that can be invoked on the client side is called - "Notify". connection.on("Notify", (value) => { traceNotification(value); }); function traceNotification(notificationList) { notificationList.forEach(function (notification) { var notificationType = ""; switch (notification.type) { case 1: notificationType = "UserAccount"; break; case 2: notificationType = "Thermostat"; break; } var traceMessage = notificationType + ' notification for item ' + notification.id + " at " + notification.timeStamp; console.logger(traceMessage); }); }
The following Python code sample shows how to subscribe to updates using signalrcore package.
import time import requests import json import logging from signalrcore.hub_connection_builder import HubConnectionBuilder from urllib.parse import urljoin from getpass import getpass def listen_for_updates(): base_identity_url = input('Base Identity URL: ') base_api_url = input('Base API URL: ') username = input('Username: ') password = getpass('Password: ') client_id = input('Client ID: ') client_secret = getpass('Client Secret: ') token_url = urljoin(base_identity_url, '/connect/token') notifications_url = urljoin(base_api_url, '/v1/changenotifications') def get_access_token(): # this is an example for "password" grant type which doesn't require callback URL # for other types please refer to https://developer.byu.edu/docs/consume-api/use-api/oauth-20/oauth-20-python-sample-code data = {'grant_type': 'password', 'username': username, 'password': password} access_token_response = requests.post( token_url, data=data, verify=False, allow_redirects=False, auth=(client_id, client_secret)) payload = json.loads(access_token_response.text) return payload['access_token'] hub_connection = HubConnectionBuilder() \ .with_url(notifications_url, options={ 'access_token_factory': lambda: get_access_token() }) \ .configure_logging(logging.DEBUG) \ .with_automatic_reconnect({ "type": "raw", "keep_alive_interval": 10, "reconnect_interval": 5, "max_attempts": 5 }).build() hub_connection.start() def on_open(): print("connection opened and handshake received ready to send messages") notification_types = [1, 2] hub_connection.send("Subscribe", [notification_types]) def on_close(): print("connection closed") def on_notify(args): notifications = args[0] for notification in notifications: handle_notification(notification) def handle_notification(notification): nt = notification['type'] ts = notification['timeStamp'] target_id = notification['id'] if nt == 1: print("{ts}: Account {acc_id} settings changed.".format(ts=ts, acc_id=target_id)) if nt == 2: print("{ts}: Thermostat SN#{serial} settings changed.".format(ts=ts, serial=target_id)) hub_connection.on_open(on_open) hub_connection.on_close(on_close) hub_connection.on("Notify", on_notify) message = None while message != "exit()": message = input(">> ") hub_connection.send("Unsubscribe", [[1, 2]]) hub_connection.stop() print("Wait for shutdown...") time.sleep(1)