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