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