Skip to content

Saveris Push API V1.1

Click on the following link to open the Push API.

The Push API is a WebSocket-based service that allows users to subscribe to alarm and measurements messages. These messages are buffered in a queue when users are disconnected from the server and are retrieved once reconnection occurs.

This documentation will guide you on how to use this API and subscribe to the alarm messages.

API Details

  • AsyncAPI Version: 2.6.0
  • API Version: 1.1.0
  • License: Apache 2.0

Server URL

The URL for this service is: wss://tds-real-time-api.REGION.ENV.savr.saveris.net/web-socket

The REGION and ENV in the URL should be replaced with the specific region and environment you are using. There are two environments available: "i" for integration and testing, and "p" for production. Furthermore we have three regions: "eu" for europe, "am" for america and "ap" for the apac region.

Security

The Push API uses Bearer token authentication. You should acquire a token and use it to authenticate your requests.

To get a token you need to authenticate using a token endpoint. Follow these steps to authenticate and obtain an ID token:

  1. Send a POST request to https://cognito-idp.eu-central-1.amazonaws.com/ with headers:
  2. X-Amz-Target: AWSCognitoIdentityProviderService.InitiateAuth
  3. Content-Type: application/x-amz-json-1.1
  4. Request body:
    {
       "AuthParameters" : {
          "USERNAME" : "example-user",
          "PASSWORD" : "your-password"
       },
       "AuthFlow" : "USER_PASSWORD_AUTH",
       "ClientId" : "your-client-id"
    }
    
  5. The response will contain a JSON object with an IdToken field. This token is valid for 24 hours and must be used for authentication in the following steps.

Another Approach is to get the token from the /token endpoint of the Data API. Here you will just need to send your username and password in the body and you will receive the idToken. The documentation for the Data API is to be found here: Data API

Subscription

You can subscribe to the alarm messages by subscribing to the channel /queue/<username>/alarms. The username should be replaced with the username you used while acquiring the token.

A sample javascript client showing how to subscribe to the alarm channel:

import {RxStomp, StompHeaders} from "@stomp/rx-stomp";
import { WebSocket } from 'ws';

Object.assign(global, { WebSocket});

const token = ""; // replace with your token

const rxStomp = new RxStomp();

rxStomp.configure({
    brokerURL: 'wss://tds-real-time-api.eu.i.savr.saveris.net/web-socket',
    debug: console.log.bind(console),
    connectHeaders: {Authorization: `Bearer ${token}`}
});

rxStomp.activate();

let subHeaders = new StompHeaders();

const subscription = rxStomp.watch({ 
    destination: "/queue/<username>/alarms", 
    subHeaders 
}).subscribe((message) => {console.log(message.body)});

A sample spring boot client showing how to subscribe to the alarm channel:

How to run the spring boot client? Take Intellij as an example:

  • Import the project to Intellij. Dependencies from build.gradle should be automatically resolved when importing.
  • Create a spring boot config with JDK 17 in Run/Debug Configurations and set the main class to com.example.demo.WebsocketApplication, which is our app annotated with @SpringBootApplication.
  • The following info can be set as environment variables or added in the application.yml file for authentication against the API:
    • TESTO_COGNITO_CLIENT_NAME // your username
    • TESTO_COGNITO_CLIENT_SECRET // password
    • TESTO_COGNITO_CLIENT_ID // each region and env has its own client id, e.g. "2r2u2bl029vu8pk65hanr238dl" for europe integration
  • Build and run the application. With the config above, you should be able to connect to the websocket api and see the payloads from the logs

    INFO [demo,,] 30104 --- [ient-SecureIO-2] c.e.d.w.StompAlarmSessionHandler: handle frame:
    {"uuid":"11111111-xxxx-xxxx-xxxx-111111111111","tenant_uuid":"11111111-xxxx-xxxx-xxxx-111111111111","alarm_reason":"Alarm condition is violated","alarm_status":"Alarm","last_status_change_time":"2023-08-01T11:20:00Z","alarm_condition_type":"Upper limit","alarm_severity":"Alarm","alarm_time":"2023-08-01T11:20:00Z","alarm_time_local":"2023-08-01T13:20:00+02:00","alarm_value":"25.2","physical_unit":"°C","physical_value":"Temperature","physical_value_extension":"Air Temperature","alarm_source_uuid":"11111111-xxxx-xxxx-xxxx-111111111111","alarm_type":"measurement alarm","processed_at":"2023-08-01T11:21:25Z"}
    
  • Switch the topic to "/queue/{username}/measurements" in StompSessionHandler to receive the messages from our measurements channel

The code block below, from the class named StompAlarmSessionHandler, is a key part of the example client. It is designed to handle WebSocket connections and subscribe to a specific topic to receive messages. StompAlarmSessionHandler serves as a Spring Boot WebSocket client, and it communicates with a WebSocket server. When the application starts (triggered by an ApplicationReadyEvent), it establishes a WebSocket connection, passing an access token for authentication. Once connected, the client subscribes to the ALARM_TOPIC using a specific username. This subscription allows the client to receive alarm messages intended for that user. The class includes various callback methods to manage WebSocket events, such as a successful connection, exceptions, transport errors, and incoming messages. If a transport error occurs, the client attempts to reestablish the connection after a brief delay.

package com.example.demo.websocket;

import com.example.demo.authentication.AuthTokenService;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompConversionException;
import org.springframework.messaging.simp.stomp.StompHeaders;
import org.springframework.messaging.simp.stomp.StompSession;
import org.springframework.messaging.simp.stomp.StompSessionHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHttpHeaders;
import org.springframework.web.socket.messaging.WebSocketStompClient;
import org.springframework.web.util.UriComponentsBuilder;

import java.lang.reflect.Type;
import java.nio.channels.UnresolvedAddressException;
import java.util.concurrent.TimeUnit;

@Component
public class StompAlarmSessionHandler implements StompSessionHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger(StompAlarmSessionHandler.class);

    private static final String ALARM_TOPIC = "/queue/{username}/alarms";
    private static final long WAIT_SECONDS_UNTIL_RETRY = 5;

    private final String username;
    private final String webSocketBaseUrl;
    private final WebSocketStompClient webSocketStompClient;
    private final AuthTokenService authTokenService;


    public StompAlarmSessionHandler(@Value("${spring.security.oauth2.client.registration.cognito.client-name}") String username,
                                    @Value("${testo.ws.url}") String webSocketBaseUrl,
                                    WebSocketStompClient webSocketStompClient,
                                    AuthTokenService authTokenService) {
        this.username = username;
        this.webSocketStompClient = webSocketStompClient;
        this.webSocketBaseUrl = webSocketBaseUrl;
        this.authTokenService = authTokenService;
    }

    @EventListener(classes = {ApplicationReadyEvent.class})
    public void handleApplicationReadyEvent() {
        LOGGER.info("Connect on ready event");
        connectWebSocket();
    }

    @Override
    public void afterConnected(StompSession session, StompHeaders connectedHeaders) {
        String alarmTopic = UriComponentsBuilder.fromUriString(ALARM_TOPIC).build(username).toString();
        session.subscribe(UriComponentsBuilder.fromUriString(alarmTopic).build(username).toString(), this); // TODO PP uri builder twice?
        LOGGER.info("Stomp alarm session subscribed to topic: {}", alarmTopic);
    }

    @Override
    public void handleException(StompSession session, StompCommand command, StompHeaders headers, byte[] payload, Throwable exception) {
        LOGGER.error("handle exception, session: {}, command: {}, headers: {}, payload: {}", session, command, headers, payload, exception);
    }

    @Override
    public void handleTransportError(StompSession session, Throwable exception) {
        LOGGER.warn("handle transport error for sessionID: {} connected: {}", session.getSessionId(), session.isConnected());
        if (exception instanceof StompConversionException) {
            LOGGER.error("Stomp topic not reachable: {}", ALARM_TOPIC, exception);
        }
        if (!session.isConnected()) {
            reestablishConnection();
        } else {
            LOGGER.error("Unknown Exception {}", exception.getMessage(), exception);
        }
    }

    @Override
    public Type getPayloadType(StompHeaders headers) {
        return String.class;
    }

    @Override
    public void handleFrame(StompHeaders headers, Object payload) {
        LOGGER.info("handle frame: {}", payload);
    }

    private void connectWebSocket() {
        webSocketStompClient.connectAsync(webSocketBaseUrl, new WebSocketHttpHeaders(), stompHeaders(authTokenService.getAccessToken()), this);
    }

    private StompHeaders stompHeaders(String token) {
        StompHeaders stompHeaders = new StompHeaders();
        stompHeaders.set("Authorization", String.format("Bearer %s", token));
        return stompHeaders;
    }

    private void reestablishConnection() {
        try {
            TimeUnit.SECONDS.sleep(WAIT_SECONDS_UNTIL_RETRY);
        } catch (InterruptedException e) {
            LOGGER.error("Thread sleep error", e);
            Thread.currentThread().interrupt();
        }
        try {
            LOGGER.info("Reconnect");
            connectWebSocket();
            LOGGER.info("Connected");
        } catch (Exception e) {
            if (e.getCause() instanceof UnresolvedAddressException) {
                LOGGER.error("Failed to reconnect");
            } else {
                LOGGER.error("Exception", e);
            }
        }
    }

}

Messages

The API provides two types of alarm events and one type of measurements:

  1. Measurement Alarm: This event type provides information about a specific measurement alarm. The content type of the message is application/json.

  2. System Alarm: This event type provides information about a device/sensor system alarm. The content type of the message is application/json.

Each event type carries a payload of alarm information, including details such as the unique identifier of the alarm, tenant, alarm reason, status, condition type, severity, alarm time, and more.

  1. Measurements: This event type provides information about the measurements. The content type of the message is application/json.

The event type carries a payload of measurement information, including details such as the unique identifier of the measurement, device, sensor, tenant, measurement, physical_property_name, physical_unit, measurement time, and more.

Please refer to the API's AsyncAPI document for the detailed structure of the messages.