2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.mcd.internal.handler;
15 import static org.openhab.binding.mcd.internal.McdBindingConstants.*;
17 import java.nio.charset.StandardCharsets;
18 import java.text.SimpleDateFormat;
19 import java.util.Date;
20 import java.util.concurrent.TimeUnit;
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.eclipse.jetty.client.HttpClient;
25 import org.eclipse.jetty.client.api.Request;
26 import org.eclipse.jetty.client.api.Result;
27 import org.eclipse.jetty.client.util.BufferingResponseListener;
28 import org.eclipse.jetty.client.util.StringContentProvider;
29 import org.eclipse.jetty.http.HttpHeader;
30 import org.eclipse.jetty.http.HttpMethod;
31 import org.openhab.binding.mcd.internal.util.Callback;
32 import org.openhab.binding.mcd.internal.util.SensorEventDef;
33 import org.openhab.core.library.types.StringType;
34 import org.openhab.core.thing.Bridge;
35 import org.openhab.core.thing.ChannelUID;
36 import org.openhab.core.thing.Thing;
37 import org.openhab.core.thing.ThingStatus;
38 import org.openhab.core.thing.ThingStatusDetail;
39 import org.openhab.core.thing.binding.BaseThingHandler;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.types.RefreshType;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
45 import com.google.gson.Gson;
46 import com.google.gson.JsonArray;
47 import com.google.gson.JsonObject;
50 * Handler for the SensorThing of the MCD Binding.
52 * @author Simon Dengler - Initial contribution
55 public class SensorThingHandler extends BaseThingHandler {
57 private static final int REQUEST_TIMEOUT_MS = 10_000;
58 private final Logger logger = LoggerFactory.getLogger(SensorThingHandler.class);
60 private final HttpClient httpClient;
61 private final @Nullable Gson gson;
62 private @Nullable McdBridgeHandler mcdBridgeHandler;
63 private @Nullable String serialNumber = "";
64 private @Nullable SensorThingConfiguration config;
65 private int maxSensorEventId = 0;
66 private boolean initIsDone = false;
68 public SensorThingHandler(Thing thing, HttpClient httpClient) {
70 this.httpClient = httpClient;
75 public void initialize() {
76 config = getConfigAs(SensorThingConfiguration.class);
77 Bridge bridge = getBridge();
79 mcdBridgeHandler = (McdBridgeHandler) bridge.getHandler();
81 mcdBridgeHandler = null;
83 updateStatus(ThingStatus.UNKNOWN);
84 scheduler.execute(this::init);
88 public void handleCommand(ChannelUID channelUID, Command command) {
89 if (command instanceof RefreshType) {
90 refreshChannelValue();
91 } else if (mcdBridgeHandler != null) {
92 String channelId = channelUID.getId();
93 // check for the right channel id
94 if (channelId.equals(SEND_EVENT)) {
95 String commandString = command.toString();
96 int sensorEventId = SensorEventDef.getSensorEventId(commandString);
97 if (sensorEventId < 1 || sensorEventId > maxSensorEventId) {
98 // check, if an id is passed as number
100 sensorEventId = Integer.parseInt(commandString);
101 if (sensorEventId < 1 || sensorEventId > maxSensorEventId) {
102 logger.warn("Invalid Command!");
104 sendSensorEvent(serialNumber, sensorEventId);
106 } catch (Exception e) {
107 logger.warn("Invalid Command!");
110 // command was valid (and id is between 1 and max)
111 sendSensorEvent(serialNumber, sensorEventId);
114 logger.warn("Received command for unexpected channel!");
116 refreshChannelValue();
118 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Bridge is offline.");
122 // this is called from initialize()
123 private void init() {
124 SensorThingConfiguration localConfig = config;
125 if (localConfig != null) {
126 serialNumber = localConfig.getSerialNumber();
128 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Cannot access config data.");
130 McdBridgeHandler localMcdBridgeHandler = mcdBridgeHandler;
131 if (localMcdBridgeHandler != null) {
132 updateStatus(ThingStatus.ONLINE);
134 // build and register listener
135 localMcdBridgeHandler.register(() -> {
137 // determine, if thing is specified correctly and if it is online
138 fetchDeviceInfo(res -> {
140 JsonObject result = res.getAsJsonObject();
141 if (result.has("SerialNumber")) {
142 // check for serial number in MCD cloud
143 if (result.get("SerialNumber").isJsonNull()) {
144 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
145 "Serial number does not exist in MCD!");
147 // refresh channel values and set thing status to ONLINE
148 refreshChannelValue();
149 updateStatus(ThingStatus.ONLINE);
154 fetchEventDef(jsonElement -> {
155 if (jsonElement != null) {
156 JsonArray eventDefArray = jsonElement.getAsJsonArray();
157 maxSensorEventId = eventDefArray.size();
160 } catch (Exception e) {
161 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
167 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "unable to access bridge");
172 * This method uses the things serial number in order to obtain the latest
173 * sensor event, that was registered in the
174 * C&S MCD cloud, and then updates the channels with this latest value.
176 private void refreshChannelValue() {
179 * First, the device info for the given serial number is requested from the
180 * cloud, which is then used fetch
181 * the latest sensor event and update the channels.
183 fetchDeviceInfo(deviceInfo -> {
184 // build request URI String
185 String requestUrl = getUrlStringFromDeviceInfo((JsonObject) deviceInfo);
187 if (requestUrl != null) {
188 // get latest sensor event
189 fetchLatestValue(requestUrl, result -> {
190 JsonObject latestValue = getLatestValueFromJsonArray((JsonArray) result);
192 updateChannels(latestValue);
196 "Unable to synchronize! Please assign sensor to patient or organization unit in MCD!");
198 } catch (Exception e) {
199 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
202 } catch (Exception e) {
203 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
208 * Updates the channels of the sensor thing with the latest value.
210 * @param latestValue the latest value as JsonObject as obtained from the REST
213 private void updateChannels(@Nullable JsonObject latestValue) {
214 if (latestValue != null) {
215 String event = latestValue.get("EventDef").getAsString();
216 String dateString = latestValue.get("DateEntry").getAsString();
218 Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse(dateString);
219 dateString = new SimpleDateFormat("dd.MM.yyyy', 'HH:mm:ss").format(date);
220 } catch (Exception e) {
221 logger.debug("{}", e.getMessage());
223 updateState(LAST_VALUE, new StringType(event + ", " + dateString));
228 * Make asynchronous HTTP request to fetch the sensors last value as JsonObject.
230 * @param urlString Contains the request URI as String
231 * @param callback Implementation of interface Callback
232 * (org.openhab.binding.mcd.internal.util), that includes
233 * the proceeding of the obtained JsonObject.
234 * @throws Exception Throws HTTP related Exceptions.
236 private void fetchLatestValue(String urlString, Callback callback) throws Exception {
237 McdBridgeHandler localMcdBridgeHandler = mcdBridgeHandler;
238 if (localMcdBridgeHandler != null) {
239 String accessToken = localMcdBridgeHandler.getAccessToken();
240 Request request = httpClient.newRequest(urlString).method(HttpMethod.GET)
241 .header(HttpHeader.HOST, "cunds-syncapi.azurewebsites.net")
242 .header(HttpHeader.ACCEPT, "application/json")
243 .header(HttpHeader.AUTHORIZATION, "Bearer " + accessToken)
244 .timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS);
246 request.send(new BufferingResponseListener() {
247 @NonNullByDefault({})
249 public void onComplete(Result result) {
250 String contentString = getContentAsString();
251 Gson localGson = gson;
252 if (localGson != null) {
253 JsonArray content = localGson.fromJson(contentString, JsonArray.class);
254 callback.jsonElementTypeCallback(content);
262 * get device info as json via http request
264 * @param callback instance of callback interface
265 * @throws Exception throws http related exceptions
267 private void fetchDeviceInfo(Callback callback) throws Exception {
268 McdBridgeHandler localMcdBridgeHandler = mcdBridgeHandler;
269 if (localMcdBridgeHandler != null) {
270 String accessToken = localMcdBridgeHandler.getAccessToken();
271 Request request = httpClient
272 .newRequest("https://cunds-syncapi.azurewebsites.net/api/Device?serialNumber=" + serialNumber)
273 .timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS).method(HttpMethod.GET)
274 .header(HttpHeader.HOST, "cunds-syncapi.azurewebsites.net")
275 .header(HttpHeader.ACCEPT, "application/json")
276 .header(HttpHeader.AUTHORIZATION, "Bearer " + accessToken);
277 request.send(new BufferingResponseListener() {
278 @NonNullByDefault({})
280 public void onComplete(Result result) {
281 String contentString = getContentAsString();
282 Gson localGson = gson;
283 if (localGson != null) {
284 JsonObject content = localGson.fromJson(contentString, JsonObject.class);
285 callback.jsonElementTypeCallback(content);
293 * Sends a GET request to the C&S REST API to receive the list of sensor event
296 * @param callback Implementation of interface Callback
297 * (org.openhab.binding.mcd.internal.util), that includes
298 * the proceeding of the obtained JsonObject.
299 * @throws Exception Throws HTTP related Exceptions.
301 private void fetchEventDef(Callback callback) throws Exception {
302 McdBridgeHandler localMcdBridgeHandler = mcdBridgeHandler;
303 if (localMcdBridgeHandler != null) {
304 String accessToken = localMcdBridgeHandler.getAccessToken();
305 Request request = httpClient.newRequest("https://cunds-syncapi.azurewebsites.net/api/ApiSensor/GetEventDef")
306 .timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS).method(HttpMethod.GET)
307 .header(HttpHeader.HOST, "cunds-syncapi.azurewebsites.net")
308 .header(HttpHeader.ACCEPT, "application/json")
309 .header(HttpHeader.AUTHORIZATION, "Bearer " + accessToken);
310 request.send(new BufferingResponseListener() {
311 @NonNullByDefault({})
313 public void onComplete(Result result) {
314 String contentString = getContentAsString();
315 Gson localGson = gson;
316 if (localGson != null) {
317 JsonArray content = localGson.fromJson(contentString, JsonArray.class);
318 callback.jsonElementTypeCallback(content);
326 * Builds the URI String for requesting the latest sensor event from the API. In
327 * order to do that, the parameter
328 * deviceInfo is needed.
330 * @param deviceInfo JsonObject that contains the device info as received from
332 * @return returns the URI as String or null, if no patient or organisation unit
333 * is assigned to the sensor in the
337 String getUrlStringFromDeviceInfo(@Nullable JsonObject deviceInfo) {
338 if (deviceInfo != null) {
339 if (deviceInfo.has("SerialNumber") && deviceInfo.get("SerialNumber").getAsString().equals(serialNumber)) {
340 if (deviceInfo.has("PatientDevices") && deviceInfo.getAsJsonArray("PatientDevices").size() != 0) {
341 JsonArray array = deviceInfo.getAsJsonArray("PatientDevices");
342 JsonObject patient = array.get(0).getAsJsonObject();
343 if (patient.has("UuidPerson") && !patient.get("UuidPerson").isJsonNull()) {
345 https://cunds-syncapi.azurewebsites.net/api/ApiSensor/GetLatestApiSensorEvents\
347 """ + patient.get("UuidPerson").getAsString() + "&SerialNumber=" + serialNumber
350 } else if (deviceInfo.has("OrganisationUnitDevices")
351 && deviceInfo.getAsJsonArray("OrganisationUnitDevices").size() != 0) {
352 JsonArray array = deviceInfo.getAsJsonArray("OrganisationUnitDevices");
353 JsonObject orgUnit = array.get(0).getAsJsonObject();
354 if (orgUnit.has("UuidOrganisationUnit") && !orgUnit.get("UuidOrganisationUnit").isJsonNull()) {
356 https://cunds-syncapi.azurewebsites.net/api/ApiSensor/GetLatestApiSensorEvents\
357 ?UuidOrganisationUnit=\
358 """ + orgUnit.get("UuidOrganisationUnit").getAsString() + "&SerialNumber="
359 + serialNumber + "&Count=1";
370 * Extracts the latest value from the JsonArray, that is obtained by the C&S
373 * @param jsonArray the array that contains the latest value
374 * @return the latest value as JsonObject or null.
377 static JsonObject getLatestValueFromJsonArray(@Nullable JsonArray jsonArray) {
378 if (jsonArray != null) {
379 if (jsonArray.size() != 0) {
380 JsonObject patientObject = jsonArray.get(0).getAsJsonObject();
381 JsonArray devicesArray = patientObject.getAsJsonArray("Devices");
382 if (devicesArray.size() != 0) {
383 JsonObject deviceObject = devicesArray.get(0).getAsJsonObject();
384 if (deviceObject.has("Events")) {
385 JsonArray eventsArray = deviceObject.getAsJsonArray("Events");
386 if (eventsArray.size() != 0) {
387 return eventsArray.get(0).getAsJsonObject();
397 * Sends data to the cloud via POST request and switches the channel states from
398 * ON to OFF for a number of channels.
400 * @param serialNumber serial number of the sensor in the MCD cloud
401 * @param sensorEventDef specifies the type of sensor event, that will be sent
403 private void sendSensorEvent(@Nullable String serialNumber, int sensorEventDef) {
405 McdBridgeHandler localMcdBridgeHandler = mcdBridgeHandler;
406 if (localMcdBridgeHandler != null) {
407 String accessToken = localMcdBridgeHandler.getAccessToken();
408 Date date = new Date();
409 String dateString = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(date);
410 Request request = httpClient.newRequest("https://cunds-syncapi.azurewebsites.net/api/ApiSensor")
411 .timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS).method(HttpMethod.POST)
412 .header(HttpHeader.CONTENT_TYPE, "application/json")
413 .header(HttpHeader.ACCEPT, "application/json")
414 .header(HttpHeader.AUTHORIZATION, "Bearer " + accessToken);
415 JsonObject jsonObject = new JsonObject();
416 jsonObject.addProperty("SerialNumber", serialNumber);
417 jsonObject.addProperty("IdApiSensorEventDef", sensorEventDef);
418 jsonObject.addProperty("DateEntry", dateString);
419 jsonObject.addProperty("DateSend", dateString);
421 new StringContentProvider("application/json", jsonObject.toString(), StandardCharsets.UTF_8));
422 request.send(new BufferingResponseListener() {
423 @NonNullByDefault({})
425 public void onComplete(Result result) {
426 if (result.getResponse().getStatus() != 201) {
427 logger.debug("Unable to send sensor event:\n{}", result.getResponse().toString());
429 logger.debug("Sensor event was stored successfully.");
430 refreshChannelValue();
435 } catch (Exception e) {
436 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());