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