]> git.basschouten.com Git - openhab-addons.git/blob
20be68f71733d68e1b8d88cd3ec543b05d20edee
[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.io.File;
18 import java.io.FileWriter;
19 import java.io.IOException;
20 import java.net.URL;
21 import java.time.LocalDateTime;
22 import java.time.format.DateTimeFormatter;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Collections;
26 import java.util.HashSet;
27 import java.util.LinkedHashMap;
28 import java.util.LinkedHashSet;
29 import java.util.List;
30 import java.util.concurrent.TimeUnit;
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.MiIoDevices;
37 import org.openhab.binding.miio.internal.MiIoSendCommand;
38 import org.openhab.binding.miio.internal.Utils;
39 import org.openhab.binding.miio.internal.basic.DeviceMapping;
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.cloud.CloudConnector;
44 import org.openhab.core.cache.ExpiringCache;
45 import org.openhab.core.library.types.OnOffType;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.types.Command;
49 import org.openhab.core.types.RefreshType;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 import com.google.gson.Gson;
54 import com.google.gson.GsonBuilder;
55 import com.google.gson.JsonIOException;
56 import com.google.gson.JsonObject;
57 import com.google.gson.JsonSyntaxException;
58
59 /**
60  * The {@link MiIoUnsupportedHandler} is responsible for handling commands, which are
61  * sent to one of the channels.
62  *
63  * @author Marcel Verpaalen - Initial contribution
64  */
65 @NonNullByDefault
66 public class MiIoUnsupportedHandler extends MiIoAbstractHandler {
67
68     private static final DateTimeFormatter DATEFORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
69     private static final Gson GSONP = new GsonBuilder().setPrettyPrinting().create();
70
71     private final Logger logger = LoggerFactory.getLogger(MiIoUnsupportedHandler.class);
72     private final MiIoBindingConfiguration conf = getConfigAs(MiIoBindingConfiguration.class);
73
74     private StringBuilder sb = new StringBuilder();
75     private String info = "";
76     private int lastCommand = -1;
77     private LinkedHashMap<Integer, MiIoBasicChannel> testChannelList = new LinkedHashMap<>();
78     private LinkedHashMap<MiIoBasicChannel, String> supportedChannelList = new LinkedHashMap<>();
79     private String model = conf.model;
80
81     private final ExpiringCache<Boolean> updateDataCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
82         miIoScheduler.schedule(this::updateData, 0, TimeUnit.SECONDS);
83         return true;
84     });
85
86     public MiIoUnsupportedHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
87             CloudConnector cloudConnector) {
88         super(thing, miIoDatabaseWatchService, cloudConnector);
89     }
90
91     @Override
92     public void handleCommand(ChannelUID channelUID, Command command) {
93         if (command == RefreshType.REFRESH) {
94             if (updateDataCache.isExpired()) {
95                 logger.debug("Refreshing {}", channelUID);
96                 updateDataCache.getValue();
97             } else {
98                 logger.debug("Refresh {} skipped. Already refreshing", channelUID);
99             }
100             return;
101         }
102         if (channelUID.getId().equals(CHANNEL_POWER)) {
103             if (command.equals(OnOffType.ON)) {
104                 sendCommand("set_power[\"on\"]");
105             } else {
106                 sendCommand("set_power[\"off\"]");
107             }
108         }
109         if (handleCommandsChannels(channelUID, command)) {
110             return;
111         }
112         if (channelUID.getId().equals(CHANNEL_TESTCOMMANDS)) {
113             executeExperimentalCommands();
114         }
115     }
116
117     @Override
118     protected synchronized void updateData() {
119         if (skipUpdate()) {
120             return;
121         }
122         logger.debug("Periodic update for '{}' ({})", getThing().getUID().toString(), getThing().getThingTypeUID());
123         try {
124             refreshNetwork();
125         } catch (Exception e) {
126             logger.debug("Error while updating '{}' ({})", getThing().getUID().toString(), getThing().getThingTypeUID(),
127                     e);
128         }
129     }
130
131     @Override
132     public void onMessageReceived(MiIoSendCommand response) {
133         super.onMessageReceived(response);
134         if (MiIoCommand.MIIO_INFO.equals(response.getCommand()) && !response.isError()) {
135             JsonObject miioinfo = response.getResult().getAsJsonObject();
136             if (miioinfo.has("model")) {
137                 model = miioinfo.get("model").getAsString();
138             }
139             miioinfo.remove("token");
140             miioinfo.remove("ap");
141             miioinfo.remove("mac");
142             info = miioinfo.toString();
143             sb.append(info);
144             sb.append("\r\n");
145         }
146         if (lastCommand >= response.getId()) {
147             sb.append(response.getCommandString());
148             sb.append(" -> ");
149             sb.append(response.getResponse());
150             sb.append("\r\n");
151             String res = response.getResult().toString();
152             if (!response.isError() && !res.contentEquals("[null]") && !res.contentEquals("[]")
153                     && !res.contentEquals("[\"\"]")) {
154                 if (testChannelList.containsKey(response.getId())) {
155                     supportedChannelList.put(testChannelList.get(response.getId()), res);
156                 }
157             }
158         }
159         if (lastCommand >= 0 & lastCommand <= response.getId()) {
160             lastCommand = -1;
161             finishChannelTest();
162             updateState(CHANNEL_TESTCOMMANDS, OnOffType.OFF);
163         }
164     }
165
166     private void executeExperimentalCommands() {
167         LinkedHashMap<String, MiIoBasicChannel> channelList = collectProperties(conf.model);
168         sendCommand(MiIoCommand.MIIO_INFO);
169         sb = new StringBuilder();
170         logger.info("Start experimental testing of supported properties for device '{}'. ", miDevice.toString());
171         sb.append("Info for ");
172         sb.append(conf.model);
173         sb.append("\r\n");
174         sb.append("Properties: ");
175         int lastCommand = -1;
176         for (String c : channelList.keySet()) {
177             MiIoBasicChannel ch = channelList.get(c);
178             String cmd = ch.getChannelCustomRefreshCommand().isBlank() ? ("get_prop[" + c + "]")
179                     : ch.getChannelCustomRefreshCommand();
180             sb.append(c);
181             sb.append(" -> ");
182             lastCommand = sendCommand(cmd);
183             sb.append(lastCommand);
184             sb.append(", ");
185             testChannelList.put(lastCommand, channelList.get(c));
186         }
187         this.lastCommand = lastCommand;
188         sb.append("\r\n");
189         logger.info("{}", sb.toString());
190     }
191
192     private LinkedHashMap<String, MiIoBasicChannel> collectProperties(@Nullable String model) {
193         LinkedHashMap<String, MiIoBasicChannel> testChannelsList = new LinkedHashMap<>();
194         LinkedHashSet<MiIoDevices> testDeviceList = new LinkedHashSet<>();
195         if (model == null || model.length() < 2) {
196             logger.info("Wait until the model is determined, than try again");
197             return testChannelsList;
198         }
199         // first add similar devices to test those channels first, then test all others
200         int[] subset = { model.length() - 1, model.lastIndexOf("."), model.indexOf("."), 0 };
201         for (int i : subset) {
202             try {
203                 final String mm = model.substring(0, model.lastIndexOf("."));
204                 for (MiIoDevices dev : MiIoDevices.values()) {
205                     if (dev.getThingType().equals(THING_TYPE_BASIC) && (i == 0 || dev.getModel().contains(mm))) {
206                         testDeviceList.add(dev);
207                     }
208                 }
209             } catch (IndexOutOfBoundsException e) {
210                 // swallow
211             }
212         }
213         for (MiIoDevices dev : testDeviceList) {
214             for (MiIoBasicChannel ch : getBasicChannels(dev.getModel())) {
215                 // Add all (unique) properties
216                 if (!ch.isMiOt() && !ch.getProperty().isBlank() && ch.getChannelCustomRefreshCommand().isBlank()
217                         && !testChannelsList.containsKey(ch.getProperty())) {
218                     testChannelsList.putIfAbsent(ch.getProperty(), ch);
219                 }
220                 // Add all (unique) custom refresh commands
221                 if (!ch.isMiOt() && !ch.getChannelCustomRefreshCommand().isBlank()
222                         && !testChannelsList.containsKey(ch.getChannelCustomRefreshCommand())) {
223                     testChannelsList.putIfAbsent(ch.getChannelCustomRefreshCommand(), ch);
224                 }
225             }
226         }
227         return testChannelsList;
228     }
229
230     private List<MiIoBasicChannel> getBasicChannels(String deviceName) {
231         logger.debug("Adding Channels from model: {}", deviceName);
232         URL fn = miIoDatabaseWatchService.getDatabaseUrl(deviceName);
233         if (fn == null) {
234             logger.warn("Database entry for model '{}' cannot be found.", deviceName);
235             return Collections.emptyList();
236         }
237         try {
238             JsonObject deviceMapping = Utils.convertFileToJSON(fn);
239             logger.debug("Using device database: {} for device {}", fn.getFile(), deviceName);
240             final MiIoBasicDevice device = GSONP.fromJson(deviceMapping, MiIoBasicDevice.class);
241             return device.getDevice().getChannels();
242         } catch (JsonIOException | JsonSyntaxException e) {
243             logger.warn("Error parsing database Json", e);
244         } catch (IOException e) {
245             logger.warn("Error reading database file", e);
246         } catch (Exception e) {
247             logger.warn("Error creating channel structure", e);
248         }
249         return Collections.emptyList();
250     }
251
252     private void finishChannelTest() {
253         sb.append("===================================\r\n");
254         sb.append("Responsive properties\r\n");
255         sb.append("===================================\r\n");
256         sb.append("Device Info: ");
257         sb.append(info);
258         for (MiIoBasicChannel ch : supportedChannelList.keySet()) {
259             sb.append("Property: ");
260             sb.append(Utils.minLengthString(ch.getProperty(), 15));
261             sb.append(" Friendly Name: ");
262             sb.append(Utils.minLengthString(ch.getFriendlyName(), 25));
263             sb.append(" Response: ");
264             sb.append(supportedChannelList.get(ch));
265             sb.append("\r\n");
266         }
267         if (!supportedChannelList.isEmpty()) {
268             MiIoBasicDevice mbd = createBasicDeviceDb(model, new ArrayList<>(supportedChannelList.keySet()));
269             sb.append("Created experimental database for your device:\r\n");
270             sb.append(GSONP.toJson(mbd));
271             sb.append("\r\nDatabase file saved to: ");
272             sb.append(writeDevice(mbd));
273             isIdentified = false;
274         } else {
275             sb.append("No supported channels found.\r\n");
276         }
277         sb.append("\r\nDevice testing file saved to: ");
278         sb.append(writeLog());
279         sb.append(
280                 "\r\nPlease share your these files on the community forum or github to get this device supported.\r\n");
281         logger.info("{}", sb.toString());
282     }
283
284     private MiIoBasicDevice createBasicDeviceDb(String model, List<MiIoBasicChannel> miIoBasicChannels) {
285         MiIoBasicDevice device = new MiIoBasicDevice();
286         DeviceMapping deviceMapping = new DeviceMapping();
287         deviceMapping.setPropertyMethod(MiIoCommand.GET_PROPERTY.getCommand());
288         deviceMapping.setMaxProperties(2);
289         deviceMapping.setId(Arrays.asList(new String[] { model }));
290         int duplicates = 0;
291         HashSet<String> chNames = new HashSet<>();
292         for (MiIoBasicChannel ch : miIoBasicChannels) {
293             String channelName = ch.getChannel();
294             if (chNames.contains(channelName)) {
295                 channelName = channelName + duplicates;
296                 ch.setChannel(channelName);
297                 duplicates++;
298             }
299             chNames.add(channelName);
300         }
301         deviceMapping.setChannels(miIoBasicChannels);
302         device.setDevice(deviceMapping);
303         return device;
304     }
305
306     private String writeDevice(MiIoBasicDevice device) {
307         File folder = new File(BINDING_DATABASE_PATH);
308         if (!folder.exists()) {
309             folder.mkdirs();
310         }
311         File dataFile = new File(folder, model + "-experimental.json");
312         try (FileWriter writer = new FileWriter(dataFile)) {
313             writer.write(GSONP.toJson(device));
314             logger.debug("Experimental database file created: {}", dataFile.getAbsolutePath());
315             return dataFile.getAbsolutePath().toString();
316         } catch (IOException e) {
317             logger.info("Error writing database file {}: {}", dataFile.getAbsolutePath(), e.getMessage());
318         }
319         return "Failed creating database file";
320     }
321
322     private String writeLog() {
323         File folder = new File(BINDING_USERDATA_PATH);
324         if (!folder.exists()) {
325             folder.mkdirs();
326         }
327         File dataFile = new File(folder, "test-" + model + "-" + LocalDateTime.now().format(DATEFORMATTER) + ".txt");
328         try (FileWriter writer = new FileWriter(dataFile)) {
329             writer.write(sb.toString());
330             logger.debug("Saved device testing file to {}", dataFile.getAbsolutePath());
331             return dataFile.getAbsolutePath().toString();
332         } catch (IOException e) {
333             logger.info("Error writing file {}: {}", dataFile.getAbsolutePath(), e.getMessage());
334         }
335         return "Failed creating testlog file";
336     }
337 }