]> git.basschouten.com Git - openhab-addons.git/blob
74f7b3e45eda4768b495be9aeceae61426a2035c
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.ByteArrayInputStream;
18 import java.io.ByteArrayOutputStream;
19 import java.io.File;
20 import java.io.IOException;
21 import java.time.Instant;
22 import java.time.LocalDateTime;
23 import java.time.ZoneId;
24 import java.time.ZonedDateTime;
25 import java.time.format.DateTimeFormatter;
26 import java.util.Collections;
27 import java.util.Map.Entry;
28 import java.util.Set;
29 import java.util.concurrent.ConcurrentHashMap;
30 import java.util.concurrent.TimeUnit;
31 import java.util.stream.Collectors;
32 import java.util.stream.Stream;
33
34 import javax.imageio.ImageIO;
35
36 import org.eclipse.jdt.annotation.NonNullByDefault;
37 import org.eclipse.jdt.annotation.Nullable;
38 import org.openhab.binding.miio.internal.MiIoBindingConfiguration;
39 import org.openhab.binding.miio.internal.MiIoCommand;
40 import org.openhab.binding.miio.internal.MiIoSendCommand;
41 import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService;
42 import org.openhab.binding.miio.internal.cloud.CloudConnector;
43 import org.openhab.binding.miio.internal.cloud.CloudUtil;
44 import org.openhab.binding.miio.internal.cloud.MiCloudException;
45 import org.openhab.binding.miio.internal.robot.ConsumablesType;
46 import org.openhab.binding.miio.internal.robot.FanModeType;
47 import org.openhab.binding.miio.internal.robot.RRMapDraw;
48 import org.openhab.binding.miio.internal.robot.RRMapDrawOptions;
49 import org.openhab.binding.miio.internal.robot.RobotCababilities;
50 import org.openhab.binding.miio.internal.robot.StatusDTO;
51 import org.openhab.binding.miio.internal.robot.StatusType;
52 import org.openhab.binding.miio.internal.robot.VacuumErrorType;
53 import org.openhab.binding.miio.internal.transport.MiIoAsyncCommunication;
54 import org.openhab.core.cache.ExpiringCache;
55 import org.openhab.core.library.types.DateTimeType;
56 import org.openhab.core.library.types.DecimalType;
57 import org.openhab.core.library.types.OnOffType;
58 import org.openhab.core.library.types.QuantityType;
59 import org.openhab.core.library.types.RawType;
60 import org.openhab.core.library.types.StringType;
61 import org.openhab.core.library.unit.SIUnits;
62 import org.openhab.core.library.unit.Units;
63 import org.openhab.core.thing.Channel;
64 import org.openhab.core.thing.ChannelUID;
65 import org.openhab.core.thing.Thing;
66 import org.openhab.core.thing.ThingStatusDetail;
67 import org.openhab.core.thing.binding.builder.ChannelBuilder;
68 import org.openhab.core.thing.binding.builder.ThingBuilder;
69 import org.openhab.core.thing.type.ChannelType;
70 import org.openhab.core.thing.type.ChannelTypeRegistry;
71 import org.openhab.core.types.Command;
72 import org.openhab.core.types.RefreshType;
73 import org.openhab.core.types.State;
74 import org.openhab.core.types.UnDefType;
75 import org.slf4j.Logger;
76 import org.slf4j.LoggerFactory;
77
78 import com.google.gson.Gson;
79 import com.google.gson.GsonBuilder;
80 import com.google.gson.JsonArray;
81 import com.google.gson.JsonObject;
82
83 /**
84  * The {@link MiIoVacuumHandler} is responsible for handling commands, which are
85  * sent to one of the channels.
86  *
87  * @author Marcel Verpaalen - Initial contribution
88  */
89 @NonNullByDefault
90 public class MiIoVacuumHandler extends MiIoAbstractHandler {
91     private final Logger logger = LoggerFactory.getLogger(MiIoVacuumHandler.class);
92     private static final DateTimeFormatter DATEFORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
93     private static final Gson GSON = new GsonBuilder().serializeNulls().create();
94     private final ChannelUID mapChannelUid;
95
96     private static final Set<RobotCababilities> FEATURES_CHANNELS = Collections.unmodifiableSet(Stream
97             .of(RobotCababilities.SEGMENT_STATUS, RobotCababilities.MAP_STATUS, RobotCababilities.LED_STATUS,
98                     RobotCababilities.CARPET_MODE, RobotCababilities.FW_FEATURES, RobotCababilities.ROOM_MAPPING,
99                     RobotCababilities.MULTI_MAP_LIST, RobotCababilities.CUSTOMIZE_CLEAN_MODE)
100             .collect(Collectors.toSet()));
101
102     private ExpiringCache<String> status;
103     private ExpiringCache<String> consumables;
104     private ExpiringCache<String> dnd;
105     private ExpiringCache<String> history;
106     private int stateId;
107     private ExpiringCache<String> map;
108     private String lastHistoryId = "";
109     private String lastMap = "";
110     private boolean hasChannelStructure;
111     private ConcurrentHashMap<RobotCababilities, Boolean> deviceCapabilities = new ConcurrentHashMap<>();
112     private ChannelTypeRegistry channelTypeRegistry;
113     private RRMapDrawOptions mapDrawOptions = new RRMapDrawOptions();
114
115     public MiIoVacuumHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
116             CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry) {
117         super(thing, miIoDatabaseWatchService, cloudConnector);
118         this.channelTypeRegistry = channelTypeRegistry;
119         mapChannelUid = new ChannelUID(thing.getUID(), CHANNEL_VACUUM_MAP);
120         status = new ExpiringCache<>(CACHE_EXPIRY, () -> {
121             try {
122                 int ret = sendCommand(MiIoCommand.GET_STATUS);
123                 if (ret != 0) {
124                     return "id:" + ret;
125                 }
126             } catch (Exception e) {
127                 logger.debug("Error during status refresh: {}", e.getMessage(), e);
128             }
129             return null;
130         });
131         consumables = new ExpiringCache<>(CACHE_EXPIRY, () -> {
132             try {
133                 int ret = sendCommand(MiIoCommand.CONSUMABLES_GET);
134                 if (ret != 0) {
135                     return "id:" + ret;
136                 }
137             } catch (Exception e) {
138                 logger.debug("Error during consumables refresh: {}", e.getMessage(), e);
139             }
140             return null;
141         });
142         dnd = new ExpiringCache<>(CACHE_EXPIRY, () -> {
143             try {
144                 int ret = sendCommand(MiIoCommand.DND_GET);
145                 if (ret != 0) {
146                     return "id:" + ret;
147                 }
148             } catch (Exception e) {
149                 logger.debug("Error during dnd refresh: {}", e.getMessage(), e);
150             }
151             return null;
152         });
153         history = new ExpiringCache<>(CACHE_EXPIRY, () -> {
154             try {
155                 int ret = sendCommand(MiIoCommand.CLEAN_SUMMARY_GET);
156                 if (ret != 0) {
157                     return "id:" + ret;
158                 }
159             } catch (Exception e) {
160                 logger.debug("Error during cleaning data refresh: {}", e.getMessage(), e);
161             }
162             return null;
163         });
164         map = new ExpiringCache<String>(CACHE_EXPIRY, () -> {
165             try {
166                 int ret = sendCommand(MiIoCommand.GET_MAP);
167                 if (ret != 0) {
168                     return "id:" + ret;
169                 }
170             } catch (Exception e) {
171                 logger.debug("Error during dnd refresh: {}", e.getMessage(), e);
172             }
173             return null;
174         });
175     }
176
177     @Override
178     public void handleCommand(ChannelUID channelUID, Command command) {
179         if (getConnection() == null) {
180             logger.debug("Vacuum {} not online. Command {} ignored", getThing().getUID(), command.toString());
181             return;
182         }
183         if (command == RefreshType.REFRESH) {
184             logger.debug("Refreshing {}", channelUID);
185             updateData();
186             lastMap = "";
187             if (channelUID.getId().equals(CHANNEL_VACUUM_MAP)) {
188                 sendCommand(MiIoCommand.GET_MAP);
189             }
190             return;
191         }
192         if (handleCommandsChannels(channelUID, command)) {
193             forceStatusUpdate();
194             return;
195         }
196         if (channelUID.getId().equals(CHANNEL_VACUUM)) {
197             if (command instanceof OnOffType) {
198                 if (command.equals(OnOffType.ON)) {
199                     sendCommand(MiIoCommand.START_VACUUM);
200                     forceStatusUpdate();
201                     return;
202                 } else {
203                     sendCommand(MiIoCommand.STOP_VACUUM);
204                     miIoScheduler.schedule(() -> {
205                         sendCommand(MiIoCommand.CHARGE);
206                         forceStatusUpdate();
207                     }, 2000, TimeUnit.MILLISECONDS);
208                     return;
209                 }
210             }
211         }
212         if (channelUID.getId().equals(CHANNEL_CONTROL)) {
213             if (command.toString().equals("vacuum")) {
214                 sendCommand(MiIoCommand.START_VACUUM);
215             } else if (command.toString().equals("spot")) {
216                 sendCommand(MiIoCommand.START_SPOT);
217             } else if (command.toString().equals("pause")) {
218                 sendCommand(MiIoCommand.PAUSE);
219             } else if (command.toString().equals("dock")) {
220                 sendCommand(MiIoCommand.STOP_VACUUM);
221                 miIoScheduler.schedule(() -> {
222                     sendCommand(MiIoCommand.CHARGE);
223                     forceStatusUpdate();
224                 }, 2000, TimeUnit.MILLISECONDS);
225                 return;
226             } else {
227                 logger.info("Command {} not recognised", command.toString());
228             }
229             forceStatusUpdate();
230             return;
231         }
232         if (channelUID.getId().equals(CHANNEL_FAN_POWER)) {
233             sendCommand(MiIoCommand.SET_MODE, "[" + command.toString() + "]");
234             forceStatusUpdate();
235             return;
236         }
237         if (channelUID.getId().equals(RobotCababilities.WATERBOX_MODE.getChannel())) {
238             sendCommand(MiIoCommand.SET_WATERBOX_MODE, "[" + command.toString() + "]");
239             forceStatusUpdate();
240             return;
241         }
242         if (channelUID.getId().equals(RobotCababilities.SEGMENT_CLEAN.getChannel()) && !command.toString().isEmpty()
243                 && !command.toString().contentEquals("-")) {
244             sendCommand(MiIoCommand.START_SEGMENT, "[" + command.toString() + "]");
245             updateState(RobotCababilities.SEGMENT_CLEAN.getChannel(), new StringType("-"));
246             forceStatusUpdate();
247             return;
248         }
249         if (channelUID.getId().equals(CHANNEL_FAN_CONTROL)) {
250             if (Integer.valueOf(command.toString()) > 0) {
251                 sendCommand(MiIoCommand.SET_MODE, "[" + command.toString() + "]");
252             }
253             forceStatusUpdate();
254             return;
255         }
256         if (channelUID.getId().equals(CHANNEL_CONSUMABLE_RESET)) {
257             sendCommand(MiIoCommand.CONSUMABLES_RESET, "[" + command.toString() + "]");
258             updateState(CHANNEL_CONSUMABLE_RESET, new StringType("none"));
259         }
260     }
261
262     private void forceStatusUpdate() {
263         status.invalidateValue();
264         miIoScheduler.schedule(() -> {
265             status.getValue();
266         }, 3000, TimeUnit.MILLISECONDS);
267     }
268
269     private void safeUpdateState(String channelID, @Nullable Integer state) {
270         if (state != null) {
271             updateState(channelID, new DecimalType(state));
272         } else {
273             logger.debug("Channel {} not update. value not available.", channelID);
274         }
275     }
276
277     private boolean updateVacuumStatus(JsonObject statusData) {
278         StatusDTO statusInfo = GSON.fromJson(statusData, StatusDTO.class);
279         safeUpdateState(CHANNEL_BATTERY, statusInfo.getBattery());
280         if (statusInfo.getCleanArea() != null) {
281             updateState(CHANNEL_CLEAN_AREA,
282                     new QuantityType<>(statusInfo.getCleanArea() / 1000000.0, SIUnits.SQUARE_METRE));
283         }
284         if (statusInfo.getCleanTime() != null) {
285             updateState(CHANNEL_CLEAN_TIME,
286                     new QuantityType<>(TimeUnit.SECONDS.toMinutes(statusInfo.getCleanTime()), Units.MINUTE));
287         }
288         safeUpdateState(CHANNEL_DND_ENABLED, statusInfo.getDndEnabled());
289
290         if (statusInfo.getErrorCode() != null) {
291             updateState(CHANNEL_ERROR_CODE,
292                     new StringType(VacuumErrorType.getType(statusInfo.getErrorCode()).getDescription()));
293             safeUpdateState(CHANNEL_ERROR_ID, statusInfo.getErrorCode());
294         }
295
296         if (statusInfo.getFanPower() != null) {
297             updateState(CHANNEL_FAN_POWER, new DecimalType(statusInfo.getFanPower()));
298             updateState(CHANNEL_FAN_CONTROL, new DecimalType(FanModeType.getType(statusInfo.getFanPower()).getId()));
299         }
300         safeUpdateState(CHANNEL_IN_CLEANING, statusInfo.getInCleaning());
301         safeUpdateState(CHANNEL_MAP_PRESENT, statusInfo.getMapPresent());
302         if (statusInfo.getState() != null) {
303             stateId = statusInfo.getState();
304             StatusType state = StatusType.getType(statusInfo.getState());
305             updateState(CHANNEL_STATE, new StringType(state.getDescription()));
306             updateState(CHANNEL_STATE_ID, new DecimalType(statusInfo.getState()));
307
308             State vacuum = OnOffType.OFF;
309             String control;
310             switch (state) {
311                 case ZONE:
312                 case ROOM:
313                 case CLEANING:
314                 case RETURNING:
315                     control = "vacuum";
316                     vacuum = OnOffType.ON;
317                     break;
318                 case CHARGING:
319                 case CHARGING_ERROR:
320                 case DOCKING:
321                 case FULL:
322                     control = "dock";
323                     break;
324                 case SLEEPING:
325                 case PAUSED:
326                 case IDLE:
327                     control = "pause";
328                     break;
329                 case SPOTCLEAN:
330                     control = "spot";
331                     vacuum = OnOffType.ON;
332                     break;
333                 default:
334                     control = "undef";
335                     break;
336             }
337             if ("undef".equals(control)) {
338                 updateState(CHANNEL_CONTROL, UnDefType.UNDEF);
339             } else {
340                 updateState(CHANNEL_CONTROL, new StringType(control));
341             }
342             updateState(CHANNEL_VACUUM, vacuum);
343         }
344         if (deviceCapabilities.containsKey(RobotCababilities.WATERBOX_MODE)) {
345             safeUpdateState(RobotCababilities.WATERBOX_MODE.getChannel(), statusInfo.getWaterBoxMode());
346         }
347         if (deviceCapabilities.containsKey(RobotCababilities.WATERBOX_STATUS)) {
348             safeUpdateState(RobotCababilities.WATERBOX_STATUS.getChannel(), statusInfo.getWaterBoxStatus());
349         }
350         if (deviceCapabilities.containsKey(RobotCababilities.WATERBOX_CARRIAGE)) {
351             safeUpdateState(RobotCababilities.WATERBOX_CARRIAGE.getChannel(), statusInfo.getWaterBoxCarriageStatus());
352         }
353         if (deviceCapabilities.containsKey(RobotCababilities.LOCKSTATUS)) {
354             safeUpdateState(RobotCababilities.LOCKSTATUS.getChannel(), statusInfo.getLockStatus());
355         }
356         if (deviceCapabilities.containsKey(RobotCababilities.MOP_FORBIDDEN)) {
357             safeUpdateState(RobotCababilities.MOP_FORBIDDEN.getChannel(), statusInfo.getMopForbiddenEnable());
358         }
359         if (deviceCapabilities.containsKey(RobotCababilities.LOCATING)) {
360             safeUpdateState(RobotCababilities.LOCATING.getChannel(), statusInfo.getIsLocating());
361         }
362         return true;
363     }
364
365     private boolean updateConsumables(JsonObject consumablesData) {
366         int mainBrush = consumablesData.get("main_brush_work_time").getAsInt();
367         int sideBrush = consumablesData.get("side_brush_work_time").getAsInt();
368         int filter = consumablesData.get("filter_work_time").getAsInt();
369         int sensor = consumablesData.get("sensor_dirty_time").getAsInt();
370         updateState(CHANNEL_CONSUMABLE_MAIN_TIME,
371                 new QuantityType<>(ConsumablesType.remainingHours(mainBrush, ConsumablesType.MAIN_BRUSH), Units.HOUR));
372         updateState(CHANNEL_CONSUMABLE_MAIN_PERC,
373                 new DecimalType(ConsumablesType.remainingPercent(mainBrush, ConsumablesType.MAIN_BRUSH)));
374         updateState(CHANNEL_CONSUMABLE_SIDE_TIME,
375                 new QuantityType<>(ConsumablesType.remainingHours(sideBrush, ConsumablesType.SIDE_BRUSH), Units.HOUR));
376         updateState(CHANNEL_CONSUMABLE_SIDE_PERC,
377                 new DecimalType(ConsumablesType.remainingPercent(sideBrush, ConsumablesType.SIDE_BRUSH)));
378         updateState(CHANNEL_CONSUMABLE_FILTER_TIME,
379                 new QuantityType<>(ConsumablesType.remainingHours(filter, ConsumablesType.FILTER), Units.HOUR));
380         updateState(CHANNEL_CONSUMABLE_FILTER_PERC,
381                 new DecimalType(ConsumablesType.remainingPercent(filter, ConsumablesType.FILTER)));
382         updateState(CHANNEL_CONSUMABLE_SENSOR_TIME,
383                 new QuantityType<>(ConsumablesType.remainingHours(sensor, ConsumablesType.SENSOR), Units.HOUR));
384         updateState(CHANNEL_CONSUMABLE_SENSOR_PERC,
385                 new DecimalType(ConsumablesType.remainingPercent(sensor, ConsumablesType.SENSOR)));
386         return true;
387     }
388
389     private boolean updateDnD(JsonObject dndData) {
390         logger.trace("Do not disturb data: {}", dndData.toString());
391         updateState(CHANNEL_DND_FUNCTION, new DecimalType(dndData.get("enabled").getAsBigDecimal()));
392         updateState(CHANNEL_DND_START, new StringType(String.format("%02d:%02d", dndData.get("start_hour").getAsInt(),
393                 dndData.get("start_minute").getAsInt())));
394         updateState(CHANNEL_DND_END, new StringType(
395                 String.format("%02d:%02d", dndData.get("end_hour").getAsInt(), dndData.get("end_minute").getAsInt())));
396         return true;
397     }
398
399     private boolean updateHistory(JsonArray historyData) {
400         logger.trace("Cleaning history data: {}", historyData.toString());
401         updateState(CHANNEL_HISTORY_TOTALTIME,
402                 new QuantityType<>(TimeUnit.SECONDS.toMinutes(historyData.get(0).getAsLong()), Units.MINUTE));
403         updateState(CHANNEL_HISTORY_TOTALAREA,
404                 new QuantityType<>(historyData.get(1).getAsDouble() / 1000000D, SIUnits.SQUARE_METRE));
405         updateState(CHANNEL_HISTORY_COUNT, new DecimalType(historyData.get(2).toString()));
406         if (historyData.get(3).getAsJsonArray().size() > 0) {
407             String lastClean = historyData.get(3).getAsJsonArray().get(0).getAsString();
408             if (!lastClean.equals(lastHistoryId)) {
409                 lastHistoryId = lastClean;
410                 sendCommand(MiIoCommand.CLEAN_RECORD_GET, "[" + lastClean + "]");
411             }
412         }
413         return true;
414     }
415
416     private void updateHistoryRecord(JsonArray historyData) {
417         ZonedDateTime startTime = ZonedDateTime.ofInstant(Instant.ofEpochSecond(historyData.get(0).getAsLong()),
418                 ZoneId.systemDefault());
419         ZonedDateTime endTime = ZonedDateTime.ofInstant(Instant.ofEpochSecond(historyData.get(1).getAsLong()),
420                 ZoneId.systemDefault());
421         long duration = TimeUnit.SECONDS.toMinutes(historyData.get(2).getAsLong());
422         double area = historyData.get(3).getAsDouble() / 1000000D;
423         int error = historyData.get(4).getAsInt();
424         int finished = historyData.get(5).getAsInt();
425         JsonObject historyRecord = new JsonObject();
426         historyRecord.addProperty("start", startTime.toString());
427         historyRecord.addProperty("end", endTime.toString());
428         historyRecord.addProperty("duration", duration);
429         historyRecord.addProperty("area", area);
430         historyRecord.addProperty("error", error);
431         historyRecord.addProperty("finished", finished);
432         updateState(CHANNEL_HISTORY_START_TIME, new DateTimeType(startTime));
433         updateState(CHANNEL_HISTORY_END_TIME, new DateTimeType(endTime));
434         updateState(CHANNEL_HISTORY_DURATION, new QuantityType<>(duration, Units.MINUTE));
435         updateState(CHANNEL_HISTORY_AREA, new QuantityType<>(area, SIUnits.SQUARE_METRE));
436         updateState(CHANNEL_HISTORY_ERROR, new DecimalType(error));
437         updateState(CHANNEL_HISTORY_FINISH, new DecimalType(finished));
438         updateState(CHANNEL_HISTORY_RECORD, new StringType(historyRecord.toString()));
439     }
440
441     @Override
442     protected boolean skipUpdate() {
443         if (!hasConnection()) {
444             logger.debug("Skipping periodic update for '{}'. No Connection", getThing().getUID().toString());
445             return true;
446         }
447         if (ThingStatusDetail.CONFIGURATION_ERROR.equals(getThing().getStatusInfo().getStatusDetail())) {
448             logger.debug("Skipping periodic update for '{}' UID '{}'. Thing Status", getThing().getUID().toString(),
449                     getThing().getStatusInfo().getStatusDetail());
450             refreshNetwork();
451             return true;
452         }
453         final MiIoAsyncCommunication mc = miioCom;
454         if (mc != null && mc.getQueueLength() > MAX_QUEUE) {
455             logger.debug("Skipping periodic update for '{}'. {} elements in queue.", getThing().getUID().toString(),
456                     mc.getQueueLength());
457             return true;
458         }
459         return false;
460     }
461
462     @Override
463     protected synchronized void updateData() {
464         if (!hasConnection() || skipUpdate()) {
465             return;
466         }
467         logger.debug("Periodic update for '{}' ({})", getThing().getUID().toString(), getThing().getThingTypeUID());
468         try {
469             dnd.getValue();
470             history.getValue();
471             status.getValue();
472             refreshNetwork();
473             consumables.getValue();
474             if (lastMap.isEmpty() || stateId != 8) {
475                 if (isLinked(mapChannelUid)) {
476                     map.getValue();
477                 }
478             }
479             for (RobotCababilities cmd : FEATURES_CHANNELS) {
480                 if (isLinked(cmd.getChannel())) {
481                     sendCommand(cmd.getCommand());
482                 }
483             }
484         } catch (Exception e) {
485             logger.debug("Error while updating '{}': '{}", getThing().getUID().toString(), e.getLocalizedMessage());
486         }
487     }
488
489     @Override
490     public void initialize() {
491         super.initialize();
492         hasChannelStructure = false;
493         this.mapDrawOptions = RRMapDrawOptions
494                 .getOptionsFromFile(BINDING_USERDATA_PATH + File.separator + "mapConfig.json", logger);
495         updateState(RobotCababilities.SEGMENT_CLEAN.getChannel(), new StringType("-"));
496     }
497
498     @Override
499     protected boolean initializeData() {
500         updateState(CHANNEL_CONSUMABLE_RESET, new StringType("none"));
501         return super.initializeData();
502     }
503
504     @Override
505     public void onMessageReceived(MiIoSendCommand response) {
506         super.onMessageReceived(response);
507         if (response.isError()) {
508             return;
509         }
510         switch (response.getCommand()) {
511             case GET_STATUS:
512                 if (response.getResult().isJsonArray()) {
513                     JsonObject statusResponse = response.getResult().getAsJsonArray().get(0).getAsJsonObject();
514                     if (!hasChannelStructure) {
515                         setCapabilities(statusResponse);
516                         createCapabilityChannels();
517                     }
518                     updateVacuumStatus(statusResponse);
519                 }
520                 break;
521             case CONSUMABLES_GET:
522                 if (response.getResult().isJsonArray()) {
523                     updateConsumables(response.getResult().getAsJsonArray().get(0).getAsJsonObject());
524                 }
525                 break;
526             case DND_GET:
527                 if (response.getResult().isJsonArray()) {
528                     updateDnD(response.getResult().getAsJsonArray().get(0).getAsJsonObject());
529                 }
530                 break;
531             case CLEAN_SUMMARY_GET:
532                 if (response.getResult().isJsonArray()) {
533                     updateHistory(response.getResult().getAsJsonArray());
534                 }
535                 break;
536             case CLEAN_RECORD_GET:
537                 if (response.getResult().isJsonArray() && response.getResult().getAsJsonArray().size() > 0
538                         && response.getResult().getAsJsonArray().get(0).isJsonArray()) {
539                     updateHistoryRecord(response.getResult().getAsJsonArray().get(0).getAsJsonArray());
540                 } else {
541                     logger.debug("Could not extract cleaning history record from: {}", response);
542                 }
543                 break;
544             case GET_MAP:
545                 if (response.getResult().isJsonArray()) {
546                     String mapresponse = response.getResult().getAsJsonArray().get(0).getAsString();
547                     if (!mapresponse.contentEquals("retry") && !mapresponse.contentEquals(lastMap)) {
548                         lastMap = mapresponse;
549                         miIoScheduler.submit(() -> updateState(CHANNEL_VACUUM_MAP, getMap(mapresponse)));
550                     }
551                 }
552                 break;
553             case GET_MAP_STATUS:
554             case GET_SEGMENT_STATUS:
555             case GET_LED_STATUS:
556                 updateNumericChannel(response);
557                 break;
558             case GET_CARPET_MODE:
559             case GET_FW_FEATURES:
560             case GET_CUSTOMIZED_CLEAN_MODE:
561             case GET_MULTI_MAP_LIST:
562             case GET_ROOM_MAPPING:
563                 for (RobotCababilities cmd : FEATURES_CHANNELS) {
564                     if (response.getCommand().getCommand().contentEquals(cmd.getCommand())) {
565                         updateState(cmd.getChannel(), new StringType(response.getResult().toString()));
566                         break;
567                     }
568                 }
569                 break;
570             default:
571                 break;
572         }
573     }
574
575     private void updateNumericChannel(MiIoSendCommand response) {
576         RobotCababilities capabilityChannel = null;
577         for (RobotCababilities cmd : FEATURES_CHANNELS) {
578             if (response.getCommand().getCommand().contentEquals(cmd.getCommand())) {
579                 capabilityChannel = cmd;
580                 break;
581             }
582         }
583         if (capabilityChannel != null) {
584             if (response.getResult().isJsonArray() && response.getResult().getAsJsonArray().get(0).isJsonPrimitive()) {
585                 try {
586                     Integer stat = response.getResult().getAsJsonArray().get(0).getAsInt();
587                     updateState(capabilityChannel.getChannel(), new DecimalType(stat));
588                     return;
589                 } catch (ClassCastException | IllegalStateException e) {
590                     logger.debug("Could not update numeric channel {} with '{}': {}", capabilityChannel.getChannel(),
591                             response.getResult(), e.getMessage());
592                 }
593             } else {
594                 logger.debug("Could not update numeric channel {} with '{}': Not in expected format",
595                         capabilityChannel.getChannel(), response.getResult());
596             }
597             updateState(capabilityChannel.getChannel(), UnDefType.UNDEF);
598         }
599     }
600
601     private void setCapabilities(JsonObject statusResponse) {
602         for (RobotCababilities capability : RobotCababilities.values()) {
603             if (statusResponse.has(capability.getStatusFieldName())) {
604                 deviceCapabilities.putIfAbsent(capability, false);
605                 logger.debug("Setting additional vacuum {}", capability);
606             }
607         }
608     }
609
610     private void createCapabilityChannels() {
611         ThingBuilder thingBuilder = editThing();
612         int cnt = 0;
613
614         for (Entry<RobotCababilities, Boolean> robotCapability : deviceCapabilities.entrySet()) {
615             RobotCababilities capability = robotCapability.getKey();
616             Boolean channelCreated = robotCapability.getValue();
617             if (!channelCreated) {
618                 if (thing.getChannels().stream()
619                         .anyMatch(ch -> ch.getUID().getId().equalsIgnoreCase(capability.getChannel()))) {
620                     logger.debug("Channel already available...skip creation of channel '{}'.", capability.getChannel());
621                     deviceCapabilities.replace(capability, true);
622                     continue;
623                 }
624                 logger.debug("Creating dynamic channel for capability {}", capability);
625                 ChannelType channelType = channelTypeRegistry.getChannelType(capability.getChannelType());
626                 if (channelType != null) {
627                     logger.debug("Found channelType '{}' for capability {}", channelType, capability.name());
628                     ChannelUID channelUID = new ChannelUID(getThing().getUID(), capability.getChannel());
629                     Channel channel = ChannelBuilder.create(channelUID, channelType.getItemType())
630                             .withType(capability.getChannelType()).withLabel(channelType.getLabel()).build();
631                     thingBuilder.withChannel(channel);
632                     cnt++;
633                 } else {
634                     logger.debug("ChannelType {} not found (Unexpected). Available types:",
635                             capability.getChannelType());
636                     for (ChannelType ct : channelTypeRegistry.getChannelTypes()) {
637                         logger.debug("Available channelType: '{}' '{}' '{}'", ct.getUID(), ct.toString(),
638                                 ct.getConfigDescriptionURI());
639                     }
640                 }
641             }
642         }
643         if (cnt > 0) {
644             updateThing(thingBuilder.build());
645         }
646         hasChannelStructure = true;
647     }
648
649     private State getMap(String map) {
650         final MiIoBindingConfiguration configuration = this.configuration;
651         if (configuration != null && cloudConnector.isConnected()) {
652             try {
653                 final @Nullable RawType mapDl = cloudConnector.getMap(map, configuration.cloudServer);
654                 if (mapDl != null) {
655                     byte[] mapData = mapDl.getBytes();
656                     RRMapDraw rrMap = RRMapDraw.loadImage(new ByteArrayInputStream(mapData));
657                     rrMap.setDrawOptions(mapDrawOptions);
658                     ByteArrayOutputStream baos = new ByteArrayOutputStream();
659                     if (logger.isDebugEnabled()) {
660                         final String mapPath = BINDING_USERDATA_PATH + File.separator + map
661                                 + LocalDateTime.now().format(DATEFORMATTER) + ".rrmap";
662                         CloudUtil.writeBytesToFileNio(mapData, mapPath);
663                         logger.debug("Mapdata saved to {}", mapPath);
664                     }
665                     ImageIO.write(rrMap.getImage(), "jpg", baos);
666                     byte[] byteArray = baos.toByteArray();
667                     if (byteArray != null && byteArray.length > 0) {
668                         return new RawType(byteArray, "image/jpeg");
669                     } else {
670                         logger.debug("Mapdata empty removing image");
671                         return UnDefType.UNDEF;
672                     }
673                 }
674             } catch (MiCloudException e) {
675                 logger.debug("Error getting data from Xiaomi cloud. Mapdata could not be updated: {}", e.getMessage());
676             } catch (IOException e) {
677                 logger.debug("Mapdata could not be updated: {}", e.getMessage());
678             }
679         } else {
680             logger.debug("Not connected to Xiaomi cloud. Cannot retreive new map: {}", map);
681         }
682         return UnDefType.UNDEF;
683     }
684 }