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