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;
21 import org.eclipse.jdt.annotation.NonNullByDefault;
22 import org.eclipse.jdt.annotation.Nullable;
23 import org.eclipse.jetty.client.HttpClient;
24 import org.eclipse.jetty.client.api.Request;
25 import org.eclipse.jetty.client.api.Result;
26 import org.eclipse.jetty.client.util.BufferingResponseListener;
27 import org.eclipse.jetty.client.util.StringContentProvider;
28 import org.eclipse.jetty.http.HttpHeader;
29 import org.eclipse.jetty.http.HttpMethod;
30 import org.openhab.binding.mcd.internal.util.Callback;
31 import org.openhab.binding.mcd.internal.util.SensorEventDef;
32 import org.openhab.core.library.types.StringType;
33 import org.openhab.core.thing.Bridge;
34 import org.openhab.core.thing.ChannelUID;
35 import org.openhab.core.thing.Thing;
36 import org.openhab.core.thing.ThingStatus;
37 import org.openhab.core.thing.ThingStatusDetail;
38 import org.openhab.core.thing.binding.BaseThingHandler;
39 import org.openhab.core.types.Command;
40 import org.openhab.core.types.RefreshType;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
44 import com.google.gson.Gson;
45 import com.google.gson.JsonArray;
46 import com.google.gson.JsonObject;
49 * Handler for the SensorThing of the MCD Binding.
51 * @author Simon Dengler - Initial contribution
54 public class SensorThingHandler extends BaseThingHandler {
56 private final Logger logger = LoggerFactory.getLogger(SensorThingHandler.class);
57 private final HttpClient httpClient;
58 private final @Nullable Gson gson;
59 private @Nullable McdBridgeHandler mcdBridgeHandler;
60 private @Nullable String serialNumber = "";
61 private @Nullable SensorThingConfiguration config;
62 private int maxSensorEventId = 0;
63 private boolean initIsDone = false;
65 public SensorThingHandler(Thing thing, HttpClient httpClient) {
67 this.httpClient = httpClient;
72 public void initialize() {
73 config = getConfigAs(SensorThingConfiguration.class);
74 Bridge bridge = getBridge();
76 mcdBridgeHandler = (McdBridgeHandler) bridge.getHandler();
78 mcdBridgeHandler = null;
80 updateStatus(ThingStatus.UNKNOWN);
81 scheduler.execute(this::init);
85 public void handleCommand(ChannelUID channelUID, Command command) {
86 if (command instanceof RefreshType) {
87 refreshChannelValue();
88 } else if (mcdBridgeHandler != null) {
89 String channelId = channelUID.getId();
90 // check for the right channel id
91 if (channelId.equals(SEND_EVENT)) {
92 String commandString = command.toString();
93 int sensorEventId = SensorEventDef.getSensorEventId(commandString);
94 if (sensorEventId < 1 || sensorEventId > maxSensorEventId) {
95 // check, if an id is passed as number
97 sensorEventId = Integer.parseInt(commandString);
98 if (sensorEventId < 1 || sensorEventId > maxSensorEventId) {
99 logger.warn("Invalid Command!");
101 sendSensorEvent(serialNumber, sensorEventId);
103 } catch (Exception e) {
104 logger.warn("Invalid Command!");
107 // command was valid (and id is between 1 and max)
108 sendSensorEvent(serialNumber, sensorEventId);
111 logger.warn("Received command for unexpected channel!");
113 refreshChannelValue();
115 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Bridge is offline.");
119 // this is called from initialize()
120 private void init() {
121 SensorThingConfiguration localConfig = config;
122 if (localConfig != null) {
123 serialNumber = localConfig.getSerialNumber();
125 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Cannot access config data.");
127 McdBridgeHandler localMcdBridgeHandler = mcdBridgeHandler;
128 if (localMcdBridgeHandler != null) {
129 updateStatus(ThingStatus.ONLINE);
131 // build and register listener
132 localMcdBridgeHandler.register(() -> {
134 // determine, if thing is specified correctly and if it is online
135 fetchDeviceInfo(res -> {
137 JsonObject result = res.getAsJsonObject();
138 if (result.has("SerialNumber")) {
139 // check for serial number in MCD cloud
140 if (result.get("SerialNumber").isJsonNull()) {
141 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
142 "Serial number does not exist in MCD!");
144 // refresh channel values and set thing status to ONLINE
145 refreshChannelValue();
146 updateStatus(ThingStatus.ONLINE);
151 fetchEventDef(jsonElement -> {
152 if (jsonElement != null) {
153 JsonArray eventDefArray = jsonElement.getAsJsonArray();
154 maxSensorEventId = eventDefArray.size();
157 } catch (Exception e) {
158 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
164 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "unable to access bridge");
169 * This method uses the things serial number in order to obtain the latest
170 * sensor event, that was registered in the
171 * C&S MCD cloud, and then updates the channels with this latest value.
173 private void refreshChannelValue() {
176 * First, the device info for the given serial number is requested from the
177 * cloud, which is then used fetch
178 * the latest sensor event and update the channels.
180 fetchDeviceInfo(deviceInfo -> {
181 // build request URI String
182 String requestUrl = getUrlStringFromDeviceInfo((JsonObject) deviceInfo);
184 if (requestUrl != null) {
185 // get latest sensor event
186 fetchLatestValue(requestUrl, result -> {
187 JsonObject latestValue = getLatestValueFromJsonArray((JsonArray) result);
189 updateChannels(latestValue);
193 "Unable to synchronize! Please assign sensor to patient or organization unit in MCD!");
195 } catch (Exception e) {
196 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
199 } catch (Exception e) {
200 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
205 * Updates the channels of the sensor thing with the latest value.
207 * @param latestValue the latest value as JsonObject as obtained from the REST
210 private void updateChannels(@Nullable JsonObject latestValue) {
211 if (latestValue != null) {
212 String event = latestValue.get("EventDef").getAsString();
213 String dateString = latestValue.get("DateEntry").getAsString();
215 Date date = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").parse(dateString);
216 dateString = new SimpleDateFormat("dd.MM.yyyy', 'HH:mm:ss").format(date);
217 } catch (Exception e) {
218 logger.debug("{}", e.getMessage());
220 updateState(LAST_VALUE, new StringType(event + ", " + dateString));
225 * Make asynchronous HTTP request to fetch the sensors last value as JsonObject.
227 * @param urlString Contains the request URI as String
228 * @param callback Implementation of interface Callback
229 * (org.openhab.binding.mcd.internal.util), that includes
230 * the proceeding of the obtained JsonObject.
231 * @throws Exception Throws HTTP related Exceptions.
233 private void fetchLatestValue(String urlString, Callback callback) throws Exception {
234 McdBridgeHandler localMcdBridgeHandler = mcdBridgeHandler;
235 if (localMcdBridgeHandler != null) {
236 String accessToken = localMcdBridgeHandler.getAccessToken();
237 Request request = httpClient.newRequest(urlString).method(HttpMethod.GET)
238 .header(HttpHeader.HOST, "cunds-syncapi.azurewebsites.net")
239 .header(HttpHeader.ACCEPT, "application/json")
240 .header(HttpHeader.AUTHORIZATION, "Bearer " + accessToken);
241 request.send(new BufferingResponseListener() {
242 @NonNullByDefault({})
244 public void onComplete(Result result) {
245 String contentString = getContentAsString();
246 Gson localGson = gson;
247 if (localGson != null) {
248 JsonArray content = localGson.fromJson(contentString, JsonArray.class);
249 callback.jsonElementTypeCallback(content);
257 * get device info as json via http request
259 * @param callback instance of callback interface
260 * @throws Exception throws http related exceptions
262 private void fetchDeviceInfo(Callback callback) throws Exception {
263 McdBridgeHandler localMcdBridgeHandler = mcdBridgeHandler;
264 if (localMcdBridgeHandler != null) {
265 String accessToken = localMcdBridgeHandler.getAccessToken();
266 Request request = httpClient
267 .newRequest("https://cunds-syncapi.azurewebsites.net/api/Device?serialNumber=" + serialNumber)
268 .method(HttpMethod.GET).header(HttpHeader.HOST, "cunds-syncapi.azurewebsites.net")
269 .header(HttpHeader.ACCEPT, "application/json")
270 .header(HttpHeader.AUTHORIZATION, "Bearer " + accessToken);
271 request.send(new BufferingResponseListener() {
272 @NonNullByDefault({})
274 public void onComplete(Result result) {
275 String contentString = getContentAsString();
276 Gson localGson = gson;
277 if (localGson != null) {
278 JsonObject content = localGson.fromJson(contentString, JsonObject.class);
279 callback.jsonElementTypeCallback(content);
287 * Sends a GET request to the C&S REST API to receive the list of sensor event
290 * @param callback Implementation of interface Callback
291 * (org.openhab.binding.mcd.internal.util), that includes
292 * the proceeding of the obtained JsonObject.
293 * @throws Exception Throws HTTP related Exceptions.
295 private void fetchEventDef(Callback callback) throws Exception {
296 McdBridgeHandler localMcdBridgeHandler = mcdBridgeHandler;
297 if (localMcdBridgeHandler != null) {
298 String accessToken = localMcdBridgeHandler.getAccessToken();
299 Request request = httpClient.newRequest("https://cunds-syncapi.azurewebsites.net/api/ApiSensor/GetEventDef")
300 .method(HttpMethod.GET).header(HttpHeader.HOST, "cunds-syncapi.azurewebsites.net")
301 .header(HttpHeader.ACCEPT, "application/json")
302 .header(HttpHeader.AUTHORIZATION, "Bearer " + accessToken);
303 request.send(new BufferingResponseListener() {
304 @NonNullByDefault({})
306 public void onComplete(Result result) {
307 String contentString = getContentAsString();
308 Gson localGson = gson;
309 if (localGson != null) {
310 JsonArray content = localGson.fromJson(contentString, JsonArray.class);
311 callback.jsonElementTypeCallback(content);
319 * Builds the URI String for requesting the latest sensor event from the API. In
320 * order to do that, the parameter
321 * deviceInfo is needed.
323 * @param deviceInfo JsonObject that contains the device info as received from
325 * @return returns the URI as String or null, if no patient or organisation unit
326 * is assigned to the sensor in the
330 String getUrlStringFromDeviceInfo(@Nullable JsonObject deviceInfo) {
331 if (deviceInfo != null) {
332 if (deviceInfo.has("SerialNumber") && deviceInfo.get("SerialNumber").getAsString().equals(serialNumber)) {
333 if (deviceInfo.has("PatientDevices") && deviceInfo.getAsJsonArray("PatientDevices").size() != 0) {
334 JsonArray array = deviceInfo.getAsJsonArray("PatientDevices");
335 JsonObject patient = array.get(0).getAsJsonObject();
336 if (patient.has("UuidPerson") && !patient.get("UuidPerson").isJsonNull()) {
338 https://cunds-syncapi.azurewebsites.net/api/ApiSensor/GetLatestApiSensorEvents\
340 """ + patient.get("UuidPerson").getAsString() + "&SerialNumber=" + serialNumber
343 } else if (deviceInfo.has("OrganisationUnitDevices")
344 && deviceInfo.getAsJsonArray("OrganisationUnitDevices").size() != 0) {
345 JsonArray array = deviceInfo.getAsJsonArray("OrganisationUnitDevices");
346 JsonObject orgUnit = array.get(0).getAsJsonObject();
347 if (orgUnit.has("UuidOrganisationUnit") && !orgUnit.get("UuidOrganisationUnit").isJsonNull()) {
349 https://cunds-syncapi.azurewebsites.net/api/ApiSensor/GetLatestApiSensorEvents\
350 ?UuidOrganisationUnit=\
351 """ + orgUnit.get("UuidOrganisationUnit").getAsString() + "&SerialNumber="
352 + serialNumber + "&Count=1";
363 * Extracts the latest value from the JsonArray, that is obtained by the C&S
366 * @param jsonArray the array that contains the latest value
367 * @return the latest value as JsonObject or null.
370 static JsonObject getLatestValueFromJsonArray(@Nullable JsonArray jsonArray) {
371 if (jsonArray != null) {
372 if (jsonArray.size() != 0) {
373 JsonObject patientObject = jsonArray.get(0).getAsJsonObject();
374 JsonArray devicesArray = patientObject.getAsJsonArray("Devices");
375 if (devicesArray.size() != 0) {
376 JsonObject deviceObject = devicesArray.get(0).getAsJsonObject();
377 if (deviceObject.has("Events")) {
378 JsonArray eventsArray = deviceObject.getAsJsonArray("Events");
379 if (eventsArray.size() != 0) {
380 return eventsArray.get(0).getAsJsonObject();
390 * Sends data to the cloud via POST request and switches the channel states from
391 * ON to OFF for a number of channels.
393 * @param serialNumber serial number of the sensor in the MCD cloud
394 * @param sensorEventDef specifies the type of sensor event, that will be sent
396 private void sendSensorEvent(@Nullable String serialNumber, int sensorEventDef) {
398 McdBridgeHandler localMcdBridgeHandler = mcdBridgeHandler;
399 if (localMcdBridgeHandler != null) {
400 String accessToken = localMcdBridgeHandler.getAccessToken();
401 Date date = new Date();
402 String dateString = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(date);
403 Request request = httpClient.newRequest("https://cunds-syncapi.azurewebsites.net/api/ApiSensor")
404 .method(HttpMethod.POST).header(HttpHeader.CONTENT_TYPE, "application/json")
405 .header(HttpHeader.ACCEPT, "application/json")
406 .header(HttpHeader.AUTHORIZATION, "Bearer " + accessToken);
407 JsonObject jsonObject = new JsonObject();
408 jsonObject.addProperty("SerialNumber", serialNumber);
409 jsonObject.addProperty("IdApiSensorEventDef", sensorEventDef);
410 jsonObject.addProperty("DateEntry", dateString);
411 jsonObject.addProperty("DateSend", dateString);
413 new StringContentProvider("application/json", jsonObject.toString(), StandardCharsets.UTF_8));
414 request.send(new BufferingResponseListener() {
415 @NonNullByDefault({})
417 public void onComplete(Result result) {
418 if (result.getResponse().getStatus() != 201) {
419 logger.debug("Unable to send sensor event:\n{}", result.getResponse().toString());
421 logger.debug("Sensor event was stored successfully.");
422 refreshChannelValue();
427 } catch (Exception e) {
428 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());