2 * Copyright (c) 2010-2022 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.*;
17 import java.awt.Color;
18 import java.io.IOException;
20 import java.time.Instant;
21 import java.util.ArrayList;
22 import java.util.HashMap;
23 import java.util.LinkedHashSet;
24 import java.util.List;
26 import java.util.Map.Entry;
28 import java.util.concurrent.TimeUnit;
30 import javax.measure.Unit;
31 import javax.measure.format.MeasurementParseException;
33 import org.eclipse.jdt.annotation.NonNullByDefault;
34 import org.eclipse.jdt.annotation.Nullable;
35 import org.openhab.binding.miio.internal.MiIoBindingConfiguration;
36 import org.openhab.binding.miio.internal.MiIoCommand;
37 import org.openhab.binding.miio.internal.MiIoQuantiyTypes;
38 import org.openhab.binding.miio.internal.MiIoSendCommand;
39 import org.openhab.binding.miio.internal.Utils;
40 import org.openhab.binding.miio.internal.basic.ActionConditions;
41 import org.openhab.binding.miio.internal.basic.BasicChannelTypeProvider;
42 import org.openhab.binding.miio.internal.basic.CommandParameterType;
43 import org.openhab.binding.miio.internal.basic.Conversions;
44 import org.openhab.binding.miio.internal.basic.MiIoBasicChannel;
45 import org.openhab.binding.miio.internal.basic.MiIoBasicDevice;
46 import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService;
47 import org.openhab.binding.miio.internal.basic.MiIoDeviceAction;
48 import org.openhab.binding.miio.internal.basic.MiIoDeviceActionCondition;
49 import org.openhab.binding.miio.internal.cloud.CloudConnector;
50 import org.openhab.binding.miio.internal.transport.MiIoAsyncCommunication;
51 import org.openhab.core.cache.ExpiringCache;
52 import org.openhab.core.i18n.LocaleProvider;
53 import org.openhab.core.i18n.TranslationProvider;
54 import org.openhab.core.library.types.DecimalType;
55 import org.openhab.core.library.types.HSBType;
56 import org.openhab.core.library.types.OnOffType;
57 import org.openhab.core.library.types.OpenClosedType;
58 import org.openhab.core.library.types.PercentType;
59 import org.openhab.core.library.types.QuantityType;
60 import org.openhab.core.library.types.StringType;
61 import org.openhab.core.library.unit.SIUnits;
62 import org.openhab.core.library.unit.Units;
63 import org.openhab.core.thing.Channel;
64 import org.openhab.core.thing.ChannelUID;
65 import org.openhab.core.thing.Thing;
66 import org.openhab.core.thing.binding.builder.ChannelBuilder;
67 import org.openhab.core.thing.binding.builder.ThingBuilder;
68 import org.openhab.core.thing.type.ChannelTypeRegistry;
69 import org.openhab.core.thing.type.ChannelTypeUID;
70 import org.openhab.core.types.Command;
71 import org.openhab.core.types.RefreshType;
72 import org.slf4j.Logger;
73 import org.slf4j.LoggerFactory;
75 import com.google.gson.Gson;
76 import com.google.gson.GsonBuilder;
77 import com.google.gson.JsonArray;
78 import com.google.gson.JsonElement;
79 import com.google.gson.JsonIOException;
80 import com.google.gson.JsonObject;
81 import com.google.gson.JsonParser;
82 import com.google.gson.JsonPrimitive;
83 import com.google.gson.JsonSyntaxException;
86 * The {@link MiIoBasicHandler} is responsible for handling commands, which are
87 * sent to one of the channels.
89 * @author Marcel Verpaalen - Initial contribution
92 public class MiIoBasicHandler extends MiIoAbstractHandler {
93 protected final Logger logger = LoggerFactory.getLogger(MiIoBasicHandler.class);
94 protected boolean hasChannelStructure;
96 protected final ExpiringCache<Boolean> updateDataCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
97 miIoScheduler.schedule(this::updateData, 0, TimeUnit.SECONDS);
101 protected List<MiIoBasicChannel> refreshList = new ArrayList<>();
102 protected Map<String, MiIoBasicChannel> refreshListCustomCommands = new HashMap<>();
104 protected @Nullable MiIoBasicDevice miioDevice;
105 protected Map<ChannelUID, MiIoBasicChannel> actions = new HashMap<>();
106 protected ChannelTypeRegistry channelTypeRegistry;
107 protected BasicChannelTypeProvider basicChannelTypeProvider;
108 private Map<String, Integer> customRefreshInterval = new HashMap<>();
110 public MiIoBasicHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
111 CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry,
112 BasicChannelTypeProvider basicChannelTypeProvider, TranslationProvider i18nProvider,
113 LocaleProvider localeProvider) {
114 super(thing, miIoDatabaseWatchService, cloudConnector, i18nProvider, localeProvider);
115 this.channelTypeRegistry = channelTypeRegistry;
116 this.basicChannelTypeProvider = basicChannelTypeProvider;
120 public void initialize() {
122 hasChannelStructure = false;
123 isIdentified = false;
124 refreshList = new ArrayList<>();
125 refreshListCustomCommands = new HashMap<>();
129 public void handleCommand(ChannelUID channelUID, Command receivedCommand) {
130 Command command = receivedCommand;
131 deviceVariables.put(TIMESTAMP, Instant.now().getEpochSecond());
132 if (command == RefreshType.REFRESH) {
133 if (updateDataCache.isExpired()) {
134 logger.debug("Refreshing {}", channelUID);
135 updateDataCache.getValue();
137 logger.debug("Refresh {} skipped. Already refreshing", channelUID);
141 if (handleCommandsChannels(channelUID, command)) {
145 logger.debug("Locating action for {} channel '{}': '{}'", getThing().getUID(), channelUID.getId(), command);
146 if (!actions.isEmpty()) {
147 final MiIoBasicChannel miIoBasicChannel = actions.get(channelUID);
148 if (miIoBasicChannel != null) {
150 for (MiIoDeviceAction action : miIoBasicChannel.getActions()) {
152 JsonElement value = null;
153 JsonArray parameters = action.getParameters().deepCopy();
154 for (int i = 0; i < action.getParameters().size(); i++) {
155 JsonElement p = action.getParameters().get(i);
156 if (p.isJsonPrimitive() && p.getAsString().toLowerCase().contains("$value$")) {
161 String cmd = action.getCommand();
162 CommandParameterType paramType = action.getparameterType();
163 if (command instanceof QuantityType) {
164 QuantityType<?> qtc = null;
166 if (!miIoBasicChannel.getUnit().isBlank()) {
167 Unit<?> unit = MiIoQuantiyTypes.get(miIoBasicChannel.getUnit());
169 qtc = ((QuantityType<?>) command).toUnit(unit);
172 } catch (MeasurementParseException e) {
176 command = new DecimalType(qtc.toBigDecimal());
178 logger.debug("Could not convert QuantityType to '{}'", miIoBasicChannel.getUnit());
179 command = new DecimalType(((QuantityType<?>) command).toBigDecimal());
182 if (paramType == CommandParameterType.OPENCLOSE) {
183 if (command instanceof OpenClosedType) {
184 value = new JsonPrimitive(command == OpenClosedType.OPEN ? "open" : "close");
186 value = new JsonPrimitive(("ON".contentEquals(command.toString().toUpperCase())
187 || "1".contentEquals(command.toString())) ? "open" : "close");
190 if (paramType == CommandParameterType.OPENCLOSENUMBER) {
191 if (command instanceof OpenClosedType) {
192 value = new JsonPrimitive(command == OpenClosedType.OPEN ? 1 : 0);
194 value = new JsonPrimitive(("ON".contentEquals(command.toString().toUpperCase())
195 || "1".contentEquals(command.toString())) ? 1 : 0);
198 if (paramType == CommandParameterType.OPENCLOSESWITCH) {
199 if (command instanceof OpenClosedType) {
200 value = new JsonPrimitive(command == OpenClosedType.OPEN ? "on" : "off");
202 value = new JsonPrimitive(("ON".contentEquals(command.toString().toUpperCase())
203 || "1".contentEquals(command.toString())) ? "on" : "off");
206 if (paramType == CommandParameterType.COLOR) {
207 if (command instanceof HSBType) {
208 HSBType hsb = (HSBType) command;
209 Color color = Color.getHSBColor(hsb.getHue().floatValue() / 360,
210 hsb.getSaturation().floatValue() / 100, hsb.getBrightness().floatValue() / 100);
211 value = new JsonPrimitive(
212 (color.getRed() << 16) + (color.getGreen() << 8) + color.getBlue());
213 } else if (command instanceof DecimalType) {
214 // actually brightness is being set instead of a color
215 value = new JsonPrimitive(((DecimalType) command).toBigDecimal());
216 } else if (command instanceof OnOffType) {
217 value = new JsonPrimitive(command == OnOffType.ON ? 100 : 0);
219 logger.debug("Unsupported command for COLOR: {}", command);
221 } else if (command instanceof OnOffType) {
222 if (paramType == CommandParameterType.ONOFF) {
223 value = new JsonPrimitive(command == OnOffType.ON ? "on" : "off");
224 } else if (paramType == CommandParameterType.ONOFFPARA) {
225 cmd = cmd.replace("*", command == OnOffType.ON ? "on" : "off");
226 value = new JsonArray();
227 } else if (paramType == CommandParameterType.ONOFFBOOL) {
228 boolean boolCommand = command == OnOffType.ON;
229 value = new JsonPrimitive(boolCommand);
230 } else if (paramType == CommandParameterType.ONOFFBOOLSTRING) {
231 value = new JsonPrimitive(command == OnOffType.ON ? "true" : "false");
232 } else if (paramType == CommandParameterType.ONOFFNUMBER) {
233 value = new JsonPrimitive(command == OnOffType.ON ? 1 : 0);
235 } else if (command instanceof DecimalType) {
236 value = new JsonPrimitive(((DecimalType) command).toBigDecimal());
237 } else if (command instanceof StringType) {
238 if (paramType == CommandParameterType.STRING) {
239 value = new JsonPrimitive(command.toString().toLowerCase());
240 } else if (paramType == CommandParameterType.CUSTOMSTRING) {
241 value = new JsonPrimitive(parameters.get(valuePos).getAsString().replace("$value",
242 command.toString().toLowerCase()));
245 value = new JsonPrimitive(command.toString().toLowerCase());
247 if (paramType == CommandParameterType.EMPTY) {
248 value = parameters.deepCopy();
250 final MiIoDeviceActionCondition miIoDeviceActionCondition = action.getCondition();
251 if (miIoDeviceActionCondition != null) {
252 value = ActionConditions.executeAction(miIoDeviceActionCondition, deviceVariables, value,
255 // Check for miot channel
257 if (action.isMiOtAction()) {
258 value = miotActionTransform(action, miIoBasicChannel, value);
259 } else if (miIoBasicChannel.isMiOt()) {
260 value = miotTransform(miIoBasicChannel, value);
263 if (paramType != CommandParameterType.NONE && paramType != CommandParameterType.ONOFFPARA
265 if (parameters.size() > 0) {
266 parameters.set(valuePos, value);
268 parameters.add(value);
271 if (action.isMiOtAction() && parameters.size() > 0 && parameters.get(0).isJsonObject()) {
272 // hack as unlike any other commands miot actions parameters appear to be send as a json object
273 // instead of a json array
274 cmd = cmd + parameters.get(0).getAsJsonObject().toString();
276 cmd = cmd + parameters.toString();
279 logger.debug("Sending command {}", cmd);
282 if (miIoDeviceActionCondition != null) {
283 logger.debug("Conditional command {} not send, condition '{}' not met", cmd,
284 miIoDeviceActionCondition.getName());
286 logger.debug("Command not send. Value null");
291 logger.debug("Channel Id {} not in mapping.", channelUID.getId());
292 if (logger.isTraceEnabled()) {
293 for (Entry<ChannelUID, MiIoBasicChannel> a : actions.entrySet()) {
294 logger.trace("Available entries: {} : {}", a.getKey(), a.getValue().getFriendlyName());
300 logger.debug("Actions not loaded yet, or none available");
304 protected void forceStatusUpdate() {
305 updateDataCache.invalidateValue();
306 miIoScheduler.schedule(() -> {
308 }, 3000, TimeUnit.MILLISECONDS);
311 protected @Nullable JsonElement miotTransform(MiIoBasicChannel miIoBasicChannel, @Nullable JsonElement value) {
312 JsonObject json = new JsonObject();
313 json.addProperty("did", miIoBasicChannel.getChannel());
314 json.addProperty("siid", miIoBasicChannel.getSiid());
315 json.addProperty("piid", miIoBasicChannel.getPiid());
316 json.add("value", value);
320 protected @Nullable JsonElement miotActionTransform(MiIoDeviceAction action, MiIoBasicChannel miIoBasicChannel,
321 @Nullable JsonElement value) {
322 JsonObject json = new JsonObject();
323 json.addProperty("did", miIoBasicChannel.getChannel());
324 json.addProperty("siid", action.getSiid());
325 json.addProperty("aiid", action.getAiid());
327 json.add("in", value);
333 protected synchronized void updateData() {
334 logger.debug("Periodic update for '{}' ({})", getThing().getUID().toString(), getThing().getThingTypeUID());
335 final MiIoAsyncCommunication miioCom = getConnection();
337 if (!hasConnection() || skipUpdate() || miioCom == null) {
340 checkChannelStructure();
342 sendCommand(MiIoCommand.MIIO_INFO);
344 final MiIoBasicDevice midevice = miioDevice;
345 if (midevice != null) {
346 deviceVariables.put(TIMESTAMP, Instant.now().getEpochSecond());
347 refreshProperties(midevice, "");
348 refreshCustomProperties(midevice, false);
351 } catch (Exception e) {
352 logger.debug("Error while updating '{}': ", getThing().getUID().toString(), e);
356 private boolean customRefreshIntervalCheck(MiIoBasicChannel miChannel) {
357 if (miChannel.getRefreshInterval() > 1) {
358 int iteration = customRefreshInterval.getOrDefault(miChannel.getChannel(), 0);
360 customRefreshInterval.put(miChannel.getChannel(), miChannel.getRefreshInterval() - 1);
362 logger.debug("Skip refresh of channel {} for {}. Next refresh in {} cycles.", miChannel.getChannel(),
363 getThing().getUID(), iteration);
364 customRefreshInterval.put(miChannel.getChannel(), iteration - 1);
371 private boolean linkedChannelCheck(MiIoBasicChannel miChannel) {
372 if (!isLinked(miChannel.getChannel())) {
373 logger.debug("Skip refresh of channel {} for {} as it is not linked", miChannel.getChannel(),
374 getThing().getUID());
380 protected void refreshCustomProperties(MiIoBasicDevice midevice, boolean cloudOnly) {
381 logger.debug("Custom Refresh for device '{}': {} channels ", getThing().getUID(),
382 refreshListCustomCommands.size());
383 for (MiIoBasicChannel miChannel : refreshListCustomCommands.values()) {
384 if (customRefreshIntervalCheck(miChannel) || !linkedChannelCheck(miChannel)) {
387 final JsonElement para = miChannel.getCustomRefreshParameters();
388 String cmd = miChannel.getChannelCustomRefreshCommand() + (para != null ? para.toString() : "");
389 if (!cmd.startsWith("/") && !cloudOnly) {
390 cmds.put(sendCommand(cmd), miChannel.getChannel());
392 if (cloudServer.isBlank()) {
393 logger.debug("Cloudserver empty. Skipping refresh for {} channel '{}'", getThing().getUID(),
394 miChannel.getChannel());
396 cmds.put(sendCommand(cmd, cloudServer), miChannel.getChannel());
402 protected boolean refreshProperties(MiIoBasicDevice device, String childId) {
403 String command = device.getDevice().getPropertyMethod();
404 int maxProperties = device.getDevice().getMaxProperties();
405 JsonArray getPropString = new JsonArray();
406 if (!childId.isBlank()) {
407 getPropString.add(childId);
410 for (MiIoBasicChannel miChannel : refreshList) {
411 if (customRefreshIntervalCheck(miChannel) || !linkedChannelCheck(miChannel)) {
414 JsonElement property;
415 if (miChannel.isMiOt()) {
416 JsonObject json = new JsonObject();
417 json.addProperty("did", miChannel.getProperty());
418 json.addProperty("siid", miChannel.getSiid());
419 json.addProperty("piid", miChannel.getPiid());
422 property = new JsonPrimitive(miChannel.getProperty());
424 getPropString.add(property);
425 if (getPropString.size() >= maxProperties) {
426 sendRefreshProperties(command, getPropString);
427 getPropString = new JsonArray();
428 if (!childId.isBlank()) {
429 getPropString.add(childId);
433 if (getPropString.size() > (childId.isBlank() ? 0 : 1)) {
434 sendRefreshProperties(command, getPropString);
439 protected void sendRefreshProperties(String command, JsonArray getPropString) {
440 JsonArray para = getPropString;
441 if (MiIoCommand.GET_DEVICE_PROPERTY_EXP.getCommand().contentEquals(command)) {
442 logger.debug("This seems a subdevice propery refresh for {}... ({} {})", getThing().getUID(), command,
443 getPropString.toString());
444 para = new JsonArray();
445 para.add(getPropString);
447 sendCommand(command, para.toString(), getCloudServer());
451 * Checks if the channel structure has been build already based on the model data. If not build it.
453 protected void checkChannelStructure() {
454 final MiIoBindingConfiguration configuration = this.configuration;
455 if (configuration == null) {
458 if (!hasChannelStructure) {
459 if (configuration.model.isEmpty()) {
460 logger.debug("Model needs to be determined");
461 isIdentified = false;
463 hasChannelStructure = buildChannelStructure(configuration.model);
466 if (hasChannelStructure) {
467 refreshList = new ArrayList<>();
468 refreshListCustomCommands = new HashMap<>();
469 final MiIoBasicDevice miioDevice = this.miioDevice;
470 if (miioDevice != null) {
471 for (MiIoBasicChannel miChannel : miioDevice.getDevice().getChannels()) {
472 if (miChannel.getRefresh()) {
473 if (miChannel.getChannelCustomRefreshCommand().isBlank()) {
474 refreshList.add(miChannel);
476 String cm = miChannel.getChannelCustomRefreshCommand();
477 refreshListCustomCommands.put(cm.trim(), miChannel);
485 protected boolean buildChannelStructure(String deviceName) {
486 logger.debug("Building Channel Structure for {} - Model: {}", getThing().getUID().toString(), deviceName);
487 URL fn = miIoDatabaseWatchService.getDatabaseUrl(deviceName);
489 logger.warn("Database entry for model '{}' cannot be found.", deviceName);
493 JsonObject deviceMapping = Utils.convertFileToJSON(fn);
494 logger.debug("Using device database: {} for device {}", fn.getFile(), deviceName);
495 String key = fn.getFile().replaceFirst("/database/", "").split("json")[0];
496 Gson gson = new GsonBuilder().serializeNulls().create();
497 miioDevice = gson.fromJson(deviceMapping, MiIoBasicDevice.class);
498 for (Channel ch : getThing().getChannels()) {
499 logger.debug("Current thing channels {}, type: {}", ch.getUID(), ch.getChannelTypeUID());
501 ThingBuilder thingBuilder = editThing();
502 int channelsAdded = 0;
504 // make a map of the actions
505 actions = new HashMap<>();
506 final MiIoBasicDevice device = this.miioDevice;
507 if (device != null) {
508 for (Channel cn : getThing().getChannels()) {
509 logger.trace("Channel '{}' for thing {} already exist... removing", cn.getUID(),
510 getThing().getUID());
511 if (!PERSISTENT_CHANNELS.contains(cn.getUID().getId().toString())) {
512 thingBuilder.withoutChannels(cn);
515 for (MiIoBasicChannel miChannel : device.getDevice().getChannels()) {
516 logger.debug("properties {}", miChannel);
517 if (!miChannel.getType().isEmpty()) {
518 basicChannelTypeProvider.addChannelType(miChannel, deviceName);
519 ChannelUID channelUID = addChannel(thingBuilder, miChannel, deviceName, key);
520 if (channelUID != null) {
521 actions.put(channelUID, miChannel);
524 logger.debug("Channel for {} ({}) not loaded", miChannel.getChannel(),
525 miChannel.getFriendlyName());
528 logger.debug("Channel {} ({}), not loaded, missing type", miChannel.getChannel(),
529 miChannel.getFriendlyName());
533 // only update if channels were added/removed
534 if (channelsAdded > 0) {
535 logger.debug("Current thing channels added: {}", channelsAdded);
536 updateThing(thingBuilder.build());
539 } catch (JsonIOException | JsonSyntaxException e) {
540 logger.warn("Error parsing database Json", e);
541 } catch (IOException e) {
542 logger.warn("Error reading database file", e);
543 } catch (Exception e) {
544 logger.warn("Error creating channel structure", e);
549 protected @Nullable ChannelUID addChannel(ThingBuilder thingBuilder, MiIoBasicChannel miChannel, String model,
551 String channel = miChannel.getChannel();
552 String dataType = miChannel.getType();
553 if (channel.isEmpty() || dataType.isEmpty()) {
554 logger.info("Channel '{}', UID '{}' cannot be added incorrectly configured database. ", channel,
555 getThing().getUID());
558 ChannelUID channelUID = new ChannelUID(getThing().getUID(), channel);
559 String label = getLocalText(I18N_CHANNEL_PREFIX + key + channel, miChannel.getFriendlyName());
560 ChannelBuilder newChannel = ChannelBuilder.create(channelUID, dataType).withLabel(label);
561 boolean useGeneratedChannelType = false;
562 if (!miChannel.getChannelType().isBlank()) {
563 ChannelTypeUID channelTypeUID = new ChannelTypeUID(miChannel.getChannelType());
564 if (channelTypeRegistry.getChannelType(channelTypeUID) != null) {
565 newChannel = newChannel.withType(channelTypeUID);
566 final LinkedHashSet<String> tags = miChannel.getTags();
567 if (tags != null && !tags.isEmpty()) {
568 newChannel.withDefaultTags(tags);
571 logger.debug("ChannelType '{}' is not available. Check the Json file for {}", channelTypeUID, model);
572 useGeneratedChannelType = true;
575 useGeneratedChannelType = true;
577 if (useGeneratedChannelType) {
578 newChannel = newChannel
579 .withType(new ChannelTypeUID(BINDING_ID, model.toUpperCase().replace(".", "_") + "_" + channel));
580 final Set<String> tags = miChannel.getTags();
581 if (tags != null && !tags.isEmpty()) {
582 newChannel.withDefaultTags(tags);
585 thingBuilder.withChannel(newChannel.build());
589 protected @Nullable MiIoBasicChannel getChannel(String parameter) {
590 for (MiIoBasicChannel refreshEntry : refreshList) {
591 if (refreshEntry.getProperty().equals(parameter)) {
595 logger.trace("Did not find channel for {} in {}", parameter, refreshList);
599 private @Nullable MiIoBasicChannel getCustomRefreshChannel(String channelName) {
600 for (MiIoBasicChannel refreshEntry : refreshListCustomCommands.values()) {
601 if (refreshEntry.getChannel().equals(channelName)) {
605 logger.trace("Did not find channel for {} in {}", channelName, refreshList);
609 protected void updatePropsFromJsonArray(MiIoSendCommand response) {
610 boolean isSubdeviceUpdate = false;
611 JsonArray res = response.getResult().getAsJsonArray();
612 JsonArray para = JsonParser.parseString(response.getCommandString()).getAsJsonObject().get("params")
614 if (para.get(0).isJsonArray()) {
615 isSubdeviceUpdate = true;
616 para = para.get(0).getAsJsonArray();
618 if (res.get(0).isJsonArray()) {
619 res = res.get(0).getAsJsonArray();
622 if (res.size() != para.size()) {
623 logger.debug("Unexpected size different{}. Request size {}, response size {}. (Req: {}, Resp:{})",
624 isSubdeviceUpdate ? " for childdevice refresh" : "", para.size(), res.size(), para, res);
627 for (int i = 0; i < para.size(); i++) {
628 // This is a miot parameter
630 final JsonElement paraElement = para.get(i);
631 if (paraElement.isJsonObject()) { // miot channel
632 param = paraElement.getAsJsonObject().get("did").getAsString();
634 param = paraElement.getAsString();
636 JsonElement val = res.get(i);
637 if (val.isJsonNull()) {
638 logger.debug("Property '{}' returned null (is it supported?).", param);
640 } else if (val.isJsonObject()) { // miot channel
641 val = val.getAsJsonObject().get("value");
643 MiIoBasicChannel basicChannel = getChannel(param);
644 updateChannel(basicChannel, param, val);
648 protected void updatePropsFromJsonObject(MiIoSendCommand response) {
649 JsonObject res = response.getResult().getAsJsonObject();
650 for (Object k : res.keySet()) {
651 String param = (String) k;
652 JsonElement val = res.get(param);
653 if (val.isJsonNull()) {
654 logger.debug("Property '{}' returned null (is it supported?).", param);
657 MiIoBasicChannel basicChannel = getChannel(param);
658 updateChannel(basicChannel, param, val);
662 protected void updateChannel(@Nullable MiIoBasicChannel basicChannel, String param, JsonElement value) {
663 JsonElement val = value;
664 deviceVariables.put(param, val);
665 if (basicChannel == null) {
666 logger.debug("Channel not found for {}", param);
669 final String transformation = basicChannel.getTransformation();
670 if (transformation != null) {
671 JsonElement transformed = Conversions.execute(transformation, val, deviceVariables);
672 logger.debug("Transformed with '{}': {} {} -> {} ", transformation, basicChannel.getFriendlyName(), val,
677 String[] chType = basicChannel.getType().toLowerCase().split(":");
680 quantityTypeUpdate(basicChannel, val, chType.length > 1 ? chType[1] : "");
683 updateState(basicChannel.getChannel(), new PercentType(val.getAsBigDecimal()));
686 if (val.isJsonPrimitive()) {
687 updateState(basicChannel.getChannel(), new StringType(val.getAsString()));
689 updateState(basicChannel.getChannel(), new StringType(val.toString()));
693 if (val.getAsJsonPrimitive().isNumber()) {
694 updateState(basicChannel.getChannel(), val.getAsInt() > 0 ? OnOffType.ON : OnOffType.OFF);
696 String strVal = val.getAsString().toLowerCase();
697 updateState(basicChannel.getChannel(),
698 "on".equals(strVal) || "true".equals(strVal) || "1".equals(strVal) ? OnOffType.ON
703 if (val.getAsJsonPrimitive().isNumber()) {
704 updateState(basicChannel.getChannel(),
705 val.getAsInt() > 0 ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
707 String strVal = val.getAsString().toLowerCase();
708 updateState(basicChannel.getChannel(),
709 "open".equals(strVal) || "on".equals(strVal) || "true".equals(strVal)
710 || "1".equals(strVal) ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
714 if (val.isJsonPrimitive()
715 && (val.getAsJsonPrimitive().isNumber() || val.getAsString().matches("^[0-9]+$"))) {
716 Color rgb = new Color(val.getAsInt());
717 HSBType hsb = HSBType.fromRGB(rgb.getRed(), rgb.getGreen(), rgb.getBlue());
718 updateState(basicChannel.getChannel(), hsb);
721 HSBType hsb = HSBType.valueOf(val.getAsString().replace("[", "").replace("]", ""));
722 updateState(basicChannel.getChannel(), hsb);
723 } catch (IllegalArgumentException e) {
724 logger.debug("Failed updating channel '{}'. Could not convert '{}' to color",
725 basicChannel.getChannel(), val.getAsString());
730 logger.debug("No update logic for channeltype '{}' ", basicChannel.getType());
732 } catch (Exception e) {
733 logger.debug("Error updating {} property {} with '{}' : {}: {}", getThing().getUID(),
734 basicChannel.getChannel(), val, e.getClass().getCanonicalName(), e.getMessage());
735 logger.trace("Property update error detail:", e);
739 protected void quantityTypeUpdate(MiIoBasicChannel basicChannel, JsonElement val, String type) {
740 if (!basicChannel.getUnit().isBlank()) {
741 Unit<?> unit = MiIoQuantiyTypes.get(basicChannel.getUnit());
743 logger.debug("'{}' channel '{}' has unit '{}' with symbol '{}'.", getThing().getUID(),
744 basicChannel.getChannel(), basicChannel.getUnit(), unit);
745 updateState(basicChannel.getChannel(), new QuantityType<>(val.getAsBigDecimal(), unit));
748 "Unit '{}' used by '{}' channel '{}' is not found in conversion table... Trying anyway to submit as the update.",
749 basicChannel.getUnit(), getThing().getUID(), basicChannel.getChannel());
750 updateState(basicChannel.getChannel(),
751 new QuantityType<>(val.getAsBigDecimal().toPlainString() + " " + basicChannel.getUnit()));
755 // if no unit is provided or unit not found use default units, these units have so far been seen for miio
757 switch (type.toLowerCase()) {
759 updateState(basicChannel.getChannel(), new QuantityType<>(val.getAsBigDecimal(), SIUnits.CELSIUS));
761 case "electriccurrent":
762 updateState(basicChannel.getChannel(), new QuantityType<>(val.getAsBigDecimal(), Units.AMPERE));
765 updateState(basicChannel.getChannel(), new QuantityType<>(val.getAsBigDecimal(), Units.WATT));
768 updateState(basicChannel.getChannel(), new QuantityType<>(val.getAsBigDecimal(), Units.HOUR));
771 updateState(basicChannel.getChannel(), new DecimalType(val.getAsBigDecimal()));
776 public void onMessageReceived(MiIoSendCommand response) {
777 super.onMessageReceived(response);
778 if (response.isError() || (!response.getSender().isBlank()
779 && !response.getSender().contentEquals(getThing().getUID().getAsString()))) {
780 logger.trace("Device {} is not processing command {} as no match. Sender id:'{}'", getThing().getUID(),
781 response.getId(), response.getSender());
785 switch (response.getCommand()) {
788 case GET_DEVICE_PROPERTY_EXP:
792 if (response.getResult().isJsonArray()) {
793 updatePropsFromJsonArray(response);
794 } else if (response.getResult().isJsonObject()) {
795 updatePropsFromJsonObject(response);
799 String channel = cmds.get(response.getId());
800 if (channel != null) {
801 logger.debug("Processing custom refresh command response for '{}' - {}", response.getMethod(),
802 response.getResult());
803 final MiIoBasicChannel ch = getCustomRefreshChannel(channel);
805 if (response.getResult().isJsonArray()) {
806 JsonArray cmdResponse = response.getResult().getAsJsonArray();
807 final String transformation = ch.getTransformation();
808 if (transformation == null || transformation.isBlank()) {
809 JsonElement response0 = cmdResponse.get(0);
810 updateChannel(ch, ch.getChannel(), response0.isJsonPrimitive() ? response0
811 : new JsonPrimitive(response0.toString()));
813 updateChannel(ch, ch.getChannel(), cmdResponse);
816 updateChannel(ch, ch.getChannel(), new JsonPrimitive(response.getResult().toString()));
819 cmds.remove(response.getId());
821 logger.debug("Could not identify channel for {}. Device {} has {} commands in queue.",
822 response.getMethod(), getThing().getUID(), cmds.size());
826 } catch (Exception e) {
827 logger.debug("Error while handing message {}", response.getResponse(), e);