2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.miio.internal.handler;
15 import static org.openhab.binding.miio.internal.MiIoBindingConstants.*;
17 import java.io.ByteArrayInputStream;
18 import java.io.ByteArrayOutputStream;
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;
29 import java.util.concurrent.ConcurrentHashMap;
30 import java.util.concurrent.TimeUnit;
31 import java.util.stream.Collectors;
32 import java.util.stream.Stream;
34 import javax.imageio.ImageIO;
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;
80 import com.google.gson.Gson;
81 import com.google.gson.GsonBuilder;
82 import com.google.gson.JsonArray;
83 import com.google.gson.JsonObject;
86 * The {@link MiIoVacuumHandler} is responsible for handling commands, which are
87 * sent to one of the channels.
89 * @author Marcel Verpaalen - Initial contribution
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;
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()));
104 private ExpiringCache<String> status;
105 private ExpiringCache<String> consumables;
106 private ExpiringCache<String> dnd;
107 private ExpiringCache<String> history;
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();
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, () -> {
125 int ret = sendCommand(MiIoCommand.GET_STATUS);
129 } catch (Exception e) {
130 logger.debug("Error during status refresh: {}", e.getMessage(), e);
134 consumables = new ExpiringCache<>(CACHE_EXPIRY, () -> {
136 int ret = sendCommand(MiIoCommand.CONSUMABLES_GET);
140 } catch (Exception e) {
141 logger.debug("Error during consumables refresh: {}", e.getMessage(), e);
145 dnd = new ExpiringCache<>(CACHE_EXPIRY, () -> {
147 int ret = sendCommand(MiIoCommand.DND_GET);
151 } catch (Exception e) {
152 logger.debug("Error during dnd refresh: {}", e.getMessage(), e);
156 history = new ExpiringCache<>(CACHE_EXPIRY, () -> {
158 int ret = sendCommand(MiIoCommand.CLEAN_SUMMARY_GET);
162 } catch (Exception e) {
163 logger.debug("Error during cleaning data refresh: {}", e.getMessage(), e);
167 map = new ExpiringCache<String>(CACHE_EXPIRY, () -> {
169 int ret = sendCommand(MiIoCommand.GET_MAP);
173 } catch (Exception e) {
174 logger.debug("Error during dnd refresh: {}", e.getMessage(), e);
181 public void handleCommand(ChannelUID channelUID, Command command) {
182 if (getConnection() == null) {
183 logger.debug("Vacuum {} not online. Command {} ignored", getThing().getUID(), command.toString());
186 if (command == RefreshType.REFRESH) {
187 logger.debug("Refreshing {}", channelUID);
190 if (channelUID.getId().equals(CHANNEL_VACUUM_MAP)) {
191 sendCommand(MiIoCommand.GET_MAP);
195 if (handleCommandsChannels(channelUID, command)) {
199 if (channelUID.getId().equals(CHANNEL_VACUUM)) {
200 if (command instanceof OnOffType) {
201 if (command.equals(OnOffType.ON)) {
202 sendCommand(MiIoCommand.START_VACUUM);
206 sendCommand(MiIoCommand.STOP_VACUUM);
207 miIoScheduler.schedule(() -> {
208 sendCommand(MiIoCommand.CHARGE);
210 }, 2000, TimeUnit.MILLISECONDS);
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);
227 }, 2000, TimeUnit.MILLISECONDS);
230 logger.info("Command {} not recognised", command.toString());
235 if (channelUID.getId().equals(CHANNEL_FAN_POWER)) {
236 sendCommand(MiIoCommand.SET_MODE, "[" + command.toString() + "]");
240 if (channelUID.getId().equals(RobotCababilities.WATERBOX_MODE.getChannel())) {
241 sendCommand(MiIoCommand.SET_WATERBOX_MODE, "[" + command.toString() + "]");
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("-"));
252 if (channelUID.getId().equals(CHANNEL_FAN_CONTROL)) {
253 if (Integer.valueOf(command.toString()) > 0) {
254 sendCommand(MiIoCommand.SET_MODE, "[" + command.toString() + "]");
259 if (channelUID.getId().equals(CHANNEL_CONSUMABLE_RESET)) {
260 sendCommand(MiIoCommand.CONSUMABLES_RESET, "[" + command.toString() + "]");
261 updateState(CHANNEL_CONSUMABLE_RESET, new StringType("none"));
265 private void forceStatusUpdate() {
266 status.invalidateValue();
267 miIoScheduler.schedule(() -> {
269 }, 3000, TimeUnit.MILLISECONDS);
272 private void safeUpdateState(String channelID, @Nullable Integer state) {
274 updateState(channelID, new DecimalType(state));
276 logger.debug("Channel {} not update. value not available.", channelID);
280 private boolean updateVacuumStatus(JsonObject statusData) {
281 StatusDTO statusInfo = GSON.fromJson(statusData, StatusDTO.class);
282 if (statusInfo == null) {
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));
290 if (statusInfo.getCleanTime() != null) {
291 updateState(CHANNEL_CLEAN_TIME,
292 new QuantityType<>(TimeUnit.SECONDS.toMinutes(statusInfo.getCleanTime()), Units.MINUTE));
294 safeUpdateState(CHANNEL_DND_ENABLED, statusInfo.getDndEnabled());
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());
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()));
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()));
314 State vacuum = OnOffType.OFF;
322 vacuum = OnOffType.ON;
337 vacuum = OnOffType.ON;
343 if ("undef".equals(control)) {
344 updateState(CHANNEL_CONTROL, UnDefType.UNDEF);
346 updateState(CHANNEL_CONTROL, new StringType(control));
348 updateState(CHANNEL_VACUUM, vacuum);
350 if (deviceCapabilities.containsKey(RobotCababilities.WATERBOX_MODE)) {
351 safeUpdateState(RobotCababilities.WATERBOX_MODE.getChannel(), statusInfo.getWaterBoxMode());
353 if (deviceCapabilities.containsKey(RobotCababilities.WATERBOX_STATUS)) {
354 safeUpdateState(RobotCababilities.WATERBOX_STATUS.getChannel(), statusInfo.getWaterBoxStatus());
356 if (deviceCapabilities.containsKey(RobotCababilities.WATERBOX_CARRIAGE)) {
357 safeUpdateState(RobotCababilities.WATERBOX_CARRIAGE.getChannel(), statusInfo.getWaterBoxCarriageStatus());
359 if (deviceCapabilities.containsKey(RobotCababilities.LOCKSTATUS)) {
360 safeUpdateState(RobotCababilities.LOCKSTATUS.getChannel(), statusInfo.getLockStatus());
362 if (deviceCapabilities.containsKey(RobotCababilities.MOP_FORBIDDEN)) {
363 safeUpdateState(RobotCababilities.MOP_FORBIDDEN.getChannel(), statusInfo.getMopForbiddenEnable());
365 if (deviceCapabilities.containsKey(RobotCababilities.LOCATING)) {
366 safeUpdateState(RobotCababilities.LOCATING.getChannel(), statusInfo.getIsLocating());
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)));
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())));
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 + "]");
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));
428 if (historyData.has("clean_area")) {
429 updateState(CHANNEL_HISTORY_TOTALAREA,
430 new QuantityType<>(historyData.get("clean_area").getAsDouble() / 1000000D, SIUnits.SQUARE_METRE));
432 if (historyData.has("clean_count")) {
433 updateState(CHANNEL_HISTORY_COUNT, new DecimalType(historyData.get("clean_count").getAsLong()));
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 + "]");
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()));
474 protected boolean skipUpdate() {
475 if (!hasConnection()) {
476 logger.debug("Skipping periodic update for '{}'. No Connection", getThing().getUID().toString());
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());
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());
495 protected synchronized void updateData() {
496 if (!hasConnection() || skipUpdate()) {
499 logger.debug("Periodic update for '{}' ({})", getThing().getUID().toString(), getThing().getThingTypeUID());
505 consumables.getValue();
506 if (lastMap.isEmpty() || stateId != 8) {
507 if (isLinked(mapChannelUid)) {
511 for (RobotCababilities cmd : FEATURES_CHANNELS) {
512 if (isLinked(cmd.getChannel())) {
513 sendCommand(cmd.getCommand());
516 } catch (Exception e) {
517 logger.debug("Error while updating '{}': '{}", getThing().getUID().toString(), e.getLocalizedMessage());
522 public void 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("-"));
531 protected boolean initializeData() {
532 updateState(CHANNEL_CONSUMABLE_RESET, new StringType("none"));
533 return super.initializeData();
537 public void onMessageReceived(MiIoSendCommand response) {
538 super.onMessageReceived(response);
539 if (response.isError()) {
542 switch (response.getCommand()) {
544 if (response.getResult().isJsonArray()) {
545 JsonObject statusResponse = response.getResult().getAsJsonArray().get(0).getAsJsonObject();
546 if (!hasChannelStructure) {
547 setCapabilities(statusResponse);
548 createCapabilityChannels();
550 updateVacuumStatus(statusResponse);
553 case CONSUMABLES_GET:
554 if (response.getResult().isJsonArray()) {
555 updateConsumables(response.getResult().getAsJsonArray().get(0).getAsJsonObject());
559 if (response.getResult().isJsonArray()) {
560 updateDnD(response.getResult().getAsJsonArray().get(0).getAsJsonObject());
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());
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());
575 logger.debug("Could not extract cleaning history record from: {}", response);
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)));
588 case GET_SEGMENT_STATUS:
590 updateNumericChannel(response);
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()));
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;
617 if (capabilityChannel != null) {
618 if (response.getResult().isJsonArray() && response.getResult().getAsJsonArray().get(0).isJsonPrimitive()) {
620 Integer stat = response.getResult().getAsJsonArray().get(0).getAsInt();
621 updateState(capabilityChannel.getChannel(), new DecimalType(stat));
623 } catch (ClassCastException | IllegalStateException e) {
624 logger.debug("Could not update numeric channel {} with '{}': {}", capabilityChannel.getChannel(),
625 response.getResult(), e.getMessage());
628 logger.debug("Could not update numeric channel {} with '{}': Not in expected format",
629 capabilityChannel.getChannel(), response.getResult());
631 updateState(capabilityChannel.getChannel(), UnDefType.UNDEF);
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);
644 private void createCapabilityChannels() {
645 ThingBuilder thingBuilder = editThing();
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);
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);
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());
678 updateThing(thingBuilder.build());
680 hasChannelStructure = true;
683 private State getMap(String map) {
684 final MiIoBindingConfiguration configuration = this.configuration;
685 if (configuration != null && cloudConnector.isConnected()) {
687 final @Nullable RawType mapDl = cloudConnector.getMap(map, configuration.cloudServer);
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);
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");
704 logger.debug("Mapdata empty removing image");
705 return UnDefType.UNDEF;
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());
714 logger.debug("Not connected to Xiaomi cloud. Cannot retreive new map: {}", map);
716 return UnDefType.UNDEF;