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