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