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