Saveris Push API
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.
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:
- Send a POST request to https://cognito-idp.eu-central-1.amazonaws.com/ with headers:
X-Amz-Target
:AWSCognitoIdentityProviderService.InitiateAuth
Content-Type
:application/x-amz-json-1.1
- Request body:
{ "AuthParameters" : { "USERNAME" : "example-user", "PASSWORD" : "your-password" }, "AuthFlow" : "USER_PASSWORD_AUTH", "ClientId" : "your-client-id" }
- 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 the following messages:
1. Alarm Message
This event type provides information about an alarm. The content type of the message is application/json
.
Alarms can be triggered by different conditions, such as exceeding a threshold, a device malfunction, or a sensor failure.
The AlarmType
field in the message indicates the type of alarm, which can be a MEASUREMENT_ALARM
or SENSOR_SYSTEM_ALARM
or DEVICE_SYSTEM_ALARM
.
Depended on the AlarmType
the AlarmSourceUuid
field will contain the UUID of a sensor or a device that triggered the alarm.
All alarm messages 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.
2. Measurement Message
This event type provides information about the measurements. The content type of the message is application/json
.
The measurement message 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.