]> git.basschouten.com Git - openhab-addons.git/blob
78ea4e8bc6233246ab1838c225da276fc9c6f338
[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.miele.internal.handler;
14
15 import static org.openhab.binding.miele.internal.MieleBindingConstants.*;
16
17 import java.io.BufferedInputStream;
18 import java.io.ByteArrayOutputStream;
19 import java.io.IOException;
20 import java.io.InputStream;
21 import java.io.OutputStream;
22 import java.io.StringReader;
23 import java.net.DatagramPacket;
24 import java.net.HttpURLConnection;
25 import java.net.InetAddress;
26 import java.net.MalformedURLException;
27 import java.net.MulticastSocket;
28 import java.net.SocketTimeoutException;
29 import java.net.URL;
30 import java.net.UnknownHostException;
31 import java.util.ArrayList;
32 import java.util.Collections;
33 import java.util.HashMap;
34 import java.util.Iterator;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Map.Entry;
38 import java.util.Random;
39 import java.util.Set;
40 import java.util.concurrent.ConcurrentHashMap;
41 import java.util.concurrent.CopyOnWriteArrayList;
42 import java.util.concurrent.ExecutorService;
43 import java.util.concurrent.Executors;
44 import java.util.concurrent.Future;
45 import java.util.concurrent.ScheduledFuture;
46 import java.util.concurrent.TimeUnit;
47 import java.util.regex.Pattern;
48 import java.util.zip.GZIPInputStream;
49
50 import org.eclipse.jdt.annotation.NonNull;
51 import org.openhab.binding.miele.internal.FullyQualifiedApplianceIdentifier;
52 import org.openhab.core.common.NamedThreadFactory;
53 import org.openhab.core.thing.Bridge;
54 import org.openhab.core.thing.ChannelUID;
55 import org.openhab.core.thing.Thing;
56 import org.openhab.core.thing.ThingStatus;
57 import org.openhab.core.thing.ThingStatusDetail;
58 import org.openhab.core.thing.ThingTypeUID;
59 import org.openhab.core.thing.binding.BaseBridgeHandler;
60 import org.openhab.core.types.Command;
61 import org.openhab.core.types.RefreshType;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
64
65 import com.google.gson.Gson;
66 import com.google.gson.JsonArray;
67 import com.google.gson.JsonElement;
68 import com.google.gson.JsonObject;
69 import com.google.gson.JsonParser;
70
71 /**
72  * The {@link MieleBridgeHandler} is responsible for handling commands, which are
73  * sent to one of the channels.
74  *
75  * @author Karel Goderis - Initial contribution
76  * @author Kai Kreuzer - Fixed lifecycle issues
77  * @author Martin Lepsy - Added protocol information to support WiFi devices & some refactoring for HomeDevice
78  * @author Jacob Laursen - Fixed multicast and protocol support (ZigBee/LAN)
79  **/
80 public class MieleBridgeHandler extends BaseBridgeHandler {
81
82     @NonNull
83     public static final Set<@NonNull ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_XGW3000);
84
85     private static final String MIELE_CLASS = "com.miele.xgw3000.gateway.hdm.deviceclasses.Miele";
86
87     private static final Pattern IP_PATTERN = Pattern
88             .compile("^(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");
89
90     protected static final int POLLING_PERIOD = 15; // in seconds
91     protected static final int JSON_RPC_PORT = 2810;
92     protected static final String JSON_RPC_MULTICAST_IP1 = "239.255.68.139";
93     protected static final String JSON_RPC_MULTICAST_IP2 = "224.255.68.139";
94     private boolean lastBridgeConnectionState = false;
95     private boolean currentBridgeConnectionState = false;
96
97     protected Random rand = new Random();
98     protected Gson gson = new Gson();
99     private final Logger logger = LoggerFactory.getLogger(MieleBridgeHandler.class);
100
101     protected List<ApplianceStatusListener> applianceStatusListeners = new CopyOnWriteArrayList<>();
102     protected ScheduledFuture<?> pollingJob;
103     protected ExecutorService executor;
104     protected Future<?> eventListenerJob;
105
106     @NonNull
107     protected Map<String, HomeDevice> cachedHomeDevicesByApplianceId = new ConcurrentHashMap<String, HomeDevice>();
108     protected Map<String, HomeDevice> cachedHomeDevicesByRemoteUid = new ConcurrentHashMap<String, HomeDevice>();
109
110     protected URL url;
111     protected Map<String, String> headers;
112
113     // Data structures to de-JSONify whatever Miele appliances are sending us
114     public class HomeDevice {
115
116         private static final String MIELE_APPLIANCE_CLASS = "com.miele.xgw3000.gateway.hdm.deviceclasses.MieleAppliance";
117
118         public String Name;
119         public String Status;
120         public String ParentUID;
121         public String ProtocolAdapterName;
122         public String Vendor;
123         public String UID;
124         public String Type;
125         public JsonArray DeviceClasses;
126         public String Version;
127         public String TimestampAdded;
128         public JsonObject Error;
129         public JsonObject Properties;
130
131         HomeDevice() {
132         }
133
134         public FullyQualifiedApplianceIdentifier getApplianceIdentifier() {
135             return new FullyQualifiedApplianceIdentifier(this.UID);
136         }
137
138         @NonNull
139         public String getSerialNumber() {
140             return Properties.get("serial.number").getAsString();
141         }
142
143         @NonNull
144         public String getRemoteUid() {
145             JsonElement remoteUid = Properties.get("remote.uid");
146             if (remoteUid == null) {
147                 // remote.uid and serial.number seems to be the same. If remote.uid
148                 // is missing for some reason, it makes sense to provide fallback
149                 // to serial number.
150                 return getSerialNumber();
151             }
152             return remoteUid.getAsString();
153         }
154
155         public String getConnectionType() {
156             JsonElement connectionType = Properties.get("connection.type");
157             if (connectionType == null) {
158                 return null;
159             }
160             return connectionType.getAsString();
161         }
162
163         @NonNull
164         public String getApplianceModel() {
165             JsonElement model = Properties.get("miele.model");
166             if (model == null) {
167                 return "";
168             }
169             return model.getAsString();
170         }
171
172         public String getDeviceClass() {
173             for (JsonElement dc : DeviceClasses) {
174                 String dcStr = dc.getAsString();
175                 if (dcStr.contains(MIELE_CLASS) && !dcStr.equals(MIELE_APPLIANCE_CLASS)) {
176                     return dcStr.substring(MIELE_CLASS.length());
177                 }
178             }
179             return null;
180         }
181     }
182
183     public class DeviceClassObject {
184         public String DeviceClassType;
185         public JsonArray Operations;
186         public String DeviceClass;
187         public JsonArray Properties;
188
189         DeviceClassObject() {
190         }
191     }
192
193     public class DeviceOperation {
194         public String Name;
195         public String Arguments;
196         public JsonObject Metadata;
197
198         DeviceOperation() {
199         }
200     }
201
202     public class DeviceProperty {
203         public String Name;
204         public String Value;
205         public int Polling;
206         public JsonObject Metadata;
207
208         DeviceProperty() {
209         }
210     }
211
212     public class DeviceMetaData {
213         public String Filter;
214         public String description;
215         public String LocalizedID;
216         public String LocalizedValue;
217         public JsonObject MieleEnum;
218         public String access;
219     }
220
221     public MieleBridgeHandler(Bridge bridge) {
222         super(bridge);
223     }
224
225     @Override
226     public void initialize() {
227         logger.debug("Initializing the Miele bridge handler.");
228
229         if (getConfig().get(HOST) != null && getConfig().get(INTERFACE) != null) {
230             if (IP_PATTERN.matcher((String) getConfig().get(HOST)).matches()
231                     && IP_PATTERN.matcher((String) getConfig().get(INTERFACE)).matches()) {
232                 try {
233                     url = new URL("http://" + (String) getConfig().get(HOST) + "/remote/json-rpc");
234                 } catch (MalformedURLException e) {
235                     logger.debug("An exception occurred while defining an URL :'{}'", e.getMessage());
236                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, e.getMessage());
237                     return;
238                 }
239
240                 // for future usage - no headers to be set for now
241                 headers = new HashMap<>();
242
243                 onUpdate();
244                 updateStatus(ThingStatus.UNKNOWN);
245             } else {
246                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
247                         "Invalid IP address for the Miele@Home gateway or multicast interface:" + getConfig().get(HOST)
248                                 + "/" + getConfig().get(INTERFACE));
249             }
250         } else {
251             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
252                     "Cannot connect to the Miele gateway. host IP address or multicast interface are not set.");
253         }
254     }
255
256     private Runnable pollingRunnable = new Runnable() {
257         @Override
258         public void run() {
259             if (!IP_PATTERN.matcher((String) getConfig().get(HOST)).matches()) {
260                 logger.debug("Invalid IP address for the Miele@Home gateway : '{}'", getConfig().get(HOST));
261                 return;
262             }
263
264             try {
265                 if (isReachable((String) getConfig().get(HOST))) {
266                     currentBridgeConnectionState = true;
267                 } else {
268                     currentBridgeConnectionState = false;
269                     lastBridgeConnectionState = false;
270                     onConnectionLost();
271                 }
272
273                 if (!lastBridgeConnectionState && currentBridgeConnectionState) {
274                     logger.debug("Connection to Miele Gateway {} established.", getConfig().get(HOST));
275                     lastBridgeConnectionState = true;
276                     onConnectionResumed();
277                 }
278
279                 if (!currentBridgeConnectionState || getThing().getStatus() != ThingStatus.ONLINE) {
280                     return;
281                 }
282
283                 List<HomeDevice> homeDevices = getHomeDevices();
284                 for (HomeDevice hd : homeDevices) {
285                     String key = hd.getApplianceIdentifier().getApplianceId();
286                     if (!cachedHomeDevicesByApplianceId.containsKey(key)) {
287                         logger.debug("A new appliance with ID '{}' has been added", hd.UID);
288                         for (ApplianceStatusListener listener : applianceStatusListeners) {
289                             listener.onApplianceAdded(hd);
290                         }
291                     }
292                     cachedHomeDevicesByApplianceId.put(key, hd);
293                     cachedHomeDevicesByRemoteUid.put(hd.getRemoteUid(), hd);
294                 }
295
296                 @NonNull
297                 Set<@NonNull Entry<String, HomeDevice>> cachedEntries = cachedHomeDevicesByApplianceId.entrySet();
298                 @NonNull
299                 Iterator<@NonNull Entry<String, HomeDevice>> iterator = cachedEntries.iterator();
300
301                 while (iterator.hasNext()) {
302                     Entry<String, HomeDevice> cachedEntry = iterator.next();
303                     HomeDevice cachedHomeDevice = cachedEntry.getValue();
304                     if (!homeDevices.stream().anyMatch(d -> d.UID.equals(cachedHomeDevice.UID))) {
305                         logger.debug("The appliance with ID '{}' has been removed", cachedHomeDevice.UID);
306                         for (ApplianceStatusListener listener : applianceStatusListeners) {
307                             listener.onApplianceRemoved(cachedHomeDevice);
308                         }
309                         cachedHomeDevicesByRemoteUid.remove(cachedHomeDevice.getRemoteUid());
310                         iterator.remove();
311                     }
312                 }
313
314                 for (Thing appliance : getThing().getThings()) {
315                     if (appliance.getStatus() == ThingStatus.ONLINE) {
316                         String applianceId = (String) appliance.getConfiguration().getProperties().get(APPLIANCE_ID);
317                         FullyQualifiedApplianceIdentifier applianceIdentifier = getApplianceIdentifierFromApplianceId(
318                                 applianceId);
319
320                         if (applianceIdentifier == null) {
321                             logger.error("The appliance with ID '{}' was not found in appliance list from bridge.",
322                                     applianceId);
323                             continue;
324                         }
325
326                         Object[] args = new Object[2];
327                         args[0] = applianceIdentifier.getUid();
328                         args[1] = true;
329                         JsonElement result = invokeRPC("HDAccess/getDeviceClassObjects", args);
330
331                         if (result != null) {
332                             for (JsonElement obj : result.getAsJsonArray()) {
333                                 try {
334                                     DeviceClassObject dco = gson.fromJson(obj, DeviceClassObject.class);
335
336                                     // Skip com.prosyst.mbs.services.zigbee.hdm.deviceclasses.ReportingControl
337                                     if (dco == null || !dco.DeviceClass.startsWith(MIELE_CLASS)) {
338                                         continue;
339                                     }
340
341                                     for (ApplianceStatusListener listener : applianceStatusListeners) {
342                                         listener.onApplianceStateChanged(applianceIdentifier, dco);
343                                     }
344                                 } catch (Exception e) {
345                                     logger.debug("An exception occurred while querying an appliance : '{}'",
346                                             e.getMessage());
347                                 }
348                             }
349                         }
350                     }
351                 }
352             } catch (Exception e) {
353                 logger.debug("An exception occurred while polling an appliance :'{}'", e.getMessage());
354             }
355         }
356
357         private boolean isReachable(String ipAddress) {
358             try {
359                 // note that InetAddress.isReachable is unreliable, see
360                 // http://stackoverflow.com/questions/9922543/why-does-inetaddress-isreachable-return-false-when-i-can-ping-the-ip-address
361                 // That's why we do an HTTP access instead
362
363                 // If there is no connection, this line will fail
364                 JsonElement result = invokeRPC("system.listMethods", null);
365                 if (result == null) {
366                     logger.debug("{} is not reachable", ipAddress);
367                     return false;
368                 }
369             } catch (Exception e) {
370                 return false;
371             }
372
373             logger.debug("{} is reachable", ipAddress);
374             return true;
375         }
376     };
377
378     public List<HomeDevice> getHomeDevices() {
379         List<HomeDevice> devices = new ArrayList<>();
380
381         if (getThing().getStatus() == ThingStatus.ONLINE) {
382             try {
383                 String[] args = new String[1];
384                 args[0] = "(type=SuperVision)";
385                 JsonElement result = invokeRPC("HDAccess/getHomeDevices", args);
386
387                 for (JsonElement obj : result.getAsJsonArray()) {
388                     HomeDevice hd = gson.fromJson(obj, HomeDevice.class);
389                     devices.add(hd);
390                 }
391             } catch (Exception e) {
392                 logger.debug("An exception occurred while getting the home devices :'{}'", e.getMessage());
393             }
394         }
395         return devices;
396     }
397
398     private FullyQualifiedApplianceIdentifier getApplianceIdentifierFromApplianceId(String applianceId) {
399         HomeDevice homeDevice = this.cachedHomeDevicesByApplianceId.get(applianceId);
400         if (homeDevice == null) {
401             return null;
402         }
403
404         return homeDevice.getApplianceIdentifier();
405     }
406
407     private Runnable eventListenerRunnable = () -> {
408         if (IP_PATTERN.matcher((String) getConfig().get(INTERFACE)).matches()) {
409             while (true) {
410                 // Get the address that we are going to connect to.
411                 InetAddress address1 = null;
412                 InetAddress address2 = null;
413                 try {
414                     address1 = InetAddress.getByName(JSON_RPC_MULTICAST_IP1);
415                     address2 = InetAddress.getByName(JSON_RPC_MULTICAST_IP2);
416                 } catch (UnknownHostException e) {
417                     logger.debug("An exception occurred while setting up the multicast receiver : '{}'",
418                             e.getMessage());
419                 }
420
421                 byte[] buf = new byte[256];
422                 MulticastSocket clientSocket = null;
423
424                 while (true) {
425                     try {
426                         clientSocket = new MulticastSocket(JSON_RPC_PORT);
427                         clientSocket.setSoTimeout(100);
428
429                         clientSocket.setInterface(InetAddress.getByName((String) getConfig().get(INTERFACE)));
430                         clientSocket.joinGroup(address1);
431                         clientSocket.joinGroup(address2);
432
433                         while (true) {
434                             try {
435                                 buf = new byte[256];
436                                 DatagramPacket packet = new DatagramPacket(buf, buf.length);
437                                 clientSocket.receive(packet);
438
439                                 String event = new String(packet.getData());
440                                 logger.debug("Received a multicast event '{}' from '{}:{}'", event, packet.getAddress(),
441                                         packet.getPort());
442
443                                 DeviceProperty dp = new DeviceProperty();
444                                 String id = null;
445
446                                 String[] parts = event.split("&");
447                                 for (String p : parts) {
448                                     String[] subparts = p.split("=");
449                                     switch (subparts[0]) {
450                                         case "property": {
451                                             dp.Name = subparts[1];
452                                             break;
453                                         }
454                                         case "value": {
455                                             dp.Value = subparts[1].strip().trim();
456                                             break;
457                                         }
458                                         case "id": {
459                                             id = subparts[1];
460                                             break;
461                                         }
462                                     }
463                                 }
464
465                                 if (id == null) {
466                                     continue;
467                                 }
468
469                                 // In XGW 3000 firmware 2.03 this was changed from UID (hdm:ZigBee:0123456789abcdef#210)
470                                 // to serial number (001234567890)
471                                 FullyQualifiedApplianceIdentifier applianceIdentifier;
472                                 if (id.startsWith("hdm:")) {
473                                     applianceIdentifier = new FullyQualifiedApplianceIdentifier(id);
474                                 } else {
475                                     HomeDevice device = cachedHomeDevicesByRemoteUid.get(id);
476                                     if (device == null) {
477                                         logger.debug("Multicast event not handled as id {} is unknown.", id);
478                                         continue;
479                                     }
480                                     applianceIdentifier = device.getApplianceIdentifier();
481                                 }
482                                 for (ApplianceStatusListener listener : applianceStatusListeners) {
483                                     listener.onAppliancePropertyChanged(applianceIdentifier, dp);
484                                 }
485                             } catch (SocketTimeoutException e) {
486                                 try {
487                                     Thread.sleep(500);
488                                 } catch (InterruptedException ex) {
489                                     logger.debug("Eventlistener has been interrupted.");
490                                     break;
491                                 }
492                             }
493                         }
494                     } catch (Exception ex) {
495                         logger.debug("An exception occurred while receiving multicast packets : '{}'", ex.getMessage());
496                     }
497
498                     // restart the cycle with a clean slate
499                     try {
500                         if (clientSocket != null) {
501                             clientSocket.leaveGroup(address1);
502                             clientSocket.leaveGroup(address2);
503                         }
504                     } catch (IOException e) {
505                         logger.debug("An exception occurred while leaving multicast group : '{}'", e.getMessage());
506                     }
507                     if (clientSocket != null) {
508                         clientSocket.close();
509                     }
510                 }
511             }
512         } else {
513             logger.debug("Invalid IP address for the multicast interface : '{}'", getConfig().get(INTERFACE));
514         }
515     };
516
517     public JsonElement invokeOperation(String applianceId, String modelID, String methodName) {
518         if (getThing().getStatus() != ThingStatus.ONLINE) {
519             logger.debug("The Bridge is offline - operations can not be invoked.");
520             return null;
521         }
522
523         FullyQualifiedApplianceIdentifier applianceIdentifier = getApplianceIdentifierFromApplianceId(applianceId);
524         if (applianceIdentifier == null) {
525             logger.error(
526                     "The appliance with ID '{}' was not found in appliance list from bridge - operations can not be invoked.",
527                     applianceId);
528             return null;
529         }
530
531         Object[] args = new Object[4];
532         args[0] = applianceIdentifier.getUid();
533         args[1] = MIELE_CLASS + modelID;
534         args[2] = methodName;
535         args[3] = null;
536
537         return invokeRPC("HDAccess/invokeDCOOperation", args);
538     }
539
540     protected JsonElement invokeRPC(String methodName, Object[] args) {
541         int id = rand.nextInt(Integer.MAX_VALUE);
542
543         JsonObject req = new JsonObject();
544         req.addProperty("jsonrpc", "2.0");
545         req.addProperty("id", id);
546         req.addProperty("method", methodName);
547
548         JsonElement result = null;
549
550         JsonArray params = new JsonArray();
551         if (args != null) {
552             for (Object o : args) {
553                 params.add(gson.toJsonTree(o));
554             }
555         }
556         req.add("params", params);
557
558         String requestData = req.toString();
559         String responseData = null;
560         try {
561             responseData = post(url, headers, requestData);
562         } catch (Exception e) {
563             logger.debug("An exception occurred while posting data : '{}'", e.getMessage());
564         }
565
566         if (responseData != null) {
567             logger.debug("The request '{}' yields '{}'", requestData, responseData);
568             JsonObject resp = (JsonObject) JsonParser.parseReader(new StringReader(responseData));
569
570             result = resp.get("result");
571             JsonElement error = resp.get("error");
572
573             if (error != null && !error.isJsonNull()) {
574                 if (error.isJsonPrimitive()) {
575                     logger.debug("A remote exception occurred: '{}'", error.getAsString());
576                 } else if (error.isJsonObject()) {
577                     JsonObject o = error.getAsJsonObject();
578                     Integer code = (o.has("code") ? o.get("code").getAsInt() : null);
579                     String message = (o.has("message") ? o.get("message").getAsString() : null);
580                     String data = (o.has("data") ? (o.get("data") instanceof JsonObject ? o.get("data").toString()
581                             : o.get("data").getAsString()) : null);
582                     logger.debug("A remote exception occurred: '{}':'{}':'{}'", code, message, data);
583                 } else {
584                     logger.debug("An unknown remote exception occurred: '{}'", error.toString());
585                 }
586             }
587         }
588
589         return result;
590     }
591
592     protected String post(URL url, Map<String, String> headers, String data) throws IOException {
593         HttpURLConnection connection = (HttpURLConnection) url.openConnection();
594
595         if (headers != null) {
596             for (Map.Entry<String, String> entry : headers.entrySet()) {
597                 connection.addRequestProperty(entry.getKey(), entry.getValue());
598             }
599         }
600
601         connection.addRequestProperty("Accept-Encoding", "gzip");
602
603         connection.setRequestMethod("POST");
604         connection.setDoOutput(true);
605         connection.connect();
606
607         OutputStream out = null;
608
609         try {
610             out = connection.getOutputStream();
611
612             out.write(data.getBytes());
613             out.flush();
614
615             int statusCode = connection.getResponseCode();
616             if (statusCode != HttpURLConnection.HTTP_OK) {
617                 logger.debug("An unexpected status code was returned: '{}'", statusCode);
618             }
619         } finally {
620             if (out != null) {
621                 out.close();
622             }
623         }
624
625         String responseEncoding = connection.getHeaderField("Content-Encoding");
626         responseEncoding = (responseEncoding == null ? "" : responseEncoding.trim());
627
628         ByteArrayOutputStream bos = new ByteArrayOutputStream();
629
630         InputStream in = connection.getInputStream();
631         try {
632             in = connection.getInputStream();
633             if ("gzip".equalsIgnoreCase(responseEncoding)) {
634                 in = new GZIPInputStream(in);
635             }
636             in = new BufferedInputStream(in);
637
638             byte[] buff = new byte[1024];
639             int n;
640             while ((n = in.read(buff)) > 0) {
641                 bos.write(buff, 0, n);
642             }
643             bos.flush();
644             bos.close();
645         } finally {
646             if (in != null) {
647                 in.close();
648             }
649         }
650
651         return bos.toString();
652     }
653
654     private synchronized void onUpdate() {
655         logger.debug("Scheduling the Miele polling job");
656         if (pollingJob == null || pollingJob.isCancelled()) {
657             logger.trace("Scheduling the Miele polling job period is {}", POLLING_PERIOD);
658             pollingJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 0, POLLING_PERIOD, TimeUnit.SECONDS);
659             logger.trace("Scheduling the Miele polling job Job is done ?{}", pollingJob.isDone());
660         }
661         logger.debug("Scheduling the Miele event listener job");
662
663         if (eventListenerJob == null || eventListenerJob.isCancelled()) {
664             executor = Executors.newSingleThreadExecutor(new NamedThreadFactory("binding-miele"));
665             eventListenerJob = executor.submit(eventListenerRunnable);
666         }
667     }
668
669     /**
670      * This method is called whenever the connection to the given {@link MieleBridge} is lost.
671      *
672      */
673     public void onConnectionLost() {
674         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR);
675     }
676
677     /**
678      * This method is called whenever the connection to the given {@link MieleBridge} is resumed.
679      *
680      * @param bridge the Miele bridge the connection is resumed to
681      */
682     public void onConnectionResumed() {
683         updateStatus(ThingStatus.ONLINE);
684         for (Thing thing : getThing().getThings()) {
685             MieleApplianceHandler<?> handler = (MieleApplianceHandler<?>) thing.getHandler();
686             if (handler != null) {
687                 handler.onBridgeConnectionResumed();
688             }
689         }
690     }
691
692     public boolean registerApplianceStatusListener(ApplianceStatusListener applianceStatusListener) {
693         if (applianceStatusListener == null) {
694             throw new IllegalArgumentException("It's not allowed to pass a null ApplianceStatusListener.");
695         }
696         boolean result = applianceStatusListeners.add(applianceStatusListener);
697         if (result && isInitialized()) {
698             onUpdate();
699
700             for (HomeDevice hd : getHomeDevices()) {
701                 applianceStatusListener.onApplianceAdded(hd);
702             }
703         }
704         return result;
705     }
706
707     public boolean unregisterApplianceStatusListener(ApplianceStatusListener applianceStatusListener) {
708         boolean result = applianceStatusListeners.remove(applianceStatusListener);
709         if (result && isInitialized()) {
710             onUpdate();
711         }
712         return result;
713     }
714
715     @Override
716     public void handleCommand(ChannelUID channelUID, Command command) {
717         // Nothing to do here - the XGW bridge does not handle commands, for now
718         if (command instanceof RefreshType) {
719             // Placeholder for future refinement
720             return;
721         }
722     }
723
724     @Override
725     public void dispose() {
726         super.dispose();
727         if (pollingJob != null) {
728             pollingJob.cancel(true);
729             pollingJob = null;
730         }
731         if (eventListenerJob != null) {
732             eventListenerJob.cancel(true);
733             eventListenerJob = null;
734         }
735         if (executor != null) {
736             executor.shutdownNow();
737             executor = null;
738         }
739     }
740 }