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