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