]> git.basschouten.com Git - openhab-addons.git/blob
16881b570e9beffd6b2e7c36ac1b421880814880
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.mcd.internal.handler;
14
15 import static org.openhab.binding.mcd.internal.McdBindingConstants.*;
16
17 import java.nio.charset.StandardCharsets;
18 import java.text.SimpleDateFormat;
19 import java.util.Date;
20 import java.util.concurrent.TimeUnit;
21
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;
44
45 import com.google.gson.Gson;
46 import com.google.gson.JsonArray;
47 import com.google.gson.JsonObject;
48
49 /**
50  * Handler for the SensorThing of the MCD Binding.
51  * 
52  * @author Simon Dengler - Initial contribution
53  */
54 @NonNullByDefault
55 public class SensorThingHandler extends BaseThingHandler {
56
57     private static final int REQUEST_TIMEOUT_MS = 10_000;
58     private final Logger logger = LoggerFactory.getLogger(SensorThingHandler.class);
59
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;
67
68     public SensorThingHandler(Thing thing, HttpClient httpClient) {
69         super(thing);
70         this.httpClient = httpClient;
71         gson = new Gson();
72     }
73
74     @Override
75     public void initialize() {
76         config = getConfigAs(SensorThingConfiguration.class);
77         Bridge bridge = getBridge();
78         if (bridge != null) {
79             mcdBridgeHandler = (McdBridgeHandler) bridge.getHandler();
80         } else {
81             mcdBridgeHandler = null;
82         }
83         updateStatus(ThingStatus.UNKNOWN);
84         scheduler.execute(this::init);
85     }
86
87     @Override
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
99                     try {
100                         sensorEventId = Integer.parseInt(commandString);
101                         if (sensorEventId < 1 || sensorEventId > maxSensorEventId) {
102                             logger.warn("Invalid Command!");
103                         } else {
104                             sendSensorEvent(serialNumber, sensorEventId);
105                         }
106                     } catch (Exception e) {
107                         logger.warn("Invalid Command!");
108                     }
109                 } else {
110                     // command was valid (and id is between 1 and max)
111                     sendSensorEvent(serialNumber, sensorEventId);
112                 }
113             } else {
114                 logger.warn("Received command for unexpected channel!");
115             }
116             refreshChannelValue();
117         } else {
118             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Bridge is offline.");
119         }
120     }
121
122     // this is called from initialize()
123     private void init() {
124         SensorThingConfiguration localConfig = config;
125         if (localConfig != null) {
126             serialNumber = localConfig.getSerialNumber();
127         } else {
128             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Cannot access config data.");
129         }
130         McdBridgeHandler localMcdBridgeHandler = mcdBridgeHandler;
131         if (localMcdBridgeHandler != null) {
132             updateStatus(ThingStatus.ONLINE);
133             if (!initIsDone) {
134                 // build and register listener
135                 localMcdBridgeHandler.register(() -> {
136                     try {
137                         // determine, if thing is specified correctly and if it is online
138                         fetchDeviceInfo(res -> {
139                             if (res != null) {
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!");
146                                     } else {
147                                         // refresh channel values and set thing status to ONLINE
148                                         refreshChannelValue();
149                                         updateStatus(ThingStatus.ONLINE);
150                                     }
151                                 }
152                             }
153                         });
154                         fetchEventDef(jsonElement -> {
155                             if (jsonElement != null) {
156                                 JsonArray eventDefArray = jsonElement.getAsJsonArray();
157                                 maxSensorEventId = eventDefArray.size();
158                             }
159                         });
160                     } catch (Exception e) {
161                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
162                     }
163                 });
164                 initIsDone = true;
165             }
166         } else {
167             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "unable to access bridge");
168         }
169     }
170
171     /**
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.
175      */
176     private void refreshChannelValue() {
177         try {
178             /*
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.
182              */
183             fetchDeviceInfo(deviceInfo -> {
184                 // build request URI String
185                 String requestUrl = getUrlStringFromDeviceInfo((JsonObject) deviceInfo);
186                 try {
187                     if (requestUrl != null) {
188                         // get latest sensor event
189                         fetchLatestValue(requestUrl, result -> {
190                             JsonObject latestValue = getLatestValueFromJsonArray((JsonArray) result);
191                             // update channels
192                             updateChannels(latestValue);
193                         });
194                     } else {
195                         logger.warn(
196                                 "Unable to synchronize! Please assign sensor to patient or organization unit in MCD!");
197                     }
198                 } catch (Exception e) {
199                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
200                 }
201             });
202         } catch (Exception e) {
203             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
204         }
205     }
206
207     /**
208      * Updates the channels of the sensor thing with the latest value.
209      * 
210      * @param latestValue the latest value as JsonObject as obtained from the REST
211      *            API
212      */
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();
217             try {
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());
222             }
223             updateState(LAST_VALUE, new StringType(event + ", " + dateString));
224         }
225     }
226
227     /**
228      * Make asynchronous HTTP request to fetch the sensors last value as JsonObject.
229      * 
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.
235      */
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);
245
246             request.send(new BufferingResponseListener() {
247                 @NonNullByDefault({})
248                 @Override
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);
255                     }
256                 }
257             });
258         }
259     }
260
261     /**
262      * get device info as json via http request
263      * 
264      * @param callback instance of callback interface
265      * @throws Exception throws http related exceptions
266      */
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({})
279                 @Override
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);
286                     }
287                 }
288             });
289         }
290     }
291
292     /**
293      * Sends a GET request to the C&S REST API to receive the list of sensor event
294      * definitions.
295      * 
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.
300      */
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({})
312                 @Override
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);
319                     }
320                 }
321             });
322         }
323     }
324
325     /**
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.
329      * 
330      * @param deviceInfo JsonObject that contains the device info as received from
331      *            the C&S API
332      * @return returns the URI as String or null, if no patient or organisation unit
333      *         is assigned to the sensor in the
334      *         MCD cloud
335      */
336     @Nullable
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()) {
344                         return """
345                                 https://cunds-syncapi.azurewebsites.net/api/ApiSensor/GetLatestApiSensorEvents\
346                                 ?UuidPatient=\
347                                 """ + patient.get("UuidPerson").getAsString() + "&SerialNumber=" + serialNumber
348                                 + "&Count=1";
349                     }
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()) {
355                         return """
356                                 https://cunds-syncapi.azurewebsites.net/api/ApiSensor/GetLatestApiSensorEvents\
357                                 ?UuidOrganisationUnit=\
358                                 """ + orgUnit.get("UuidOrganisationUnit").getAsString() + "&SerialNumber="
359                                 + serialNumber + "&Count=1";
360                     }
361                 }
362             } else {
363                 init();
364             }
365         }
366         return null;
367     }
368
369     /**
370      * Extracts the latest value from the JsonArray, that is obtained by the C&S
371      * SensorApi.
372      * 
373      * @param jsonArray the array that contains the latest value
374      * @return the latest value as JsonObject or null.
375      */
376     @Nullable
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();
388                         }
389                     }
390                 }
391             }
392         }
393         return null;
394     }
395
396     /**
397      * Sends data to the cloud via POST request and switches the channel states from
398      * ON to OFF for a number of channels.
399      * 
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
402      */
403     private void sendSensorEvent(@Nullable String serialNumber, int sensorEventDef) {
404         try {
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);
420                 request.content(
421                         new StringContentProvider("application/json", jsonObject.toString(), StandardCharsets.UTF_8));
422                 request.send(new BufferingResponseListener() {
423                     @NonNullByDefault({})
424                     @Override
425                     public void onComplete(Result result) {
426                         if (result.getResponse().getStatus() != 201) {
427                             logger.debug("Unable to send sensor event:\n{}", result.getResponse().toString());
428                         } else {
429                             logger.debug("Sensor event was stored successfully.");
430                             refreshChannelValue();
431                         }
432                     }
433                 });
434             }
435         } catch (Exception e) {
436             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
437         }
438     }
439 }