2 * Copyright (c) 2010-2023 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 = Set.of(THING_TYPE_BRIDGE);
70 private static final EncryptionHelper CRYPTER = new EncryptionHelper();
71 private static Map<String, JsonObject> retentionInbox = new ConcurrentHashMap<>();
73 private final Logger logger = LoggerFactory.getLogger(XiaomiBridgeHandler.class);
75 private List<XiaomiItemUpdateListener> itemListeners = new ArrayList<>();
76 private List<XiaomiItemUpdateListener> itemDiscoveryListeners = new ArrayList<>();
78 private String gatewayToken;
79 private long lastDiscoveryTime;
80 private Map<String, Long> lastOnlineMap = new ConcurrentHashMap<>();
82 private Configuration config;
83 private InetAddress host;
85 private XiaomiBridgeSocket socket;
86 private Timer connectionTimeout = new Timer();
87 private boolean timerIsRunning = false;
89 public XiaomiBridgeHandler(Bridge bridge) {
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();
99 private class TimerAction extends TimerTask {
101 public synchronized void run() {
102 updateStatus(ThingStatus.OFFLINE);
103 timerIsRunning = false;
107 synchronized void startTimer() {
108 cancelRunningTimer();
109 connectionTimeout.schedule(new TimerAction(), BRIDGE_CONNECTION_TIMEOUT_MILLIS);
110 timerIsRunning = true;
113 synchronized void cancelRunningTimer() {
114 if (timerIsRunning) {
115 connectionTimeout.cancel();
116 connectionTimeout = new Timer();
117 timerIsRunning = false;
122 public void initialize() {
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);
132 logger.debug("Init socket on Port: {}", port);
133 socket = new XiaomiBridgeSocket(port, (String) config.get(INTERFACE), getThing().getUID().getId());
135 socket.registerListener(this);
137 scheduler.schedule(() -> {
139 }, 1, TimeUnit.SECONDS);
143 public void readDeviceList() {
144 sendCommandToBridge("get_id_list");
148 public void dispose() {
149 logger.debug("dispose");
150 socket.unregisterListener(this);
155 public void handleCommand(ChannelUID channelUID, Command command) {
156 logger.debug("Gateway doesn't handle command: {}", command);
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();
165 updateDeviceStatus(sid);
166 updateStatus(ThingStatus.ONLINE);
172 if (message.has("token")) {
173 this.gatewayToken = message.get("token").getAsString();
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);
182 // as well get gateway status
183 sendCommandToBridge("read", getGatewaySid());
186 logger.debug("Device {} honored read request", sid);
190 logger.debug("Device {} honored write request", sid);
193 notifyListeners(command, message);
196 private synchronized void defer(String sid, JsonObject message) {
197 synchronized (retentionInbox) {
198 retentionInbox.remove(sid);
199 retentionInbox.put(sid, message);
201 scheduler.schedule(new RemoveRetentionRunnable(sid), READ_ACK_RETENTION_MILLIS, TimeUnit.MILLISECONDS);
204 private class RemoveRetentionRunnable implements Runnable {
207 public RemoveRetentionRunnable(String sid) {
212 public synchronized void run() {
213 synchronized (retentionInbox) {
214 retentionInbox.remove(sid);
219 public synchronized JsonObject getDeferredMessage(String sid) {
220 synchronized (retentionInbox) {
221 JsonObject ret = retentionInbox.get(sid);
223 retentionInbox.remove(sid);
229 private synchronized void notifyListeners(String command, JsonObject message) {
230 boolean knownDevice = false;
231 String sid = message.get("sid").getAsString();
233 // Not a message to pass to any itemListener
237 for (XiaomiItemUpdateListener itemListener : itemListeners) {
238 if (sid.equals(itemListener.getItemId())) {
239 itemListener.onItemUpdate(sid, command, message);
244 for (XiaomiItemUpdateListener itemListener : itemDiscoveryListeners) {
245 itemListener.onItemUpdate(sid, command, message);
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());
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());
265 public synchronized boolean unregisterItemListener(XiaomiItemUpdateListener listener) {
266 return itemListeners.remove(listener);
269 private void sendMessageToBridge(String message) {
270 logger.debug("Send to bridge {}: {}", this.getThing().getUID(), message);
271 socket.sendMessage(message, host, port);
274 private void sendCommandToBridge(String cmd) {
275 sendCommandToBridge(cmd, null, null, null);
278 private void sendCommandToBridge(String cmd, String[] keys, Object[] values) {
279 sendCommandToBridge(cmd, null, keys, values);
282 private void sendCommandToBridge(String cmd, String sid) {
283 sendCommandToBridge(cmd, sid, null, null);
286 private void sendCommandToBridge(String cmd, String sid, String[] keys, Object[] values) {
287 StringBuilder message = new StringBuilder("{");
288 message.append("\"cmd\": \"").append(cmd).append("\"");
290 message.append(", \"sid\": \"").append(sid).append("\"");
293 for (int i = 0; i < keys.length; i++) {
294 message.append(", ").append("\"").append(keys[i]).append("\"").append(": ");
297 message.append(toJsonValue(values[i]));
302 sendMessageToBridge(message.toString());
305 void writeToDevice(String itemId, String[] keys, Object[] values) {
306 sendCommandToBridge("write", new String[] { "sid", "data" },
307 new Object[] { itemId, createDataJsonString(keys, values) });
310 void writeToDevice(String itemId, String command) {
311 sendCommandToBridge("write", new String[] { "sid", "data" },
312 new Object[] { itemId, "{" + command + ", \\\"key\\\": \\\"" + getEncryptedKey() + "\\\"}" });
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) });
320 private String createDataJsonString(String[] keys, Object[] values) {
321 return "{" + createDataString(keys, values) + ", \\\"key\\\": \\\"" + getEncryptedKey() + "\\\"}";
324 private String getGatewaySid() {
325 return (String) getConfig().get(SERIAL_NUMBER);
328 private String getEncryptedKey() {
329 String key = (String) getConfig().get("key");
332 logger.warn("No key set in the gateway settings. Edit it in the configuration.");
335 if (gatewayToken == null) {
336 logger.warn("No token received from the gateway yet. Unable to encrypt the access key.");
339 key = CRYPTER.encrypt(gatewayToken, key);
343 private String createDataString(String[] keys, Object[] values) {
344 StringBuilder builder = new StringBuilder();
346 if (keys.length != values.length) {
350 for (int i = 0; i < keys.length; i++) {
356 builder.append("\\\"").append(keys[i]).append("\\\"").append(": ");
359 builder.append(escapeQuotes(toJsonValue(values[i])));
361 return builder.toString();
364 private String toJsonValue(Object o) {
365 if (o instanceof String) {
366 return "\"" + o + "\"";
372 private String escapeQuotes(String string) {
373 return string.replace("\"", "\\\\\"");
376 private int getConfigInteger(Configuration config, String key) {
377 Object value = config.get(key);
378 if (value instanceof BigDecimal decimal) {
379 return decimal.intValue();
380 } else if (value instanceof String str) {
381 return Integer.parseInt(str);
383 return (Integer) value;
387 public void discoverItems(long discoveryTimeout) {
388 long lockedFor = discoveryTimeout - (System.currentTimeMillis() - lastDiscoveryTime);
389 if (lockedFor <= 0) {
390 logger.debug("Triggered discovery");
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();
398 logger.debug("A discovery has already been triggered, please wait for {}ms", lockedFor);
402 boolean hasItemActivity(String itemId, long withinLastMillis) {
403 Long lastOnlineTimeMillis = lastOnlineMap.get(itemId);
404 return lastOnlineTimeMillis != null && System.currentTimeMillis() - lastOnlineTimeMillis < withinLastMillis;
407 private void updateDeviceStatus(String sid) {
409 lastOnlineMap.put(sid, System.currentTimeMillis());
410 logger.trace("Updated \"last time seen\" for device {}", sid);
414 public InetAddress getHost() {