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