2 * Copyright (c) 2010-2020 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.miio.internal.handler;
15 import static org.openhab.binding.miio.internal.MiIoBindingConstants.*;
17 import java.io.IOException;
18 import java.time.LocalDateTime;
19 import java.util.HashMap;
21 import java.util.concurrent.ConcurrentHashMap;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.miio.internal.Message;
28 import org.openhab.binding.miio.internal.MiIoBindingConfiguration;
29 import org.openhab.binding.miio.internal.MiIoCommand;
30 import org.openhab.binding.miio.internal.MiIoCrypto;
31 import org.openhab.binding.miio.internal.MiIoCryptoException;
32 import org.openhab.binding.miio.internal.MiIoDevices;
33 import org.openhab.binding.miio.internal.MiIoInfoDTO;
34 import org.openhab.binding.miio.internal.MiIoMessageListener;
35 import org.openhab.binding.miio.internal.MiIoSendCommand;
36 import org.openhab.binding.miio.internal.Utils;
37 import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService;
38 import org.openhab.binding.miio.internal.transport.MiIoAsyncCommunication;
39 import org.openhab.core.cache.ExpiringCache;
40 import org.openhab.core.config.core.Configuration;
41 import org.openhab.core.library.types.DecimalType;
42 import org.openhab.core.library.types.StringType;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.Thing;
45 import org.openhab.core.thing.ThingStatus;
46 import org.openhab.core.thing.ThingStatusDetail;
47 import org.openhab.core.thing.ThingTypeUID;
48 import org.openhab.core.thing.binding.BaseThingHandler;
49 import org.openhab.core.thing.binding.builder.ThingBuilder;
50 import org.openhab.core.types.Command;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
54 import com.google.gson.Gson;
55 import com.google.gson.GsonBuilder;
56 import com.google.gson.JsonObject;
57 import com.google.gson.JsonParser;
60 * The {@link MiIoAbstractHandler} is responsible for handling commands, which are
61 * sent to one of the channels.
63 * @author Marcel Verpaalen - Initial contribution
66 public abstract class MiIoAbstractHandler extends BaseThingHandler implements MiIoMessageListener {
67 protected static final int MAX_QUEUE = 5;
68 protected static final Gson GSON = new GsonBuilder().create();
70 protected @Nullable ScheduledFuture<?> pollingJob;
71 protected MiIoDevices miDevice = MiIoDevices.UNKNOWN;
72 protected boolean isIdentified;
74 protected final JsonParser parser = new JsonParser();
75 protected byte[] token = new byte[0];
77 protected @Nullable MiIoBindingConfiguration configuration;
78 protected @Nullable MiIoAsyncCommunication miioCom;
81 protected Map<Integer, String> cmds = new ConcurrentHashMap<>();
82 protected Map<String, Object> deviceVariables = new HashMap<>();
83 protected final ExpiringCache<String> network = new ExpiringCache<>(CACHE_EXPIRY_NETWORK, () -> {
84 int ret = sendCommand(MiIoCommand.MIIO_INFO);
90 protected static final long CACHE_EXPIRY = TimeUnit.SECONDS.toMillis(5);
91 protected static final long CACHE_EXPIRY_NETWORK = TimeUnit.SECONDS.toMillis(60);
93 private final Logger logger = LoggerFactory.getLogger(MiIoAbstractHandler.class);
94 protected MiIoDatabaseWatchService miIoDatabaseWatchService;
96 public MiIoAbstractHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService) {
98 this.miIoDatabaseWatchService = miIoDatabaseWatchService;
102 public abstract void handleCommand(ChannelUID channelUID, Command command);
105 public void initialize() {
106 logger.debug("Initializing Mi IO device handler '{}' with thingType {}", getThing().getUID(),
107 getThing().getThingTypeUID());
108 final MiIoBindingConfiguration configuration = getConfigAs(MiIoBindingConfiguration.class);
109 this.configuration = configuration;
110 if (configuration.host == null || configuration.host.isEmpty()) {
111 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
112 "IP address required. Configure IP address");
115 if (!tokenCheckPass(configuration.token)) {
116 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Token required. Configure token");
119 isIdentified = false;
120 scheduler.schedule(this::initializeData, 1, TimeUnit.SECONDS);
121 int pollingPeriod = configuration.refreshInterval;
122 if (pollingPeriod > 0) {
123 pollingJob = scheduler.scheduleWithFixedDelay(() -> {
126 } catch (Exception e) {
127 logger.debug("Unexpected error during refresh.", e);
129 }, 10, pollingPeriod, TimeUnit.SECONDS);
130 logger.debug("Polling job scheduled to run every {} sec. for '{}'", pollingPeriod, getThing().getUID());
132 logger.debug("Polling job disabled. for '{}'", getThing().getUID());
133 scheduler.schedule(this::updateData, 10, TimeUnit.SECONDS);
135 updateStatus(ThingStatus.OFFLINE);
138 private boolean tokenCheckPass(@Nullable String tokenSting) {
139 if (tokenSting == null) {
142 switch (tokenSting.length()) {
144 token = tokenSting.getBytes();
147 if (!IGNORED_TOKENS.contains(tokenSting)) {
148 token = Utils.hexStringToByteArray(tokenSting);
154 token = Utils.hexStringToByteArray(MiIoCrypto.decryptToken(Utils.hexStringToByteArray(tokenSting)));
155 logger.debug("IOS token decrypted to {}", Utils.getHex(token));
156 } catch (MiIoCryptoException e) {
157 logger.warn("Could not decrypt token {}{}", tokenSting, e.getMessage());
167 public void dispose() {
168 logger.debug("Disposing Xiaomi Mi IO handler '{}'", getThing().getUID());
169 final ScheduledFuture<?> pollingJob = this.pollingJob;
170 if (pollingJob != null) {
171 pollingJob.cancel(true);
172 this.pollingJob = null;
174 final @Nullable MiIoAsyncCommunication miioCom = this.miioCom;
175 if (miioCom != null) {
176 lastId = miioCom.getId();
177 miioCom.unregisterListener(this);
183 protected int sendCommand(MiIoCommand command) {
184 return sendCommand(command, "[]");
187 protected int sendCommand(MiIoCommand command, String params) {
189 final MiIoAsyncCommunication connection = getConnection();
190 return (connection != null) ? connection.queueCommand(command, params) : 0;
191 } catch (MiIoCryptoException | IOException e) {
192 logger.debug("Command {} for {} failed (type: {}): {}", command.toString(), getThing().getUID(),
193 getThing().getThingTypeUID(), e.getLocalizedMessage());
199 * This is used to execute arbitrary commands by sending to the commands channel. Command parameters to be added
201 * [] brackets. This to allow for unimplemented commands to be executed (e.g. get detailed historical cleaning
204 * @param commandString command to be executed
205 * @return vacuum response
207 protected int sendCommand(String commandString) {
208 final MiIoAsyncCommunication connection = getConnection();
210 String command = commandString.trim();
212 int sb = command.indexOf("[");
213 int cb = command.indexOf("{");
214 logger.debug("locs {}, {}", sb, cb);
215 if (Math.max(sb, cb) > 0) {
216 int loc = (Math.min(sb, cb) > 0 ? Math.min(sb, cb) : Math.max(sb, cb));
217 param = command.substring(loc).trim();
218 command = command.substring(0, loc).trim();
220 return (connection != null) ? connection.queueCommand(command, param) : 0;
221 } catch (MiIoCryptoException | IOException e) {
222 disconnected(e.getMessage());
227 protected boolean skipUpdate() {
228 final MiIoAsyncCommunication miioCom = this.miioCom;
229 if (!hasConnection() || miioCom == null) {
230 logger.debug("Skipping periodic update for '{}'. No Connection", getThing().getUID().toString());
233 if (getThing().getStatusInfo().getStatusDetail().equals(ThingStatusDetail.CONFIGURATION_ERROR)) {
234 logger.debug("Skipping periodic update for '{}'. Thing Status {}", getThing().getUID().toString(),
235 getThing().getStatusInfo().getStatusDetail());
237 miioCom.queueCommand(MiIoCommand.MIIO_INFO);
238 } catch (MiIoCryptoException | IOException e) {
243 if (miioCom.getQueueLength() > MAX_QUEUE) {
244 logger.debug("Skipping periodic update for '{}'. {} elements in queue.", getThing().getUID().toString(),
245 miioCom.getQueueLength());
251 protected abstract void updateData();
253 protected boolean updateNetwork(JsonObject networkData) {
255 updateState(CHANNEL_SSID, new StringType(networkData.getAsJsonObject("ap").get("ssid").getAsString()));
256 updateState(CHANNEL_BSSID, new StringType(networkData.getAsJsonObject("ap").get("bssid").getAsString()));
257 if (networkData.getAsJsonObject("ap").get("rssi") != null) {
258 updateState(CHANNEL_RSSI, new DecimalType(networkData.getAsJsonObject("ap").get("rssi").getAsLong()));
259 } else if (networkData.getAsJsonObject("ap").get("wifi_rssi") != null) {
260 updateState(CHANNEL_RSSI,
261 new DecimalType(networkData.getAsJsonObject("ap").get("wifi_rssi").getAsLong()));
263 logger.debug("No RSSI info in response");
265 updateState(CHANNEL_LIFE, new DecimalType(networkData.get("life").getAsLong()));
267 } catch (Exception e) {
268 logger.debug("Could not parse network response: {}", networkData, e);
273 protected boolean hasConnection() {
274 return getConnection() != null;
277 protected void disconnectedNoResponse() {
278 disconnected("No Response from device");
281 protected void disconnected(@Nullable String message) {
282 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
283 message != null ? message : "");
284 final MiIoAsyncCommunication miioCom = this.miioCom;
285 if (miioCom != null) {
286 lastId = miioCom.getId();
291 protected synchronized @Nullable MiIoAsyncCommunication getConnection() {
292 if (miioCom != null) {
295 final MiIoBindingConfiguration configuration = getConfigAs(MiIoBindingConfiguration.class);
296 if (configuration.host == null || configuration.host.isEmpty()) {
300 String deviceId = configuration.deviceId;
302 if (deviceId != null && deviceId.length() == 8 && tokenCheckPass(configuration.token)) {
303 logger.debug("Ping Mi device {} at {}", deviceId, configuration.host);
304 final MiIoAsyncCommunication miioCom = new MiIoAsyncCommunication(configuration.host, token,
305 Utils.hexStringToByteArray(deviceId), lastId, configuration.timeout);
306 Message miIoResponse = miioCom.sendPing(configuration.host);
307 if (miIoResponse != null) {
308 logger.debug("Ping response from device {} at {}. Time stamp: {}, OH time {}, delta {}",
309 Utils.getHex(miIoResponse.getDeviceId()), configuration.host, miIoResponse.getTimestamp(),
310 LocalDateTime.now(), miioCom.getTimeDelta());
311 miioCom.registerListener(this);
312 this.miioCom = miioCom;
318 logger.debug("No device ID defined. Retrieving Mi device ID");
319 final MiIoAsyncCommunication miioCom = new MiIoAsyncCommunication(configuration.host, token,
320 new byte[0], lastId, configuration.timeout);
321 Message miIoResponse = miioCom.sendPing(configuration.host);
322 if (miIoResponse != null) {
323 logger.debug("Ping response from device {} at {}. Time stamp: {}, OH time {}, delta {}",
324 Utils.getHex(miIoResponse.getDeviceId()), configuration.host, miIoResponse.getTimestamp(),
325 LocalDateTime.now(), miioCom.getTimeDelta());
326 deviceId = Utils.getHex(miIoResponse.getDeviceId());
327 logger.debug("Ping response from device {} at {}. Time stamp: {}, OH time {}, delta {}", deviceId,
328 configuration.host, miIoResponse.getTimestamp(), LocalDateTime.now(),
329 miioCom.getTimeDelta());
330 miioCom.setDeviceId(miIoResponse.getDeviceId());
331 logger.debug("Using retrieved Mi device ID: {}", deviceId);
332 updateDeviceIdConfig(deviceId);
333 miioCom.registerListener(this);
334 this.miioCom = miioCom;
340 logger.debug("Ping response from device {} at {} FAILED", configuration.deviceId, configuration.host);
341 disconnectedNoResponse();
343 } catch (IOException e) {
344 logger.debug("Could not connect to {} at {}", getThing().getUID().toString(), configuration.host);
345 disconnected(e.getMessage());
350 private void updateDeviceIdConfig(String deviceId) {
351 if (!deviceId.isEmpty()) {
352 updateProperty(Thing.PROPERTY_SERIAL_NUMBER, deviceId);
353 Configuration config = editConfiguration();
354 config.put(PROPERTY_DID, deviceId);
355 updateConfiguration(config);
357 logger.debug("Could not update config with device ID: {}", deviceId);
361 protected boolean initializeData() {
362 this.miioCom = getConnection();
366 protected void refreshNetwork() {
370 protected void defineDeviceType(JsonObject miioInfo) {
371 updateProperties(miioInfo);
372 isIdentified = updateThingType(miioInfo);
375 private void updateProperties(JsonObject miioInfo) {
376 final MiIoInfoDTO info = GSON.fromJson(miioInfo, MiIoInfoDTO.class);
377 Map<String, String> properties = editProperties();
378 if (info.model != null) {
379 properties.put(Thing.PROPERTY_MODEL_ID, info.model);
381 if (info.fwVer != null) {
382 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, info.fwVer);
384 if (info.hwVer != null) {
385 properties.put(Thing.PROPERTY_HARDWARE_VERSION, info.hwVer);
387 if (info.wifiFwVer != null) {
388 properties.put("wifiFirmware", info.wifiFwVer);
390 if (info.mcuFwVer != null) {
391 properties.put("mcuFirmware", info.mcuFwVer);
393 deviceVariables.putAll(properties);
394 updateProperties(properties);
397 protected boolean updateThingType(JsonObject miioInfo) {
398 MiIoBindingConfiguration configuration = getConfigAs(MiIoBindingConfiguration.class);
399 String model = miioInfo.get("model").getAsString();
400 miDevice = MiIoDevices.getType(model);
401 if (configuration.model == null || configuration.model.isEmpty()) {
402 Configuration config = editConfiguration();
403 config.put(PROPERTY_MODEL, model);
404 updateConfiguration(config);
405 configuration = getConfigAs(MiIoBindingConfiguration.class);
407 if (!configuration.model.equals(model)) {
408 logger.info("Mi Device model {} has model config: {}. Unexpected unless manual override", model,
409 configuration.model);
411 if (miDevice.getThingType().equals(getThing().getThingTypeUID())
412 && !(miDevice.getThingType().equals(THING_TYPE_UNSUPPORTED)
413 && miIoDatabaseWatchService.getDatabaseUrl(model) != null)) {
414 logger.debug("Mi Device model {} identified as: {}. Matches thingtype {}", model, miDevice.toString(),
415 miDevice.getThingType().toString());
418 if (getThing().getThingTypeUID().equals(THING_TYPE_MIIO)
419 || getThing().getThingTypeUID().equals(THING_TYPE_UNSUPPORTED)) {
423 "Mi Device model {} identified as: {}, thingtype {}. Does not matches thingtype {}. Unexpected, unless manual override.",
424 miDevice.toString(), miDevice.getThingType(), getThing().getThingTypeUID().toString(),
425 miDevice.getThingType().toString());
433 * Changes the {@link org.openhab.core.thing.type.ThingType} to the right type once it is retrieved from
436 * @param modelId String with the model id
438 private void changeType(final String modelId) {
439 final ScheduledFuture<?> pollingJob = this.pollingJob;
440 if (pollingJob != null) {
441 pollingJob.cancel(true);
442 this.pollingJob = null;
444 scheduler.schedule(() -> {
445 ThingBuilder thingBuilder = editThing();
446 thingBuilder.withLabel(miDevice.getDescription());
447 updateThing(thingBuilder.build());
448 logger.info("Mi Device model {} identified as: {}. Does not match thingtype {}. Changing thingtype to {}",
449 modelId, miDevice.toString(), getThing().getThingTypeUID().toString(),
450 miDevice.getThingType().toString());
451 ThingTypeUID thingTypeUID = MiIoDevices.getType(modelId).getThingType();
452 if (thingTypeUID.equals(THING_TYPE_UNSUPPORTED)
453 && miIoDatabaseWatchService.getDatabaseUrl(modelId) != null) {
454 thingTypeUID = THING_TYPE_BASIC;
456 changeThingType(thingTypeUID, getConfig());
457 }, 10, TimeUnit.SECONDS);
461 public void onStatusUpdated(ThingStatus status, ThingStatusDetail statusDetail) {
462 updateStatus(status, statusDetail);
466 public void onMessageReceived(MiIoSendCommand response) {
467 logger.debug("Received response for {} type: {}, result: {}, fullresponse: {}", getThing().getUID().getId(),
468 response.getCommand(), response.getResult(), response.getResponse());
469 if (response.isError()) {
470 logger.debug("Error received: {}", response.getResponse().get("error"));
471 if (MiIoCommand.MIIO_INFO.equals(response.getCommand())) {
472 network.invalidateValue();
477 switch (response.getCommand()) {
480 defineDeviceType(response.getResult().getAsJsonObject());
482 updateNetwork(response.getResult().getAsJsonObject());
487 if (cmds.containsKey(response.getId())) {
488 updateState(CHANNEL_COMMAND, new StringType(response.getResponse().toString()));
489 cmds.remove(response.getId());
491 } catch (Exception e) {
492 logger.debug("Error while handing message {}", response.getResponse(), e);