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