2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.miio.internal.handler;
15 import static org.openhab.binding.miio.internal.MiIoBindingConstants.*;
18 import java.io.IOException;
19 import java.math.BigDecimal;
20 import java.net.URISyntaxException;
22 import java.time.Instant;
23 import java.time.LocalDateTime;
24 import java.util.HashMap;
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;
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;
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;
78 * The {@link MiIoAbstractHandler} is responsible for handling commands, which are
79 * sent to one of the channels.
81 * @author Marcel Verpaalen - Initial contribution
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 protected final Map<Thing, MiIoLumiHandler> childDevices = new ConcurrentHashMap<>();
93 protected ScheduledExecutorService miIoScheduler = new ScheduledThreadPoolExecutor(3,
94 new NamedThreadFactory("binding-" + getThing().getUID().getAsString(), true));
96 protected @Nullable ScheduledFuture<?> pollingJob;
97 protected MiIoDevices miDevice = MiIoDevices.UNKNOWN;
98 protected boolean isIdentified;
100 protected byte[] token = new byte[0];
102 protected @Nullable MiIoBindingConfiguration configuration;
103 protected @Nullable MiIoAsyncCommunication miioCom;
104 protected CloudConnector cloudConnector;
105 protected String cloudServer = "";
106 protected String deviceId = "";
107 protected int lastId;
109 protected Map<Integer, String> cmds = new ConcurrentHashMap<>();
110 protected Map<String, Object> deviceVariables = new HashMap<>();
111 protected final ExpiringCache<String> network = new ExpiringCache<>(CACHE_EXPIRY_NETWORK, () -> {
112 int ret = sendCommand(MiIoCommand.MIIO_INFO);
118 protected static final long CACHE_EXPIRY = TimeUnit.SECONDS.toMillis(5);
119 protected static final long CACHE_EXPIRY_NETWORK = TimeUnit.SECONDS.toMillis(60);
121 private final Logger logger = LoggerFactory.getLogger(MiIoAbstractHandler.class);
122 protected MiIoDatabaseWatchService miIoDatabaseWatchService;
124 public MiIoAbstractHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
125 CloudConnector cloudConnector, TranslationProvider i18nProvider, LocaleProvider localeProvider) {
127 this.miIoDatabaseWatchService = miIoDatabaseWatchService;
128 this.cloudConnector = cloudConnector;
129 this.i18nProvider = i18nProvider;
130 this.localeProvider = localeProvider;
131 this.bundle = FrameworkUtil.getBundle(this.getClass());
135 public abstract void handleCommand(ChannelUID channelUID, Command command);
137 protected boolean handleCommandsChannels(ChannelUID channelUID, Command command) {
138 String cmd = processSubstitutions(command.toString(), deviceVariables);
139 if (channelUID.getId().equals(CHANNEL_COMMAND)) {
140 cmds.put(sendCommand(cmd), channelUID.getId());
143 if (channelUID.getId().equals(CHANNEL_RPC)) {
144 cmds.put(sendCommand(cmd, cloudServer), channelUID.getId());
151 public void initialize() {
152 logger.debug("Initializing Mi IO device handler '{}' with thingType {}", getThing().getUID(),
153 getThing().getThingTypeUID());
155 ScheduledThreadPoolExecutor miIoScheduler = new ScheduledThreadPoolExecutor(3,
156 new NamedThreadFactory("binding-" + getThing().getUID().getAsString(), true));
157 miIoScheduler.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);
158 miIoScheduler.setRemoveOnCancelPolicy(true);
159 this.miIoScheduler = miIoScheduler;
161 final MiIoBindingConfiguration configuration = getConfigAs(MiIoBindingConfiguration.class);
162 this.configuration = configuration;
163 if (!getThing().getThingTypeUID().equals(THING_TYPE_LUMI)) {
164 if (configuration.host.isEmpty()) {
165 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
166 "@text/offline.config-error-ip");
169 if (!tokenCheckPass(configuration.token)) {
170 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
171 "@text/offline.config-error-token");
175 this.cloudServer = configuration.cloudServer;
176 this.deviceId = configuration.deviceId;
177 isIdentified = false;
178 deviceVariables.put(TIMESTAMP, Instant.now().getEpochSecond());
179 deviceVariables.put(PROPERTY_DID, configuration.deviceId);
181 URL url = new File(BINDING_USERDATA_PATH, "info_" + getThing().getUID().getId() + ".json").toURI().toURL();
182 SavedDeviceInfoDTO lastIdInfo = GSON.fromJson(Utils.convertFileToJSON(url), SavedDeviceInfoDTO.class);
183 if (lastIdInfo != null) {
184 lastId = lastIdInfo.getLastId();
185 logger.debug("Last Id set to {} for {}", lastId, getThing().getUID());
187 } catch (JsonParseException | IOException | URISyntaxException e) {
188 logger.debug("Could not read last connection id for {} : {}", getThing().getUID(), e.getMessage());
191 miIoScheduler.schedule(this::initializeData, 1, TimeUnit.SECONDS);
192 int pollingPeriod = configuration.refreshInterval;
193 if (pollingPeriod > 0) {
194 pollingJob = miIoScheduler.scheduleWithFixedDelay(() -> {
197 } catch (Exception e) {
198 logger.debug("Unexpected error during refresh.", e);
200 }, 10, pollingPeriod, TimeUnit.SECONDS);
201 logger.debug("Polling job scheduled to run every {} sec. for '{}'", pollingPeriod, getThing().getUID());
203 logger.debug("Polling job disabled. for '{}'", getThing().getUID());
204 miIoScheduler.schedule(this::updateData, 10, TimeUnit.SECONDS);
206 updateStatus(ThingStatus.OFFLINE);
209 private boolean tokenCheckPass(@Nullable String tokenSting) {
210 if (tokenSting == null) {
213 switch (tokenSting.length()) {
215 token = tokenSting.getBytes();
218 if (!IGNORED_TOKENS.contains(tokenSting)) {
219 token = Utils.hexStringToByteArray(tokenSting);
225 token = Utils.hexStringToByteArray(MiIoCrypto.decryptToken(Utils.hexStringToByteArray(tokenSting)));
226 logger.debug("IOS token decrypted to {}", Utils.getHex(token));
227 } catch (MiIoCryptoException e) {
228 logger.warn("Could not decrypt token {}{}", tokenSting, e.getMessage());
238 public void dispose() {
239 logger.debug("Disposing Xiaomi Mi IO handler '{}'", getThing().getUID());
240 miIoScheduler.shutdown();
241 final ScheduledFuture<?> pollingJob = this.pollingJob;
242 if (pollingJob != null) {
243 pollingJob.cancel(true);
244 this.pollingJob = null;
246 final @Nullable MiIoAsyncCommunication miioCom = this.miioCom;
247 if (miioCom != null) {
248 lastId = miioCom.getId();
249 miioCom.unregisterListener(this);
253 miIoScheduler.shutdownNow();
254 logger.debug("Last Id saved for {}: {} -> {} ", getThing().getUID(), lastId,
255 GSON.toJson(new SavedDeviceInfoDTO(lastId,
256 (String) deviceVariables.getOrDefault(PROPERTY_DID, getThing().getUID().getId()))));
257 Utils.saveToFile("info_" + getThing().getUID().getId() + ".json", GSON.toJson(new SavedDeviceInfoDTO(lastId,
258 (String) deviceVariables.getOrDefault(PROPERTY_DID, getThing().getUID().getId()))), logger);
261 protected int sendCommand(MiIoCommand command) {
262 return sendCommand(command, "[]");
265 protected int sendCommand(MiIoCommand command, String params) {
266 return sendCommand(command.getCommand(), processSubstitutions(params, deviceVariables), getCloudServer(), "");
269 protected int sendCommand(String commandString) {
270 return sendCommand(commandString, getCloudServer());
274 * This is used to execute arbitrary commands by sending to the commands channel. Command parameters to be added
275 * between [] brackets. This to allow for unimplemented commands to be executed
277 * @param commandString command to be executed
278 * @param cloud server to be used or empty string for direct sending to the device
281 protected int sendCommand(String commandString, String cloudServer) {
282 String command = commandString.trim();
283 command = processSubstitutions(commandString.trim(), deviceVariables);
285 int sb = command.indexOf("[");
286 int cb = command.indexOf("{");
287 if (Math.max(sb, cb) > 0) {
288 int loc = (Math.min(sb, cb) > 0 ? Math.min(sb, cb) : Math.max(sb, cb));
289 param = command.substring(loc).trim();
290 command = command.substring(0, loc).trim();
292 return sendCommand(command, param, cloudServer, "");
295 protected int sendCommand(String command, String params, String cloudServer) {
296 return sendCommand(command, processSubstitutions(params, deviceVariables), cloudServer, "");
300 * Sends commands to the {@link MiIoAsyncCommunication} for transmission to the Mi devices or cloud
302 * @param command (method) to be queued for execution
303 * @param parameters to be send with the command
304 * @param cloud server to be used or empty string for direct sending to the device
305 * @param sending subdevice or empty string for regular device
308 protected int sendCommand(String command, String params, String cloudServer, String sender) {
310 if (!sender.isBlank()) {
311 logger.debug("Received child command from {} : {} - {} (via: {})", sender, command, params,
312 getThing().getUID());
314 final MiIoAsyncCommunication connection = getConnection();
315 return (connection != null) ? connection.queueCommand(command, params, cloudServer, sender) : 0;
316 } catch (MiIoCryptoException | IOException e) {
317 logger.debug("Command {} for {} failed (type: {}): {}", command.toString(), getThing().getUID(),
318 getThing().getThingTypeUID(), e.getLocalizedMessage());
319 disconnected(e.getMessage());
324 public String getCloudServer() {
325 // This can be improved in the future with additional / more advanced options like e.g. directFirst which would
326 // use direct communications and in case of failures fall back to cloud communication. For now we keep it
327 // simple and only have the option for cloud or direct.
328 final MiIoBindingConfiguration configuration = this.configuration;
329 if (configuration != null) {
330 return configuration.communication.equals("cloud") ? cloudServer : "";
335 protected boolean skipUpdate() {
336 final MiIoAsyncCommunication miioCom = this.miioCom;
337 if (!hasConnection() || miioCom == null) {
338 logger.debug("Skipping periodic update for '{}'. No Connection", getThing().getUID().toString());
341 if (getThing().getStatusInfo().getStatusDetail().equals(ThingStatusDetail.CONFIGURATION_ERROR)) {
342 logger.debug("Skipping periodic update for '{}'. Thing Status {}", getThing().getUID().toString(),
343 getThing().getStatusInfo().getStatusDetail());
344 sendCommand(MiIoCommand.MIIO_INFO);
347 if (miioCom.getQueueLength() > MAX_QUEUE) {
348 logger.debug("Skipping periodic update for '{}'. {} elements in queue.", getThing().getUID().toString(),
349 miioCom.getQueueLength());
355 protected abstract void updateData();
357 protected boolean updateNetwork(JsonObject networkData) {
359 final MiIoInfoDTO miioInfo = GSON.fromJson(networkData, MiIoInfoDTO.class);
360 final MiIoInfoApDTO ap = miioInfo != null ? miioInfo.ap : null;
361 if (miioInfo != null && ap != null) {
362 if (ap.getSsid() != null) {
363 updateState(CHANNEL_SSID, new StringType(ap.getSsid()));
365 if (ap.getBssid() != null) {
366 updateState(CHANNEL_BSSID, new StringType(ap.getBssid()));
368 if (ap.getRssi() != null) {
369 updateState(CHANNEL_RSSI, new DecimalType(ap.getRssi()));
370 } else if (ap.getWifiRssi() != null) {
371 updateState(CHANNEL_RSSI, new DecimalType(ap.getWifiRssi()));
373 logger.debug("No RSSI info in response");
375 if (miioInfo.life != null) {
376 updateState(CHANNEL_LIFE, new DecimalType(miioInfo.life));
380 } catch (NumberFormatException e) {
381 logger.debug("Could not parse number in network response: {}", networkData);
382 } catch (JsonSyntaxException e) {
383 logger.debug("Could not parse network response: {}", networkData, e);
388 protected boolean hasConnection() {
389 return getConnection() != null;
392 protected void disconnectedNoResponse() {
393 disconnected("No Response from device");
396 protected void disconnected(@Nullable String message) {
397 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
398 message != null ? message : "");
399 final MiIoAsyncCommunication miioCom = this.miioCom;
400 if (miioCom != null) {
401 lastId = miioCom.getId();
406 protected synchronized @Nullable MiIoAsyncCommunication getConnection() {
407 if (miioCom != null) {
410 final MiIoBindingConfiguration configuration = getConfigAs(MiIoBindingConfiguration.class);
411 if (configuration.host.isBlank()) {
414 if (deviceId.isBlank() && !getCloudServer().isBlank()) {
415 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
416 "@text/offline.config-error-cloud");
419 if (deviceId.length() == 8 && deviceId.matches("^.*[a-zA-Z]+.*$")) {
421 "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 '{}'",
422 getThing().getUID(), deviceId, Utils.fromHEX(deviceId));
423 if (getCloudServer().isBlank()) {
426 final String id = Utils.fromHEX(deviceId);
428 miIoScheduler.execute(() -> updateDeviceIdConfig(id));
432 if (!deviceId.isBlank() && (tokenCheckPass(configuration.token) || !getCloudServer().isBlank())) {
433 final MiIoAsyncCommunication miioComF = new MiIoAsyncCommunication(configuration.host, token, deviceId,
434 lastId, configuration.timeout, cloudConnector);
435 miioComF.registerListener(this);
436 this.miioCom = miioComF;
439 logger.debug("No deviceId defined. Retrieving Mi deviceId");
440 final MiIoAsyncCommunication miioComF = new MiIoAsyncCommunication(configuration.host, token, "", lastId,
441 configuration.timeout, cloudConnector);
443 Message miIoResponse = miioComF.sendPing(configuration.host);
444 if (miIoResponse != null) {
445 deviceId = Utils.fromHEX(Utils.getHex(miIoResponse.getDeviceId()));
446 logger.debug("Ping response from deviceId '{}' at {}. Time stamp: {}, OH time {}, delta {}",
447 deviceId, configuration.host, miIoResponse.getTimestamp(), LocalDateTime.now(),
448 miioComF.getTimeDelta());
449 miioComF.setDeviceId(deviceId);
450 logger.debug("Using retrieved Mi deviceId: {}", deviceId);
451 miioComF.registerListener(this);
452 this.miioCom = miioComF;
453 final String id = deviceId;
454 miIoScheduler.execute(() -> updateDeviceIdConfig(id));
458 logger.debug("Ping response from deviceId '{}' at {} FAILED", configuration.deviceId,
460 disconnectedNoResponse();
463 } catch (IOException e) {
465 logger.debug("Could not connect to {} at {}", getThing().getUID().toString(), configuration.host);
466 disconnected(e.getMessage());
473 private void updateDeviceIdConfig(String deviceId) {
474 if (!deviceId.isEmpty()) {
475 updateProperty(Thing.PROPERTY_SERIAL_NUMBER, deviceId);
476 Configuration config = editConfiguration();
477 config.put(PROPERTY_DID, deviceId);
478 updateConfiguration(config);
479 deviceVariables.put(PROPERTY_DID, deviceId);
481 logger.debug("Could not update config with deviceId: {}", deviceId);
485 protected boolean initializeData() {
486 this.miioCom = getConnection();
490 protected void refreshNetwork() {
494 protected void defineDeviceType(JsonObject miioInfo) {
495 updateProperties(miioInfo);
496 isIdentified = updateThingType(miioInfo);
499 private void updateProperties(JsonObject miioInfo) {
500 final MiIoInfoDTO info = GSON.fromJson(miioInfo, MiIoInfoDTO.class);
504 Map<String, String> properties = editProperties();
505 if (info.model != null) {
506 properties.put(Thing.PROPERTY_MODEL_ID, info.model);
508 if (info.fwVer != null) {
509 properties.put(Thing.PROPERTY_FIRMWARE_VERSION, info.fwVer);
511 if (info.hwVer != null) {
512 properties.put(Thing.PROPERTY_HARDWARE_VERSION, info.hwVer);
514 if (info.wifiFwVer != null) {
515 properties.put("wifiFirmware", info.wifiFwVer);
517 if (info.mcuFwVer != null) {
518 properties.put("mcuFirmware", info.mcuFwVer);
520 deviceVariables.putAll(properties);
521 updateProperties(properties);
524 protected String processSubstitutions(String cmd, Map<String, Object> deviceVariables) {
525 if (!cmd.contains("$")) {
528 String returnCmd = cmd.replace("\"$", "$").replace("$\"", "$");
529 String cmdParts[] = cmd.split("\\$");
530 if (logger.isTraceEnabled()) {
531 logger.debug("processSubstitutions {} ", cmd);
532 for (Entry<String, Object> e : deviceVariables.entrySet()) {
533 logger.debug("key, value: {} -> {}", e.getKey(), e.getValue());
536 for (String substitute : cmdParts) {
537 if (deviceVariables.containsKey(substitute)) {
538 String replacementString = "";
539 Object replacement = deviceVariables.get(substitute);
540 if (replacement == null) {
541 logger.debug("Replacement for '{}' is null. skipping replacement", substitute);
544 if (replacement instanceof Integer || replacement instanceof Long || replacement instanceof Double
545 || replacement instanceof BigDecimal || replacement instanceof Boolean) {
546 replacementString = replacement.toString();
547 } else if (replacement instanceof JsonPrimitive) {
548 replacementString = ((JsonPrimitive) replacement).getAsString();
549 } else if (replacement instanceof String) {
550 replacementString = "\"" + (String) replacement + "\"";
552 replacementString = String.valueOf(replacement);
554 returnCmd = returnCmd.replace("$" + substitute + "$", replacementString);
560 protected boolean updateThingType(JsonObject miioInfo) {
561 MiIoBindingConfiguration configuration = getConfigAs(MiIoBindingConfiguration.class);
562 String model = miioInfo.get("model").getAsString();
563 miDevice = MiIoDevices.getType(model);
564 if (configuration.model.isEmpty()) {
565 Configuration config = editConfiguration();
566 config.put(PROPERTY_MODEL, model);
567 updateConfiguration(config);
568 configuration = getConfigAs(MiIoBindingConfiguration.class);
570 if (!configuration.model.equals(model)) {
571 logger.info("Mi Device model {} has model config: {}. Unexpected unless manual override", model,
572 configuration.model);
574 if (miDevice.getThingType().equals(getThing().getThingTypeUID())
575 && !(miDevice.getThingType().equals(THING_TYPE_UNSUPPORTED)
576 && miIoDatabaseWatchService.getDatabaseUrl(model) != null)) {
577 logger.debug("Mi Device model {} identified as: {}. Matches thingtype {}", model, miDevice.toString(),
578 miDevice.getThingType().toString());
581 if (getThing().getThingTypeUID().equals(THING_TYPE_MIIO)
582 || getThing().getThingTypeUID().equals(THING_TYPE_UNSUPPORTED)) {
586 "Mi Device model {} identified as: {}, thingtype {}. Does not matches thingtype {}. Unexpected, unless manual override.",
587 miDevice.toString(), miDevice.getThingType(), getThing().getThingTypeUID().toString(),
588 miDevice.getThingType().toString());
596 * Changes the {@link org.openhab.core.thing.type.ThingType} to the right type once it is retrieved from
599 * @param modelId String with the model id
601 private void changeType(final String modelId) {
602 final ScheduledFuture<?> pollingJob = this.pollingJob;
603 if (pollingJob != null) {
604 pollingJob.cancel(true);
605 this.pollingJob = null;
607 miIoScheduler.schedule(() -> {
608 String label = getThing().getLabel();
609 if (label == null || label.startsWith("Xiaomi Mi Device")) {
610 ThingBuilder thingBuilder = editThing();
611 label = getLocalText(I18N_THING_PREFIX + modelId, miDevice.getDescription());
612 thingBuilder.withLabel(label);
613 updateThing(thingBuilder.build());
615 logger.info("Mi Device model {} identified as: {}. Does not match thingtype {}. Changing thingtype to {}",
616 modelId, miDevice.toString(), getThing().getThingTypeUID().toString(),
617 miDevice.getThingType().toString());
618 ThingTypeUID thingTypeUID = MiIoDevices.getType(modelId).getThingType();
619 if (thingTypeUID.equals(THING_TYPE_UNSUPPORTED)
620 && miIoDatabaseWatchService.getDatabaseUrl(modelId) != null) {
621 thingTypeUID = THING_TYPE_BASIC;
623 changeThingType(thingTypeUID, getConfig());
624 }, 10, TimeUnit.SECONDS);
628 public void onStatusUpdated(ThingStatus status, ThingStatusDetail statusDetail) {
629 updateStatus(status, statusDetail);
633 public void onMessageReceived(MiIoSendCommand response) {
634 if (!response.getSender().isBlank() && !response.getSender().contentEquals(getThing().getUID().getAsString())) {
635 for (Entry<Thing, MiIoLumiHandler> entry : childDevices.entrySet()) {
636 if (entry.getKey().getUID().getAsString().contentEquals(response.getSender())) {
637 logger.trace("Submit response to to child {} -> {}", response.getSender(), entry.getKey().getUID());
638 entry.getValue().onMessageReceived(response);
642 logger.debug("{} Could not find match in {} child devices for submitter {}", getThing().getUID(),
643 childDevices.size(), response.getSender());
647 logger.debug("Received response for device {} type: {}, result: {}, fullresponse: {}",
648 getThing().getUID().getId(),
649 MiIoCommand.UNKNOWN.equals(response.getCommand())
650 ? response.getCommand().toString() + "(" + response.getCommandString() + ")"
651 : response.getCommand(),
652 response.getResult(), response.getResponse());
653 if (response.isError()) {
654 logger.debug("Error received for command '{}': {}.", response.getCommandString(),
655 response.getResponse().get("error"));
656 if (MiIoCommand.MIIO_INFO.equals(response.getCommand())) {
657 network.invalidateValue();
662 switch (response.getCommand()) {
665 defineDeviceType(response.getResult().getAsJsonObject());
667 updateNetwork(response.getResult().getAsJsonObject());
672 if (cmds.containsKey(response.getId())) {
673 String channel = cmds.get(response.getId());
674 if (channel != null && (CHANNEL_COMMAND.contentEquals(channel) || CHANNEL_RPC.contentEquals(channel))) {
675 updateState(channel, new StringType(response.getResponse().toString()));
676 cmds.remove(response.getId());
679 } catch (Exception e) {
680 logger.debug("Error while handing message {}", response.getResponse(), e);
684 protected String getLocalText(String key, String defaultText) {
686 String text = i18nProvider.getText(bundle, key, defaultText, localeProvider.getLocale());
687 return text != null ? text : defaultText;
688 } catch (IllegalArgumentException e) {