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.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;
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;
59 * The {@link MiIoUnsupportedHandler} is responsible for handling commands, which are
60 * sent to one of the channels.
62 * @author Marcel Verpaalen - Initial contribution
65 public class MiIoUnsupportedHandler extends MiIoAbstractHandler {
67 private static final DateTimeFormatter DATEFORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
68 private static final Gson GSONP = new GsonBuilder().setPrettyPrinting().create();
70 private final Logger logger = LoggerFactory.getLogger(MiIoUnsupportedHandler.class);
71 private final MiIoBindingConfiguration conf = getConfigAs(MiIoBindingConfiguration.class);
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 : "";
80 private final ExpiringCache<Boolean> updateDataCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
81 scheduler.schedule(this::updateData, 0, TimeUnit.SECONDS);
85 public MiIoUnsupportedHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService) {
86 super(thing, miIoDatabaseWatchService);
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();
96 logger.debug("Refresh {} skipped. Already refreshing", channelUID);
100 if (channelUID.getId().equals(CHANNEL_POWER)) {
101 if (command.equals(OnOffType.ON)) {
102 sendCommand("set_power[\"on\"]");
104 sendCommand("set_power[\"off\"]");
107 if (channelUID.getId().equals(CHANNEL_COMMAND)) {
108 cmds.put(sendCommand(command.toString()), command.toString());
110 if (channelUID.getId().equals(CHANNEL_TESTCOMMANDS)) {
111 executeExperimentalCommands();
116 protected synchronized void updateData() {
120 logger.debug("Periodic update for '{}' ({})", getThing().getUID().toString(), getThing().getThingTypeUID());
123 } catch (Exception e) {
124 logger.debug("Error while updating '{}' ({})", getThing().getUID().toString(), getThing().getThingTypeUID(),
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();
137 miioinfo.remove("token");
138 miioinfo.remove("ap");
139 miioinfo.remove("mac");
140 info = miioinfo.toString();
144 if (lastCommand >= response.getId()) {
145 sb.append(response.getCommandString());
147 sb.append(response.getResponse());
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);
156 if (lastCommand >= 0 & lastCommand <= response.getId()) {
159 updateState(CHANNEL_TESTCOMMANDS, OnOffType.OFF);
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);
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();
179 lastCommand = sendCommand(cmd);
180 sb.append(lastCommand);
182 testChannelList.put(lastCommand, channelList.get(c));
184 this.lastCommand = lastCommand;
186 logger.info("{}", sb.toString());
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;
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) {
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);
206 } catch (IndexOutOfBoundsException e) {
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);
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);
224 return testChannelsList;
227 private List<MiIoBasicChannel> getBasicChannels(String deviceName) {
228 logger.debug("Adding Channels from model: {}", deviceName);
229 URL fn = miIoDatabaseWatchService.getDatabaseUrl(deviceName);
231 logger.warn("Database entry for model '{}' cannot be found.", deviceName);
232 return Collections.emptyList();
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);
246 return Collections.emptyList();
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: ");
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));
264 if (supportedChannelList.size() > 0) {
265 MiIoBasicDevice mbd = createBasicDeviceDb(model, new ArrayList<>(supportedChannelList.keySet()));
267 sb.append("Created experimental database for your device:\r\n");
268 sb.append(GSONP.toJson(mbd));
269 isIdentified = false;
271 sb.append("No supported channels found.\r\n");
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());
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 }));
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);
294 chNames.add(channelName);
296 deviceMapping.setChannels(miIoBasicChannels);
297 device.setDevice(deviceMapping);
301 private void writeDevice(MiIoBasicDevice device) {
302 File folder = new File(BINDING_DATABASE_PATH);
303 if (!folder.exists()) {
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());
315 private void writeLog() {
316 File folder = new File(BINDING_USERDATA_PATH);
317 if (!folder.exists()) {
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());