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