]> git.basschouten.com Git - openhab-addons.git/blob
e2761d8ebb34975cdf3649b9310b0937f269bf6c
[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                                 Thread.sleep(500);
402                             }
403                         }
404                     } catch (Exception ex) {
405                         logger.debug("An exception occurred while receiving multicast packets : '{}'", ex.getMessage());
406                     }
407
408                     // restart the cycle with a clean slate
409                     try {
410                         if (clientSocket != null) {
411                             clientSocket.leaveGroup(address1);
412                             clientSocket.leaveGroup(address2);
413                         }
414                     } catch (IOException e) {
415                         logger.debug("An exception occurred while leaving multicast group : '{}'", e.getMessage());
416                     }
417                     if (clientSocket != null) {
418                         clientSocket.close();
419                     }
420                 }
421             }
422         } else {
423             logger.debug("Invalid IP address for the multicast interface : '{}'", getConfig().get(INTERFACE));
424         }
425     };
426
427     public JsonElement invokeOperation(String UID, String modelID, String methodName) {
428         return invokeOperation(UID, modelID, methodName, HDM_ZIGBEE);
429     }
430
431     public JsonElement invokeOperation(String UID, String modelID, String methodName, String protocol) {
432         if (getThing().getStatus() == ThingStatus.ONLINE) {
433             Object[] args = new Object[4];
434             args[0] = protocol + UID;
435             args[1] = "com.miele.xgw3000.gateway.hdm.deviceclasses.Miele" + modelID;
436             args[2] = methodName;
437             args[3] = null;
438             return invokeRPC("HDAccess/invokeDCOOperation", args);
439         } else {
440             logger.debug("The Bridge is offline - operations can not be invoked.");
441             return null;
442         }
443     }
444
445     protected JsonElement invokeRPC(String methodName, Object[] args) {
446         int id = rand.nextInt(Integer.MAX_VALUE);
447
448         JsonObject req = new JsonObject();
449         req.addProperty("jsonrpc", "2.0");
450         req.addProperty("id", id);
451         req.addProperty("method", methodName);
452
453         JsonElement result = null;
454
455         JsonArray params = new JsonArray();
456         if (args != null) {
457             for (Object o : args) {
458                 params.add(gson.toJsonTree(o));
459             }
460         }
461         req.add("params", params);
462
463         String requestData = req.toString();
464         String responseData = null;
465         try {
466             responseData = post(url, headers, requestData);
467         } catch (Exception e) {
468             logger.debug("An exception occurred while posting data : '{}'", e.getMessage());
469         }
470
471         if (responseData != null) {
472             logger.debug("The request '{}' yields '{}'", requestData, responseData);
473             JsonParser parser = new JsonParser();
474             JsonObject resp = (JsonObject) parser.parse(new StringReader(responseData));
475
476             result = resp.get("result");
477             JsonElement error = resp.get("error");
478
479             if (error != null && !error.isJsonNull()) {
480                 if (error.isJsonPrimitive()) {
481                     logger.debug("A remote exception occurred: '{}'", error.getAsString());
482                 } else if (error.isJsonObject()) {
483                     JsonObject o = error.getAsJsonObject();
484                     Integer code = (o.has("code") ? o.get("code").getAsInt() : null);
485                     String message = (o.has("message") ? o.get("message").getAsString() : null);
486                     String data = (o.has("data") ? (o.get("data") instanceof JsonObject ? o.get("data").toString()
487                             : o.get("data").getAsString()) : null);
488                     logger.debug("A remote exception occurred: '{}':'{}':'{}'", code, message, data);
489                 } else {
490                     logger.debug("An unknown remote exception occurred: '{}'", error.toString());
491                 }
492             }
493         }
494
495         return result;
496     }
497
498     protected String post(URL url, Map<String, String> headers, String data) throws IOException {
499         HttpURLConnection connection = (HttpURLConnection) url.openConnection();
500
501         if (headers != null) {
502             for (Map.Entry<String, String> entry : headers.entrySet()) {
503                 connection.addRequestProperty(entry.getKey(), entry.getValue());
504             }
505         }
506
507         connection.addRequestProperty("Accept-Encoding", "gzip");
508
509         connection.setRequestMethod("POST");
510         connection.setDoOutput(true);
511         connection.connect();
512
513         OutputStream out = null;
514
515         try {
516             out = connection.getOutputStream();
517
518             out.write(data.getBytes());
519             out.flush();
520
521             int statusCode = connection.getResponseCode();
522             if (statusCode != HttpURLConnection.HTTP_OK) {
523                 logger.debug("An unexpected status code was returned: '{}'", statusCode);
524             }
525         } finally {
526             if (out != null) {
527                 out.close();
528             }
529         }
530
531         String responseEncoding = connection.getHeaderField("Content-Encoding");
532         responseEncoding = (responseEncoding == null ? "" : responseEncoding.trim());
533
534         ByteArrayOutputStream bos = new ByteArrayOutputStream();
535
536         InputStream in = connection.getInputStream();
537         try {
538             in = connection.getInputStream();
539             if ("gzip".equalsIgnoreCase(responseEncoding)) {
540                 in = new GZIPInputStream(in);
541             }
542             in = new BufferedInputStream(in);
543
544             byte[] buff = new byte[1024];
545             int n;
546             while ((n = in.read(buff)) > 0) {
547                 bos.write(buff, 0, n);
548             }
549             bos.flush();
550             bos.close();
551         } finally {
552             if (in != null) {
553                 in.close();
554             }
555         }
556
557         return bos.toString();
558     }
559
560     private synchronized void onUpdate() {
561         logger.debug("Scheduling the Miele polling job");
562         if (pollingJob == null || pollingJob.isCancelled()) {
563             logger.trace("Scheduling the Miele polling job period is {}", POLLING_PERIOD);
564             pollingJob = scheduler.scheduleWithFixedDelay(pollingRunnable, 0, POLLING_PERIOD, TimeUnit.SECONDS);
565             logger.trace("Scheduling the Miele polling job Job is done ?{}", pollingJob.isDone());
566         }
567         logger.debug("Scheduling the Miele event listener job");
568
569         if (eventListenerJob == null || eventListenerJob.isCancelled()) {
570             eventListenerJob = scheduler.schedule(eventListenerRunnable, 0, TimeUnit.SECONDS);
571         }
572     }
573
574     /**
575      * This method is called whenever the connection to the given {@link MieleBridge} is lost.
576      *
577      */
578     public void onConnectionLost() {
579         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR);
580     }
581
582     /**
583      * This method is called whenever the connection to the given {@link MieleBridge} is resumed.
584      *
585      * @param bridge the hue bridge the connection is resumed to
586      */
587     public void onConnectionResumed() {
588         updateStatus(ThingStatus.ONLINE);
589         for (Thing thing : getThing().getThings()) {
590             MieleApplianceHandler<?> handler = (MieleApplianceHandler<?>) thing.getHandler();
591             if (handler != null) {
592                 handler.onBridgeConnectionResumed();
593             }
594         }
595     }
596
597     public boolean registerApplianceStatusListener(ApplianceStatusListener applianceStatusListener) {
598         if (applianceStatusListener == null) {
599             throw new IllegalArgumentException("It's not allowed to pass a null ApplianceStatusListener.");
600         }
601         boolean result = applianceStatusListeners.add(applianceStatusListener);
602         if (result && isInitialized()) {
603             onUpdate();
604
605             for (HomeDevice hd : getHomeDevices()) {
606                 applianceStatusListener.onApplianceAdded(hd);
607             }
608         }
609         return result;
610     }
611
612     public boolean unregisterApplianceStatusListener(ApplianceStatusListener applianceStatusListener) {
613         boolean result = applianceStatusListeners.remove(applianceStatusListener);
614         if (result && isInitialized()) {
615             onUpdate();
616         }
617         return result;
618     }
619
620     @Override
621     public void handleCommand(ChannelUID channelUID, Command command) {
622         // Nothing to do here - the XGW bridge does not handle commands, for now
623         if (command instanceof RefreshType) {
624             // Placeholder for future refinement
625             return;
626         }
627     }
628
629     @Override
630     public void dispose() {
631         super.dispose();
632         if (pollingJob != null) {
633             pollingJob.cancel(true);
634             pollingJob = null;
635         }
636     }
637 }