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