2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.shelly.internal.api1;
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.*;
19 import java.net.SocketException;
20 import java.net.UnknownHostException;
21 import java.util.LinkedHashMap;
22 import java.util.List;
24 import java.util.TreeMap;
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;
57 import com.google.gson.Gson;
58 import com.google.gson.GsonBuilder;
59 import com.google.gson.JsonSyntaxException;
62 * The {@link Shelly1CoapHandler} handles the CoIoT/CoAP registration and events.
64 * @author Markus Michels - Initial contribution
67 public class Shelly1CoapHandler implements Shelly1CoapListener {
68 private static final byte[] EMPTY_BYTE = new byte[0];
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;
77 private boolean coiotBound = false;
78 private Shelly1CoIoTInterface coiot;
79 private int coiotVers = -1;
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;
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;
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
103 gsonBuilder.registerTypeAdapter(CoIotDevDescription.class, new CoIotDevDescrTypeAdapter());
104 gsonBuilder.registerTypeAdapter(CoIotGenericSensorList.class, new CoIotSensorTypeAdapter());
105 gson = gsonBuilder.create();
109 * Initialize CoAP access, send discovery packet and start Status server
111 * @param thingName Thing name derived from Thing Type/hostname
112 * @param config ShellyThingConfiguration
113 * @throws ShellyApiException
115 public synchronized void start(String thingName, ShellyThingConfiguration config) throws ShellyApiException {
117 this.thingName = thingName;
118 this.config = config;
119 this.profile = thingHandler.getProfile();
121 logger.trace("{}: CoAP Listener was already started", thingName);
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);
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());
134 Endpoint endpoint = null;
135 CoapClient client = statusClient;
136 if (client != null) {
137 endpoint = client.getEndpoint();
139 if ((endpoint == null) || !endpoint.isStarted()) {
140 logger.warn("{}: Unable to initialize CoAP access (network error)", thingName);
141 throw new ShellyApiException("Network initialization failed");
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);
154 public boolean isStarted() {
155 return statusClient != null;
159 * Process an inbound Response (or mapped Request): decode CoAP options. handle discovery result or status updates
161 * @param response The Response packet
164 public void processResponse(@Nullable Response response) {
165 if (response == null) {
166 thingHandler.incProtErrors();
167 return; // other device instance
169 ResponseCode code = response.getCode();
170 if (code != ResponseCode.CONTENT) {
172 logger.debug("{}: Unknown Response Code {} received, payload={}", thingName, code,
173 response.getPayloadString());
174 thingHandler.incProtErrors();
178 List<Option> options = response.getOptions().asSortedList();
179 String ip = response.getSourceContext().getPeerAddress().toString();
180 boolean match = ip.contains("/" + config.deviceIp + ":");
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())) {
207 thingHandler.incProtMessages();
208 if (logger.isDebugEnabled()) {
209 logger.debug("{}: CoIoT Message from {} (MID={}): {}", thingName,
210 response.getSourceContext().getPeerAddress(), response.getMID(), response.getPayloadString());
212 if (thingHandler.isStopping()) {
213 logger.debug("{}: Thing is shutting down, ignore CoIOT message", thingName);
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();
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();
229 case OptionNumberRegistry.URI_HOST: // ignore
231 case OptionNumberRegistry.CONTENT_FORMAT: // ignore
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();
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);
251 logger.warn("{}: Unsupported CoAP version detected: {}", thingName, sVersion);
254 coiotVers = iVersion;
258 case COIOT_OPTION_STATUS_VALIDITY:
260 case COIOT_OPTION_STATUS_SERIAL:
261 serial = opt.getIntegerValue();
264 logger.debug("{} ({}): COAP option {} with value {} skipped", thingName, devId, opt.getNumber(),
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);
276 // If we received a CoAP message successful the thing must be online
277 thingHandler.setThingOnline();
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);
287 // fixed malformed JSON :-(
288 payload = fixJSON(payload);
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);
297 } catch (ShellyApiException e) {
298 logger.debug("{}: Unable to process CoIoT message: {}", thingName, e.toString());
299 thingHandler.incProtErrors();
302 if (!updatesRequested) {
303 // Observe Status Updates
304 reqStatus = sendRequest(reqStatus, config.deviceIp, COLOIT_URI_DEVSTATUS, Type.NON);
305 updatesRequested = true;
307 } catch (JsonSyntaxException | IllegalArgumentException | NullPointerException e) {
308 logger.debug("{}: Unable to process CoIoT Message for payload={}", thingName, payload, e);
310 thingHandler.incProtErrors();
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.
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"}]}]}
323 private void handleDeviceDescription(String devId, String payload) throws ShellyApiException {
324 logger.debug("{}: CoIoT Device Description for {}: {}", thingName, devId, payload);
327 boolean valid = true;
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);
337 blkMap.replace(blk.id, blk);
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,
343 CoIotDescrSen sen = new CoIotDescrSen();
347 sen.range = blk.range;
348 sen.links = blk.links;
349 valid &= addSensor(sen);
353 // Save to thing properties
354 thingHandler.updateProperties(PROPERTY_COAP_DESCR, payload);
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));
362 coiot.completeMissingSensorDefinition(sensorMap);
366 "{}: Incompatible device description detected for CoIoT version {} (id length mismatch), discarding!",
367 thingName, coiot.getVersion());
372 } catch (JsonSyntaxException e) {
373 logger.warn("{}: Unable to parse CoAP Device Description! JSON={}", thingName, payload);
374 } catch (NullPointerException | IllegalArgumentException e) {
375 logger.warn("{}: Unable to parse CoAP Device Description! JSON={}", thingName, payload, e);
380 * Add a new sensor to the sensor table
382 * @param sen CoIotDescrSen of the sensor
384 private synchronized boolean addSensor(CoIotDescrSen sen) {
385 logger.debug("{}: id {}: {}, Type={}, Range={}, Links={}", thingName, sen.id, sen.desc, sen.type, sen.range,
387 // CoIoT version 2 changes from 3 digit IDs to 4 digit IDs
388 // We need to make sure that the persisted device description matches,
389 // otherwise the stored one is discarded and a new discovery is triggered
390 // This happens on firmware up/downgrades (version 1.8 brings CoIoT v2 with 4 digit IDs)
391 int vers = coiot.getVersion();
392 if (((vers == COIOT_VERSION_1) && (sen.id.length() > 3))
393 || ((vers >= COIOT_VERSION_2) && (sen.id.length() < 4))) {
394 logger.debug("{}: Invalid format for sensor defition detected, id={}", thingName, sen.id);
399 CoIotDescrSen fixed = coiot.fixDescription(sen, blkMap);
400 if (!sensorMap.containsKey(fixed.id)) {
401 sensorMap.put(sen.id, fixed);
403 sensorMap.replace(sen.id, fixed);
405 } catch (NullPointerException | IllegalArgumentException e) { // depending on firmware release the CoAP device
406 // description is buggy
407 logger.debug("{}: Unable to decode sensor definition -> skip", thingName, e);
414 * Process CoIoT status update message. If a status update is received, but the device description has not been
415 * received yet a GET is send to query device description.
417 * @param devId device id included in the status packet
418 * @param payload CoAP payload (Json format), example: {"G":[[0,112,0]]}
419 * @param serial Serial for this request. If this the the same as last serial
420 * the update was already sent and processed so this one gets
422 * @throws ShellyApiException
424 private void handleStatusUpdate(String devId, String payload, int serial) throws ShellyApiException {
425 logger.debug("{}: CoIoT Sensor data {} (serial={})", thingName, payload, serial);
426 if (blkMap.isEmpty()) {
427 // send discovery packet
431 // try to uses description from last initialization
432 String savedDescr = thingHandler.getProperty(PROPERTY_COAP_DESCR);
433 if (savedDescr.isEmpty()) {
434 logger.debug("{}: Device description not yet received, trigger auto-initialization", thingName);
438 // simulate received device description to create element table
439 logger.debug("{}: Device description for {} restored: {}", thingName, devId, savedDescr);
440 handleDeviceDescription(devId, savedDescr);
444 CoIotGenericSensorList list = fromJson(gson, fixJSON(payload), CoIotGenericSensorList.class);
445 if (list.generic == null) {
446 logger.debug("{}: Sensor list has invalid format! Payload: {}", devId, payload);
450 List<CoIotSensor> sensorUpdates = list.generic;
451 Map<String, State> updates = new TreeMap<String, State>();
452 logger.debug("{}: {} CoAP sensor updates received", thingName, sensorUpdates.size());
454 ShellyColorUtils col = new ShellyColorUtils();
455 for (int i = 0; i < sensorUpdates.size(); i++) {
457 CoIotSensor s = sensorUpdates.get(i);
458 CoIotDescrSen sen = sensorMap.get(s.id);
460 logger.debug("{}: Unable to sensor definition for id={}, payload={}", thingName, s.id, payload);
463 // find matching sensor definition from device description, use the Link ID as index
464 CoIotDescrBlk element = null;
465 sen = coiot.fixDescription(sen, blkMap);
466 element = blkMap.get(sen.links);
467 if (element == null) {
468 logger.debug("{}: Unable to find BLK for link {} from sen.id={}, payload={}", thingName, sen.links,
472 logger.trace("{}: Sensor value[{}]: id={}, Value={} ({}, Type={}, Range={}, Link={}: {})", thingName,
473 i, s.id, getString(s.valueStr).isEmpty() ? s.value : s.valueStr, sen.desc, sen.type, sen.range,
474 sen.links, element.desc);
476 if (!coiot.handleStatusUpdate(sensorUpdates, sen, serial, s, updates, col)) {
477 logger.debug("{}: CoIoT data for id {}, type {}/{} not processed, value={}; payload={}", thingName,
478 sen.id, sen.type, sen.desc, s.value, payload);
480 } catch (NullPointerException | IllegalArgumentException e) {
481 // even the processing of one value failed we continue with the next one (sometimes this is caused by
482 // buggy formats provided by the device
483 logger.debug("{}: Unable to process data from sensor[{}], devId={}, payload={}", thingName, i, devId,
488 if (!updates.isEmpty()) {
490 for (Map.Entry<String, State> u : updates.entrySet()) {
491 String key = u.getKey();
492 updated += thingHandler.updateChannel(key, u.getValue(), false) ? 1 : 0;
495 logger.debug("{}: {} channels updated from CoIoT status, serial={}", thingName, updated, serial);
496 if (profile.isSensor || profile.isRoller) {
497 // CoAP is currently lacking the lastUpdate info, so we use host timestamp
498 thingHandler.updateChannel(profile.getControlGroup(0), CHANNEL_LAST_UPDATE, getTimestamp());
502 if (profile.isLight && profile.inColor && col.isRgbValid()) {
503 // Update color picker from single values
504 if (col.isRgbValid()) {
505 thingHandler.updateChannel(mkChannelId(CHANNEL_GROUP_COLOR_CONTROL, CHANNEL_COLOR_PICKER),
510 if ((profile.isRGBW2 && !profile.inColor) || profile.isRoller) {
511 // Aggregate Meter Data from different Coap updates
513 double totalCurrent = 0.0;
514 @SuppressWarnings("unused")
515 double totalKWH = 0.0;
516 boolean updateMeter = false;
517 while (i <= thingHandler.getProfile().numMeters) {
518 String meter = CHANNEL_GROUP_METER + i;
519 double current = thingHandler.getChannelDouble(meter, CHANNEL_METER_CURRENTWATTS);
520 double total = thingHandler.getChannelDouble(meter, CHANNEL_METER_TOTALKWH);
521 totalCurrent += current >= 0 ? current : 0;
522 totalKWH += total >= 0 ? total : 0;
523 updateMeter |= current >= 0 | total >= 0;
527 thingHandler.updateChannel(CHANNEL_GROUP_METER, CHANNEL_METER_CURRENTWATTS,
528 toQuantityType(totalCurrent, DIGITS_WATT, Units.WATT));
529 thingHandler.updateChannel(CHANNEL_GROUP_METER, CHANNEL_LAST_UPDATE, getTimestamp());
533 // Old firmware release are lacking various status values, which are not updated using CoIoT.
534 // In this case we keep a refresh so it gets polled using REST. Beginning with Firmware 1.6 most
535 // of the values are available
536 thingHandler.triggerUpdateFromCoap();
538 if (failed == sensorUpdates.size()) {
539 logger.debug("{}: Device description problem detected, re-discover", thingName);
545 // Remember serial, new packets with same serial will be ignored
547 lastPayload = payload;
550 private void discover() {
551 if (coiot.getVersion() >= 2) {
554 // Try to device description using http request (FW 1.10+)
555 String payload = api.getCoIoTDescription();
556 if (!payload.isEmpty()) {
557 logger.debug("{}: Using CoAP device description from successful HTTP /cit/d", thingName);
558 handleDeviceDescription(thingName, payload);
561 } catch (ShellyApiException e) {
562 // ignore if not supported by device
566 reqDescription = sendRequest(reqDescription, config.deviceIp, COLOIT_URI_DEVDESC, Type.CON);
570 * Fix malformed JSON - stupid, but the devices sometimes return malformed JSON with then causes a
571 * JsonSyntaxException
573 * @param json to be checked/fixed
575 private static String fixJSON(String payload) {
576 String json = payload;
577 json = json.replace("}{", "},{");
578 json = json.replace("][", "],[");
579 json = json.replace("],,[", "],[");
584 * Send a new request (Discovery to get Device Description). Before a pending
585 * request will be canceled.
587 * @param request The current request (this will be canceled an a new one will
589 * @param ipAddress Device's IP address
590 * @param uri The URI we are calling (CoIoT = /cit/d or /cit/s)
591 * @param con true: send as CON, false: send as NON
594 private Request sendRequest(@Nullable Request request, String ipAddress, String uri, Type con) {
595 if ((request != null) && !request.isCanceled()) {
600 return newRequest(ipAddress, coiotPort, uri, con).send();
604 * Allocate a new Request structure. A message observer will be added to get the
605 * callback when a response has been received.
607 * @param ipAddress IP address of the device
608 * @param uri URI to be addressed
609 * @param uri The URI we are calling (CoIoT = /cit/d or /cit/s)
610 * @param con true: send as CON, false: send as NON
614 private Request newRequest(String ipAddress, int port, String uri, Type con) {
615 // We need to build our own Request to set an empty Token
616 Request request = new Request(Code.GET, con);
617 request.setURI(completeUrl(ipAddress, port, uri));
618 request.setToken(EMPTY_BYTE);
619 request.addMessageObserver(new MessageObserverAdapter() {
621 public void onResponse(@Nullable Response response) {
622 processResponse(response);
626 public void onCancel() {
627 logger.debug("{}: CoAP Request was canceled", thingName);
631 public void onTimeout() {
632 logger.debug("{}: CoAP Request timed out", thingName);
639 * Reset serial and payload used to detect duplicate messages, which have to be ignored.
640 * We can't rely that the device manages serials correctly all the time. There are firmware releases sending updated
641 * sensor information with the serial from the last packet, which is wrong. We bypass this problem by comparing also
644 private void resetSerial() {
649 public int getVersion() {
654 * Cancel pending requests and shutdown the client
656 public synchronized void stop() {
658 logger.debug("{}: Stopping CoAP Listener", thingName);
659 coapServer.stop(this);
660 CoapClient cclient = statusClient;
661 if (cclient != null) {
665 Request request = reqDescription;
666 if (!request.isCanceled()) {
670 if (!request.isCanceled()) {
678 public void dispose() {
682 private static String completeUrl(String ipAddress, int port, String uri) {
683 return "coap://" + ipAddress + ":" + port + uri;