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