]> git.basschouten.com Git - openhab-addons.git/blob
1cb9d403fbc94c9b56fa12dc7b9c23035213fba0
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.math.BigDecimal;
22 import java.math.BigInteger;
23 import java.time.Instant;
24 import java.time.LocalDateTime;
25 import java.time.ZoneId;
26 import java.time.ZonedDateTime;
27 import java.time.format.DateTimeFormatter;
28 import java.util.Collections;
29 import java.util.Map.Entry;
30 import java.util.Set;
31 import java.util.concurrent.ConcurrentHashMap;
32 import java.util.concurrent.TimeUnit;
33 import java.util.stream.Collectors;
34 import java.util.stream.Stream;
35
36 import javax.imageio.ImageIO;
37
38 import org.eclipse.jdt.annotation.NonNullByDefault;
39 import org.eclipse.jdt.annotation.Nullable;
40 import org.openhab.binding.miio.internal.MiIoBindingConfiguration;
41 import org.openhab.binding.miio.internal.MiIoCommand;
42 import org.openhab.binding.miio.internal.MiIoSendCommand;
43 import org.openhab.binding.miio.internal.basic.MiIoDatabaseWatchService;
44 import org.openhab.binding.miio.internal.cloud.CloudConnector;
45 import org.openhab.binding.miio.internal.cloud.CloudUtil;
46 import org.openhab.binding.miio.internal.cloud.HomeRoomDTO;
47 import org.openhab.binding.miio.internal.cloud.MiCloudException;
48 import org.openhab.binding.miio.internal.robot.ConsumablesType;
49 import org.openhab.binding.miio.internal.robot.FanModeType;
50 import org.openhab.binding.miio.internal.robot.HistoryRecordDTO;
51 import org.openhab.binding.miio.internal.robot.RRMapDraw;
52 import org.openhab.binding.miio.internal.robot.RRMapDrawOptions;
53 import org.openhab.binding.miio.internal.robot.RobotCababilities;
54 import org.openhab.binding.miio.internal.robot.StatusDTO;
55 import org.openhab.binding.miio.internal.robot.StatusType;
56 import org.openhab.binding.miio.internal.robot.VacuumErrorType;
57 import org.openhab.binding.miio.internal.transport.MiIoAsyncCommunication;
58 import org.openhab.core.cache.ExpiringCache;
59 import org.openhab.core.i18n.LocaleProvider;
60 import org.openhab.core.i18n.TranslationProvider;
61 import org.openhab.core.library.types.DateTimeType;
62 import org.openhab.core.library.types.DecimalType;
63 import org.openhab.core.library.types.OnOffType;
64 import org.openhab.core.library.types.QuantityType;
65 import org.openhab.core.library.types.RawType;
66 import org.openhab.core.library.types.StringType;
67 import org.openhab.core.library.unit.SIUnits;
68 import org.openhab.core.library.unit.Units;
69 import org.openhab.core.thing.Channel;
70 import org.openhab.core.thing.ChannelUID;
71 import org.openhab.core.thing.Thing;
72 import org.openhab.core.thing.ThingStatusDetail;
73 import org.openhab.core.thing.binding.builder.ChannelBuilder;
74 import org.openhab.core.thing.binding.builder.ThingBuilder;
75 import org.openhab.core.thing.type.ChannelType;
76 import org.openhab.core.thing.type.ChannelTypeRegistry;
77 import org.openhab.core.types.Command;
78 import org.openhab.core.types.RefreshType;
79 import org.openhab.core.types.State;
80 import org.openhab.core.types.UnDefType;
81 import org.slf4j.Logger;
82 import org.slf4j.LoggerFactory;
83
84 import com.google.gson.Gson;
85 import com.google.gson.GsonBuilder;
86 import com.google.gson.JsonArray;
87 import com.google.gson.JsonElement;
88 import com.google.gson.JsonObject;
89
90 /**
91  * The {@link MiIoVacuumHandler} is responsible for handling commands, which are
92  * sent to one of the channels.
93  *
94  * @author Marcel Verpaalen - Initial contribution
95  */
96 @NonNullByDefault
97 public class MiIoVacuumHandler extends MiIoAbstractHandler {
98     private final Logger logger = LoggerFactory.getLogger(MiIoVacuumHandler.class);
99     private static final DateTimeFormatter DATEFORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
100     private static final Gson GSON = new GsonBuilder().serializeNulls().create();
101     private final ChannelUID mapChannelUid;
102
103     private static final Set<RobotCababilities> FEATURES_CHANNELS = Collections.unmodifiableSet(Stream
104             .of(RobotCababilities.SEGMENT_STATUS, RobotCababilities.MAP_STATUS, RobotCababilities.LED_STATUS,
105                     RobotCababilities.CARPET_MODE, RobotCababilities.FW_FEATURES, RobotCababilities.ROOM_MAPPING,
106                     RobotCababilities.MULTI_MAP_LIST, RobotCababilities.CUSTOMIZE_CLEAN_MODE)
107             .collect(Collectors.toSet()));
108
109     private ExpiringCache<String> status;
110     private ExpiringCache<String> consumables;
111     private ExpiringCache<String> dnd;
112     private ExpiringCache<String> history;
113     private int stateId;
114     private ExpiringCache<String> map;
115     private String lastHistoryId = "";
116     private String lastMap = "";
117     private boolean hasChannelStructure;
118     private ConcurrentHashMap<RobotCababilities, Boolean> deviceCapabilities = new ConcurrentHashMap<>();
119     private ChannelTypeRegistry channelTypeRegistry;
120     private RRMapDrawOptions mapDrawOptions = new RRMapDrawOptions();
121
122     public MiIoVacuumHandler(Thing thing, MiIoDatabaseWatchService miIoDatabaseWatchService,
123             CloudConnector cloudConnector, ChannelTypeRegistry channelTypeRegistry, TranslationProvider i18nProvider,
124             LocaleProvider localeProvider) {
125         super(thing, miIoDatabaseWatchService, cloudConnector, i18nProvider, localeProvider);
126         this.channelTypeRegistry = channelTypeRegistry;
127         mapChannelUid = new ChannelUID(thing.getUID(), CHANNEL_VACUUM_MAP);
128         status = new ExpiringCache<>(CACHE_EXPIRY, () -> {
129             try {
130                 int ret = sendCommand(MiIoCommand.GET_STATUS);
131                 if (ret != 0) {
132                     return "id:" + ret;
133                 }
134             } catch (Exception e) {
135                 logger.debug("Error during status refresh: {}", e.getMessage(), e);
136             }
137             return null;
138         });
139         consumables = new ExpiringCache<>(CACHE_EXPIRY, () -> {
140             try {
141                 int ret = sendCommand(MiIoCommand.CONSUMABLES_GET);
142                 if (ret != 0) {
143                     return "id:" + ret;
144                 }
145             } catch (Exception e) {
146                 logger.debug("Error during consumables refresh: {}", e.getMessage(), e);
147             }
148             return null;
149         });
150         dnd = new ExpiringCache<>(CACHE_EXPIRY, () -> {
151             try {
152                 int ret = sendCommand(MiIoCommand.DND_GET);
153                 if (ret != 0) {
154                     return "id:" + ret;
155                 }
156             } catch (Exception e) {
157                 logger.debug("Error during dnd refresh: {}", e.getMessage(), e);
158             }
159             return null;
160         });
161         history = new ExpiringCache<>(CACHE_EXPIRY, () -> {
162             try {
163                 int ret = sendCommand(MiIoCommand.CLEAN_SUMMARY_GET);
164                 if (ret != 0) {
165                     return "id:" + ret;
166                 }
167             } catch (Exception e) {
168                 logger.debug("Error during cleaning data refresh: {}", e.getMessage(), e);
169             }
170             return null;
171         });
172         map = new ExpiringCache<String>(CACHE_EXPIRY, () -> {
173             try {
174                 int ret = sendCommand(MiIoCommand.GET_MAP);
175                 if (ret != 0) {
176                     return "id:" + ret;
177                 }
178             } catch (Exception e) {
179                 logger.debug("Error during dnd refresh: {}", e.getMessage(), e);
180             }
181             return null;
182         });
183     }
184
185     @Override
186     public void handleCommand(ChannelUID channelUID, Command command) {
187         if (getConnection() == null) {
188             logger.debug("Vacuum {} not online. Command {} ignored", getThing().getUID(), command.toString());
189             return;
190         }
191         if (command == RefreshType.REFRESH) {
192             logger.debug("Refreshing {}", channelUID);
193             updateData();
194             lastMap = "";
195             if (channelUID.getId().equals(CHANNEL_VACUUM_MAP)) {
196                 sendCommand(MiIoCommand.GET_MAP);
197             }
198             return;
199         }
200         if (handleCommandsChannels(channelUID, command)) {
201             forceStatusUpdate();
202             return;
203         }
204         if (channelUID.getId().equals(CHANNEL_VACUUM)) {
205             if (command instanceof OnOffType) {
206                 if (command.equals(OnOffType.ON)) {
207                     sendCommand(MiIoCommand.START_VACUUM);
208                     forceStatusUpdate();
209                     return;
210                 } else {
211                     sendCommand(MiIoCommand.STOP_VACUUM);
212                     miIoScheduler.schedule(() -> {
213                         sendCommand(MiIoCommand.CHARGE);
214                         forceStatusUpdate();
215                     }, 2000, TimeUnit.MILLISECONDS);
216                     return;
217                 }
218             }
219         }
220         if (channelUID.getId().equals(CHANNEL_CONTROL)) {
221             if ("vacuum".equals(command.toString())) {
222                 sendCommand(MiIoCommand.START_VACUUM);
223             } else if ("spot".equals(command.toString())) {
224                 sendCommand(MiIoCommand.START_SPOT);
225             } else if ("pause".equals(command.toString())) {
226                 sendCommand(MiIoCommand.PAUSE);
227             } else if ("dock".equals(command.toString())) {
228                 sendCommand(MiIoCommand.STOP_VACUUM);
229                 miIoScheduler.schedule(() -> {
230                     sendCommand(MiIoCommand.CHARGE);
231                     forceStatusUpdate();
232                 }, 2000, TimeUnit.MILLISECONDS);
233                 return;
234             } else {
235                 logger.info("Command {} not recognised", command.toString());
236             }
237             forceStatusUpdate();
238             return;
239         }
240         if (channelUID.getId().equals(CHANNEL_FAN_POWER)) {
241             sendCommand(MiIoCommand.SET_MODE, "[" + command.toString() + "]");
242             forceStatusUpdate();
243             return;
244         }
245         if (channelUID.getId().equals(RobotCababilities.WATERBOX_MODE.getChannel())) {
246             sendCommand(MiIoCommand.SET_WATERBOX_MODE, "[" + command.toString() + "]");
247             forceStatusUpdate();
248             return;
249         }
250         if (channelUID.getId().equals(RobotCababilities.SEGMENT_CLEAN.getChannel()) && !command.toString().isEmpty()
251                 && !command.toString().contentEquals("-")) {
252             sendCommand(MiIoCommand.START_SEGMENT, "[" + command.toString() + "]");
253             updateState(RobotCababilities.SEGMENT_CLEAN.getChannel(), new StringType("-"));
254             forceStatusUpdate();
255             return;
256         }
257         if (channelUID.getId().equals(CHANNEL_FAN_CONTROL)) {
258             if (Integer.valueOf(command.toString()) > 0) {
259                 sendCommand(MiIoCommand.SET_MODE, "[" + command.toString() + "]");
260             }
261             forceStatusUpdate();
262             return;
263         }
264         if (channelUID.getId().equals(CHANNEL_CONSUMABLE_RESET)) {
265             sendCommand(MiIoCommand.CONSUMABLES_RESET, "[" + command.toString() + "]");
266             updateState(CHANNEL_CONSUMABLE_RESET, new StringType("none"));
267         }
268     }
269
270     private void forceStatusUpdate() {
271         status.invalidateValue();
272         miIoScheduler.schedule(() -> {
273             status.getValue();
274         }, 3000, TimeUnit.MILLISECONDS);
275     }
276
277     private void safeUpdateState(String channelID, @Nullable Integer state) {
278         if (state != null) {
279             updateState(channelID, new DecimalType(state));
280         } else {
281             logger.debug("Channel {} not update. value not available.", channelID);
282         }
283     }
284
285     private boolean updateVacuumStatus(JsonObject statusData) {
286         StatusDTO statusInfo = GSON.fromJson(statusData, StatusDTO.class);
287         if (statusInfo == null) {
288             return false;
289         }
290         safeUpdateState(CHANNEL_BATTERY, statusInfo.getBattery());
291         if (statusInfo.getCleanArea() != null) {
292             updateState(CHANNEL_CLEAN_AREA,
293                     new QuantityType<>(statusInfo.getCleanArea() / 1000000.0, SIUnits.SQUARE_METRE));
294         }
295         if (statusInfo.getCleanTime() != null) {
296             updateState(CHANNEL_CLEAN_TIME,
297                     new QuantityType<>(TimeUnit.SECONDS.toMinutes(statusInfo.getCleanTime()), Units.MINUTE));
298         }
299         safeUpdateState(CHANNEL_DND_ENABLED, statusInfo.getDndEnabled());
300
301         if (statusInfo.getErrorCode() != null) {
302             updateState(CHANNEL_ERROR_CODE,
303                     new StringType(VacuumErrorType.getType(statusInfo.getErrorCode()).getDescription()));
304             safeUpdateState(CHANNEL_ERROR_ID, statusInfo.getErrorCode());
305         }
306
307         if (statusInfo.getFanPower() != null) {
308             updateState(CHANNEL_FAN_POWER, new DecimalType(statusInfo.getFanPower()));
309             updateState(CHANNEL_FAN_CONTROL, new DecimalType(FanModeType.getType(statusInfo.getFanPower()).getId()));
310         }
311         safeUpdateState(CHANNEL_IN_CLEANING, statusInfo.getInCleaning());
312         safeUpdateState(CHANNEL_MAP_PRESENT, statusInfo.getMapPresent());
313         if (statusInfo.getState() != null) {
314             stateId = statusInfo.getState();
315             StatusType state = StatusType.getType(statusInfo.getState());
316             updateState(CHANNEL_STATE, new StringType(state.getDescription()));
317             updateState(CHANNEL_STATE_ID, new DecimalType(statusInfo.getState()));
318
319             State vacuum = OnOffType.OFF;
320             String control;
321             switch (state) {
322                 case ZONE:
323                 case ROOM:
324                 case CLEANING:
325                 case RETURNING:
326                     control = "vacuum";
327                     vacuum = OnOffType.ON;
328                     break;
329                 case CHARGING:
330                 case CHARGING_ERROR:
331                 case DOCKING:
332                 case FULL:
333                     control = "dock";
334                     break;
335                 case SLEEPING:
336                 case PAUSED:
337                 case IDLE:
338                     control = "pause";
339                     break;
340                 case SPOTCLEAN:
341                     control = "spot";
342                     vacuum = OnOffType.ON;
343                     break;
344                 default:
345                     control = "undef";
346                     break;
347             }
348             if ("undef".equals(control)) {
349                 updateState(CHANNEL_CONTROL, UnDefType.UNDEF);
350             } else {
351                 updateState(CHANNEL_CONTROL, new StringType(control));
352             }
353             updateState(CHANNEL_VACUUM, vacuum);
354         }
355         if (deviceCapabilities.containsKey(RobotCababilities.WATERBOX_MODE)) {
356             safeUpdateState(RobotCababilities.WATERBOX_MODE.getChannel(), statusInfo.getWaterBoxMode());
357         }
358         if (deviceCapabilities.containsKey(RobotCababilities.WATERBOX_STATUS)) {
359             safeUpdateState(RobotCababilities.WATERBOX_STATUS.getChannel(), statusInfo.getWaterBoxStatus());
360         }
361         if (deviceCapabilities.containsKey(RobotCababilities.WATERBOX_CARRIAGE)) {
362             safeUpdateState(RobotCababilities.WATERBOX_CARRIAGE.getChannel(), statusInfo.getWaterBoxCarriageStatus());
363         }
364         if (deviceCapabilities.containsKey(RobotCababilities.LOCKSTATUS)) {
365             safeUpdateState(RobotCababilities.LOCKSTATUS.getChannel(), statusInfo.getLockStatus());
366         }
367         if (deviceCapabilities.containsKey(RobotCababilities.MOP_FORBIDDEN)) {
368             safeUpdateState(RobotCababilities.MOP_FORBIDDEN.getChannel(), statusInfo.getMopForbiddenEnable());
369         }
370         if (deviceCapabilities.containsKey(RobotCababilities.LOCATING)) {
371             safeUpdateState(RobotCababilities.LOCATING.getChannel(), statusInfo.getIsLocating());
372         }
373         return true;
374     }
375
376     private boolean updateConsumables(JsonObject consumablesData) {
377         int mainBrush = consumablesData.get("main_brush_work_time").getAsInt();
378         int sideBrush = consumablesData.get("side_brush_work_time").getAsInt();
379         int filter = consumablesData.get("filter_work_time").getAsInt();
380         int sensor = consumablesData.get("sensor_dirty_time").getAsInt();
381         updateState(CHANNEL_CONSUMABLE_MAIN_TIME,
382                 new QuantityType<>(ConsumablesType.remainingHours(mainBrush, ConsumablesType.MAIN_BRUSH), Units.HOUR));
383         updateState(CHANNEL_CONSUMABLE_MAIN_PERC,
384                 new DecimalType(ConsumablesType.remainingPercent(mainBrush, ConsumablesType.MAIN_BRUSH)));
385         updateState(CHANNEL_CONSUMABLE_SIDE_TIME,
386                 new QuantityType<>(ConsumablesType.remainingHours(sideBrush, ConsumablesType.SIDE_BRUSH), Units.HOUR));
387         updateState(CHANNEL_CONSUMABLE_SIDE_PERC,
388                 new DecimalType(ConsumablesType.remainingPercent(sideBrush, ConsumablesType.SIDE_BRUSH)));
389         updateState(CHANNEL_CONSUMABLE_FILTER_TIME,
390                 new QuantityType<>(ConsumablesType.remainingHours(filter, ConsumablesType.FILTER), Units.HOUR));
391         updateState(CHANNEL_CONSUMABLE_FILTER_PERC,
392                 new DecimalType(ConsumablesType.remainingPercent(filter, ConsumablesType.FILTER)));
393         updateState(CHANNEL_CONSUMABLE_SENSOR_TIME,
394                 new QuantityType<>(ConsumablesType.remainingHours(sensor, ConsumablesType.SENSOR), Units.HOUR));
395         updateState(CHANNEL_CONSUMABLE_SENSOR_PERC,
396                 new DecimalType(ConsumablesType.remainingPercent(sensor, ConsumablesType.SENSOR)));
397         return true;
398     }
399
400     private boolean updateDnD(JsonObject dndData) {
401         logger.trace("Do not disturb data: {}", dndData.toString());
402         updateState(CHANNEL_DND_FUNCTION, new DecimalType(dndData.get("enabled").getAsBigDecimal()));
403         updateState(CHANNEL_DND_START, new StringType(String.format("%02d:%02d", dndData.get("start_hour").getAsInt(),
404                 dndData.get("start_minute").getAsInt())));
405         updateState(CHANNEL_DND_END, new StringType(
406                 String.format("%02d:%02d", dndData.get("end_hour").getAsInt(), dndData.get("end_minute").getAsInt())));
407         return true;
408     }
409
410     private boolean updateHistoryLegacy(JsonArray historyData) {
411         logger.trace("Cleaning history data: {}", historyData.toString());
412         updateState(CHANNEL_HISTORY_TOTALTIME,
413                 new QuantityType<>(TimeUnit.SECONDS.toMinutes(historyData.get(0).getAsLong()), Units.MINUTE));
414         updateState(CHANNEL_HISTORY_TOTALAREA,
415                 new QuantityType<>(historyData.get(1).getAsDouble() / 1000000D, SIUnits.SQUARE_METRE));
416         updateState(CHANNEL_HISTORY_COUNT, new DecimalType(historyData.get(2).toString()));
417         if (historyData.get(3).getAsJsonArray().size() > 0) {
418             String lastClean = historyData.get(3).getAsJsonArray().get(0).getAsString();
419             if (!lastClean.equals(lastHistoryId)) {
420                 lastHistoryId = lastClean;
421                 sendCommand(MiIoCommand.CLEAN_RECORD_GET, "[" + lastClean + "]");
422             }
423         }
424         return true;
425     }
426
427     private boolean updateHistory(JsonObject historyData) {
428         logger.trace("Cleaning history data: {}", historyData);
429         if (historyData.has("clean_time")) {
430             updateState(CHANNEL_HISTORY_TOTALTIME, new QuantityType<>(
431                     TimeUnit.SECONDS.toMinutes(historyData.get("clean_time").getAsLong()), Units.MINUTE));
432         }
433         if (historyData.has("clean_area")) {
434             updateState(CHANNEL_HISTORY_TOTALAREA,
435                     new QuantityType<>(historyData.get("clean_area").getAsDouble() / 1000000D, SIUnits.SQUARE_METRE));
436         }
437         if (historyData.has("clean_count")) {
438             updateState(CHANNEL_HISTORY_COUNT, new DecimalType(historyData.get("clean_count").getAsLong()));
439         }
440         if (historyData.has("records") & historyData.get("records").isJsonArray()) {
441             JsonArray historyRecords = historyData.get("records").getAsJsonArray();
442             if (!historyRecords.isEmpty()) {
443                 String lastClean = historyRecords.get(0).getAsString();
444                 if (!lastClean.equals(lastHistoryId)) {
445                     lastHistoryId = lastClean;
446                     sendCommand(MiIoCommand.CLEAN_RECORD_GET, "[" + lastClean + "]");
447                 }
448             }
449         }
450         return true;
451     }
452
453     private void updateHistoryRecordLegacy(JsonArray historyData) {
454         HistoryRecordDTO historyRecord = new HistoryRecordDTO();
455         for (int i = 0; i < historyData.size(); ++i) {
456             try {
457                 BigInteger value = historyData.get(i).getAsBigInteger();
458                 switch (i) {
459                     case 0:
460                         historyRecord.setStart(ZonedDateTime
461                                 .ofInstant(Instant.ofEpochSecond(value.longValue()), ZoneId.systemDefault())
462                                 .toString());
463                         break;
464                     case 1:
465                         historyRecord.setStart(ZonedDateTime
466                                 .ofInstant(Instant.ofEpochSecond(value.longValue()), ZoneId.systemDefault())
467                                 .toString());
468                         break;
469                     case 2:
470                         historyRecord.setDuration(value.intValue());
471                         break;
472                     case 3:
473                         historyRecord.setArea(new BigDecimal(value).divide(BigDecimal.valueOf(1000000)));
474                         break;
475                     case 4:
476                         historyRecord.setError(value.intValue());
477                         break;
478                     case 5:
479                         historyRecord.setFinished(value.intValue());
480                         break;
481                     case 6:
482                         historyRecord.setStartType(value.intValue());
483                         break;
484                     case 7:
485                         historyRecord.setCleanType(value.intValue());
486                         break;
487                     case 8:
488                         historyRecord.setFinishReason(value.intValue());
489                         break;
490                 }
491             } catch (ClassCastException | NumberFormatException | IllegalStateException e) {
492             }
493         }
494         updateHistoryRecord(historyRecord);
495     }
496
497     private void updateHistoryRecord(HistoryRecordDTO historyRecordDTO) {
498         JsonObject historyRecord = new JsonObject();
499         if (historyRecordDTO.getStart() != null) {
500             historyRecord.addProperty("start", historyRecordDTO.getStart().split("\\+")[0]);
501             updateState(CHANNEL_HISTORY_START_TIME, new DateTimeType(historyRecordDTO.getStart().split("\\+")[0]));
502         }
503         if (historyRecordDTO.getEnd() != null) {
504             historyRecord.addProperty("end", historyRecordDTO.getEnd().split("\\+")[0]);
505             updateState(CHANNEL_HISTORY_END_TIME, new DateTimeType(historyRecordDTO.getEnd().split("\\+")[0]));
506         }
507         if (historyRecordDTO.getDuration() != null) {
508             long duration = TimeUnit.SECONDS.toMinutes(historyRecordDTO.getDuration().longValue());
509             historyRecord.addProperty("duration", duration);
510             updateState(CHANNEL_HISTORY_DURATION, new QuantityType<>(duration, Units.MINUTE));
511         }
512         if (historyRecordDTO.getArea() != null) {
513             historyRecord.addProperty("area", historyRecordDTO.getArea());
514             updateState(CHANNEL_HISTORY_AREA, new QuantityType<>(historyRecordDTO.getArea(), SIUnits.SQUARE_METRE));
515         }
516         if (historyRecordDTO.getError() != null) {
517             historyRecord.addProperty("error", historyRecordDTO.getError());
518             updateState(CHANNEL_HISTORY_ERROR, new DecimalType(historyRecordDTO.getError()));
519         }
520         if (historyRecordDTO.getFinished() != null) {
521             historyRecord.addProperty("finished", historyRecordDTO.getFinished());
522             updateState(CHANNEL_HISTORY_FINISH, new DecimalType(historyRecordDTO.getFinished()));
523         }
524         if (historyRecordDTO.getFinishReason() != null) {
525             historyRecord.addProperty("finish_reason", historyRecordDTO.getFinishReason());
526             updateState(CHANNEL_HISTORY_FINISHREASON, new DecimalType(historyRecordDTO.getFinishReason()));
527         }
528         if (historyRecordDTO.getDustCollectionStatus() != null) {
529             historyRecord.addProperty("dust_collection_status", historyRecordDTO.getDustCollectionStatus());
530             updateState(CHANNEL_HISTORY_DUSTCOLLECTION, new DecimalType(historyRecordDTO.getDustCollectionStatus()));
531         }
532         updateState(CHANNEL_HISTORY_RECORD, new StringType(historyRecord.toString()));
533     }
534
535     private void updateRoomMapping(MiIoSendCommand response) {
536         for (RobotCababilities cmd : FEATURES_CHANNELS) {
537             if (response.getCommand().getCommand().contentEquals(cmd.getCommand())) {
538                 if (response.getResult().isJsonArray()) {
539                     JsonArray rooms = response.getResult().getAsJsonArray();
540                     JsonArray mappedRoom = new JsonArray();
541                     for (JsonElement roomE : rooms) {
542                         JsonArray room = roomE.getAsJsonArray();
543                         HomeRoomDTO name = cloudConnector.getRoom(room.get(1).getAsString());
544                         if (name != null && name.getName() != null) {
545                             room.add(name.getName());
546                         } else {
547                             room.add("not found");
548                         }
549                         mappedRoom.add(room);
550                     }
551                     updateState(cmd.getChannel(), new StringType(mappedRoom.toString()));
552                 } else {
553                     updateState(cmd.getChannel(), new StringType(response.getResult().toString()));
554                 }
555                 break;
556             }
557         }
558     }
559
560     @Override
561     protected boolean skipUpdate() {
562         if (!hasConnection()) {
563             logger.debug("Skipping periodic update for '{}'. No Connection", getThing().getUID().toString());
564             return true;
565         }
566         if (ThingStatusDetail.CONFIGURATION_ERROR.equals(getThing().getStatusInfo().getStatusDetail())) {
567             logger.debug("Skipping periodic update for '{}' UID '{}'. Thing Status", getThing().getUID().toString(),
568                     getThing().getStatusInfo().getStatusDetail());
569             refreshNetwork();
570             return true;
571         }
572         final MiIoAsyncCommunication mc = miioCom;
573         if (mc != null && mc.getQueueLength() > MAX_QUEUE) {
574             logger.debug("Skipping periodic update for '{}'. {} elements in queue.", getThing().getUID().toString(),
575                     mc.getQueueLength());
576             return true;
577         }
578         return false;
579     }
580
581     @Override
582     protected synchronized void updateData() {
583         if (!hasConnection() || skipUpdate()) {
584             return;
585         }
586         logger.debug("Periodic update for '{}' ({})", getThing().getUID().toString(), getThing().getThingTypeUID());
587         try {
588             dnd.getValue();
589             history.getValue();
590             status.getValue();
591             refreshNetwork();
592             consumables.getValue();
593             if (lastMap.isEmpty() || stateId != 8) {
594                 if (isLinked(mapChannelUid)) {
595                     map.getValue();
596                 }
597             }
598             for (RobotCababilities cmd : FEATURES_CHANNELS) {
599                 if (isLinked(cmd.getChannel())) {
600                     sendCommand(cmd.getCommand());
601                 }
602             }
603         } catch (Exception e) {
604             logger.debug("Error while updating '{}': '{}", getThing().getUID().toString(), e.getLocalizedMessage());
605         }
606     }
607
608     @Override
609     public void initialize() {
610         super.initialize();
611         hasChannelStructure = false;
612         this.mapDrawOptions = RRMapDrawOptions
613                 .getOptionsFromFile(BINDING_USERDATA_PATH + File.separator + "mapConfig.json", logger);
614         updateState(RobotCababilities.SEGMENT_CLEAN.getChannel(), new StringType("-"));
615         cloudConnector.getHomeLists();
616     }
617
618     @Override
619     protected boolean initializeData() {
620         updateState(CHANNEL_CONSUMABLE_RESET, new StringType("none"));
621         return super.initializeData();
622     }
623
624     @Override
625     public void onMessageReceived(MiIoSendCommand response) {
626         super.onMessageReceived(response);
627         if (response.isError()) {
628             return;
629         }
630         switch (response.getCommand()) {
631             case GET_STATUS:
632                 if (response.getResult().isJsonArray()) {
633                     JsonObject statusResponse = response.getResult().getAsJsonArray().get(0).getAsJsonObject();
634                     if (!hasChannelStructure) {
635                         setCapabilities(statusResponse);
636                         createCapabilityChannels();
637                     }
638                     updateVacuumStatus(statusResponse);
639                 }
640                 break;
641             case CONSUMABLES_GET:
642                 if (response.getResult().isJsonArray()) {
643                     updateConsumables(response.getResult().getAsJsonArray().get(0).getAsJsonObject());
644                 }
645                 break;
646             case DND_GET:
647                 if (response.getResult().isJsonArray()) {
648                     updateDnD(response.getResult().getAsJsonArray().get(0).getAsJsonObject());
649                 }
650                 break;
651             case CLEAN_SUMMARY_GET:
652                 if (response.getResult().isJsonArray()) {
653                     updateHistoryLegacy(response.getResult().getAsJsonArray());
654                 } else if (response.getResult().isJsonObject()) {
655                     updateHistory(response.getResult().getAsJsonObject());
656                 }
657                 break;
658             case CLEAN_RECORD_GET:
659                 if (response.getResult().isJsonArray() && response.getResult().getAsJsonArray().size() > 0
660                         && response.getResult().getAsJsonArray().get(0).isJsonArray()) {
661                     updateHistoryRecordLegacy(response.getResult().getAsJsonArray().get(0).getAsJsonArray());
662                 } else if (response.getResult().isJsonObject()) {
663                     final HistoryRecordDTO historyRecordDTO = GSON.fromJson(response.getResult().getAsJsonObject(),
664                             HistoryRecordDTO.class);
665                     if (historyRecordDTO != null) {
666                         updateHistoryRecord(historyRecordDTO);
667                     }
668                 } else {
669                     logger.debug("Could not extract cleaning history record from: {}", response);
670                 }
671                 break;
672             case GET_MAP:
673                 if (response.getResult().isJsonArray()) {
674                     String mapresponse = response.getResult().getAsJsonArray().get(0).getAsString();
675                     if (!mapresponse.contentEquals("retry") && !mapresponse.contentEquals(lastMap)) {
676                         lastMap = mapresponse;
677                         miIoScheduler.submit(() -> updateState(CHANNEL_VACUUM_MAP, getMap(mapresponse)));
678                     }
679                 }
680                 break;
681             case GET_MAP_STATUS:
682             case GET_SEGMENT_STATUS:
683             case GET_LED_STATUS:
684                 updateNumericChannel(response);
685                 break;
686             case GET_ROOM_MAPPING:
687                 updateRoomMapping(response);
688                 break;
689             case GET_CARPET_MODE:
690             case GET_FW_FEATURES:
691             case GET_CUSTOMIZED_CLEAN_MODE:
692             case GET_MULTI_MAP_LIST:
693                 for (RobotCababilities cmd : FEATURES_CHANNELS) {
694                     if (response.getCommand().getCommand().contentEquals(cmd.getCommand())) {
695                         updateState(cmd.getChannel(), new StringType(response.getResult().toString()));
696                         break;
697                     }
698                 }
699                 break;
700             default:
701                 break;
702         }
703     }
704
705     private void updateNumericChannel(MiIoSendCommand response) {
706         RobotCababilities capabilityChannel = null;
707         for (RobotCababilities cmd : FEATURES_CHANNELS) {
708             if (response.getCommand().getCommand().contentEquals(cmd.getCommand())) {
709                 capabilityChannel = cmd;
710                 break;
711             }
712         }
713         if (capabilityChannel != null) {
714             if (response.getResult().isJsonArray() && response.getResult().getAsJsonArray().get(0).isJsonPrimitive()) {
715                 try {
716                     Integer stat = response.getResult().getAsJsonArray().get(0).getAsInt();
717                     updateState(capabilityChannel.getChannel(), new DecimalType(stat));
718                     return;
719                 } catch (ClassCastException | IllegalStateException e) {
720                     logger.debug("Could not update numeric channel {} with '{}': {}", capabilityChannel.getChannel(),
721                             response.getResult(), e.getMessage());
722                 }
723             } else {
724                 logger.debug("Could not update numeric channel {} with '{}': Not in expected format",
725                         capabilityChannel.getChannel(), response.getResult());
726             }
727             updateState(capabilityChannel.getChannel(), UnDefType.UNDEF);
728         }
729     }
730
731     private void setCapabilities(JsonObject statusResponse) {
732         for (RobotCababilities capability : RobotCababilities.values()) {
733             if (statusResponse.has(capability.getStatusFieldName())) {
734                 deviceCapabilities.putIfAbsent(capability, false);
735                 logger.debug("Setting additional vacuum {}", capability);
736             }
737         }
738     }
739
740     private void createCapabilityChannels() {
741         ThingBuilder thingBuilder = editThing();
742         int cnt = 0;
743
744         for (Entry<RobotCababilities, Boolean> robotCapability : deviceCapabilities.entrySet()) {
745             RobotCababilities capability = robotCapability.getKey();
746             Boolean channelCreated = robotCapability.getValue();
747             if (!channelCreated) {
748                 if (thing.getChannels().stream()
749                         .anyMatch(ch -> ch.getUID().getId().equalsIgnoreCase(capability.getChannel()))) {
750                     logger.debug("Channel already available...skip creation of channel '{}'.", capability.getChannel());
751                     deviceCapabilities.replace(capability, true);
752                     continue;
753                 }
754                 logger.debug("Creating dynamic channel for capability {}", capability);
755                 ChannelType channelType = channelTypeRegistry.getChannelType(capability.getChannelType());
756                 if (channelType != null) {
757                     logger.debug("Found channelType '{}' for capability {}", channelType, capability.name());
758                     ChannelUID channelUID = new ChannelUID(getThing().getUID(), capability.getChannel());
759                     Channel channel = ChannelBuilder.create(channelUID, channelType.getItemType())
760                             .withType(capability.getChannelType()).withLabel(channelType.getLabel()).build();
761                     thingBuilder.withChannel(channel);
762                     cnt++;
763                 } else {
764                     logger.debug("ChannelType {} not found (Unexpected). Available types:",
765                             capability.getChannelType());
766                     for (ChannelType ct : channelTypeRegistry.getChannelTypes()) {
767                         logger.debug("Available channelType: '{}' '{}' '{}'", ct.getUID(), ct.toString(),
768                                 ct.getConfigDescriptionURI());
769                     }
770                 }
771             }
772         }
773         if (cnt > 0) {
774             updateThing(thingBuilder.build());
775         }
776         hasChannelStructure = true;
777     }
778
779     private State getMap(String map) {
780         final MiIoBindingConfiguration configuration = this.configuration;
781         if (configuration != null && cloudConnector.isConnected()) {
782             try {
783                 final @Nullable RawType mapDl = cloudConnector.getMap(map, configuration.cloudServer);
784                 if (mapDl != null) {
785                     byte[] mapData = mapDl.getBytes();
786                     RRMapDraw rrMap = RRMapDraw.loadImage(new ByteArrayInputStream(mapData));
787                     rrMap.setDrawOptions(mapDrawOptions);
788                     ByteArrayOutputStream baos = new ByteArrayOutputStream();
789                     if (logger.isDebugEnabled()) {
790                         final String mapPath = BINDING_USERDATA_PATH + File.separator + map
791                                 + LocalDateTime.now().format(DATEFORMATTER) + ".rrmap";
792                         CloudUtil.writeBytesToFileNio(mapData, mapPath);
793                         logger.debug("Mapdata saved to {}", mapPath);
794                     }
795                     ImageIO.write(rrMap.getImage(), "jpg", baos);
796                     byte[] byteArray = baos.toByteArray();
797                     if (byteArray != null && byteArray.length > 0) {
798                         return new RawType(byteArray, "image/jpeg");
799                     } else {
800                         logger.debug("Mapdata empty removing image");
801                         return UnDefType.UNDEF;
802                     }
803                 }
804             } catch (MiCloudException e) {
805                 logger.debug("Error getting data from Xiaomi cloud. Mapdata could not be updated: {}", e.getMessage());
806             } catch (IOException e) {
807                 logger.debug("Mapdata could not be updated: {}", e.getMessage());
808             }
809         } else {
810             logger.debug("Not connected to Xiaomi cloud. Cannot retreive new map: {}", map);
811         }
812         return UnDefType.UNDEF;
813     }
814 }