]> git.basschouten.com Git - openhab-addons.git/blob
d4d058066d0b38816574792d3e7f449362440ab0
[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
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;
43
44 import com.google.gson.Gson;
45 import com.google.gson.JsonArray;
46 import com.google.gson.JsonObject;
47
48 /**
49  * Handler for the SensorThing of the MCD Binding.
50  * 
51  * @author Simon Dengler - Initial contribution
52  */
53 @NonNullByDefault
54 public class SensorThingHandler extends BaseThingHandler {
55
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;
64
65     public SensorThingHandler(Thing thing, HttpClient httpClient) {
66         super(thing);
67         this.httpClient = httpClient;
68         gson = new Gson();
69     }
70
71     @Override
72     public void initialize() {
73         config = getConfigAs(SensorThingConfiguration.class);
74         Bridge bridge = getBridge();
75         if (bridge != null) {
76             mcdBridgeHandler = (McdBridgeHandler) bridge.getHandler();
77         } else {
78             mcdBridgeHandler = null;
79         }
80         updateStatus(ThingStatus.UNKNOWN);
81         scheduler.execute(this::init);
82     }
83
84     @Override
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
96                     try {
97                         sensorEventId = Integer.parseInt(commandString);
98                         if (sensorEventId < 1 || sensorEventId > maxSensorEventId) {
99                             logger.warn("Invalid Command!");
100                         } else {
101                             sendSensorEvent(serialNumber, sensorEventId);
102                         }
103                     } catch (Exception e) {
104                         logger.warn("Invalid Command!");
105                     }
106                 } else {
107                     // command was valid (and id is between 1 and max)
108                     sendSensorEvent(serialNumber, sensorEventId);
109                 }
110             } else {
111                 logger.warn("Received command for unexpected channel!");
112             }
113             refreshChannelValue();
114         } else {
115             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "Bridge is offline.");
116         }
117     }
118
119     // this is called from initialize()
120     private void init() {
121         SensorThingConfiguration localConfig = config;
122         if (localConfig != null) {
123             serialNumber = localConfig.getSerialNumber();
124         } else {
125             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Cannot access config data.");
126         }
127         McdBridgeHandler localMcdBridgeHandler = mcdBridgeHandler;
128         if (localMcdBridgeHandler != null) {
129             updateStatus(ThingStatus.ONLINE);
130             if (!initIsDone) {
131                 // build and register listener
132                 localMcdBridgeHandler.register(() -> {
133                     try {
134                         // determine, if thing is specified correctly and if it is online
135                         fetchDeviceInfo(res -> {
136                             if (res != null) {
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!");
143                                     } else {
144                                         // refresh channel values and set thing status to ONLINE
145                                         refreshChannelValue();
146                                         updateStatus(ThingStatus.ONLINE);
147                                     }
148                                 }
149                             }
150                         });
151                         fetchEventDef(jsonElement -> {
152                             if (jsonElement != null) {
153                                 JsonArray eventDefArray = jsonElement.getAsJsonArray();
154                                 maxSensorEventId = eventDefArray.size();
155                             }
156                         });
157                     } catch (Exception e) {
158                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
159                     }
160                 });
161                 initIsDone = true;
162             }
163         } else {
164             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, "unable to access bridge");
165         }
166     }
167
168     /**
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.
172      */
173     private void refreshChannelValue() {
174         try {
175             /*
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.
179              */
180             fetchDeviceInfo(deviceInfo -> {
181                 // build request URI String
182                 String requestUrl = getUrlStringFromDeviceInfo((JsonObject) deviceInfo);
183                 try {
184                     if (requestUrl != null) {
185                         // get latest sensor event
186                         fetchLatestValue(requestUrl, result -> {
187                             JsonObject latestValue = getLatestValueFromJsonArray((JsonArray) result);
188                             // update channels
189                             updateChannels(latestValue);
190                         });
191                     } else {
192                         logger.warn(
193                                 "Unable to synchronize! Please assign sensor to patient or organization unit in MCD!");
194                     }
195                 } catch (Exception e) {
196                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
197                 }
198             });
199         } catch (Exception e) {
200             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
201         }
202     }
203
204     /**
205      * Updates the channels of the sensor thing with the latest value.
206      * 
207      * @param latestValue the latest value as JsonObject as obtained from the REST
208      *            API
209      */
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();
214             try {
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());
219             }
220             updateState(LAST_VALUE, new StringType(event + ", " + dateString));
221         }
222     }
223
224     /**
225      * Make asynchronous HTTP request to fetch the sensors last value as JsonObject.
226      * 
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.
232      */
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({})
243                 @Override
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);
250                     }
251                 }
252             });
253         }
254     }
255
256     /**
257      * get device info as json via http request
258      * 
259      * @param callback instance of callback interface
260      * @throws Exception throws http related exceptions
261      */
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({})
273                 @Override
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);
280                     }
281                 }
282             });
283         }
284     }
285
286     /**
287      * Sends a GET request to the C&S REST API to receive the list of sensor event
288      * definitions.
289      * 
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.
294      */
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({})
305                 @Override
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);
312                     }
313                 }
314             });
315         }
316     }
317
318     /**
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.
322      * 
323      * @param deviceInfo JsonObject that contains the device info as received from
324      *            the C&S API
325      * @return returns the URI as String or null, if no patient or organisation unit
326      *         is assigned to the sensor in the
327      *         MCD cloud
328      */
329     @Nullable
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()) {
337                         return """
338                                 https://cunds-syncapi.azurewebsites.net/api/ApiSensor/GetLatestApiSensorEvents\
339                                 ?UuidPatient=\
340                                 """ + patient.get("UuidPerson").getAsString() + "&SerialNumber=" + serialNumber
341                                 + "&Count=1";
342                     }
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()) {
348                         return """
349                                 https://cunds-syncapi.azurewebsites.net/api/ApiSensor/GetLatestApiSensorEvents\
350                                 ?UuidOrganisationUnit=\
351                                 """ + orgUnit.get("UuidOrganisationUnit").getAsString() + "&SerialNumber="
352                                 + serialNumber + "&Count=1";
353                     }
354                 }
355             } else {
356                 init();
357             }
358         }
359         return null;
360     }
361
362     /**
363      * Extracts the latest value from the JsonArray, that is obtained by the C&S
364      * SensorApi.
365      * 
366      * @param jsonArray the array that contains the latest value
367      * @return the latest value as JsonObject or null.
368      */
369     @Nullable
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();
381                         }
382                     }
383                 }
384             }
385         }
386         return null;
387     }
388
389     /**
390      * Sends data to the cloud via POST request and switches the channel states from
391      * ON to OFF for a number of channels.
392      * 
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
395      */
396     private void sendSensorEvent(@Nullable String serialNumber, int sensorEventDef) {
397         try {
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);
412                 request.content(
413                         new StringContentProvider("application/json", jsonObject.toString(), StandardCharsets.UTF_8));
414                 request.send(new BufferingResponseListener() {
415                     @NonNullByDefault({})
416                     @Override
417                     public void onComplete(Result result) {
418                         if (result.getResponse().getStatus() != 201) {
419                             logger.debug("Unable to send sensor event:\n{}", result.getResponse().toString());
420                         } else {
421                             logger.debug("Sensor event was stored successfully.");
422                             refreshChannelValue();
423                         }
424                     }
425                 });
426             }
427         } catch (Exception e) {
428             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
429         }
430     }
431 }