]> git.basschouten.com Git - openhab-addons.git/blob
2b9d564ca0d3552a44156deb16b98dbe6cc683bb
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.saicismart.internal;
14
15 import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.API_ENDPOINT_V11;
16
17 import java.io.IOException;
18 import java.net.URI;
19 import java.net.URISyntaxException;
20 import java.nio.charset.StandardCharsets;
21 import java.security.MessageDigest;
22 import java.security.NoSuchAlgorithmException;
23 import java.util.Collection;
24 import java.util.Collections;
25 import java.util.Objects;
26 import java.util.Optional;
27 import java.util.concurrent.ExecutionException;
28 import java.util.concurrent.Future;
29 import java.util.concurrent.TimeUnit;
30 import java.util.concurrent.TimeoutException;
31 import java.util.stream.Collectors;
32 import java.util.stream.Stream;
33
34 import javax.xml.bind.DatatypeConverter;
35
36 import org.bn.coders.IASN1PreparedElement;
37 import org.eclipse.jdt.annotation.NonNullByDefault;
38 import org.eclipse.jdt.annotation.Nullable;
39 import org.eclipse.jetty.client.HttpClient;
40 import org.eclipse.jetty.client.util.StringContentProvider;
41 import org.openhab.core.thing.Bridge;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.Thing;
44 import org.openhab.core.thing.ThingStatus;
45 import org.openhab.core.thing.ThingStatusDetail;
46 import org.openhab.core.thing.binding.BaseBridgeHandler;
47 import org.openhab.core.thing.binding.ThingHandlerService;
48 import org.openhab.core.types.Command;
49 import org.openhab.core.util.StringUtils;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
52
53 import com.google.gson.GsonBuilder;
54
55 import net.heberling.ismart.asn1.v1_1.Message;
56 import net.heberling.ismart.asn1.v1_1.MessageCoder;
57 import net.heberling.ismart.asn1.v1_1.entity.AlarmSwitch;
58 import net.heberling.ismart.asn1.v1_1.entity.AlarmSwitchReq;
59 import net.heberling.ismart.asn1.v1_1.entity.MP_AlarmSettingType;
60 import net.heberling.ismart.asn1.v1_1.entity.MP_UserLoggingInReq;
61 import net.heberling.ismart.asn1.v1_1.entity.MP_UserLoggingInResp;
62 import net.heberling.ismart.asn1.v1_1.entity.MessageListReq;
63 import net.heberling.ismart.asn1.v1_1.entity.MessageListResp;
64 import net.heberling.ismart.asn1.v1_1.entity.StartEndNumber;
65 import net.heberling.ismart.asn1.v1_1.entity.VinInfo;
66
67 /**
68  * The {@link SAICiSMARTBridgeHandler} is responsible for handling commands, which are
69  * sent to one of the channels.
70  *
71  * @author Markus Heberling - Initial contribution
72  */
73 @NonNullByDefault
74 public class SAICiSMARTBridgeHandler extends BaseBridgeHandler {
75
76     private final Logger logger = LoggerFactory.getLogger(SAICiSMARTBridgeHandler.class);
77
78     private @Nullable SAICiSMARTBridgeConfiguration config;
79
80     private @Nullable String uid;
81
82     private @Nullable String token;
83
84     private @Nullable Collection<VinInfo> vinList;
85     private HttpClient httpClient;
86     private @Nullable Future<?> pollingJob;
87
88     public SAICiSMARTBridgeHandler(Bridge bridge, HttpClient httpClient) {
89         super(bridge);
90         this.httpClient = httpClient;
91     }
92
93     @Override
94     public void handleCommand(ChannelUID channelUID, Command command) {
95         // no commands available
96     }
97
98     @Override
99     public void initialize() {
100         config = getConfigAs(SAICiSMARTBridgeConfiguration.class);
101         updateStatus(ThingStatus.UNKNOWN);
102
103         // Validate configuration
104         if (this.config.username.isBlank()) {
105             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
106                     "@text/thing-type.config.saicismart.bridge.username.required");
107             return;
108         }
109         if (this.config.password.isBlank()) {
110             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
111                     "@text/thing-type.config.saicismart.bridge.password.required");
112             return;
113         }
114         if (this.config.username.length() > 50) {
115             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
116                     "@text/thing-type.config.saicismart.bridge.username.toolong");
117             return;
118         }
119         pollingJob = scheduler.scheduleWithFixedDelay(this::updateStatus, 1,
120                 SAICiSMARTBindingConstants.REFRESH_INTERVAL, TimeUnit.SECONDS);
121     }
122
123     private void updateStatus() {
124         if (uid == null || token == null) {
125             login();
126         } else {
127             registerForMessages();
128         }
129     }
130
131     private void login() {
132         MessageCoder<MP_UserLoggingInReq> mpUserLoggingInRequestMessageCoder = new MessageCoder<>(
133                 MP_UserLoggingInReq.class);
134
135         MP_UserLoggingInReq mpUserLoggingInReq = new MP_UserLoggingInReq();
136         mpUserLoggingInReq.setPassword(config.password);
137         Message<MP_UserLoggingInReq> loginRequestMessage = mpUserLoggingInRequestMessageCoder.initializeMessage(
138                 StringUtils.padLeft("#" + config.username, 50, "0"), null, null, "501", 513, 1, mpUserLoggingInReq);
139
140         String loginRequest = mpUserLoggingInRequestMessageCoder.encodeRequest(loginRequestMessage);
141
142         try {
143             String loginResponse = sendRequest(loginRequest, API_ENDPOINT_V11);
144
145             Message<MP_UserLoggingInResp> loginResponseMessage = new MessageCoder<>(MP_UserLoggingInResp.class)
146                     .decodeResponse(loginResponse);
147
148             logger.trace("Got message: {}",
149                     new GsonBuilder().setPrettyPrinting().create().toJson(loginResponseMessage));
150
151             uid = loginResponseMessage.getBody().getUid();
152             token = loginResponseMessage.getApplicationData().getToken();
153             vinList = loginResponseMessage.getApplicationData().getVinList();
154
155             // register for all known alarm types (not all might be actually delivered)
156             for (MP_AlarmSettingType.EnumType type : MP_AlarmSettingType.EnumType.values()) {
157                 registerAlarmMessage(loginResponseMessage.getBody().getUid(),
158                         loginResponseMessage.getApplicationData().getToken(), type);
159             }
160
161             updateStatus(ThingStatus.ONLINE);
162         } catch (TimeoutException | URISyntaxException | ExecutionException | InterruptedException
163                 | NoSuchAlgorithmException | IOException e) {
164             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
165         }
166     }
167
168     private void registerForMessages() {
169         MessageCoder<MessageListReq> messageListReqMessageCoder = new MessageCoder<>(MessageListReq.class);
170         Message<MessageListReq> messageListRequestMessage = messageListReqMessageCoder.initializeMessage(uid, token,
171                 null, "531", 513, 1, new MessageListReq());
172
173         messageListRequestMessage.getHeader().setProtocolVersion(18);
174
175         // We currently assume that the newest message is the first.
176         messageListRequestMessage.getApplicationData().setStartEndNumber(new StartEndNumber());
177         messageListRequestMessage.getApplicationData().getStartEndNumber().setStartNumber(1L);
178         messageListRequestMessage.getApplicationData().getStartEndNumber().setEndNumber(5L);
179         messageListRequestMessage.getApplicationData().setMessageGroup("ALARM");
180
181         String messageListRequest = messageListReqMessageCoder.encodeRequest(messageListRequestMessage);
182
183         try {
184             String messageListResponse = sendRequest(messageListRequest, API_ENDPOINT_V11);
185
186             Message<MessageListResp> messageListResponseMessage = new MessageCoder<>(MessageListResp.class)
187                     .decodeResponse(messageListResponse);
188
189             logger.trace("Got message: {}",
190                     new GsonBuilder().setPrettyPrinting().create().toJson(messageListResponseMessage));
191
192             if (messageListResponseMessage.getApplicationData() != null
193                     && messageListResponseMessage.getApplicationData().getMessages() != null) {
194                 for (net.heberling.ismart.asn1.v1_1.entity.Message message : messageListResponseMessage
195                         .getApplicationData().getMessages()) {
196                     if (message.isVinPresent()) {
197                         String vin = message.getVin();
198                         getThing().getThings().stream().filter(t -> t.getUID().getId().equals(vin))
199                                 .map(Thing::getHandler).filter(Objects::nonNull)
200                                 .filter(SAICiSMARTHandler.class::isInstance).map(SAICiSMARTHandler.class::cast)
201                                 .forEach(t -> t.handleMessage(message));
202                     }
203                 }
204             }
205             updateStatus(ThingStatus.ONLINE);
206         } catch (TimeoutException | URISyntaxException | ExecutionException | InterruptedException e) {
207             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
208         }
209     }
210
211     private void registerAlarmMessage(String uid, String token, MP_AlarmSettingType.EnumType type)
212             throws NoSuchAlgorithmException, IOException, URISyntaxException, ExecutionException, InterruptedException,
213             TimeoutException {
214         MessageCoder<AlarmSwitchReq> alarmSwitchReqMessageCoder = new MessageCoder<>(AlarmSwitchReq.class);
215
216         AlarmSwitchReq alarmSwitchReq = new AlarmSwitchReq();
217         alarmSwitchReq
218                 .setAlarmSwitchList(Stream.of(type).map(v -> createAlarmSwitch(v, true)).collect(Collectors.toList()));
219         alarmSwitchReq.setPin(hashMD5("123456"));
220
221         Message<AlarmSwitchReq> alarmSwitchMessage = alarmSwitchReqMessageCoder.initializeMessage(uid, token, null,
222                 "521", 513, 1, alarmSwitchReq);
223         String alarmSwitchRequest = alarmSwitchReqMessageCoder.encodeRequest(alarmSwitchMessage);
224         String alarmSwitchResponse = sendRequest(alarmSwitchRequest, API_ENDPOINT_V11);
225         final MessageCoder<IASN1PreparedElement> alarmSwitchResMessageCoder = new MessageCoder<>(
226                 IASN1PreparedElement.class);
227         Message<IASN1PreparedElement> alarmSwitchResponseMessage = alarmSwitchResMessageCoder
228                 .decodeResponse(alarmSwitchResponse);
229
230         logger.trace("Got message: {}",
231                 new GsonBuilder().setPrettyPrinting().create().toJson(alarmSwitchResponseMessage));
232
233         if (alarmSwitchResponseMessage.getBody().getErrorMessage() != null) {
234             logger.debug("Could not register for {} messages: {}", type,
235                     new String(alarmSwitchResponseMessage.getBody().getErrorMessage(), StandardCharsets.UTF_8));
236         } else {
237             logger.debug("Registered for {} messages", type);
238         }
239     }
240
241     private static AlarmSwitch createAlarmSwitch(MP_AlarmSettingType.EnumType type, boolean enabled) {
242         AlarmSwitch alarmSwitch = new AlarmSwitch();
243         MP_AlarmSettingType alarmSettingType = new MP_AlarmSettingType();
244         alarmSettingType.setValue(type);
245         alarmSettingType.setIntegerForm(type.ordinal());
246         alarmSwitch.setAlarmSettingType(alarmSettingType);
247         alarmSwitch.setAlarmSwitch(enabled);
248         alarmSwitch.setFunctionSwitch(enabled);
249         return alarmSwitch;
250     }
251
252     public String hashMD5(String password) throws NoSuchAlgorithmException {
253         MessageDigest md = MessageDigest.getInstance("MD5");
254         md.update(password.getBytes());
255         byte[] digest = md.digest();
256         return DatatypeConverter.printHexBinary(digest).toUpperCase();
257     }
258
259     @Override
260     public Collection<Class<? extends ThingHandlerService>> getServices() {
261         return Collections.singleton(VehicleDiscovery.class);
262     }
263
264     @Nullable
265     public String getUid() {
266         return uid;
267     }
268
269     @Nullable
270     public String getToken() {
271         return token;
272     }
273
274     public Collection<VinInfo> getVinList() {
275         return Optional.ofNullable(vinList).orElse(Collections.emptyList());
276     }
277
278     public String sendRequest(String request, String endpoint)
279             throws URISyntaxException, ExecutionException, InterruptedException, TimeoutException {
280         return httpClient.POST(new URI(endpoint)).content(new StringContentProvider(request), "text/html").send()
281                 .getContentAsString();
282     }
283
284     public void relogin() {
285         uid = null;
286         token = null;
287     }
288
289     @Override
290     public void dispose() {
291         Future<?> job = pollingJob;
292         if (job != null) {
293             job.cancel(true);
294             pollingJob = null;
295         }
296     }
297 }