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