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