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