]> git.basschouten.com Git - openhab-addons.git/blob
4c7432b7fd650ba79a491efc5951a4632cd9db6a
[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.awt.Color;
18 import java.io.IOException;
19 import java.net.URL;
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;
25 import java.util.Map;
26 import java.util.Map.Entry;
27 import java.util.Set;
28 import java.util.concurrent.TimeUnit;
29
30 import javax.measure.Unit;
31 import javax.measure.format.MeasurementParseException;
32
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.library.types.DecimalType;
53 import org.openhab.core.library.types.HSBType;
54 import org.openhab.core.library.types.OnOffType;
55 import org.openhab.core.library.types.PercentType;
56 import org.openhab.core.library.types.QuantityType;
57 import org.openhab.core.library.types.StringType;
58 import org.openhab.core.library.unit.SIUnits;
59 import org.openhab.core.library.unit.Units;
60 import org.openhab.core.thing.Channel;
61 import org.openhab.core.thing.ChannelUID;
62 import org.openhab.core.thing.Thing;
63 import org.openhab.core.thing.binding.builder.ChannelBuilder;
64 import org.openhab.core.thing.binding.builder.ThingBuilder;
65 import org.openhab.core.thing.type.ChannelTypeRegistry;
66 import org.openhab.core.thing.type.ChannelTypeUID;
67 import org.openhab.core.types.Command;
68 import org.openhab.core.types.RefreshType;
69 import org.slf4j.Logger;
70 import org.slf4j.LoggerFactory;
71
72 import com.google.gson.Gson;
73 import com.google.gson.GsonBuilder;
74 import com.google.gson.JsonArray;
75 import com.google.gson.JsonElement;
76 import com.google.gson.JsonIOException;
77 import com.google.gson.JsonObject;
78 import com.google.gson.JsonParser;
79 import com.google.gson.JsonPrimitive;
80 import com.google.gson.JsonSyntaxException;
81
82 /**
83  * The {@link MiIoBasicHandler} is responsible for handling commands, which are
84  * sent to one of the channels.
85  *
86  * @author Marcel Verpaalen - Initial contribution
87  */
88 @NonNullByDefault
89 public class MiIoBasicHandler extends MiIoAbstractHandler {
90     private final Logger logger = LoggerFactory.getLogger(MiIoBasicHandler.class);
91     private boolean hasChannelStructure;
92
93     private final ExpiringCache<Boolean> updateDataCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
94         miIoScheduler.schedule(this::updateData, 0, TimeUnit.SECONDS);
95         return true;
96     });
97
98     List<MiIoBasicChannel> refreshList = new ArrayList<>();
99     private Map<String, MiIoBasicChannel> refreshListCustomCommands = new HashMap<>();
100
101     private @Nullable MiIoBasicDevice miioDevice;
102     private Map<ChannelUID, MiIoBasicChannel> actions = new HashMap<>();
103     private ChannelTypeRegistry channelTypeRegistry;
104     private BasicChannelTypeProvider basicChannelTypeProvider;
105
106     public MiIoBasicHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
107             CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry,
108             BasicChannelTypeProvider basicChannelTypeProvider) {
109         super(thing, miIoDatabaseWatchService, cloudConnector);
110         this.channelTypeRegistry = channelTypeRegistry;
111         this.basicChannelTypeProvider = basicChannelTypeProvider;
112     }
113
114     @Override
115     public void initialize() {
116         super.initialize();
117         hasChannelStructure = false;
118         isIdentified = false;
119         refreshList = new ArrayList<>();
120         refreshListCustomCommands = new HashMap<>();
121     }
122
123     @Override
124     public void handleCommand(ChannelUID channelUID, Command receivedCommand) {
125         Command command = receivedCommand;
126         deviceVariables.put(TIMESTAMP, Instant.now().getEpochSecond());
127         if (command == RefreshType.REFRESH) {
128             if (updateDataCache.isExpired()) {
129                 logger.debug("Refreshing {}", channelUID);
130                 updateDataCache.getValue();
131             } else {
132                 logger.debug("Refresh {} skipped. Already refreshing", channelUID);
133             }
134             return;
135         }
136         if (handleCommandsChannels(channelUID, command)) {
137             forceStatusUpdate();
138             return;
139         }
140         logger.debug("Locating action for {} channel '{}': '{}'", getThing().getUID(), channelUID.getId(), command);
141         if (!actions.isEmpty()) {
142             final MiIoBasicChannel miIoBasicChannel = actions.get(channelUID);
143             if (miIoBasicChannel != null) {
144                 int valuePos = 0;
145                 for (MiIoDeviceAction action : miIoBasicChannel.getActions()) {
146                     @Nullable
147                     JsonElement value = null;
148                     JsonArray parameters = action.getParameters().deepCopy();
149                     for (int i = 0; i < action.getParameters().size(); i++) {
150                         JsonElement p = action.getParameters().get(i);
151                         if (p.isJsonPrimitive() && p.getAsString().toLowerCase().contains("$value$")) {
152                             valuePos = i;
153                             break;
154                         }
155                     }
156                     String cmd = action.getCommand();
157                     CommandParameterType paramType = action.getparameterType();
158                     if (command instanceof QuantityType) {
159                         QuantityType<?> qtc = null;
160                         try {
161                             if (!miIoBasicChannel.getUnit().isBlank()) {
162                                 Unit<?> unit = MiIoQuantiyTypes.get(miIoBasicChannel.getUnit());
163                                 if (unit != null) {
164                                     qtc = ((QuantityType<?>) command).toUnit(unit);
165                                 }
166                             }
167                         } catch (MeasurementParseException e) {
168                             // swallow
169                         }
170                         if (qtc != null) {
171                             command = new DecimalType(qtc.toBigDecimal());
172                         } else {
173                             logger.debug("Could not convert QuantityType to '{}'", miIoBasicChannel.getUnit());
174                             command = new DecimalType(((QuantityType<?>) command).toBigDecimal());
175                         }
176                     }
177                     if (paramType == CommandParameterType.COLOR) {
178                         if (command instanceof HSBType) {
179                             HSBType hsb = (HSBType) command;
180                             Color color = Color.getHSBColor(hsb.getHue().floatValue() / 360,
181                                     hsb.getSaturation().floatValue() / 100, hsb.getBrightness().floatValue() / 100);
182                             value = new JsonPrimitive(
183                                     (color.getRed() << 16) + (color.getGreen() << 8) + color.getBlue());
184                         } else if (command instanceof DecimalType) {
185                             // actually brightness is being set instead of a color
186                             value = new JsonPrimitive(((DecimalType) command).toBigDecimal());
187                         } else if (command instanceof OnOffType) {
188                             value = new JsonPrimitive(command == OnOffType.ON ? 100 : 0);
189                         } else {
190                             logger.debug("Unsupported command for COLOR: {}", command);
191                         }
192                     } else if (command instanceof OnOffType) {
193                         if (paramType == CommandParameterType.ONOFF) {
194                             value = new JsonPrimitive(command == OnOffType.ON ? "on" : "off");
195                         } else if (paramType == CommandParameterType.ONOFFPARA) {
196                             cmd = cmd.replace("*", command == OnOffType.ON ? "on" : "off");
197                             value = new JsonArray();
198                         } else if (paramType == CommandParameterType.ONOFFBOOL) {
199                             boolean boolCommand = command == OnOffType.ON;
200                             value = new JsonPrimitive(boolCommand);
201                         } else if (paramType == CommandParameterType.ONOFFBOOLSTRING) {
202                             value = new JsonPrimitive(command == OnOffType.ON ? "true" : "false");
203                         } else if (paramType == CommandParameterType.ONOFFNUMBER) {
204                             value = new JsonPrimitive(command == OnOffType.ON ? 1 : 0);
205                         }
206                     } else if (command instanceof DecimalType) {
207                         value = new JsonPrimitive(((DecimalType) command).toBigDecimal());
208                     } else if (command instanceof StringType) {
209                         if (paramType == CommandParameterType.STRING) {
210                             value = new JsonPrimitive(command.toString().toLowerCase());
211                         } else if (paramType == CommandParameterType.CUSTOMSTRING) {
212                             value = new JsonPrimitive(parameters.get(valuePos).getAsString().replace("$value",
213                                     command.toString().toLowerCase()));
214                         }
215                     } else {
216                         value = new JsonPrimitive(command.toString().toLowerCase());
217                     }
218                     if (paramType == CommandParameterType.EMPTY) {
219                         value = new JsonArray();
220                     }
221                     final MiIoDeviceActionCondition miIoDeviceActionCondition = action.getCondition();
222                     if (miIoDeviceActionCondition != null) {
223                         value = ActionConditions.executeAction(miIoDeviceActionCondition, deviceVariables, value,
224                                 command);
225                     }
226                     // Check for miot channel
227                     if (value != null) {
228                         if (action.isMiOtAction()) {
229                             value = miotActionTransform(action, miIoBasicChannel, value);
230                         } else if (miIoBasicChannel.isMiOt()) {
231                             value = miotTransform(miIoBasicChannel, value);
232                         }
233                     }
234                     if (paramType != CommandParameterType.NONE && paramType != CommandParameterType.ONOFFPARA
235                             && value != null) {
236                         if (parameters.size() > 0) {
237                             parameters.set(valuePos, value);
238                         } else {
239                             parameters.add(value);
240                         }
241                     }
242                     if (action.isMiOtAction() && parameters.size() > 0 && parameters.get(0).isJsonObject()) {
243                         // hack as unlike any other commands miot actions parameters appear to be send as a json object
244                         // instead of a json array
245                         cmd = cmd + parameters.get(0).getAsJsonObject().toString();
246                     } else {
247                         cmd = cmd + parameters.toString();
248                     }
249                     if (value != null) {
250                         logger.debug("Sending command {}", cmd);
251                         sendCommand(cmd);
252                     } else {
253                         if (miIoDeviceActionCondition != null) {
254                             logger.debug("Conditional command {} not send, condition '{}' not met", cmd,
255                                     miIoDeviceActionCondition.getName());
256                         } else {
257                             logger.debug("Command not send. Value null");
258                         }
259                     }
260                 }
261             } else {
262                 logger.debug("Channel Id {} not in mapping.", channelUID.getId());
263                 if (logger.isTraceEnabled()) {
264                     for (Entry<ChannelUID, MiIoBasicChannel> a : actions.entrySet()) {
265                         logger.trace("Available entries: {} : {}", a.getKey(), a.getValue().getFriendlyName());
266                     }
267                 }
268             }
269             forceStatusUpdate();
270         } else {
271             logger.debug("Actions not loaded yet, or none available");
272         }
273     }
274
275     private void forceStatusUpdate() {
276         updateDataCache.invalidateValue();
277         miIoScheduler.schedule(() -> {
278             updateData();
279         }, 3000, TimeUnit.MILLISECONDS);
280     }
281
282     private @Nullable JsonElement miotTransform(MiIoBasicChannel miIoBasicChannel, @Nullable JsonElement value) {
283         JsonObject json = new JsonObject();
284         json.addProperty("did", miIoBasicChannel.getChannel());
285         json.addProperty("siid", miIoBasicChannel.getSiid());
286         json.addProperty("piid", miIoBasicChannel.getPiid());
287         json.add("value", value);
288         return json;
289     }
290
291     private @Nullable JsonElement miotActionTransform(MiIoDeviceAction action, MiIoBasicChannel miIoBasicChannel,
292             @Nullable JsonElement value) {
293         JsonObject json = new JsonObject();
294         json.addProperty("did", miIoBasicChannel.getChannel());
295         json.addProperty("siid", action.getSiid());
296         json.addProperty("aiid", action.getAiid());
297         if (value != null) {
298             json.add("in", value);
299         }
300         return json;
301     }
302
303     @Override
304     protected synchronized void updateData() {
305         logger.debug("Periodic update for '{}' ({})", getThing().getUID().toString(), getThing().getThingTypeUID());
306         final MiIoAsyncCommunication miioCom = getConnection();
307         try {
308             if (!hasConnection() || skipUpdate() || miioCom == null) {
309                 return;
310             }
311             checkChannelStructure();
312             if (!isIdentified) {
313                 sendCommand(MiIoCommand.MIIO_INFO);
314             }
315             final MiIoBasicDevice midevice = miioDevice;
316             if (midevice != null) {
317                 deviceVariables.put(TIMESTAMP, Instant.now().getEpochSecond());
318                 refreshProperties(midevice);
319                 refreshCustomProperties(midevice);
320                 refreshNetwork();
321             }
322         } catch (Exception e) {
323             logger.debug("Error while updating '{}': ", getThing().getUID().toString(), e);
324         }
325     }
326
327     private void refreshCustomProperties(MiIoBasicDevice midevice) {
328         for (MiIoBasicChannel miChannel : refreshListCustomCommands.values()) {
329             if (!isLinked(miChannel.getChannel())) {
330                 logger.debug("Skip refresh of channel {} for {} as it is not linked", miChannel.getChannel(),
331                         getThing().getUID());
332                 continue;
333             }
334             String cmd = miChannel.getChannelCustomRefreshCommand();
335             if (!cmd.startsWith("/")) {
336                 cmds.put(sendCommand(miChannel.getChannelCustomRefreshCommand()), miChannel.getChannel());
337             } else {
338                 if (cloudServer.isBlank()) {
339                     logger.debug("Cloudserver empty. Skipping refresh for {} channel '{}'", getThing().getUID(),
340                             miChannel.getChannel());
341                 } else {
342                     cmds.put(sendCommand(cmd, cloudServer), miChannel.getChannel());
343                 }
344             }
345         }
346     }
347
348     private boolean refreshProperties(MiIoBasicDevice device) {
349         MiIoCommand command = MiIoCommand.getCommand(device.getDevice().getPropertyMethod());
350         int maxProperties = device.getDevice().getMaxProperties();
351         JsonArray getPropString = new JsonArray();
352         for (MiIoBasicChannel miChannel : refreshList) {
353             if (!isLinked(miChannel.getChannel())) {
354                 logger.debug("Skip refresh of channel {} for {} as it is not linked", miChannel.getChannel(),
355                         getThing().getUID());
356                 continue;
357             }
358             JsonElement property;
359             if (miChannel.isMiOt()) {
360                 JsonObject json = new JsonObject();
361                 json.addProperty("did", miChannel.getProperty());
362                 json.addProperty("siid", miChannel.getSiid());
363                 json.addProperty("piid", miChannel.getPiid());
364                 property = json;
365             } else {
366                 property = new JsonPrimitive(miChannel.getProperty());
367             }
368             getPropString.add(property);
369             if (getPropString.size() >= maxProperties) {
370                 sendRefreshProperties(command, getPropString);
371                 getPropString = new JsonArray();
372             }
373         }
374         if (getPropString.size() > 0) {
375             sendRefreshProperties(command, getPropString);
376         }
377         return true;
378     }
379
380     private void sendRefreshProperties(MiIoCommand command, JsonArray getPropString) {
381         sendCommand(command, getPropString.toString());
382     }
383
384     /**
385      * Checks if the channel structure has been build already based on the model data. If not build it.
386      */
387     private void checkChannelStructure() {
388         final MiIoBindingConfiguration configuration = this.configuration;
389         if (configuration == null) {
390             return;
391         }
392         if (!hasChannelStructure) {
393             if (configuration.model.isEmpty()) {
394                 logger.debug("Model needs to be determined");
395                 isIdentified = false;
396             } else {
397                 hasChannelStructure = buildChannelStructure(configuration.model);
398             }
399         }
400         if (hasChannelStructure) {
401             refreshList = new ArrayList<>();
402             refreshListCustomCommands = new HashMap<>();
403             final MiIoBasicDevice miioDevice = this.miioDevice;
404             if (miioDevice != null) {
405                 for (MiIoBasicChannel miChannel : miioDevice.getDevice().getChannels()) {
406                     if (miChannel.getRefresh()) {
407                         if (miChannel.getChannelCustomRefreshCommand().isBlank()) {
408                             refreshList.add(miChannel);
409                         } else {
410                             String i = miChannel.getChannelCustomRefreshCommand().split("\\[")[0];
411                             refreshListCustomCommands.put(i.trim(), miChannel);
412                         }
413                     }
414                 }
415             }
416
417         }
418     }
419
420     private boolean buildChannelStructure(String deviceName) {
421         logger.debug("Building Channel Structure for {} - Model: {}", getThing().getUID().toString(), deviceName);
422         URL fn = miIoDatabaseWatchService.getDatabaseUrl(deviceName);
423         if (fn == null) {
424             logger.warn("Database entry for model '{}' cannot be found.", deviceName);
425             return false;
426         }
427         try {
428             JsonObject deviceMapping = Utils.convertFileToJSON(fn);
429             logger.debug("Using device database: {} for device {}", fn.getFile(), deviceName);
430             Gson gson = new GsonBuilder().serializeNulls().create();
431             miioDevice = gson.fromJson(deviceMapping, MiIoBasicDevice.class);
432             for (Channel ch : getThing().getChannels()) {
433                 logger.debug("Current thing channels {}, type: {}", ch.getUID(), ch.getChannelTypeUID());
434             }
435             ThingBuilder thingBuilder = editThing();
436             int channelsAdded = 0;
437
438             // make a map of the actions
439             actions = new HashMap<>();
440             final MiIoBasicDevice device = this.miioDevice;
441             if (device != null) {
442                 for (Channel cn : getThing().getChannels()) {
443                     logger.trace("Channel '{}' for thing {} already exist... removing", cn.getUID(),
444                             getThing().getUID());
445                     if (!PERSISTENT_CHANNELS.contains(cn.getUID().getId().toString())) {
446                         thingBuilder.withoutChannels(cn);
447                     }
448                 }
449                 for (MiIoBasicChannel miChannel : device.getDevice().getChannels()) {
450                     logger.debug("properties {}", miChannel);
451                     if (!miChannel.getType().isEmpty()) {
452                         basicChannelTypeProvider.addChannelType(miChannel, deviceName);
453                         ChannelUID channelUID = addChannel(thingBuilder, miChannel, deviceName);
454                         if (channelUID != null) {
455                             actions.put(channelUID, miChannel);
456                             channelsAdded++;
457                         } else {
458                             logger.debug("Channel for {} ({}) not loaded", miChannel.getChannel(),
459                                     miChannel.getFriendlyName());
460                         }
461                     } else {
462                         logger.debug("Channel {} ({}), not loaded, missing type", miChannel.getChannel(),
463                                 miChannel.getFriendlyName());
464                     }
465                 }
466             }
467             // only update if channels were added/removed
468             if (channelsAdded > 0) {
469                 logger.debug("Current thing channels added: {}", channelsAdded);
470                 updateThing(thingBuilder.build());
471             }
472             return true;
473         } catch (JsonIOException | JsonSyntaxException e) {
474             logger.warn("Error parsing database Json", e);
475         } catch (IOException e) {
476             logger.warn("Error reading database file", e);
477         } catch (Exception e) {
478             logger.warn("Error creating channel structure", e);
479         }
480         return false;
481     }
482
483     private @Nullable ChannelUID addChannel(ThingBuilder thingBuilder, MiIoBasicChannel miChannel, String model) {
484         String channel = miChannel.getChannel();
485         String dataType = miChannel.getType();
486         if (channel.isEmpty() || dataType.isEmpty()) {
487             logger.info("Channel '{}', UID '{}' cannot be added incorrectly configured database. ", channel,
488                     getThing().getUID());
489             return null;
490         }
491         ChannelUID channelUID = new ChannelUID(getThing().getUID(), channel);
492         ChannelBuilder newChannel = ChannelBuilder.create(channelUID, dataType).withLabel(miChannel.getFriendlyName());
493         boolean useGeneratedChannelType = false;
494         if (!miChannel.getChannelType().isBlank()) {
495             ChannelTypeUID channelTypeUID = new ChannelTypeUID(miChannel.getChannelType());
496             if (channelTypeRegistry.getChannelType(channelTypeUID) != null) {
497                 newChannel = newChannel.withType(channelTypeUID);
498                 final LinkedHashSet<String> tags = miChannel.getTags();
499                 if (tags != null && !tags.isEmpty()) {
500                     newChannel.withDefaultTags(tags);
501                 }
502             } else {
503                 logger.debug("ChannelType '{}' is not available. Check the Json file for {}", channelTypeUID, model);
504                 useGeneratedChannelType = true;
505             }
506         } else {
507             useGeneratedChannelType = true;
508         }
509         if (useGeneratedChannelType) {
510             newChannel = newChannel
511                     .withType(new ChannelTypeUID(BINDING_ID, model.toUpperCase().replace(".", "_") + "_" + channel));
512             final Set<String> tags = miChannel.getTags();
513             if (tags != null && !tags.isEmpty()) {
514                 newChannel.withDefaultTags(tags);
515             }
516         }
517         thingBuilder.withChannel(newChannel.build());
518         return channelUID;
519     }
520
521     private @Nullable MiIoBasicChannel getChannel(String parameter) {
522         for (MiIoBasicChannel refreshEntry : refreshList) {
523             if (refreshEntry.getProperty().equals(parameter)) {
524                 return refreshEntry;
525             }
526         }
527         logger.trace("Did not find channel for {} in {}", parameter, refreshList);
528         return null;
529     }
530
531     private @Nullable MiIoBasicChannel getCustomRefreshChannel(String channelName) {
532         for (MiIoBasicChannel refreshEntry : refreshListCustomCommands.values()) {
533             if (refreshEntry.getChannel().equals(channelName)) {
534                 return refreshEntry;
535             }
536         }
537         logger.trace("Did not find channel for {} in {}", channelName, refreshList);
538         return null;
539     }
540
541     private void updatePropsFromJsonArray(MiIoSendCommand response) {
542         JsonArray res = response.getResult().getAsJsonArray();
543         JsonArray para = JsonParser.parseString(response.getCommandString()).getAsJsonObject().get("params")
544                 .getAsJsonArray();
545         if (res.size() != para.size()) {
546             logger.debug("Unexpected size different. Request size {},  response size {}. (Req: {}, Resp:{})",
547                     para.size(), res.size(), para, res);
548             return;
549         }
550         for (int i = 0; i < para.size(); i++) {
551             // This is a miot parameter
552             String param;
553             final JsonElement paraElement = para.get(i);
554             if (paraElement.isJsonObject()) { // miot channel
555                 param = paraElement.getAsJsonObject().get("did").getAsString();
556             } else {
557                 param = paraElement.getAsString();
558             }
559             JsonElement val = res.get(i);
560             if (val.isJsonNull()) {
561                 logger.debug("Property '{}' returned null (is it supported?).", param);
562                 continue;
563             } else if (val.isJsonObject()) { // miot channel
564                 val = val.getAsJsonObject().get("value");
565             }
566             MiIoBasicChannel basicChannel = getChannel(param);
567             updateChannel(basicChannel, param, val);
568         }
569     }
570
571     private void updatePropsFromJsonObject(MiIoSendCommand response) {
572         JsonObject res = response.getResult().getAsJsonObject();
573         for (Object k : res.keySet()) {
574             String param = (String) k;
575             JsonElement val = res.get(param);
576             if (val.isJsonNull()) {
577                 logger.debug("Property '{}' returned null (is it supported?).", param);
578                 continue;
579             }
580             MiIoBasicChannel basicChannel = getChannel(param);
581             updateChannel(basicChannel, param, val);
582         }
583     }
584
585     private void updateChannel(@Nullable MiIoBasicChannel basicChannel, String param, JsonElement value) {
586         JsonElement val = value;
587         deviceVariables.put(param, val);
588         if (basicChannel == null) {
589             logger.debug("Channel not found for {}", param);
590             return;
591         }
592         final String transformation = basicChannel.getTransformation();
593         if (transformation != null) {
594             JsonElement transformed = Conversions.execute(transformation, val, deviceVariables);
595             logger.debug("Transformed with '{}': {} {} -> {} ", transformation, basicChannel.getFriendlyName(), val,
596                     transformed);
597             val = transformed;
598         }
599         try {
600             String[] chType = basicChannel.getType().toLowerCase().split(":");
601             switch (chType[0]) {
602                 case "number":
603                     quantityTypeUpdate(basicChannel, val, chType.length > 1 ? chType[1] : "");
604                     break;
605                 case "dimmer":
606                     updateState(basicChannel.getChannel(), new PercentType(val.getAsBigDecimal()));
607                     break;
608                 case "string":
609                     if (val.isJsonPrimitive()) {
610                         updateState(basicChannel.getChannel(), new StringType(val.getAsString()));
611                     } else {
612                         updateState(basicChannel.getChannel(), new StringType(val.toString()));
613                     }
614                     break;
615                 case "switch":
616                     if (val.getAsJsonPrimitive().isNumber()) {
617                         updateState(basicChannel.getChannel(), val.getAsInt() > 0 ? OnOffType.ON : OnOffType.OFF);
618                     } else {
619                         String strVal = val.getAsString().toLowerCase();
620                         updateState(basicChannel.getChannel(),
621                                 "on".equals(strVal) || "true".equals(strVal) || "1".equals(strVal) ? OnOffType.ON
622                                         : OnOffType.OFF);
623                     }
624                     break;
625                 case "color":
626                     if (val.isJsonPrimitive()
627                             && (val.getAsJsonPrimitive().isNumber() || val.getAsString().matches("^[0-9]+$"))) {
628                         Color rgb = new Color(val.getAsInt());
629                         HSBType hsb = HSBType.fromRGB(rgb.getRed(), rgb.getGreen(), rgb.getBlue());
630                         updateState(basicChannel.getChannel(), hsb);
631                     } else {
632                         try {
633                             HSBType hsb = HSBType.valueOf(val.getAsString().replace("[", "").replace("]", ""));
634                             updateState(basicChannel.getChannel(), hsb);
635                         } catch (IllegalArgumentException e) {
636                             logger.debug("Failed updating channel '{}'. Could not convert '{}' to color",
637                                     basicChannel.getChannel(), val.getAsString());
638                         }
639                     }
640                     break;
641                 default:
642                     logger.debug("No update logic for channeltype '{}' ", basicChannel.getType());
643             }
644         } catch (Exception e) {
645             logger.debug("Error updating {} property {} with '{}' : {}: {}", getThing().getUID(),
646                     basicChannel.getChannel(), val, e.getClass().getCanonicalName(), e.getMessage());
647             logger.trace("Property update error detail:", e);
648         }
649     }
650
651     private void quantityTypeUpdate(MiIoBasicChannel basicChannel, JsonElement val, String type) {
652         if (!basicChannel.getUnit().isBlank()) {
653             Unit<?> unit = MiIoQuantiyTypes.get(basicChannel.getUnit());
654             if (unit != null) {
655                 logger.debug("'{}' channel '{}' has unit '{}' with symbol '{}'.", getThing().getUID(),
656                         basicChannel.getChannel(), basicChannel.getUnit(), unit);
657                 updateState(basicChannel.getChannel(), new QuantityType<>(val.getAsBigDecimal(), unit));
658             } else {
659                 logger.debug(
660                         "Unit '{}' used by '{}' channel '{}' is not found in conversion table... Trying anyway to submit as the update.",
661                         basicChannel.getUnit(), getThing().getUID(), basicChannel.getChannel());
662                 updateState(basicChannel.getChannel(),
663                         new QuantityType<>(val.getAsBigDecimal().toPlainString() + " " + basicChannel.getUnit()));
664             }
665             return;
666         }
667         // if no unit is provided or unit not found use default units, these units have so far been seen for miio
668         // devices
669         switch (type.toLowerCase()) {
670             case "temperature":
671                 updateState(basicChannel.getChannel(), new QuantityType<>(val.getAsBigDecimal(), SIUnits.CELSIUS));
672                 break;
673             case "electriccurrent":
674                 updateState(basicChannel.getChannel(), new QuantityType<>(val.getAsBigDecimal(), Units.AMPERE));
675                 break;
676             case "energy":
677                 updateState(basicChannel.getChannel(), new QuantityType<>(val.getAsBigDecimal(), Units.WATT));
678                 break;
679             case "time":
680                 updateState(basicChannel.getChannel(), new QuantityType<>(val.getAsBigDecimal(), Units.HOUR));
681                 break;
682             default:
683                 updateState(basicChannel.getChannel(), new DecimalType(val.getAsBigDecimal()));
684         }
685     }
686
687     @Override
688     public void onMessageReceived(MiIoSendCommand response) {
689         super.onMessageReceived(response);
690         if (response.isError()) {
691             return;
692         }
693         try {
694             switch (response.getCommand()) {
695                 case MIIO_INFO:
696                     break;
697                 case GET_VALUE:
698                 case GET_PROPERTIES:
699                 case GET_PROPERTY:
700                     if (response.getResult().isJsonArray()) {
701                         updatePropsFromJsonArray(response);
702                     } else if (response.getResult().isJsonObject()) {
703                         updatePropsFromJsonObject(response);
704                     }
705                     break;
706                 default:
707                     String channel = cmds.get(response.getId());
708                     if (channel != null) {
709                         logger.debug("Processing custom refresh command response for '{}' - {}", response.getMethod(),
710                                 response.getResult());
711                         final MiIoBasicChannel ch = getCustomRefreshChannel(channel);
712                         if (ch != null) {
713                             if (response.getResult().isJsonArray()) {
714                                 JsonArray cmdResponse = response.getResult().getAsJsonArray();
715                                 final String transformation = ch.getTransformation();
716                                 if (transformation == null || transformation.isBlank()) {
717                                     JsonElement response0 = cmdResponse.get(0);
718                                     updateChannel(ch, ch.getChannel(), response0.isJsonPrimitive() ? response0
719                                             : new JsonPrimitive(response0.toString()));
720                                 } else {
721                                     updateChannel(ch, ch.getChannel(), cmdResponse);
722                                 }
723                             } else {
724                                 updateChannel(ch, ch.getChannel(), new JsonPrimitive(response.getResult().toString()));
725                             }
726                         }
727                         cmds.remove(response.getId());
728                     }
729                     break;
730             }
731         } catch (Exception e) {
732             logger.debug("Error while handing message {}", response.getResponse(), e);
733         }
734     }
735 }