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