]> git.basschouten.com Git - openhab-addons.git/blob
b97d68cd10d867d969ce2dea2dac9a7346db697f
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.miio.internal.handler;
14
15 import static org.openhab.binding.miio.internal.MiIoBindingConstants.*;
16
17 import java.io.IOException;
18 import java.math.BigDecimal;
19 import java.time.Instant;
20 import java.time.LocalDateTime;
21 import java.util.HashMap;
22 import java.util.Map;
23 import java.util.Map.Entry;
24 import java.util.concurrent.ConcurrentHashMap;
25 import java.util.concurrent.ScheduledExecutorService;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.ScheduledThreadPoolExecutor;
28 import java.util.concurrent.TimeUnit;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.openhab.binding.miio.internal.Message;
33 import org.openhab.binding.miio.internal.MiIoBindingConfiguration;
34 import org.openhab.binding.miio.internal.MiIoCommand;
35 import org.openhab.binding.miio.internal.MiIoCrypto;
36 import org.openhab.binding.miio.internal.MiIoCryptoException;
37 import org.openhab.binding.miio.internal.MiIoDevices;
38 import org.openhab.binding.miio.internal.MiIoInfoApDTO;
39 import org.openhab.binding.miio.internal.MiIoInfoDTO;
40 import org.openhab.binding.miio.internal.MiIoMessageListener;
41 import org.openhab.binding.miio.internal.MiIoSendCommand;
42 import org.openhab.binding.miio.internal.Utils;
43 import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService;
44 import org.openhab.binding.miio.internal.cloud.CloudConnector;
45 import org.openhab.binding.miio.internal.transport.MiIoAsyncCommunication;
46 import org.openhab.core.cache.ExpiringCache;
47 import org.openhab.core.common.NamedThreadFactory;
48 import org.openhab.core.config.core.Configuration;
49 import org.openhab.core.library.types.DecimalType;
50 import org.openhab.core.library.types.StringType;
51 import org.openhab.core.thing.ChannelUID;
52 import org.openhab.core.thing.Thing;
53 import org.openhab.core.thing.ThingStatus;
54 import org.openhab.core.thing.ThingStatusDetail;
55 import org.openhab.core.thing.ThingTypeUID;
56 import org.openhab.core.thing.binding.BaseThingHandler;
57 import org.openhab.core.thing.binding.builder.ThingBuilder;
58 import org.openhab.core.types.Command;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61
62 import com.google.gson.Gson;
63 import com.google.gson.GsonBuilder;
64 import com.google.gson.JsonObject;
65 import com.google.gson.JsonPrimitive;
66 import com.google.gson.JsonSyntaxException;
67
68 /**
69  * The {@link MiIoAbstractHandler} is responsible for handling commands, which are
70  * sent to one of the channels.
71  *
72  * @author Marcel Verpaalen - Initial contribution
73  */
74 @NonNullByDefault
75 public abstract class MiIoAbstractHandler extends BaseThingHandler implements MiIoMessageListener {
76     protected static final int MAX_QUEUE = 5;
77     protected static final Gson GSON = new GsonBuilder().create();
78     protected static final String TIMESTAMP = "timestamp";
79
80     protected ScheduledExecutorService miIoScheduler = new ScheduledThreadPoolExecutor(3,
81             new NamedThreadFactory("binding-" + getThing().getUID().getAsString(), true));
82
83     protected @Nullable ScheduledFuture<?> pollingJob;
84     protected MiIoDevices miDevice = MiIoDevices.UNKNOWN;
85     protected boolean isIdentified;
86
87     protected byte[] token = new byte[0];
88
89     protected @Nullable MiIoBindingConfiguration configuration;
90     protected @Nullable MiIoAsyncCommunication miioCom;
91     protected CloudConnector cloudConnector;
92     protected String cloudServer = "";
93     protected int lastId;
94
95     protected Map<Integer, String> cmds = new ConcurrentHashMap<>();
96     protected Map<String, Object> deviceVariables = new HashMap<>();
97     protected final ExpiringCache<String> network = new ExpiringCache<>(CACHE_EXPIRY_NETWORK, () -> {
98         int ret = sendCommand(MiIoCommand.MIIO_INFO);
99         if (ret != 0) {
100             return "id:" + ret;
101         }
102         return "failed";
103     });;
104     protected static final long CACHE_EXPIRY = TimeUnit.SECONDS.toMillis(5);
105     protected static final long CACHE_EXPIRY_NETWORK = TimeUnit.SECONDS.toMillis(60);
106
107     private final Logger logger = LoggerFactory.getLogger(MiIoAbstractHandler.class);
108     protected MiIoDatabaseWatchService miIoDatabaseWatchService;
109
110     public MiIoAbstractHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
111             CloudConnector cloudConnector) {
112         super(thing);
113         this.miIoDatabaseWatchService = miIoDatabaseWatchService;
114         this.cloudConnector = cloudConnector;
115     }
116
117     @Override
118     public abstract void handleCommand(ChannelUID channelUID, Command command);
119
120     protected boolean handleCommandsChannels(ChannelUID channelUID, Command command) {
121         String cmd = processSubstitutions(command.toString(), deviceVariables);
122         if (channelUID.getId().equals(CHANNEL_COMMAND)) {
123             cmds.put(sendCommand(cmd), channelUID.getId());
124             return true;
125         }
126         if (channelUID.getId().equals(CHANNEL_RPC)) {
127             cmds.put(sendCommand(cmd, cloudServer), channelUID.getId());
128             return true;
129         }
130         return false;
131     }
132
133     @Override
134     public void initialize() {
135         logger.debug("Initializing Mi IO device handler '{}' with thingType {}", getThing().getUID(),
136                 getThing().getThingTypeUID());
137
138         ScheduledThreadPoolExecutor miIoScheduler = new ScheduledThreadPoolExecutor(3,
139                 new NamedThreadFactory("binding-" + getThing().getUID().getAsString(), true));
140         miIoScheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
141         miIoScheduler.setRemoveOnCancelPolicy(true);
142         this.miIoScheduler = miIoScheduler;
143
144         final MiIoBindingConfiguration configuration = getConfigAs(MiIoBindingConfiguration.class);
145         this.configuration = configuration;
146         if (configuration.host.isEmpty()) {
147             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
148                     "IP address required. Configure IP address");
149             return;
150         }
151         if (!tokenCheckPass(configuration.token)) {
152             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Token required. Configure token");
153             return;
154         }
155         this.cloudServer = configuration.cloudServer;
156         isIdentified = false;
157         deviceVariables.put(TIMESTAMP, Instant.now().getEpochSecond());
158         deviceVariables.put(PROPERTY_DID, configuration.deviceId);
159         miIoScheduler.schedule(this::initializeData, 1, TimeUnit.SECONDS);
160         int pollingPeriod = configuration.refreshInterval;
161         if (pollingPeriod > 0) {
162             pollingJob = miIoScheduler.scheduleWithFixedDelay(() -> {
163                 try {
164                     updateData();
165                 } catch (Exception e) {
166                     logger.debug("Unexpected error during refresh.", e);
167                 }
168             }, 10, pollingPeriod, TimeUnit.SECONDS);
169             logger.debug("Polling job scheduled to run every {} sec. for '{}'", pollingPeriod, getThing().getUID());
170         } else {
171             logger.debug("Polling job disabled. for '{}'", getThing().getUID());
172             miIoScheduler.schedule(this::updateData, 10, TimeUnit.SECONDS);
173         }
174         updateStatus(ThingStatus.OFFLINE);
175     }
176
177     private boolean tokenCheckPass(@Nullable String tokenSting) {
178         if (tokenSting == null) {
179             return false;
180         }
181         switch (tokenSting.length()) {
182             case 16:
183                 token = tokenSting.getBytes();
184                 return true;
185             case 32:
186                 if (!IGNORED_TOKENS.contains(tokenSting)) {
187                     token = Utils.hexStringToByteArray(tokenSting);
188                     return true;
189                 }
190                 return false;
191             case 96:
192                 try {
193                     token = Utils.hexStringToByteArray(MiIoCrypto.decryptToken(Utils.hexStringToByteArray(tokenSting)));
194                     logger.debug("IOS token decrypted to {}", Utils.getHex(token));
195                 } catch (MiIoCryptoException e) {
196                     logger.warn("Could not decrypt token {}{}", tokenSting, e.getMessage());
197                     return false;
198                 }
199                 return true;
200             default:
201                 return false;
202         }
203     }
204
205     @Override
206     public void dispose() {
207         logger.debug("Disposing Xiaomi Mi IO handler '{}'", getThing().getUID());
208         miIoScheduler.shutdown();
209         final ScheduledFuture<?> pollingJob = this.pollingJob;
210         if (pollingJob != null) {
211             pollingJob.cancel(true);
212             this.pollingJob = null;
213         }
214         final @Nullable MiIoAsyncCommunication miioCom = this.miioCom;
215         if (miioCom != null) {
216             lastId = miioCom.getId();
217             miioCom.unregisterListener(this);
218             miioCom.close();
219             this.miioCom = null;
220         }
221         miIoScheduler.shutdownNow();
222     }
223
224     protected int sendCommand(MiIoCommand command) {
225         return sendCommand(command, "[]");
226     }
227
228     protected int sendCommand(MiIoCommand command, String params) {
229         return sendCommand(command.getCommand(), processSubstitutions(params, deviceVariables), getCloudServer(), "");
230     }
231
232     protected int sendCommand(String commandString) {
233         return sendCommand(commandString, getCloudServer());
234     }
235
236     /**
237      * This is used to execute arbitrary commands by sending to the commands channel. Command parameters to be added
238      * between
239      * [] brackets. This to allow for unimplemented commands to be executed (e.g. get detailed historical cleaning
240      * records)
241      *
242      * @param commandString command to be executed
243      * @param cloud server to be used or empty string for direct sending to the device
244      * @return vacuum response
245      */
246     protected int sendCommand(String commandString, String cloudServer) {
247         String command = commandString.trim();
248         command = processSubstitutions(commandString.trim(), deviceVariables);
249         String param = "[]";
250         int sb = command.indexOf("[");
251         int cb = command.indexOf("{");
252         if (Math.max(sb, cb) > 0) {
253             int loc = (Math.min(sb, cb) > 0 ? Math.min(sb, cb) : Math.max(sb, cb));
254             param = command.substring(loc).trim();
255             command = command.substring(0, loc).trim();
256         }
257         return sendCommand(command, param, cloudServer, "");
258     }
259
260     protected int sendCommand(String command, String params, String cloudServer) {
261         return sendCommand(command, processSubstitutions(params, deviceVariables), cloudServer, "");
262     }
263
264     /**
265      * Sends commands to the {@link MiIoAsyncCommunication} for transmission to the Mi devices or cloud
266      *
267      * @param command (method) to be queued for execution
268      * @param parameters to be send with the command
269      * @param cloud server to be used or empty string for direct sending to the device
270      * @param sending subdevice or empty string for regular device
271      * @return message id
272      */
273     protected int sendCommand(String command, String params, String cloudServer, String sender) {
274         try {
275             final MiIoAsyncCommunication connection = getConnection();
276             return (connection != null) ? connection.queueCommand(command, params, cloudServer, sender) : 0;
277         } catch (MiIoCryptoException | IOException e) {
278             logger.debug("Command {} for {} failed (type: {}): {}", command.toString(), getThing().getUID(),
279                     getThing().getThingTypeUID(), e.getLocalizedMessage());
280             disconnected(e.getMessage());
281         }
282         return 0;
283     }
284
285     String getCloudServer() {
286         // This can be improved in the future with additional / more advanced options like e.g. directFirst which would
287         // use direct communications and in case of failures fall back to cloud communication. For now we keep it
288         // simple and only have the option for cloud or direct.
289         final MiIoBindingConfiguration configuration = this.configuration;
290         if (configuration != null) {
291             return configuration.communication.equals("cloud") ? cloudServer : "";
292         }
293         return "";
294     }
295
296     protected boolean skipUpdate() {
297         final MiIoAsyncCommunication miioCom = this.miioCom;
298         if (!hasConnection() || miioCom == null) {
299             logger.debug("Skipping periodic update for '{}'. No Connection", getThing().getUID().toString());
300             return true;
301         }
302         if (getThing().getStatusInfo().getStatusDetail().equals(ThingStatusDetail.CONFIGURATION_ERROR)) {
303             logger.debug("Skipping periodic update for '{}'. Thing Status {}", getThing().getUID().toString(),
304                     getThing().getStatusInfo().getStatusDetail());
305             sendCommand(MiIoCommand.MIIO_INFO);
306             return true;
307         }
308         if (miioCom.getQueueLength() > MAX_QUEUE) {
309             logger.debug("Skipping periodic update for '{}'. {} elements in queue.", getThing().getUID().toString(),
310                     miioCom.getQueueLength());
311             return true;
312         }
313         return false;
314     }
315
316     protected abstract void updateData();
317
318     protected boolean updateNetwork(JsonObject networkData) {
319         try {
320             final MiIoInfoDTO miioInfo = GSON.fromJson(networkData, MiIoInfoDTO.class);
321             final MiIoInfoApDTO ap = miioInfo != null ? miioInfo.ap : null;
322             if (miioInfo != null && ap != null) {
323                 if (ap.getSsid() != null) {
324                     updateState(CHANNEL_SSID, new StringType(ap.getSsid()));
325                 }
326                 if (ap.getBssid() != null) {
327                     updateState(CHANNEL_BSSID, new StringType(ap.getBssid()));
328                 }
329                 if (ap.getRssi() != null) {
330                     updateState(CHANNEL_RSSI, new DecimalType(ap.getRssi()));
331                 } else if (ap.getWifiRssi() != null) {
332                     updateState(CHANNEL_RSSI, new DecimalType(ap.getWifiRssi()));
333                 } else {
334                     logger.debug("No RSSI info in response");
335                 }
336                 if (miioInfo.life != null) {
337                     updateState(CHANNEL_LIFE, new DecimalType(miioInfo.life));
338                 }
339             }
340             return true;
341         } catch (NumberFormatException e) {
342             logger.debug("Could not parse number in network response: {}", networkData);
343         } catch (JsonSyntaxException e) {
344             logger.debug("Could not parse network response: {}", networkData, e);
345         }
346         return false;
347     }
348
349     protected boolean hasConnection() {
350         return getConnection() != null;
351     }
352
353     protected void disconnectedNoResponse() {
354         disconnected("No Response from device");
355     }
356
357     protected void disconnected(@Nullable String message) {
358         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
359                 message != null ? message : "");
360         final MiIoAsyncCommunication miioCom = this.miioCom;
361         if (miioCom != null) {
362             lastId = miioCom.getId();
363             lastId += 10;
364         }
365     }
366
367     protected synchronized @Nullable MiIoAsyncCommunication getConnection() {
368         if (miioCom != null) {
369             return miioCom;
370         }
371         final MiIoBindingConfiguration configuration = getConfigAs(MiIoBindingConfiguration.class);
372         if (configuration.host.isBlank()) {
373             return null;
374         }
375         @Nullable
376         String deviceId = configuration.deviceId;
377         if (deviceId.length() == 8 && deviceId.matches("^.*[a-zA-Z]+.*$")) {
378             logger.warn(
379                     "As per openHAB version 3.2 the deviceId is no longer a string with hexadecimals, instead it is a string with the numeric respresentation of the deviceId. If you continue seeing this message, update deviceId in your thing configuration. Expected change for thing '{}': Update current deviceId: '{}' to '{}'",
380                     getThing().getUID(), deviceId, Utils.fromHEX(deviceId));
381             deviceId = "";
382         }
383         try {
384             if (!deviceId.isBlank() && tokenCheckPass(configuration.token)) {
385                 final MiIoAsyncCommunication miioCom = new MiIoAsyncCommunication(configuration.host, token, deviceId,
386                         lastId, configuration.timeout, cloudConnector);
387                 if (getCloudServer().isBlank()) {
388                     logger.debug("Ping Mi deviceId '{}' at {}", deviceId, configuration.host);
389                     Message miIoResponse = miioCom.sendPing(configuration.host);
390                     if (miIoResponse != null) {
391                         logger.debug("Ping response from deviceId '{}' at {}. Time stamp: {}, OH time {}, delta {}",
392                                 Utils.fromHEX(Utils.getHex(miIoResponse.getDeviceId())), configuration.host,
393                                 miIoResponse.getTimestamp(), LocalDateTime.now(), miioCom.getTimeDelta());
394                         miioCom.registerListener(this);
395                         this.miioCom = miioCom;
396                         return miioCom;
397                     } else {
398                         miioCom.close();
399                     }
400                 } else {
401                     miioCom.registerListener(this);
402                     this.miioCom = miioCom;
403                     return miioCom;
404                 }
405             } else {
406                 logger.debug("No deviceId defined. Retrieving Mi deviceId");
407                 final MiIoAsyncCommunication miioCom = new MiIoAsyncCommunication(configuration.host, token, "", lastId,
408                         configuration.timeout, cloudConnector);
409                 Message miIoResponse = miioCom.sendPing(configuration.host);
410                 if (miIoResponse != null) {
411                     deviceId = Utils.fromHEX(Utils.getHex(miIoResponse.getDeviceId()));
412                     logger.debug("Ping response from deviceId '{}' at {}. Time stamp: {}, OH time {}, delta {}",
413                             deviceId, configuration.host, miIoResponse.getTimestamp(), LocalDateTime.now(),
414                             miioCom.getTimeDelta());
415                     miioCom.setDeviceId(deviceId);
416                     logger.debug("Using retrieved Mi deviceId: {}", deviceId);
417                     updateDeviceIdConfig(deviceId);
418                     miioCom.registerListener(this);
419                     this.miioCom = miioCom;
420                     return miioCom;
421                 } else {
422                     miioCom.close();
423                 }
424             }
425             logger.debug("Ping response from deviceId '{}' at {} FAILED", configuration.deviceId, configuration.host);
426             disconnectedNoResponse();
427             return null;
428         } catch (IOException e) {
429             logger.debug("Could not connect to {} at {}", getThing().getUID().toString(), configuration.host);
430             disconnected(e.getMessage());
431             return null;
432         }
433     }
434
435     private void updateDeviceIdConfig(String deviceId) {
436         if (!deviceId.isEmpty()) {
437             updateProperty(Thing.PROPERTY_SERIAL_NUMBER, deviceId);
438             Configuration config = editConfiguration();
439             config.put(PROPERTY_DID, deviceId);
440             updateConfiguration(config);
441             deviceVariables.put(PROPERTY_DID, deviceId);
442         } else {
443             logger.debug("Could not update config with deviceId: {}", deviceId);
444         }
445     }
446
447     protected boolean initializeData() {
448         this.miioCom = getConnection();
449         return true;
450     }
451
452     protected void refreshNetwork() {
453         network.getValue();
454     }
455
456     protected void defineDeviceType(JsonObject miioInfo) {
457         updateProperties(miioInfo);
458         isIdentified = updateThingType(miioInfo);
459     }
460
461     private void updateProperties(JsonObject miioInfo) {
462         final MiIoInfoDTO info = GSON.fromJson(miioInfo, MiIoInfoDTO.class);
463         if (info == null) {
464             return;
465         }
466         Map<String, String> properties = editProperties();
467         if (info.model != null) {
468             properties.put(Thing.PROPERTY_MODEL_ID, info.model);
469         }
470         if (info.fwVer != null) {
471             properties.put(Thing.PROPERTY_FIRMWARE_VERSION, info.fwVer);
472         }
473         if (info.hwVer != null) {
474             properties.put(Thing.PROPERTY_HARDWARE_VERSION, info.hwVer);
475         }
476         if (info.wifiFwVer != null) {
477             properties.put("wifiFirmware", info.wifiFwVer);
478         }
479         if (info.mcuFwVer != null) {
480             properties.put("mcuFirmware", info.mcuFwVer);
481         }
482         deviceVariables.putAll(properties);
483         updateProperties(properties);
484     }
485
486     protected String processSubstitutions(String cmd, Map<String, Object> deviceVariables) {
487         if (!cmd.contains("$")) {
488             return cmd;
489         }
490         String returnCmd = cmd.replace("\"$", "$").replace("$\"", "$");
491         String cmdParts[] = cmd.split("\\$");
492         if (logger.isTraceEnabled()) {
493             logger.debug("processSubstitutions {} ", cmd);
494             for (Entry<String, Object> e : deviceVariables.entrySet()) {
495                 logger.debug("key, value:  {}  -> {}", e.getKey(), e.getValue());
496             }
497         }
498         for (String substitute : cmdParts) {
499             if (deviceVariables.containsKey(substitute)) {
500                 String replacementString = "";
501                 Object replacement = deviceVariables.get(substitute);
502                 if (replacement == null) {
503                     logger.debug("Replacement for '{}' is null. skipping replacement", substitute);
504                     continue;
505                 }
506                 if (replacement instanceof Integer || replacement instanceof Long || replacement instanceof Double
507                         || replacement instanceof BigDecimal || replacement instanceof Boolean) {
508                     replacementString = replacement.toString();
509                 } else if (replacement instanceof JsonPrimitive) {
510                     replacementString = ((JsonPrimitive) replacement).getAsString();
511                 } else if (replacement instanceof String) {
512                     replacementString = "\"" + (String) replacement + "\"";
513                 } else {
514                     replacementString = String.valueOf(replacement);
515                 }
516                 returnCmd = returnCmd.replace("$" + substitute + "$", replacementString);
517             }
518         }
519         return returnCmd;
520     }
521
522     protected boolean updateThingType(JsonObject miioInfo) {
523         MiIoBindingConfiguration configuration = getConfigAs(MiIoBindingConfiguration.class);
524         String model = miioInfo.get("model").getAsString();
525         miDevice = MiIoDevices.getType(model);
526         if (configuration.model.isEmpty()) {
527             Configuration config = editConfiguration();
528             config.put(PROPERTY_MODEL, model);
529             updateConfiguration(config);
530             configuration = getConfigAs(MiIoBindingConfiguration.class);
531         }
532         if (!configuration.model.equals(model)) {
533             logger.info("Mi Device model {} has model config: {}. Unexpected unless manual override", model,
534                     configuration.model);
535         }
536         if (miDevice.getThingType().equals(getThing().getThingTypeUID())
537                 && !(miDevice.getThingType().equals(THING_TYPE_UNSUPPORTED)
538                         && miIoDatabaseWatchService.getDatabaseUrl(model) != null)) {
539             logger.debug("Mi Device model {} identified as: {}. Matches thingtype {}", model, miDevice.toString(),
540                     miDevice.getThingType().toString());
541             return true;
542         } else {
543             if (getThing().getThingTypeUID().equals(THING_TYPE_MIIO)
544                     || getThing().getThingTypeUID().equals(THING_TYPE_UNSUPPORTED)) {
545                 changeType(model);
546             } else {
547                 logger.info(
548                         "Mi Device model {} identified as: {}, thingtype {}. Does not matches thingtype {}. Unexpected, unless manual override.",
549                         miDevice.toString(), miDevice.getThingType(), getThing().getThingTypeUID().toString(),
550                         miDevice.getThingType().toString());
551                 return true;
552             }
553         }
554         return false;
555     }
556
557     /**
558      * Changes the {@link org.openhab.core.thing.type.ThingType} to the right type once it is retrieved from
559      * the device.
560      *
561      * @param modelId String with the model id
562      */
563     private void changeType(final String modelId) {
564         final ScheduledFuture<?> pollingJob = this.pollingJob;
565         if (pollingJob != null) {
566             pollingJob.cancel(true);
567             this.pollingJob = null;
568         }
569         miIoScheduler.schedule(() -> {
570             String label = getThing().getLabel();
571             if (label == null || label.startsWith("Xiaomi Mi Device")) {
572                 ThingBuilder thingBuilder = editThing();
573                 thingBuilder.withLabel(miDevice.getDescription());
574                 updateThing(thingBuilder.build());
575             }
576             logger.info("Mi Device model {} identified as: {}. Does not match thingtype {}. Changing thingtype to {}",
577                     modelId, miDevice.toString(), getThing().getThingTypeUID().toString(),
578                     miDevice.getThingType().toString());
579             ThingTypeUID thingTypeUID = MiIoDevices.getType(modelId).getThingType();
580             if (thingTypeUID.equals(THING_TYPE_UNSUPPORTED)
581                     && miIoDatabaseWatchService.getDatabaseUrl(modelId) != null) {
582                 thingTypeUID = THING_TYPE_BASIC;
583             }
584             changeThingType(thingTypeUID, getConfig());
585         }, 10, TimeUnit.SECONDS);
586     }
587
588     @Override
589     public void onStatusUpdated(ThingStatus status, ThingStatusDetail statusDetail) {
590         updateStatus(status, statusDetail);
591     }
592
593     @Override
594     public void onMessageReceived(MiIoSendCommand response) {
595         logger.debug("Received response for device {} type: {}, result: {}, fullresponse: {}",
596                 getThing().getUID().getId(), response.getCommand(), response.getResult(), response.getResponse());
597         if (response.isError()) {
598             logger.debug("Error received for command '{}': {}.", response.getCommandString(),
599                     response.getResponse().get("error"));
600             if (MiIoCommand.MIIO_INFO.equals(response.getCommand())) {
601                 network.invalidateValue();
602             }
603             return;
604         }
605         try {
606             switch (response.getCommand()) {
607                 case MIIO_INFO:
608                     if (!isIdentified) {
609                         defineDeviceType(response.getResult().getAsJsonObject());
610                     }
611                     updateNetwork(response.getResult().getAsJsonObject());
612                     break;
613                 default:
614                     break;
615             }
616             if (cmds.containsKey(response.getId())) {
617                 String channel = cmds.get(response.getId());
618                 if (channel != null && (CHANNEL_COMMAND.contentEquals(channel) || CHANNEL_RPC.contentEquals(channel))) {
619                     updateState(channel, new StringType(response.getResponse().toString()));
620                     cmds.remove(response.getId());
621                 }
622             }
623         } catch (Exception e) {
624             logger.debug("Error while handing message {}", response.getResponse(), e);
625         }
626     }
627 }