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