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