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