]> git.basschouten.com Git - openhab-addons.git/blob
8b2db84103249ca00e67bdfa35e64a4385c25597
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.shelly.internal.coap;
14
15 import static org.openhab.binding.shelly.internal.ShellyBindingConstants.*;
16 import static org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.*;
17 import static org.openhab.binding.shelly.internal.util.ShellyUtils.*;
18
19 import java.net.SocketException;
20 import java.net.UnknownHostException;
21 import java.util.LinkedHashMap;
22 import java.util.List;
23 import java.util.Map;
24 import java.util.TreeMap;
25
26 import org.eclipse.californium.core.CoapClient;
27 import org.eclipse.californium.core.coap.CoAP.Code;
28 import org.eclipse.californium.core.coap.CoAP.ResponseCode;
29 import org.eclipse.californium.core.coap.CoAP.Type;
30 import org.eclipse.californium.core.coap.MessageObserverAdapter;
31 import org.eclipse.californium.core.coap.Option;
32 import org.eclipse.californium.core.coap.OptionNumberRegistry;
33 import org.eclipse.californium.core.coap.Request;
34 import org.eclipse.californium.core.coap.Response;
35 import org.eclipse.californium.core.network.Endpoint;
36 import org.eclipse.jdt.annotation.NonNullByDefault;
37 import org.eclipse.jdt.annotation.Nullable;
38 import org.openhab.binding.shelly.internal.api.ShellyApiException;
39 import org.openhab.binding.shelly.internal.api.ShellyDeviceProfile;
40 import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrBlk;
41 import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDescrSen;
42 import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDevDescrTypeAdapter;
43 import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotDevDescription;
44 import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotGenericSensorList;
45 import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensor;
46 import org.openhab.binding.shelly.internal.coap.ShellyCoapJSonDTO.CoIotSensorTypeAdapter;
47 import org.openhab.binding.shelly.internal.config.ShellyThingConfiguration;
48 import org.openhab.binding.shelly.internal.handler.ShellyBaseHandler;
49 import org.openhab.binding.shelly.internal.handler.ShellyColorUtils;
50 import org.openhab.core.library.unit.Units;
51 import org.openhab.core.types.State;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
54
55 import com.google.gson.Gson;
56 import com.google.gson.GsonBuilder;
57 import com.google.gson.JsonSyntaxException;
58
59 /**
60  * The {@link ShellyCoapHandler} handles the CoIoT/CoAP registration and events.
61  *
62  * @author Markus Michels - Initial contribution
63  */
64 @NonNullByDefault
65 public class ShellyCoapHandler implements ShellyCoapListener {
66     private static final byte[] EMPTY_BYTE = new byte[0];
67
68     private final Logger logger = LoggerFactory.getLogger(ShellyCoapHandler.class);
69     private final ShellyBaseHandler thingHandler;
70     private ShellyThingConfiguration config = new ShellyThingConfiguration();
71     private final GsonBuilder gsonBuilder = new GsonBuilder();
72     private final Gson gson;
73     private String thingName;
74
75     private boolean coiotBound = false;
76     private ShellyCoIoTInterface coiot;
77     private int coiotVers = -1;
78
79     private final ShellyCoapServer coapServer;
80     private @Nullable CoapClient statusClient;
81     private Request reqDescription = new Request(Code.GET, Type.CON);
82     private Request reqStatus = new Request(Code.GET, Type.CON);
83     private boolean discovering = false;
84
85     private int lastSerial = -1;
86     private String lastPayload = "";
87     private Map<String, CoIotDescrBlk> blkMap = new LinkedHashMap<>();
88     private Map<String, CoIotDescrSen> sensorMap = new LinkedHashMap<>();
89     private ShellyDeviceProfile profile;
90
91     public ShellyCoapHandler(ShellyBaseHandler thingHandler, ShellyCoapServer coapServer) {
92         this.thingHandler = thingHandler;
93         this.thingName = thingHandler.thingName;
94         this.profile = thingHandler.getProfile();
95         this.coapServer = coapServer;
96         this.coiot = new ShellyCoIoTVersion2(thingName, thingHandler, blkMap, sensorMap); // Default: V2
97
98         gsonBuilder.registerTypeAdapter(CoIotDevDescription.class, new CoIotDevDescrTypeAdapter());
99         gsonBuilder.registerTypeAdapter(CoIotGenericSensorList.class, new CoIotSensorTypeAdapter());
100         gson = gsonBuilder.create();
101     }
102
103     /**
104      * Initialize CoAP access, send discovery packet and start Status server
105      *
106      * @parm thingName Thing name derived from Thing Type/hostname
107      * @parm config ShellyThingConfiguration
108      * @thows ShellyApiException
109      */
110     public synchronized void start(String thingName, ShellyThingConfiguration config) throws ShellyApiException {
111         try {
112             this.thingName = thingName;
113             this.config = config;
114             this.profile = thingHandler.getProfile();
115             if (isStarted()) {
116                 logger.trace("{}: CoAP Listener was already started", thingName);
117                 stop();
118             }
119
120             logger.debug("{}: Starting CoAP Listener", thingName);
121             coapServer.start(config.localIp, this);
122             statusClient = new CoapClient(completeUrl(config.deviceIp, COLOIT_URI_DEVSTATUS))
123                     .setTimeout((long) SHELLY_API_TIMEOUT_MS).useNONs().setEndpoint(coapServer.getEndpoint());
124             @Nullable
125             Endpoint endpoint = null;
126             if (statusClient != null) {
127                 endpoint = statusClient.getEndpoint();
128             }
129             if ((endpoint == null) || !endpoint.isStarted()) {
130                 logger.warn("{}: Unable to initialize CoAP access (network error)", thingName);
131                 throw new ShellyApiException("Network initialization failed");
132             }
133             discover();
134         } catch (SocketException e) {
135             logger.warn("{}: Unable to initialize CoAP access (socket exception) - {}", thingName, e.getMessage());
136             throw new ShellyApiException("Network error", e);
137         } catch (UnknownHostException e) {
138             logger.info("{}: CoAP Exception (Unknown Host)", thingName, e);
139             throw new ShellyApiException("Unknown Host: " + config.deviceIp, e);
140         }
141     }
142
143     public boolean isStarted() {
144         return statusClient != null;
145     }
146
147     /**
148      * Process an inbound Response (or mapped Request): decode CoAP options. handle discovery result or status updates
149      *
150      * @param response The Response packet
151      */
152     @Override
153     public void processResponse(@Nullable Response response) {
154         if (response == null) {
155             return; // other device instance
156         }
157         String ip = response.getSourceContext().getPeerAddress().toString();
158         if (!ip.contains(config.deviceIp)) {
159             return;
160         }
161
162         String payload = "";
163         String devId = "";
164         String uri = "";
165         int serial = -1;
166         try {
167             if (logger.isDebugEnabled()) {
168                 logger.debug("{}: CoIoT Message from {} (MID={}): {}", thingName,
169                         response.getSourceContext().getPeerAddress(), response.getMID(), response.getPayloadString());
170             }
171             if (response.isCanceled() || response.isDuplicate() || response.isRejected()) {
172                 logger.debug("{} ({}): Packet was canceled, rejected or is a duplicate -> discard", thingName, devId);
173                 return;
174             }
175
176             if (response.getCode() == ResponseCode.CONTENT) {
177                 payload = response.getPayloadString();
178                 List<Option> options = response.getOptions().asSortedList();
179                 int i = 0;
180                 while (i < options.size()) {
181                     Option opt = options.get(i);
182                     switch (opt.getNumber()) {
183                         case OptionNumberRegistry.URI_PATH:
184                             uri = COLOIT_URI_BASE + opt.getStringValue();
185                             break;
186                         case COIOT_OPTION_GLOBAL_DEVID:
187                             devId = opt.getStringValue();
188                             String sVersion = substringAfterLast(devId, "#");
189                             int iVersion = Integer.parseInt(sVersion);
190                             if (coiotBound && (coiotVers != iVersion)) {
191                                 logger.debug(
192                                         "{}: CoIoT versopm has changed from {} to {}, maybe the firmware was upgraded",
193                                         thingName, coiotVers, iVersion);
194                                 thingHandler.reinitializeThing();
195                                 coiotBound = false;
196                             }
197                             if (!coiotBound) {
198                                 thingHandler.updateProperties(PROPERTY_COAP_VERSION, sVersion);
199                                 logger.debug("{}: CoIoT Version {} detected", thingName, iVersion);
200                                 if (iVersion == COIOT_VERSION_1) {
201                                     coiot = new ShellyCoIoTVersion1(thingName, thingHandler, blkMap, sensorMap);
202                                 } else if (iVersion == COIOT_VERSION_2) {
203                                     coiot = new ShellyCoIoTVersion2(thingName, thingHandler, blkMap, sensorMap);
204                                 } else {
205                                     logger.warn("{}: Unsupported CoAP version detected: {}", thingName, sVersion);
206                                     return;
207                                 }
208                                 coiotVers = iVersion;
209                                 coiotBound = true;
210                             }
211                             break;
212                         case COIOT_OPTION_STATUS_VALIDITY:
213                             // validity = o.getIntegerValue();
214                             break;
215                         case COIOT_OPTION_STATUS_SERIAL:
216                             serial = opt.getIntegerValue();
217                             break;
218                         default:
219                             logger.debug("{} ({}): COAP option {} with value {} skipped", thingName, devId,
220                                     opt.getNumber(), opt.getValue());
221                     }
222                     i++;
223                 }
224
225                 // If we received a CoAP message successful the thing must be online
226                 thingHandler.setThingOnline();
227
228                 // The device changes the serial on every update, receiving a message with the same serial is a
229                 // duplicate, excep for battery devices! Those reset the serial every time when they wake-up
230                 if ((serial == lastSerial) && payload.equals(lastPayload) && (!profile.hasBattery
231                         || coiot.getLastWakeup().equalsIgnoreCase("ext_power") || ((serial & 0xFF) != 0))) {
232                     logger.debug("{}: Serial {} was already processed, ignore update", thingName, serial);
233                     return;
234                 }
235
236                 // fixed malformed JSON :-(
237                 payload = fixJSON(payload);
238
239                 try {
240                     if (uri.equalsIgnoreCase(COLOIT_URI_DEVDESC)
241                             || (uri.isEmpty() && payload.contains(COIOT_TAG_BLK))) {
242                         handleDeviceDescription(devId, payload);
243                     } else if (uri.equalsIgnoreCase(COLOIT_URI_DEVSTATUS)
244                             || (uri.isEmpty() && payload.contains(COIOT_TAG_GENERIC))) {
245                         handleStatusUpdate(devId, payload, serial);
246                     }
247                 } catch (ShellyApiException e) {
248                     logger.debug("{}: Unable to process CoIoT message: {}", thingName, e.toString());
249                 }
250             } else {
251                 // error handling
252                 logger.debug("{}: Unknown Response Code {} received, payload={}", thingName, response.getCode(),
253                         response.getPayloadString());
254             }
255
256             if (!discovering) {
257                 // Observe Status Updates
258                 reqStatus = sendRequest(reqStatus, config.deviceIp, COLOIT_URI_DEVSTATUS, Type.NON);
259                 discovering = true;
260             }
261         } catch (JsonSyntaxException | IllegalArgumentException | NullPointerException e) {
262             logger.debug("{}: Unable to process CoIoT Message for payload={}", thingName, payload, e);
263             resetSerial();
264         }
265     }
266
267     /**
268      * Process a CoIoT device description message. This includes definitions on device units (Relay0, Relay1, Sensors
269      * etc.) as well as a definition of sensors and actors. This information needs to be stored allowing to map ids from
270      * status updates to the device units and matching the correct thing channel.
271      *
272      * @param devId The device id reported in the CoIoT message.
273      * @param payload Device desciption in JSon format, example:
274      *            {"blk":[{"I":0,"D":"Relay0"}],"sen":[{"I":112,"T":"Switch","R":"0/1","L":0}],"act":[{"I":211,"D":"Switch","L":0,"P":[{"I":2011,"D":"ToState","R":"0/1"}]}]}
275      */
276     private void handleDeviceDescription(String devId, String payload) throws ShellyApiException {
277         logger.debug("{}: CoIoT Device Description for {}: {}", thingName, devId, payload);
278
279         try {
280             boolean valid = true;
281
282             // Decode Json
283             CoIotDevDescription descr = fromJson(gson, payload, CoIotDevDescription.class);
284             for (int i = 0; i < descr.blk.size(); i++) {
285                 CoIotDescrBlk blk = descr.blk.get(i);
286                 logger.debug("{}:    id={}: {}", thingName, blk.id, blk.desc);
287                 if (!blkMap.containsKey(blk.id)) {
288                     blkMap.put(blk.id, blk);
289                 } else {
290                     blkMap.replace(blk.id, blk);
291                 }
292                 if ((blk.type != null) && !blk.type.isEmpty()) {
293                     // in fact it is a sen entry - that's vioaling the Spec
294                     logger.trace("{}:    fix: auto-create sensor definition for id {}/{}!", thingName, blk.id,
295                             blk.desc);
296                     CoIotDescrSen sen = new CoIotDescrSen();
297                     sen.id = blk.id;
298                     sen.desc = blk.desc;
299                     sen.type = blk.type;
300                     sen.range = blk.range;
301                     sen.links = blk.links;
302                     valid &= addSensor(sen);
303                 }
304             }
305
306             // Save to thing properties
307             thingHandler.updateProperties(PROPERTY_COAP_DESCR, payload);
308
309             logger.debug("{}: Adding {} sensor definitions", thingName, descr.sen.size());
310             if (descr.sen != null) {
311                 for (int i = 0; i < descr.sen.size(); i++) {
312                     valid &= addSensor(descr.sen.get(i));
313                 }
314             }
315             coiot.completeMissingSensorDefinition(sensorMap);
316
317             if (!valid) {
318                 logger.debug(
319                         "{}: Incompatible device description detected for CoIoT version {} (id length mismatch), discarding!",
320                         thingName, coiot.getVersion());
321
322                 discover();
323                 return;
324             }
325         } catch (JsonSyntaxException e) {
326             logger.warn("{}: Unable to parse CoAP Device Description! JSON={}", thingName, payload);
327         } catch (NullPointerException | IllegalArgumentException e) {
328             logger.warn("{}: Unable to parse CoAP Device Description! JSON={}", thingName, payload, e);
329         }
330     }
331
332     /**
333      * Add a new sensor to the sensor table
334      *
335      * @param sen CoIotDescrSen of the sensor
336      */
337     private synchronized boolean addSensor(CoIotDescrSen sen) {
338         logger.debug("{}:    id {}: {}, Type={}, Range={}, Links={}", thingName, sen.id, sen.desc, sen.type, sen.range,
339                 sen.links);
340         // CoIoT version 2 changes from 3 digit IDs to 4 digit IDs
341         // We need to make sure that the persisted device description matches,
342         // otherwise the stored one is discarded and a new discovery is triggered
343         // This happens on firmware up/downgrades (version 1.8 brings CoIoT v2 with 4 digit IDs)
344         int vers = coiot.getVersion();
345         if (((vers == COIOT_VERSION_1) && (sen.id.length() > 3))
346                 || ((vers >= COIOT_VERSION_2) && (sen.id.length() < 4))) {
347             logger.debug("{}: Invalid format for sensor defition detected, id={}", thingName, sen.id);
348             return false;
349         }
350
351         try {
352             CoIotDescrSen fixed = coiot.fixDescription(sen, blkMap);
353             if (!sensorMap.containsKey(fixed.id)) {
354                 sensorMap.put(sen.id, fixed);
355             } else {
356                 sensorMap.replace(sen.id, fixed);
357             }
358         } catch (NullPointerException | IllegalArgumentException e) { // depending on firmware release the CoAP device
359                                                                       // description is buggy
360             logger.debug("{}: Unable to decode sensor definition -> skip", thingName, e);
361         }
362
363         return true;
364     }
365
366     /**
367      * Process CoIoT status update message. If a status update is received, but the device description has not been
368      * received yet a GET is send to query device description.
369      *
370      * @param devId device id included in the status packet
371      * @param payload CoAP payload (Json format), example: {"G":[[0,112,0]]}
372      * @param serial Serial for this request. If this the the same as last serial
373      *            the update was already sent and processed so this one gets
374      *            ignored.
375      * @throws ShellyApiException
376      */
377     private void handleStatusUpdate(String devId, String payload, int serial) throws ShellyApiException {
378         logger.debug("{}: CoIoT Sensor data {} (serial={})", thingName, payload, serial);
379         if (blkMap.isEmpty()) {
380             // send discovery packet
381             resetSerial();
382             discover();
383
384             // try to uses description from last initialization
385             String savedDescr = thingHandler.getProperty(PROPERTY_COAP_DESCR);
386             if (savedDescr.isEmpty()) {
387                 logger.debug("{}: Device description not yet received, trigger auto-initialization", thingName);
388                 return;
389             }
390
391             // simulate received device description to create element table
392             logger.debug("{}: Device description for {} restored: {}", thingName, devId, savedDescr);
393             handleDeviceDescription(devId, savedDescr);
394         }
395
396         // Parse Json,
397         CoIotGenericSensorList list = fromJson(gson, fixJSON(payload), CoIotGenericSensorList.class);
398         if (list.generic == null) {
399             logger.debug("{}: Sensor list has invalid format! Payload: {}", devId, payload);
400             return;
401         }
402
403         List<CoIotSensor> sensorUpdates = list.generic;
404         Map<String, State> updates = new TreeMap<String, State>();
405         logger.debug("{}: {} CoAP sensor updates received", thingName, sensorUpdates.size());
406         int failed = 0;
407         ShellyColorUtils col = new ShellyColorUtils();
408         for (int i = 0; i < sensorUpdates.size(); i++) {
409             try {
410                 CoIotSensor s = sensorUpdates.get(i);
411                 CoIotDescrSen sen = sensorMap.get(s.id);
412                 if (sen == null) {
413                     logger.debug("{}: Unable to sensor definition for id={}, payload={}", thingName, s.id, payload);
414                     continue;
415                 }
416                 // find matching sensor definition from device description, use the Link ID as index
417                 CoIotDescrBlk element = null;
418                 sen = coiot.fixDescription(sen, blkMap);
419                 element = blkMap.get(sen.links);
420                 if (element == null) {
421                     logger.debug("{}: Unable to find BLK for link {} from sen.id={}, payload={}", thingName, sen.links,
422                             sen.id, payload);
423                     continue;
424                 }
425                 logger.trace("{}:  Sensor value[{}]: id={}, Value={} ({}, Type={}, Range={}, Link={}: {})", thingName,
426                         i, s.id, getString(s.valueStr).isEmpty() ? s.value : s.valueStr, sen.desc, sen.type, sen.range,
427                         sen.links, element.desc);
428
429                 if (!coiot.handleStatusUpdate(sensorUpdates, sen, serial, s, updates, col)) {
430                     logger.debug("{}: CoIoT data for id {}, type {}/{} not processed, value={}; payload={}", thingName,
431                             sen.id, sen.type, sen.desc, s.value, payload);
432                 }
433             } catch (NullPointerException | IllegalArgumentException e) {
434                 // even the processing of one value failed we continue with the next one (sometimes this is caused by
435                 // buggy formats provided by the device
436                 logger.debug("{}: Unable to process data from sensor[{}], devId={}, payload={}", thingName, i, devId,
437                         payload, e);
438             }
439         }
440
441         if (!updates.isEmpty()) {
442             int updated = 0;
443             for (Map.Entry<String, State> u : updates.entrySet()) {
444                 String key = u.getKey();
445                 updated += thingHandler.updateChannel(key, u.getValue(), false) ? 1 : 0;
446             }
447             if (updated > 0) {
448                 logger.debug("{}: {} channels updated from CoIoT status, serial={}", thingName, updated, serial);
449                 if (profile.isSensor || profile.isRoller) {
450                     // CoAP is currently lacking the lastUpdate info, so we use host timestamp
451                     thingHandler.updateChannel(profile.getControlGroup(0), CHANNEL_LAST_UPDATE, getTimestamp());
452                 }
453             }
454
455             if (profile.isLight && profile.inColor && col.isRgbValid()) {
456                 // Update color picker from single values
457                 if (col.isRgbValid()) {
458                     thingHandler.updateChannel(mkChannelId(CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_PICKER),
459                             col.toHSB(), false);
460                 }
461             }
462
463             if ((profile.isRGBW2 && !profile.inColor) || profile.isRoller) {
464                 // Aggregate Meter Data from different Coap updates
465                 int i = 1;
466                 double totalCurrent = 0.0;
467                 double totalKWH = 0.0;
468                 boolean updateMeter = false;
469                 while (i <= thingHandler.getProfile().numMeters) {
470                     String meter = CHANNEL_GROUP_METER + i;
471                     double current = thingHandler.getChannelDouble(meter, CHANNEL_METER_CURRENTWATTS);
472                     double total = thingHandler.getChannelDouble(meter, CHANNEL_METER_TOTALKWH);
473                     logger.debug("{}: {}#{}={}, total={}", thingName, meter, CHANNEL_METER_CURRENTWATTS, current,
474                             totalCurrent);
475                     totalCurrent += current >= 0 ? current : 0;
476                     totalKWH += total >= 0 ? total : 0;
477                     updateMeter |= current >= 0 | total >= 0;
478                     i++;
479                 }
480                 logger.debug("{}: totalCurrent={}, totalKWH={}, update={}", thingName, totalCurrent, totalKWH,
481                         updateMeter);
482                 if (updateMeter) {
483                     thingHandler.updateChannel(CHANNEL_GROUP_METER, CHANNEL_METER_CURRENTWATTS,
484                             toQuantityType(totalCurrent, DIGITS_WATT, Units.WATT));
485                     thingHandler.updateChannel(CHANNEL_GROUP_METER, CHANNEL_LAST_UPDATE, getTimestamp());
486                 }
487             }
488
489             // Old firmware release are lacking various status values, which are not updated using CoIoT.
490             // In this case we keep a refresh so it gets polled using REST. Beginning with Firmware 1.6 most
491             // of the values are available
492             if ((!thingHandler.autoCoIoT && (thingHandler.scheduledUpdates <= 1))
493                     || (thingHandler.autoCoIoT && !profile.isLight && !profile.hasBattery)) {
494                 thingHandler.requestUpdates(1, false);
495             }
496         } else {
497             if (failed == sensorUpdates.size()) {
498                 logger.debug("{}: Device description problem detected, re-discover", thingName);
499                 coiotBound = false;
500                 discover();
501             }
502         }
503
504         // Remember serial, new packets with same serial will be ignored
505         lastSerial = serial;
506         lastPayload = payload;
507     }
508
509     private void discover() {
510         reqDescription = sendRequest(reqDescription, config.deviceIp, COLOIT_URI_DEVDESC, Type.CON);
511     }
512
513     /**
514      * Fix malformed JSON - stupid, but the devices sometimes return malformed JSON with then causes a
515      * JsonSyntaxException
516      *
517      * @param json to be checked/fixed
518      */
519     private static String fixJSON(String payload) {
520         String json = payload;
521         json = json.replace("}{", "},{");
522         json = json.replace("][", "],[");
523         json = json.replace("],,[", "],[");
524         return json;
525     }
526
527     /**
528      * Send a new request (Discovery to get Device Description). Before a pending
529      * request will be canceled.
530      *
531      * @param request The current request (this will be canceled an a new one will
532      *            be created)
533      * @param ipAddress Device's IP address
534      * @param uri The URI we are calling (CoIoT = /cit/d or /cit/s)
535      * @param con true: send as CON, false: send as NON
536      * @return new packet
537      */
538     private Request sendRequest(@Nullable Request request, String ipAddress, String uri, Type con) {
539         if ((request != null) && !request.isCanceled()) {
540             request.cancel();
541         }
542
543         resetSerial();
544         return newRequest(ipAddress, uri, con).send();
545     }
546
547     /**
548      * Allocate a new Request structure. A message observer will be added to get the
549      * callback when a response has been received.
550      *
551      * @param ipAddress IP address of the device
552      * @param uri URI to be addressed
553      * @param uri The URI we are calling (CoIoT = /cit/d or /cit/s)
554      * @param con true: send as CON, false: send as NON
555      * @return new packet
556      */
557
558     private Request newRequest(String ipAddress, String uri, Type con) {
559         // We need to build our own Request to set an empty Token
560         Request request = new Request(Code.GET, con);
561         request.setURI(completeUrl(ipAddress, uri));
562         request.setToken(EMPTY_BYTE);
563         request.addMessageObserver(new MessageObserverAdapter() {
564             @Override
565             public void onResponse(@Nullable Response response) {
566                 processResponse(response);
567             }
568
569             @Override
570             public void onCancel() {
571                 logger.debug("{}: CoAP Request was canceled", thingName);
572             }
573
574             @Override
575             public void onTimeout() {
576                 logger.debug("{}: CoAP Request timed out", thingName);
577             }
578         });
579         return request;
580     }
581
582     /**
583      * Reset serial and payload used to detect duplicate messages, which have to be ignored.
584      * We can't rely that the device manages serials correctly all the time. There are firmware releases sending updated
585      * sensor information with the serial from the last packet, which is wrong. We bypass this problem by comparing also
586      * the payload.
587      */
588     private void resetSerial() {
589         lastSerial = -1;
590         lastPayload = "";
591     }
592
593     public int getVersion() {
594         return coiotVers;
595     }
596
597     /**
598      * Cancel pending requests and shutdown the client
599      */
600     public synchronized void stop() {
601         if (isStarted()) {
602             logger.debug("{}: Stopping CoAP Listener", thingName);
603             coapServer.stop(this);
604             if (statusClient != null) {
605                 statusClient.shutdown();
606                 statusClient = null;
607             }
608             if (!reqDescription.isCanceled()) {
609                 reqDescription.cancel();
610             }
611             if (!reqStatus.isCanceled()) {
612                 reqStatus.cancel();
613             }
614         }
615         resetSerial();
616         coiotBound = false;
617     }
618
619     public void dispose() {
620         stop();
621     }
622
623     private static String completeUrl(String ipAddress, String uri) {
624         return "coap://" + ipAddress + ":" + COIOT_PORT + uri;
625     }
626 }