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