]> git.basschouten.com Git - openhab-addons.git/blob
e9e9a5b1df3509412715b6917500dd1cfaf647bd
[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.ecovacs.internal.api.impl;
14
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;
23
24 import javax.net.ssl.ManagerFactoryParameters;
25 import javax.net.ssl.TrustManager;
26 import javax.net.ssl.TrustManagerFactory;
27
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;
44
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;
56
57 import io.netty.handler.ssl.util.SimpleTrustManagerFactory;
58
59 /**
60  * @author Danny Baumann - Initial contribution
61  */
62 @NonNullByDefault
63 public class EcovacsIotMqDevice implements EcovacsDevice {
64     private final Logger logger = LoggerFactory.getLogger(EcovacsIotMqDevice.class);
65
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;
71
72     EcovacsIotMqDevice(Device device, DeviceDescription desc, EcovacsApiImpl api, Gson gson)
73             throws EcovacsApiException {
74         this.device = device;
75         this.desc = desc;
76         this.api = api;
77         this.gson = gson;
78     }
79
80     @Override
81     public String getSerialNumber() {
82         return device.getName();
83     }
84
85     @Override
86     public String getModelName() {
87         return desc.modelName;
88     }
89
90     @Override
91     public boolean hasCapability(DeviceCapability cap) {
92         return desc.capabilities.contains(cap);
93     }
94
95     @Override
96     public <T> T sendCommand(IotDeviceCommand<T> command) throws EcovacsApiException, InterruptedException {
97         return api.sendIotCommand(device, desc, command);
98     }
99
100     @Override
101     public List<CleanLogRecord> getCleanLogs() throws EcovacsApiException, InterruptedException {
102         Stream<CleanLogRecord> logEntries;
103         if (desc.protoVersion == ProtocolVersion.XML) {
104             logEntries = sendCommand(new GetCleanLogsCommand()).stream();
105         } else {
106             logEntries = api.fetchCleanLogs(device).stream().map(record -> new CleanLogRecord(record.timestamp,
107                     record.duration, record.area, Optional.ofNullable(record.imageUrl), record.type));
108         }
109         return logEntries.sorted((lhs, rhs) -> rhs.timestamp.compareTo(lhs.timestamp)).collect(Collectors.toList());
110     }
111
112     @Override
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");
119         }
120
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()));
124         }
125
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());
128
129         Mqtt3SimpleAuth auth = Mqtt3SimpleAuth.builder().username(userName).password(loginData.getToken().getBytes())
130                 .build();
131
132         MqttClientSslConfig sslConfig = MqttClientSslConfig.builder().trustManagerFactory(createTrustManagerFactory())
133                 .build();
134
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());
144             }
145         };
146
147         final Mqtt3AsyncClient client = MqttClient.builder().useMqttVersion3()
148                 .identifier(userName + "/" + loginData.getResource()).simpleAuth(auth).serverHost(host).serverPort(8883)
149                 .sslConfig(sslConfig).addDisconnectedListener(disconnectListener).buildAsync();
150
151         try {
152             this.mqttClient = client;
153             client.connect().get();
154
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) {
160                     return;
161                 }
162                 String receivedTopic = publish.getTopic().toString();
163                 String payload = new String(publish.getPayloadAsBytes());
164                 try {
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);
170                 }
171             };
172
173             String topic = String.format("iot/atr/+/%s/%s/%s/+", device.getDid(), device.getDeviceClass(),
174                     device.getResource());
175
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);
183         }
184     }
185
186     @Override
187     public void disconnect(ScheduledExecutorService scheduler) {
188         Mqtt3AsyncClient client = this.mqttClient;
189         if (client != null) {
190             client.disconnect();
191         }
192         this.mqttClient = null;
193     }
194
195     private TrustManagerFactory createTrustManagerFactory() {
196         return new SimpleTrustManagerFactory() {
197             @Override
198             protected void engineInit(@Nullable KeyStore keyStore) throws Exception {
199             }
200
201             @Override
202             protected void engineInit(@Nullable ManagerFactoryParameters managerFactoryParameters) throws Exception {
203             }
204
205             @Override
206             protected TrustManager[] engineGetTrustManagers() {
207                 return new TrustManager[] { TrustAllTrustManager.getInstance() };
208             }
209         };
210     }
211 }