2 * Copyright (c) 2010-2024 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.ecovacs.internal.api.impl;
15 import java.security.KeyStore;
16 import java.util.List;
17 import java.util.Optional;
18 import java.util.concurrent.ExecutionException;
19 import java.util.concurrent.ScheduledExecutorService;
20 import java.util.function.Consumer;
21 import java.util.stream.Collectors;
22 import java.util.stream.Stream;
24 import javax.net.ssl.ManagerFactoryParameters;
25 import javax.net.ssl.TrustManager;
26 import javax.net.ssl.TrustManagerFactory;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.ecovacs.internal.api.EcovacsApiConfiguration;
31 import org.openhab.binding.ecovacs.internal.api.EcovacsApiException;
32 import org.openhab.binding.ecovacs.internal.api.EcovacsDevice;
33 import org.openhab.binding.ecovacs.internal.api.commands.GetCleanLogsCommand;
34 import org.openhab.binding.ecovacs.internal.api.commands.GetFirmwareVersionCommand;
35 import org.openhab.binding.ecovacs.internal.api.commands.IotDeviceCommand;
36 import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.Device;
37 import org.openhab.binding.ecovacs.internal.api.impl.dto.response.portal.PortalLoginResponse;
38 import org.openhab.binding.ecovacs.internal.api.model.CleanLogRecord;
39 import org.openhab.binding.ecovacs.internal.api.model.DeviceCapability;
40 import org.openhab.binding.ecovacs.internal.api.util.DataParsingException;
41 import org.openhab.core.io.net.http.TrustAllTrustManager;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
45 import com.google.gson.Gson;
46 import com.hivemq.client.mqtt.MqttClient;
47 import com.hivemq.client.mqtt.MqttClientSslConfig;
48 import com.hivemq.client.mqtt.lifecycle.MqttClientDisconnectedListener;
49 import com.hivemq.client.mqtt.lifecycle.MqttDisconnectSource;
50 import com.hivemq.client.mqtt.mqtt3.Mqtt3AsyncClient;
51 import com.hivemq.client.mqtt.mqtt3.exceptions.Mqtt3ConnAckException;
52 import com.hivemq.client.mqtt.mqtt3.exceptions.Mqtt3DisconnectException;
53 import com.hivemq.client.mqtt.mqtt3.message.auth.Mqtt3SimpleAuth;
54 import com.hivemq.client.mqtt.mqtt3.message.connect.connack.Mqtt3ConnAckReturnCode;
55 import com.hivemq.client.mqtt.mqtt3.message.publish.Mqtt3Publish;
57 import io.netty.handler.ssl.util.SimpleTrustManagerFactory;
60 * @author Danny Baumann - Initial contribution
63 public class EcovacsIotMqDevice implements EcovacsDevice {
64 private final Logger logger = LoggerFactory.getLogger(EcovacsIotMqDevice.class);
66 private final Device device;
67 private final DeviceDescription desc;
68 private final EcovacsApiImpl api;
69 private final Gson gson;
70 private @Nullable Mqtt3AsyncClient mqttClient;
72 EcovacsIotMqDevice(Device device, DeviceDescription desc, EcovacsApiImpl api, Gson gson)
73 throws EcovacsApiException {
81 public String getSerialNumber() {
82 return device.getName();
86 public String getModelName() {
87 return desc.modelName;
91 public boolean hasCapability(DeviceCapability cap) {
92 return desc.capabilities.contains(cap);
96 public <T> T sendCommand(IotDeviceCommand<T> command) throws EcovacsApiException, InterruptedException {
97 return api.sendIotCommand(device, desc, command);
101 public List<CleanLogRecord> getCleanLogs() throws EcovacsApiException, InterruptedException {
102 Stream<CleanLogRecord> logEntries;
103 if (desc.protoVersion == ProtocolVersion.XML) {
104 logEntries = sendCommand(new GetCleanLogsCommand()).stream();
106 logEntries = api.fetchCleanLogs(device).stream().map(record -> new CleanLogRecord(record.timestamp,
107 record.duration, record.area, Optional.ofNullable(record.imageUrl), record.type));
109 return logEntries.sorted((lhs, rhs) -> rhs.timestamp.compareTo(lhs.timestamp)).collect(Collectors.toList());
113 public void connect(final EventListener listener, ScheduledExecutorService scheduler)
114 throws EcovacsApiException, InterruptedException {
115 EcovacsApiConfiguration config = api.getConfig();
116 PortalLoginResponse loginData = api.getLoginData();
117 if (loginData == null) {
118 throw new EcovacsApiException("Can not connect when not logged in");
121 // XML message handler does not receive firmware version information with events, so fetch in advance
122 if (desc.protoVersion == ProtocolVersion.XML) {
123 listener.onFirmwareVersionChanged(this, sendCommand(new GetFirmwareVersionCommand()));
126 String userName = String.format("%s@%s", loginData.getUserId(), config.getRealm().split("\\.")[0]);
127 String host = String.format("mq-%s.%s", config.getContinent(), config.getRealm());
129 Mqtt3SimpleAuth auth = Mqtt3SimpleAuth.builder().username(userName).password(loginData.getToken().getBytes())
132 MqttClientSslConfig sslConfig = MqttClientSslConfig.builder().trustManagerFactory(createTrustManagerFactory())
135 final MqttClientDisconnectedListener disconnectListener = ctx -> {
136 boolean expectedShutdown = ctx.getSource() == MqttDisconnectSource.USER
137 && ctx.getCause() instanceof Mqtt3DisconnectException;
138 // As the client already was disconnected, there's no need to do it again in disconnect() later
139 this.mqttClient = null;
140 if (!expectedShutdown) {
141 logger.debug("{}: MQTT disconnected (source {}): {}", getSerialNumber(), ctx.getSource(),
142 ctx.getCause().getMessage());
143 listener.onEventStreamFailure(EcovacsIotMqDevice.this, ctx.getCause());
147 final Mqtt3AsyncClient client = MqttClient.builder().useMqttVersion3()
148 .identifier(userName + "/" + loginData.getResource()).simpleAuth(auth).serverHost(host).serverPort(8883)
149 .sslConfig(sslConfig).addDisconnectedListener(disconnectListener).buildAsync();
152 this.mqttClient = client;
153 client.connect().get();
155 final ReportParser parser = desc.protoVersion == ProtocolVersion.XML
156 ? new XmlReportParser(this, listener, gson, logger)
157 : new JsonReportParser(this, listener, desc.protoVersion, gson, logger);
158 final Consumer<@Nullable Mqtt3Publish> eventCallback = publish -> {
159 if (publish == null) {
162 String receivedTopic = publish.getTopic().toString();
163 String payload = new String(publish.getPayloadAsBytes());
165 String eventName = receivedTopic.split("/")[2].toLowerCase();
166 logger.trace("{}: Got MQTT message on topic {}: {}", getSerialNumber(), receivedTopic, payload);
167 parser.handleMessage(eventName, payload);
168 } catch (DataParsingException e) {
169 listener.onEventStreamFailure(this, e);
173 String topic = String.format("iot/atr/+/%s/%s/%s/+", device.getDid(), device.getDeviceClass(),
174 device.getResource());
176 client.subscribeWith().topicFilter(topic).callback(eventCallback).send().get();
177 logger.debug("Established MQTT connection to device {}", getSerialNumber());
178 } catch (ExecutionException e) {
179 Throwable cause = e.getCause();
180 boolean isAuthFailure = cause instanceof Mqtt3ConnAckException connAckException
181 && connAckException.getMqttMessage().getReturnCode() == Mqtt3ConnAckReturnCode.NOT_AUTHORIZED;
182 throw new EcovacsApiException(e, isAuthFailure);
187 public void disconnect(ScheduledExecutorService scheduler) {
188 Mqtt3AsyncClient client = this.mqttClient;
189 if (client != null) {
192 this.mqttClient = null;
195 private TrustManagerFactory createTrustManagerFactory() {
196 return new SimpleTrustManagerFactory() {
198 protected void engineInit(@Nullable KeyStore keyStore) throws Exception {
202 protected void engineInit(@Nullable ManagerFactoryParameters managerFactoryParameters) throws Exception {
206 protected TrustManager[] engineGetTrustManagers() {
207 return new TrustManager[] { TrustAllTrustManager.getInstance() };