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