2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.miio.internal.handler;
15 import static org.openhab.binding.miio.internal.MiIoBindingConstants.*;
18 import java.io.FileWriter;
19 import java.io.IOException;
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;
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;
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;
58 * The {@link MiIoUnsupportedHandler} is responsible for handling commands, which are
59 * sent to one of the channels.
61 * @author Marcel Verpaalen - Initial contribution
64 public class MiIoUnsupportedHandler extends MiIoAbstractHandler {
66 private static final DateTimeFormatter DATEFORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
67 private static final Gson GSONP = new GsonBuilder().setPrettyPrinting().create();
69 private final Logger logger = LoggerFactory.getLogger(MiIoUnsupportedHandler.class);
70 private final MiIoBindingConfiguration conf = getConfigAs(MiIoBindingConfiguration.class);
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 : "";
79 private final ExpiringCache<Boolean> updateDataCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
80 scheduler.schedule(this::updateData, 0, TimeUnit.SECONDS);
84 public MiIoUnsupportedHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService) {
85 super(thing, miIoDatabaseWatchService);
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();
95 logger.debug("Refresh {} skipped. Already refreshing", channelUID);
99 if (channelUID.getId().equals(CHANNEL_POWER)) {
100 if (command.equals(OnOffType.ON)) {
101 sendCommand("set_power[\"on\"]");
103 sendCommand("set_power[\"off\"]");
106 if (channelUID.getId().equals(CHANNEL_COMMAND)) {
107 cmds.put(sendCommand(command.toString()), command.toString());
109 if (channelUID.getId().equals(CHANNEL_TESTCOMMANDS)) {
110 executeExperimentalCommands();
115 protected synchronized void updateData() {
119 logger.debug("Periodic update for '{}' ({})", getThing().getUID().toString(), getThing().getThingTypeUID());
122 } catch (Exception e) {
123 logger.debug("Error while updating '{}' ({})", getThing().getUID().toString(), getThing().getThingTypeUID(),
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();
136 miioinfo.remove("token");
137 miioinfo.remove("ap");
138 miioinfo.remove("mac");
139 info = miioinfo.toString();
143 if (lastCommand >= response.getId()) {
144 sb.append(response.getCommandString());
146 sb.append(response.getResponse());
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);
155 if (lastCommand >= 0 & lastCommand <= response.getId()) {
158 updateState(CHANNEL_TESTCOMMANDS, OnOffType.OFF);
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);
170 sb.append("Properties: ");
171 int lastCommand = -1;
172 for (String c : channelList.keySet()) {
173 String cmd = "get_prop[" + c + "]";
176 lastCommand = sendCommand(cmd);
177 sb.append(lastCommand);
179 testChannelList.put(lastCommand, channelList.get(c));
181 this.lastCommand = lastCommand;
183 logger.info("{}", sb.toString());
186 private LinkedHashMap<String, MiIoBasicChannel> collectProperties(String model) {
187 LinkedHashMap<String, MiIoBasicChannel> testChannelsList = new LinkedHashMap<>();
188 LinkedHashSet<MiIoDevices> testDeviceList = new LinkedHashSet<>();
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) {
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);
200 } catch (IndexOutOfBoundsException e) {
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);
211 return testChannelsList;
214 private List<MiIoBasicChannel> getBasicChannels(String deviceName) {
215 logger.debug("Adding Channels from model: {}", deviceName);
216 URL fn = miIoDatabaseWatchService.getDatabaseUrl(deviceName);
218 logger.warn("Database entry for model '{}' cannot be found.", deviceName);
219 return Collections.emptyList();
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);
233 return Collections.emptyList();
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: ");
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));
251 if (supportedChannelList.size() > 0) {
252 MiIoBasicDevice mbd = createBasicDeviceDb(model, new ArrayList<>(supportedChannelList.keySet()));
254 sb.append("Created experimental database for your device:\r\n");
255 sb.append(GSONP.toJson(mbd));
256 isIdentified = false;
258 sb.append("No supported channels found.\r\n");
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());
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 }));
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);
281 chNames.add(channelName);
283 deviceMapping.setChannels(miIoBasicChannels);
284 device.setDevice(deviceMapping);
288 private void writeDevice(MiIoBasicDevice device) {
289 File folder = new File(BINDING_DATABASE_PATH);
290 if (!folder.exists()) {
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());
302 private void writeLog() {
303 File folder = new File(BINDING_USERDATA_PATH);
304 if (!folder.exists()) {
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());