Get Started


Welcome to the RestAPI documentation

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

Swagger documentation

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.

Go to swagger now!

OAuth2


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.

Discovery document

As part of the OpenID Connect standard, a discovery document is available to get an overview of the various endpoints and configurations for the SENZ Identity(OAuth2) Server.

OpenID Discovery document

OAuth2 concepts:

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:

Tokens are issued from the OAuth2 server to provide access to the system. Three types exist:

  • id_token: identity information about the user is encoded into the token.
  • access_token: used as bearer tokens, meaning that the bearer can access authorized resources without further identification.
    • Expiration: 1 hour
    • These tokens usually have a short lifespan (dictated by its expiration) for improved security. When the access token expires, the user must authenticate again to get a new access token limiting the exposure of the fact that it’s a bearer token. They last about an hour and cannot be revoked, once issued.
  • refresh_token: used to obtain new access tokens. These will be long-lived while access tokens are short-lived. This allows for long-lived sessions that can be killed if necessary.
    • Expiration: 15 days
    • Refresh tokens extend the connection to the users account, while still allowing revokeability. This allows an authorized application to get a new access token without the users interaction. If the user has revoked access to the 3rd party application, the refresh token is no longer valid. Refresh tokens last about 15-30 days.

Grant types/flows

The SENZ RestAPI allows the following grant types:

Implicit

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.

Requirements:
  • ClientID
  • Scopes
    • openid
    • restapi

Authorization Code

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.

Requirements:
  • ClientID
  • ClientSecret
  • Scopes
    • offline_access - [Optionally required] Add this if you need to get a refresh token
    • openid
    • restapi

Hybrid

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.

Requirements:
  • ClientID
  • ClientSecret - In the backchannel request
  • Scopes
    • offline_access - [Optionally required] Add this if you need to get a refresh token
    • openid
    • restapi

Which grant type / grant flow should I choose?

Flow Features

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

Response Types by Flow

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.

Scopes

RestAPI (restapi)

The RestAPI Scope, allows access to the RestAPI on behalf of the authenticated user.

OpenID (openid)

Required by the OpenID standard.

Profile (profile)

Optional - It is possible to call the OpenID Profile endpoint, and get a few claims about the user. We advise to use the RestAPI’s GetAccount endpoint instead.

Offline Access (offline_access)

Required if you want to receive a refresh token.

Claims

The SENZ RestAPI doesn't include many claims in the access token and id token, and the access token itself is more or less only relevant as a bearer token to be sent to the API.

  • Subject (sub)
    • Contains the "subject" of the access_token, in our case it contains the unique userId of the current user.
  • Not before (nbf)
    • Defines that the token is not valid before this time (seconds since Unix Epoch)
  • Expires (exp)
    • Defines that the token is valid until this time (seconds since Unix Epoch)
  • Issuer (who created and signed the token) (iss)
    • Defines who issed the token.
  • Audience (who or what the token is intended for) (aud)
    • List of audiences, will usually contain the API resource eg. restapi.
  • ClientID (client_id)
    • The client id the token is issued to.
  • Scope (scope)
    • List of scopes that the token is granted access to.

Web services


As a 3rd party developer, you have two (2) webservices to talk to.

Block diagram of Architecture

Webservices structure

Authentication Code Scenario


Here is an example scenario showing the communication between the components

      Initial Setup
    • During the inital part of the process, the user connects to the integrators service.
    • The integrator's service then connects with the identity service to get authenticated.
    • The end user then logs in with their credentials
    • Then if successful, the integrator website/service then receives an authentication code on its redirecturl
    • This authentication code can then be exchanged for an access token.

      Use

      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.


      Refresh Tokens
    • When Access tokens are received the Integrator also gets some information on when the token willl expire.
    • When getting the orginial Access token, in the same call, the integrator also received a refresh token.
    • A refresh token also has an expiry time, but it is generally much longer, e.g. 14 days from issue.
    • The integrator can exchange the refresh token for a new access token and new refresh token.
    • Using this method the integrator can gain indefinite access to the protected resource.

Implicit Grant Scenario


Here is an example scenario showing the communication between the components

      Initial Setup
    • During the inital part of the process, the user connects to the integrators service.
    • The integrator's service then connects with the identity service to get authenticated.
    • The end user then logs in with their credentials
    • Then if successful, the integrator website/service receives an access token on its redirecturl

      Use

      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.


Http return codes


The RestAPI only supports json data. That means that it sends and receives the mimetype application/json.

Return codes

Success responses
  • 200 - Success
    • Specifies that the request was a success, and that a there is data in the response body.
  • 204 - No Content
    • Specifies that the request was a success, but there is no data sent back in the response body.
    • This is to save bandwidth.
Error responses
  • 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.

Error handling


Error codes

All endpoints can send one of the following errors:

  • 400 - Bad Request
    • If what the client sent could not be validated.
  • 401 - Unauthorized
    • If the client is not authorized to call this method, eg. not logged in.
  • 403 - Forbidden
    • If the client doesn't have the scope for this call.
  • 404 - Not found
    • If a resource is being looked up that doesn't exist, or an endpoint that doesn't exists is being called.
  • 429 - Too many requests
    • If you are hitting the burst and rate limits, you will receive this error code, see Burst and rate limits section.
  • 500 - Internal server error
    • If an unexpected error occured.

The following status codes [400, 500] will return a json response.

Error example

{
    "messages": [
        "Description of the error that occured.",
        "Also this occured"
    ]
}

Versioning


Multiple Version

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.


Version 1

First version contains GET list for Thermostat model GET single for Thermostat and Account models PUT for Mode models

Burst and Rate limits


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.

Burst and rate limits definition

  • Rate limits
    • The overall "reasonable" allowed rate of calls to the API over a longer period. Usually defined in days and months. Eg. 250.000 in a month.
  • Burst limits
    • The limit of allowed request in a short timeframe, usually defined in seconds or minutes.


Visual representation of burst and rate limits

Current limits

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

Http Responses:

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);

Validation


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.

Thermostat Modes

ModeAuto

When using ModeAuto the thermostat will use the schedule to define the temperature it should be set to.

Example:
        {
        "serialNumber": "00000000",
        }
    

ModeHold

When using ModeHold, you temporarily set the temperature so it doesn't follow the current schedule.
In this mode you have to set the 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.
This functionality can be overwritten by supplying the HoldUntil with a specific date and time, as long as it is within the following 23 hours.
Example:
        {
        "serialNumber": "00000000",
        "temperature": 10,
        "holdUntil": "2019-07-02T09:12:48.119Z",
        "temperatureType": 0
        }
    

ModeManual

In this mode the temperature is held indefinitely, the thermostat will not revert back to ModeAuto on its own.
you can specify the Temperature to set the thermostats target temperature.
Example:
        {
        "serialNumber": "00000000",
        "temperature": 15,
        "temperatureType": 0
        }
    

DateTime format

The required date and time format is ISO-8601 and its based on UTC. All timestamps must be in 24-hour format.

Formats

Date and time
yyyy-MM-ddTHH:mm:ssZ
Date
yyyy-MM-dd
Time
HH:mm

Example:
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


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.

Notification data example

    [{"type":1,"id":"34","timeStamp":"2018-10-04T11:51:27Z"}, {"type":2,"id":"223414","timeStamp":"2018-10-04T11:52:27Z"}]

Code examples

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)