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