]> git.basschouten.com Git - openhab-addons.git/blob
f6b735f4de96cbbc30691a98cb34fd85bc8d4a49
[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.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 != null ? 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                 if (testChannelList.containsKey(response.getId())) {
154                     supportedChannelList.put(testChannelList.get(response.getId()), res);
155                 }
156             }
157         }
158         if (lastCommand >= 0 & lastCommand <= response.getId()) {
159             lastCommand = -1;
160             finishChannelTest();
161             updateState(CHANNEL_TESTCOMMANDS, OnOffType.OFF);
162         }
163     }
164
165     private void executeExperimentalCommands() {
166         LinkedHashMap<String, MiIoBasicChannel> channelList = collectProperties(conf.model);
167         sendCommand(MiIoCommand.MIIO_INFO);
168         sb = new StringBuilder();
169         logger.info("Start experimental testing of supported properties for device '{}'. ", miDevice.toString());
170         sb.append("Info for ");
171         sb.append(conf.model);
172         sb.append("\r\n");
173         sb.append("Properties: ");
174         int lastCommand = -1;
175         for (String c : channelList.keySet()) {
176             MiIoBasicChannel ch = channelList.get(c);
177             String cmd = ch.getChannelCustomRefreshCommand().isBlank() ? ("get_prop[" + c + "]")
178                     : ch.getChannelCustomRefreshCommand();
179             sb.append(c);
180             sb.append(" -> ");
181             lastCommand = sendCommand(cmd);
182             sb.append(lastCommand);
183             sb.append(", ");
184             testChannelList.put(lastCommand, channelList.get(c));
185         }
186         this.lastCommand = lastCommand;
187         sb.append("\r\n");
188         logger.info("{}", sb.toString());
189     }
190
191     private LinkedHashMap<String, MiIoBasicChannel> collectProperties(@Nullable String model) {
192         LinkedHashMap<String, MiIoBasicChannel> testChannelsList = new LinkedHashMap<>();
193         LinkedHashSet<MiIoDevices> testDeviceList = new LinkedHashSet<>();
194         if (model == null || model.length() < 2) {
195             logger.info("Wait until the model is determined, than try again");
196             return testChannelsList;
197         }
198         // first add similar devices to test those channels first, then test all others
199         int[] subset = { model.length() - 1, model.lastIndexOf("."), model.indexOf("."), 0 };
200         for (int i : subset) {
201             try {
202                 final String mm = model.substring(0, model.lastIndexOf("."));
203                 for (MiIoDevices dev : MiIoDevices.values()) {
204                     if (dev.getThingType().equals(THING_TYPE_BASIC) && (i == 0 || dev.getModel().contains(mm))) {
205                         testDeviceList.add(dev);
206                     }
207                 }
208             } catch (IndexOutOfBoundsException e) {
209                 // swallow
210             }
211         }
212         for (MiIoDevices dev : testDeviceList) {
213             for (MiIoBasicChannel ch : getBasicChannels(dev.getModel())) {
214                 // Add all (unique) properties
215                 if (!ch.isMiOt() && !ch.getProperty().isBlank() && ch.getChannelCustomRefreshCommand().isBlank()
216                         && !testChannelsList.containsKey(ch.getProperty())) {
217                     testChannelsList.put(ch.getProperty(), ch);
218                 }
219                 // Add all (unique) custom refresh commands
220                 if (!ch.isMiOt() && !ch.getChannelCustomRefreshCommand().isBlank()
221                         && !testChannelsList.containsKey(ch.getChannelCustomRefreshCommand())) {
222                     testChannelsList.put(ch.getChannelCustomRefreshCommand(), ch);
223                 }
224             }
225         }
226         return testChannelsList;
227     }
228
229     private List<MiIoBasicChannel> getBasicChannels(String deviceName) {
230         logger.debug("Adding Channels from model: {}", deviceName);
231         URL fn = miIoDatabaseWatchService.getDatabaseUrl(deviceName);
232         if (fn == null) {
233             logger.warn("Database entry for model '{}' cannot be found.", deviceName);
234             return Collections.emptyList();
235         }
236         try {
237             JsonObject deviceMapping = Utils.convertFileToJSON(fn);
238             logger.debug("Using device database: {} for device {}", fn.getFile(), deviceName);
239             final MiIoBasicDevice device = GSONP.fromJson(deviceMapping, MiIoBasicDevice.class);
240             return device.getDevice().getChannels();
241         } catch (JsonIOException | JsonSyntaxException e) {
242             logger.warn("Error parsing database Json", e);
243         } catch (IOException e) {
244             logger.warn("Error reading database file", e);
245         } catch (Exception e) {
246             logger.warn("Error creating channel structure", e);
247         }
248         return Collections.emptyList();
249     }
250
251     private void finishChannelTest() {
252         sb.append("===================================\r\n");
253         sb.append("Responsive properties\r\n");
254         sb.append("===================================\r\n");
255         sb.append("Device Info: ");
256         sb.append(info);
257         for (MiIoBasicChannel ch : supportedChannelList.keySet()) {
258             sb.append("Property: ");
259             sb.append(Utils.minLengthString(ch.getProperty(), 15));
260             sb.append(" Friendly Name: ");
261             sb.append(Utils.minLengthString(ch.getFriendlyName(), 25));
262             sb.append(" Response: ");
263             sb.append(supportedChannelList.get(ch));
264             sb.append("\r\n");
265         }
266         if (supportedChannelList.size() > 0) {
267             MiIoBasicDevice mbd = createBasicDeviceDb(model, new ArrayList<>(supportedChannelList.keySet()));
268             writeDevice(mbd);
269             sb.append("Created experimental database for your device:\r\n");
270             sb.append(GSONP.toJson(mbd));
271             isIdentified = false;
272         } else {
273             sb.append("No supported channels found.\r\n");
274         }
275         sb.append(
276                 "\r\nPlease share your this output on the community forum or github to get this device supported.\r\n");
277         logger.info("{}", sb.toString());
278         writeLog();
279     }
280
281     private MiIoBasicDevice createBasicDeviceDb(String model, List<MiIoBasicChannel> miIoBasicChannels) {
282         MiIoBasicDevice device = new MiIoBasicDevice();
283         DeviceMapping deviceMapping = new DeviceMapping();
284         deviceMapping.setPropertyMethod(MiIoCommand.GET_PROPERTY.getCommand());
285         deviceMapping.setMaxProperties(2);
286         deviceMapping.setId(Arrays.asList(new String[] { model }));
287         int duplicates = 0;
288         HashSet<String> chNames = new HashSet<>();
289         for (MiIoBasicChannel ch : miIoBasicChannels) {
290             String channelName = ch.getChannel();
291             if (chNames.contains(channelName)) {
292                 channelName = channelName + duplicates;
293                 ch.setChannel(channelName);
294                 duplicates++;
295             }
296             chNames.add(channelName);
297         }
298         deviceMapping.setChannels(miIoBasicChannels);
299         device.setDevice(deviceMapping);
300         return device;
301     }
302
303     private void writeDevice(MiIoBasicDevice device) {
304         File folder = new File(BINDING_DATABASE_PATH);
305         if (!folder.exists()) {
306             folder.mkdirs();
307         }
308         File dataFile = new File(folder, model + "-experimental.json");
309         try (FileWriter writer = new FileWriter(dataFile)) {
310             writer.write(GSONP.toJson(device));
311             logger.info("Database file created: {}", dataFile.getAbsolutePath());
312         } catch (IOException e) {
313             logger.info("Error writing database file {}: {}", dataFile.getAbsolutePath(), e.getMessage());
314         }
315     }
316
317     private void writeLog() {
318         File folder = new File(BINDING_USERDATA_PATH);
319         if (!folder.exists()) {
320             folder.mkdirs();
321         }
322         File dataFile = new File(folder, "test-" + model + "-" + LocalDateTime.now().format(DATEFORMATTER) + ".txt");
323         try (FileWriter writer = new FileWriter(dataFile)) {
324             writer.write(sb.toString());
325             logger.info("Saved device testing file to {}", dataFile.getAbsolutePath());
326         } catch (IOException e) {
327             logger.info("Error writing file {}: {}", dataFile.getAbsolutePath(), e.getMessage());
328         }
329     }
330 }