]> git.basschouten.com Git - openhab-addons.git/blob
71086077246e82a9258e53646ab43026b1bfa979
[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.mihome.internal.handler;
14
15 import static org.openhab.binding.mihome.internal.XiaomiGatewayBindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.net.InetAddress;
19 import java.net.UnknownHostException;
20 import java.util.ArrayList;
21 import java.util.Collection;
22 import java.util.Collections;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.Set;
26 import java.util.Timer;
27 import java.util.TimerTask;
28 import java.util.concurrent.ConcurrentHashMap;
29 import java.util.concurrent.TimeUnit;
30
31 import org.openhab.binding.mihome.internal.EncryptionHelper;
32 import org.openhab.binding.mihome.internal.XiaomiItemUpdateListener;
33 import org.openhab.binding.mihome.internal.discovery.XiaomiItemDiscoveryService;
34 import org.openhab.binding.mihome.internal.socket.XiaomiBridgeSocket;
35 import org.openhab.binding.mihome.internal.socket.XiaomiSocketListener;
36 import org.openhab.core.config.core.Configuration;
37 import org.openhab.core.config.core.status.ConfigStatusMessage;
38 import org.openhab.core.thing.Bridge;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.ThingTypeUID;
43 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
44 import org.openhab.core.types.Command;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
47
48 import com.google.gson.JsonArray;
49 import com.google.gson.JsonElement;
50 import com.google.gson.JsonObject;
51 import com.google.gson.JsonParser;
52
53 /**
54  * The {@link XiaomiBridgeHandler} is responsible for handling commands, which are
55  * sent to one of the channels for the bridge.
56  *
57  * @author Patrick Boos - Initial contribution
58  * @author Dieter Schmidt - added device update from heartbeat
59  */
60 public class XiaomiBridgeHandler extends ConfigStatusBridgeHandler implements XiaomiSocketListener {
61
62     private static final long READ_ACK_RETENTION_MILLIS = TimeUnit.HOURS.toMillis(2);
63     private static final long BRIDGE_CONNECTION_TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(5);
64
65     private static final String JOIN_PERMISSION = "join_permission";
66     private static final String YES = "yes";
67     private static final String NO = "no";
68
69     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE);
70     private static final JsonParser PARSER = new JsonParser();
71     private static final EncryptionHelper CRYPTER = new EncryptionHelper();
72     private static Map<String, JsonObject> retentionInbox = new ConcurrentHashMap<>();
73
74     private final Logger logger = LoggerFactory.getLogger(XiaomiBridgeHandler.class);
75
76     private List<XiaomiItemUpdateListener> itemListeners = new ArrayList<>();
77     private List<XiaomiItemUpdateListener> itemDiscoveryListeners = new ArrayList<>();
78
79     private String gatewayToken;
80     private long lastDiscoveryTime;
81     private Map<String, Long> lastOnlineMap = new ConcurrentHashMap<>();
82
83     private Configuration config;
84     private InetAddress host;
85     private int port;
86     private XiaomiBridgeSocket socket;
87     private Timer connectionTimeout = new Timer();
88     private boolean timerIsRunning = false;
89
90     public XiaomiBridgeHandler(Bridge bridge) {
91         super(bridge);
92     }
93
94     @Override
95     public Collection<ConfigStatusMessage> getConfigStatus() {
96         // Currently we have no errors. Since we always use discover, it should always be okay.
97         return Collections.emptyList();
98     }
99
100     private class TimerAction extends TimerTask {
101         @Override
102         public synchronized void run() {
103             updateStatus(ThingStatus.OFFLINE);
104             timerIsRunning = false;
105         }
106     }
107
108     synchronized void startTimer() {
109         cancelRunningTimer();
110         connectionTimeout.schedule(new TimerAction(), BRIDGE_CONNECTION_TIMEOUT_MILLIS);
111         timerIsRunning = true;
112     }
113
114     synchronized void cancelRunningTimer() {
115         if (timerIsRunning) {
116             connectionTimeout.cancel();
117             connectionTimeout = new Timer();
118             timerIsRunning = false;
119         }
120     }
121
122     @Override
123     public void initialize() {
124         try {
125             config = getThing().getConfiguration();
126             host = InetAddress.getByName(config.get(HOST).toString());
127             port = getConfigInteger(config, PORT);
128         } catch (UnknownHostException e) {
129             logger.warn("Bridge IP/PORT config is not set or not valid");
130             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR);
131             return;
132         }
133         logger.debug("Init socket on Port: {}", port);
134         socket = new XiaomiBridgeSocket(port, getThing().getUID().getId());
135         socket.initialize();
136         socket.registerListener(this);
137
138         scheduler.schedule(() -> {
139             readDeviceList();
140         }, 1, TimeUnit.SECONDS);
141         startTimer();
142     }
143
144     public void readDeviceList() {
145         sendCommandToBridge("get_id_list");
146     }
147
148     @Override
149     public void dispose() {
150         logger.debug("dispose");
151         socket.unregisterListener(this);
152         super.dispose();
153     }
154
155     @Override
156     public void handleCommand(ChannelUID channelUID, Command command) {
157         logger.debug("Gateway doesn't handle command: {}", command);
158     }
159
160     @Override
161     public void onDataReceived(JsonObject message) {
162         logger.trace("Received message {}", message);
163         String sid = message.has("sid") ? message.get("sid").getAsString() : null;
164         String command = message.get("cmd").getAsString();
165
166         updateDeviceStatus(sid);
167         updateStatus(ThingStatus.ONLINE);
168         startTimer();
169         switch (command) {
170             case "iam":
171                 return;
172             case "heartbeat":
173                 if (message.has("token")) {
174                     this.gatewayToken = message.get("token").getAsString();
175                 }
176                 break;
177             case "get_id_list_ack":
178                 JsonArray devices = PARSER.parse(message.get("data").getAsString()).getAsJsonArray();
179                 for (JsonElement deviceId : devices) {
180                     String device = deviceId.getAsString();
181                     sendCommandToBridge("read", device);
182                 }
183                 // as well get gateway status
184                 sendCommandToBridge("read", getGatewaySid());
185                 return;
186             case "read_ack":
187                 logger.debug("Device {} honored read request", sid);
188                 defer(sid, message);
189                 break;
190             case "write_ack":
191                 logger.debug("Device {} honored write request", sid);
192                 break;
193         }
194         notifyListeners(command, message);
195     }
196
197     private synchronized void defer(String sid, JsonObject message) {
198         synchronized (retentionInbox) {
199             retentionInbox.remove(sid);
200             retentionInbox.put(sid, message);
201         }
202         scheduler.schedule(new RemoveRetentionRunnable(sid), READ_ACK_RETENTION_MILLIS, TimeUnit.MILLISECONDS);
203     }
204
205     private class RemoveRetentionRunnable implements Runnable {
206         private String sid;
207
208         public RemoveRetentionRunnable(String sid) {
209             this.sid = sid;
210         }
211
212         @Override
213         public synchronized void run() {
214             synchronized (retentionInbox) {
215                 retentionInbox.remove(sid);
216             }
217         }
218     }
219
220     public synchronized JsonObject getDeferredMessage(String sid) {
221         synchronized (retentionInbox) {
222             JsonObject ret = retentionInbox.get(sid);
223             if (ret != null) {
224                 retentionInbox.remove(sid);
225             }
226             return ret;
227         }
228     }
229
230     private synchronized void notifyListeners(String command, JsonObject message) {
231         boolean knownDevice = false;
232         String sid = message.get("sid").getAsString();
233
234         // Not a message to pass to any itemListener
235         if (sid == null) {
236             return;
237         }
238         for (XiaomiItemUpdateListener itemListener : itemListeners) {
239             if (sid.equals(itemListener.getItemId())) {
240                 itemListener.onItemUpdate(sid, command, message);
241                 knownDevice = true;
242             }
243         }
244         if (!knownDevice) {
245             for (XiaomiItemUpdateListener itemListener : itemDiscoveryListeners) {
246                 itemListener.onItemUpdate(sid, command, message);
247             }
248         }
249     }
250
251     public synchronized boolean registerItemListener(XiaomiItemUpdateListener listener) {
252         boolean result = false;
253         if (listener == null) {
254             logger.warn("It's not allowed to pass a null XiaomiItemUpdateListener");
255         } else if (listener instanceof XiaomiItemDiscoveryService) {
256             result = !(itemDiscoveryListeners.contains(listener)) ? itemDiscoveryListeners.add(listener) : false;
257             logger.debug("Having {} Item Discovery listeners", itemDiscoveryListeners.size());
258         } else {
259             logger.debug("Adding item listener for device {}", listener.getItemId());
260             result = !(itemListeners.contains(listener)) ? itemListeners.add(listener) : false;
261             logger.debug("Having {} Item listeners", itemListeners.size());
262         }
263         return result;
264     }
265
266     public synchronized boolean unregisterItemListener(XiaomiItemUpdateListener listener) {
267         return itemListeners.remove(listener);
268     }
269
270     private void sendMessageToBridge(String message) {
271         logger.debug("Send to bridge {}: {}", this.getThing().getUID(), message);
272         socket.sendMessage(message, host, port);
273     }
274
275     private void sendCommandToBridge(String cmd) {
276         sendCommandToBridge(cmd, null, null, null);
277     }
278
279     private void sendCommandToBridge(String cmd, String[] keys, Object[] values) {
280         sendCommandToBridge(cmd, null, keys, values);
281     }
282
283     private void sendCommandToBridge(String cmd, String sid) {
284         sendCommandToBridge(cmd, sid, null, null);
285     }
286
287     private void sendCommandToBridge(String cmd, String sid, String[] keys, Object[] values) {
288         StringBuilder message = new StringBuilder("{");
289         message.append("\"cmd\": \"").append(cmd).append("\"");
290         if (sid != null) {
291             message.append(", \"sid\": \"").append(sid).append("\"");
292         }
293         if (keys != null) {
294             for (int i = 0; i < keys.length; i++) {
295                 message.append(", ").append("\"").append(keys[i]).append("\"").append(": ");
296
297                 // write value
298                 message.append(toJsonValue(values[i]));
299             }
300         }
301         message.append("}");
302
303         sendMessageToBridge(message.toString());
304     }
305
306     void writeToDevice(String itemId, String[] keys, Object[] values) {
307         sendCommandToBridge("write", new String[] { "sid", "data" },
308                 new Object[] { itemId, createDataJsonString(keys, values) });
309     }
310
311     void writeToDevice(String itemId, String command) {
312         sendCommandToBridge("write", new String[] { "sid", "data" },
313                 new Object[] { itemId, "{" + command + ", \\\"key\\\": \\\"" + getEncryptedKey() + "\\\"}" });
314     }
315
316     void writeToBridge(String[] keys, Object[] values) {
317         sendCommandToBridge("write", new String[] { "model", "sid", "short_id", "data" },
318                 new Object[] { "gateway", getGatewaySid(), "0", createDataJsonString(keys, values) });
319     }
320
321     private String createDataJsonString(String[] keys, Object[] values) {
322         return "{" + createDataString(keys, values) + ", \\\"key\\\": \\\"" + getEncryptedKey() + "\\\"}";
323     }
324
325     private String getGatewaySid() {
326         return (String) getConfig().get(SERIAL_NUMBER);
327     }
328
329     private String getEncryptedKey() {
330         String key = (String) getConfig().get("key");
331
332         if (key == null) {
333             logger.warn("No key set in the gateway settings. Edit it in the configuration.");
334             return "";
335         }
336         if (gatewayToken == null) {
337             logger.warn("No token received from the gateway yet. Unable to encrypt the access key.");
338             return "";
339         }
340         key = CRYPTER.encrypt(gatewayToken, key);
341         return key;
342     }
343
344     private String createDataString(String[] keys, Object[] values) {
345         StringBuilder builder = new StringBuilder();
346
347         if (keys.length != values.length) {
348             return "";
349         }
350
351         for (int i = 0; i < keys.length; i++) {
352             if (i > 0) {
353                 builder.append(",");
354             }
355
356             // write key
357             builder.append("\\\"").append(keys[i]).append("\\\"").append(": ");
358
359             // write value
360             builder.append(escapeQuotes(toJsonValue(values[i])));
361         }
362         return builder.toString();
363     }
364
365     private String toJsonValue(Object o) {
366         if (o instanceof String) {
367             return "\"" + o + "\"";
368         } else {
369             return o.toString();
370         }
371     }
372
373     private String escapeQuotes(String string) {
374         return string.replaceAll("\"", "\\\\\"");
375     }
376
377     private int getConfigInteger(Configuration config, String key) {
378         Object value = config.get(key);
379         if (value instanceof BigDecimal) {
380             return ((BigDecimal) value).intValue();
381         } else if (value instanceof String) {
382             return Integer.parseInt((String) value);
383         } else {
384             return (Integer) value;
385         }
386     }
387
388     public void discoverItems(long discoveryTimeout) {
389         long lockedFor = discoveryTimeout - (System.currentTimeMillis() - lastDiscoveryTime);
390         if (lockedFor <= 0) {
391             logger.debug("Triggered discovery");
392             readDeviceList();
393             writeToBridge(new String[] { JOIN_PERMISSION }, new Object[] { YES });
394             scheduler.schedule(() -> {
395                 writeToBridge(new String[] { JOIN_PERMISSION }, new Object[] { NO });
396             }, discoveryTimeout, TimeUnit.MILLISECONDS);
397             lastDiscoveryTime = System.currentTimeMillis();
398         } else {
399             logger.debug("A discovery has already been triggered, please wait for {}ms", lockedFor);
400         }
401     }
402
403     boolean hasItemActivity(String itemId, long withinLastMillis) {
404         Long lastOnlineTimeMillis = lastOnlineMap.get(itemId);
405         return lastOnlineTimeMillis != null && System.currentTimeMillis() - lastOnlineTimeMillis < withinLastMillis;
406     }
407
408     private void updateDeviceStatus(String sid) {
409         if (sid != null) {
410             lastOnlineMap.put(sid, System.currentTimeMillis());
411             logger.trace("Updated \"last time seen\" for device {}", sid);
412         }
413     }
414
415     public InetAddress getHost() {
416         return host;
417     }
418 }