2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.mihome.internal.handler;
15 import static org.openhab.binding.mihome.internal.XiaomiGatewayBindingConstants.*;
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;
26 import java.util.Timer;
27 import java.util.TimerTask;
28 import java.util.concurrent.ConcurrentHashMap;
29 import java.util.concurrent.TimeUnit;
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;
48 import com.google.gson.JsonArray;
49 import com.google.gson.JsonElement;
50 import com.google.gson.JsonObject;
51 import com.google.gson.JsonParser;
54 * The {@link XiaomiBridgeHandler} is responsible for handling commands, which are
55 * sent to one of the channels for the bridge.
57 * @author Patrick Boos - Initial contribution
58 * @author Dieter Schmidt - added device update from heartbeat
60 public class XiaomiBridgeHandler extends ConfigStatusBridgeHandler implements XiaomiSocketListener {
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);
65 private static final String JOIN_PERMISSION = "join_permission";
66 private static final String YES = "yes";
67 private static final String NO = "no";
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<>();
74 private final Logger logger = LoggerFactory.getLogger(XiaomiBridgeHandler.class);
76 private List<XiaomiItemUpdateListener> itemListeners = new ArrayList<>();
77 private List<XiaomiItemUpdateListener> itemDiscoveryListeners = new ArrayList<>();
79 private String gatewayToken;
80 private long lastDiscoveryTime;
81 private Map<String, Long> lastOnlineMap = new ConcurrentHashMap<>();
83 private Configuration config;
84 private InetAddress host;
86 private XiaomiBridgeSocket socket;
87 private Timer connectionTimeout = new Timer();
88 private boolean timerIsRunning = false;
90 public XiaomiBridgeHandler(Bridge bridge) {
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();
100 private class TimerAction extends TimerTask {
102 public synchronized void run() {
103 updateStatus(ThingStatus.OFFLINE);
104 timerIsRunning = false;
108 synchronized void startTimer() {
109 cancelRunningTimer();
110 connectionTimeout.schedule(new TimerAction(), BRIDGE_CONNECTION_TIMEOUT_MILLIS);
111 timerIsRunning = true;
114 synchronized void cancelRunningTimer() {
115 if (timerIsRunning) {
116 connectionTimeout.cancel();
117 connectionTimeout = new Timer();
118 timerIsRunning = false;
123 public void initialize() {
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);
133 logger.debug("Init socket on Port: {}", port);
134 socket = new XiaomiBridgeSocket(port, getThing().getUID().getId());
136 socket.registerListener(this);
138 scheduler.schedule(() -> {
140 }, 1, TimeUnit.SECONDS);
144 public void readDeviceList() {
145 sendCommandToBridge("get_id_list");
149 public void dispose() {
150 logger.debug("dispose");
151 socket.unregisterListener(this);
156 public void handleCommand(ChannelUID channelUID, Command command) {
157 logger.debug("Gateway doesn't handle command: {}", command);
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();
166 updateDeviceStatus(sid);
167 updateStatus(ThingStatus.ONLINE);
173 if (message.has("token")) {
174 this.gatewayToken = message.get("token").getAsString();
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);
183 // as well get gateway status
184 sendCommandToBridge("read", getGatewaySid());
187 logger.debug("Device {} honored read request", sid);
191 logger.debug("Device {} honored write request", sid);
194 notifyListeners(command, message);
197 private synchronized void defer(String sid, JsonObject message) {
198 synchronized (retentionInbox) {
199 retentionInbox.remove(sid);
200 retentionInbox.put(sid, message);
202 scheduler.schedule(new RemoveRetentionRunnable(sid), READ_ACK_RETENTION_MILLIS, TimeUnit.MILLISECONDS);
205 private class RemoveRetentionRunnable implements Runnable {
208 public RemoveRetentionRunnable(String sid) {
213 public synchronized void run() {
214 synchronized (retentionInbox) {
215 retentionInbox.remove(sid);
220 public synchronized JsonObject getDeferredMessage(String sid) {
221 synchronized (retentionInbox) {
222 JsonObject ret = retentionInbox.get(sid);
224 retentionInbox.remove(sid);
230 private synchronized void notifyListeners(String command, JsonObject message) {
231 boolean knownDevice = false;
232 String sid = message.get("sid").getAsString();
234 // Not a message to pass to any itemListener
238 for (XiaomiItemUpdateListener itemListener : itemListeners) {
239 if (sid.equals(itemListener.getItemId())) {
240 itemListener.onItemUpdate(sid, command, message);
245 for (XiaomiItemUpdateListener itemListener : itemDiscoveryListeners) {
246 itemListener.onItemUpdate(sid, command, message);
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());
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());
266 public synchronized boolean unregisterItemListener(XiaomiItemUpdateListener listener) {
267 return itemListeners.remove(listener);
270 private void sendMessageToBridge(String message) {
271 logger.debug("Send to bridge {}: {}", this.getThing().getUID(), message);
272 socket.sendMessage(message, host, port);
275 private void sendCommandToBridge(String cmd) {
276 sendCommandToBridge(cmd, null, null, null);
279 private void sendCommandToBridge(String cmd, String[] keys, Object[] values) {
280 sendCommandToBridge(cmd, null, keys, values);
283 private void sendCommandToBridge(String cmd, String sid) {
284 sendCommandToBridge(cmd, sid, null, null);
287 private void sendCommandToBridge(String cmd, String sid, String[] keys, Object[] values) {
288 StringBuilder message = new StringBuilder("{");
289 message.append("\"cmd\": \"").append(cmd).append("\"");
291 message.append(", \"sid\": \"").append(sid).append("\"");
294 for (int i = 0; i < keys.length; i++) {
295 message.append(", ").append("\"").append(keys[i]).append("\"").append(": ");
298 message.append(toJsonValue(values[i]));
303 sendMessageToBridge(message.toString());
306 void writeToDevice(String itemId, String[] keys, Object[] values) {
307 sendCommandToBridge("write", new String[] { "sid", "data" },
308 new Object[] { itemId, createDataJsonString(keys, values) });
311 void writeToDevice(String itemId, String command) {
312 sendCommandToBridge("write", new String[] { "sid", "data" },
313 new Object[] { itemId, "{" + command + ", \\\"key\\\": \\\"" + getEncryptedKey() + "\\\"}" });
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) });
321 private String createDataJsonString(String[] keys, Object[] values) {
322 return "{" + createDataString(keys, values) + ", \\\"key\\\": \\\"" + getEncryptedKey() + "\\\"}";
325 private String getGatewaySid() {
326 return (String) getConfig().get(SERIAL_NUMBER);
329 private String getEncryptedKey() {
330 String key = (String) getConfig().get("key");
333 logger.warn("No key set in the gateway settings. Edit it in the configuration.");
336 if (gatewayToken == null) {
337 logger.warn("No token received from the gateway yet. Unable to encrypt the access key.");
340 key = CRYPTER.encrypt(gatewayToken, key);
344 private String createDataString(String[] keys, Object[] values) {
345 StringBuilder builder = new StringBuilder();
347 if (keys.length != values.length) {
351 for (int i = 0; i < keys.length; i++) {
357 builder.append("\\\"").append(keys[i]).append("\\\"").append(": ");
360 builder.append(escapeQuotes(toJsonValue(values[i])));
362 return builder.toString();
365 private String toJsonValue(Object o) {
366 if (o instanceof String) {
367 return "\"" + o + "\"";
373 private String escapeQuotes(String string) {
374 return string.replaceAll("\"", "\\\\\"");
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);
384 return (Integer) value;
388 public void discoverItems(long discoveryTimeout) {
389 long lockedFor = discoveryTimeout - (System.currentTimeMillis() - lastDiscoveryTime);
390 if (lockedFor <= 0) {
391 logger.debug("Triggered discovery");
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();
399 logger.debug("A discovery has already been triggered, please wait for {}ms", lockedFor);
403 boolean hasItemActivity(String itemId, long withinLastMillis) {
404 Long lastOnlineTimeMillis = lastOnlineMap.get(itemId);
405 return lastOnlineTimeMillis != null && System.currentTimeMillis() - lastOnlineTimeMillis < withinLastMillis;
408 private void updateDeviceStatus(String sid) {
410 lastOnlineMap.put(sid, System.currentTimeMillis());
411 logger.trace("Updated \"last time seen\" for device {}", sid);
415 public InetAddress getHost() {