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