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.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;
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;
60 * The {@link MiIoUnsupportedHandler} is responsible for handling commands, which are
61 * sent to one of the channels.
63 * @author Marcel Verpaalen - Initial contribution
66 public class MiIoUnsupportedHandler extends MiIoAbstractHandler {
68 private static final DateTimeFormatter DATEFORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
69 private static final Gson GSONP = new GsonBuilder().setPrettyPrinting().create();
71 private final Logger logger = LoggerFactory.getLogger(MiIoUnsupportedHandler.class);
72 private final MiIoBindingConfiguration conf = getConfigAs(MiIoBindingConfiguration.class);
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 : "";
81 private final ExpiringCache<Boolean> updateDataCache = new ExpiringCache<>(CACHE_EXPIRY, () -> {
82 miIoScheduler.schedule(this::updateData, 0, TimeUnit.SECONDS);
86 public MiIoUnsupportedHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
87 CloudConnector cloudConnector) {
88 super(thing, miIoDatabaseWatchService, cloudConnector);
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();
98 logger.debug("Refresh {} skipped. Already refreshing", channelUID);
102 if (channelUID.getId().equals(CHANNEL_POWER)) {
103 if (command.equals(OnOffType.ON)) {
104 sendCommand("set_power[\"on\"]");
106 sendCommand("set_power[\"off\"]");
109 if (handleCommandsChannels(channelUID, command)) {
112 if (channelUID.getId().equals(CHANNEL_TESTCOMMANDS)) {
113 executeExperimentalCommands();
118 protected synchronized void updateData() {
122 logger.debug("Periodic update for '{}' ({})", getThing().getUID().toString(), getThing().getThingTypeUID());
125 } catch (Exception e) {
126 logger.debug("Error while updating '{}' ({})", getThing().getUID().toString(), getThing().getThingTypeUID(),
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();
139 miioinfo.remove("token");
140 miioinfo.remove("ap");
141 miioinfo.remove("mac");
142 info = miioinfo.toString();
146 if (lastCommand >= response.getId()) {
147 sb.append(response.getCommandString());
149 sb.append(response.getResponse());
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);
158 if (lastCommand >= 0 & lastCommand <= response.getId()) {
161 updateState(CHANNEL_TESTCOMMANDS, OnOffType.OFF);
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);
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();
181 lastCommand = sendCommand(cmd);
182 sb.append(lastCommand);
184 testChannelList.put(lastCommand, channelList.get(c));
186 this.lastCommand = lastCommand;
188 logger.info("{}", sb.toString());
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;
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) {
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);
208 } catch (IndexOutOfBoundsException e) {
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);
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);
226 return testChannelsList;
229 private List<MiIoBasicChannel> getBasicChannels(String deviceName) {
230 logger.debug("Adding Channels from model: {}", deviceName);
231 URL fn = miIoDatabaseWatchService.getDatabaseUrl(deviceName);
233 logger.warn("Database entry for model '{}' cannot be found.", deviceName);
234 return Collections.emptyList();
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);
248 return Collections.emptyList();
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: ");
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));
266 if (supportedChannelList.size() > 0) {
267 MiIoBasicDevice mbd = createBasicDeviceDb(model, new ArrayList<>(supportedChannelList.keySet()));
269 sb.append("Created experimental database for your device:\r\n");
270 sb.append(GSONP.toJson(mbd));
271 isIdentified = false;
273 sb.append("No supported channels found.\r\n");
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());
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 }));
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);
296 chNames.add(channelName);
298 deviceMapping.setChannels(miIoBasicChannels);
299 device.setDevice(deviceMapping);
303 private void writeDevice(MiIoBasicDevice device) {
304 File folder = new File(BINDING_DATABASE_PATH);
305 if (!folder.exists()) {
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());
317 private void writeLog() {
318 File folder = new File(BINDING_USERDATA_PATH);
319 if (!folder.exists()) {
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());