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.saicismart.internal;
15 import static org.openhab.binding.saicismart.internal.SAICiSMARTBindingConstants.API_ENDPOINT_V11;
17 import java.io.IOException;
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;
34 import javax.xml.bind.DatatypeConverter;
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;
53 import com.google.gson.GsonBuilder;
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;
68 * The {@link SAICiSMARTBridgeHandler} is responsible for handling commands, which are
69 * sent to one of the channels.
71 * @author Markus Heberling - Initial contribution
74 public class SAICiSMARTBridgeHandler extends BaseBridgeHandler {
76 private final Logger logger = LoggerFactory.getLogger(SAICiSMARTBridgeHandler.class);
78 private @Nullable SAICiSMARTBridgeConfiguration config;
80 private @Nullable String uid;
82 private @Nullable String token;
84 private @Nullable Collection<VinInfo> vinList;
85 private HttpClient httpClient;
86 private @Nullable Future<?> pollingJob;
88 public SAICiSMARTBridgeHandler(Bridge bridge, HttpClient httpClient) {
90 this.httpClient = httpClient;
94 public void handleCommand(ChannelUID channelUID, Command command) {
95 // no commands available
99 public void initialize() {
100 config = getConfigAs(SAICiSMARTBridgeConfiguration.class);
101 updateStatus(ThingStatus.UNKNOWN);
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");
109 if (this.config.password.isBlank()) {
110 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
111 "@text/thing-type.config.saicismart.bridge.password.required");
114 if (this.config.username.length() > 50) {
115 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
116 "@text/thing-type.config.saicismart.bridge.username.toolong");
119 pollingJob = scheduler.scheduleWithFixedDelay(this::updateStatus, 1,
120 SAICiSMARTBindingConstants.REFRESH_INTERVAL, TimeUnit.SECONDS);
123 private void updateStatus() {
124 if (uid == null || token == null) {
127 registerForMessages();
131 private void login() {
132 MessageCoder<MP_UserLoggingInReq> mpUserLoggingInRequestMessageCoder = new MessageCoder<>(
133 MP_UserLoggingInReq.class);
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);
140 String loginRequest = mpUserLoggingInRequestMessageCoder.encodeRequest(loginRequestMessage);
143 String loginResponse = sendRequest(loginRequest, API_ENDPOINT_V11);
145 Message<MP_UserLoggingInResp> loginResponseMessage = new MessageCoder<>(MP_UserLoggingInResp.class)
146 .decodeResponse(loginResponse);
148 logger.trace("Got message: {}",
149 new GsonBuilder().setPrettyPrinting().create().toJson(loginResponseMessage));
151 uid = loginResponseMessage.getBody().getUid();
152 token = loginResponseMessage.getApplicationData().getToken();
153 vinList = loginResponseMessage.getApplicationData().getVinList();
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);
161 updateStatus(ThingStatus.ONLINE);
162 } catch (TimeoutException | URISyntaxException | ExecutionException | InterruptedException
163 | NoSuchAlgorithmException | IOException e) {
164 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
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());
173 messageListRequestMessage.getHeader().setProtocolVersion(18);
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");
181 String messageListRequest = messageListReqMessageCoder.encodeRequest(messageListRequestMessage);
184 String messageListResponse = sendRequest(messageListRequest, API_ENDPOINT_V11);
186 Message<MessageListResp> messageListResponseMessage = new MessageCoder<>(MessageListResp.class)
187 .decodeResponse(messageListResponse);
189 logger.trace("Got message: {}",
190 new GsonBuilder().setPrettyPrinting().create().toJson(messageListResponseMessage));
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));
205 updateStatus(ThingStatus.ONLINE);
206 } catch (TimeoutException | URISyntaxException | ExecutionException | InterruptedException e) {
207 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
211 private void registerAlarmMessage(String uid, String token, MP_AlarmSettingType.EnumType type)
212 throws NoSuchAlgorithmException, IOException, URISyntaxException, ExecutionException, InterruptedException,
214 MessageCoder<AlarmSwitchReq> alarmSwitchReqMessageCoder = new MessageCoder<>(AlarmSwitchReq.class);
216 AlarmSwitchReq alarmSwitchReq = new AlarmSwitchReq();
218 .setAlarmSwitchList(Stream.of(type).map(v -> createAlarmSwitch(v, true)).collect(Collectors.toList()));
219 alarmSwitchReq.setPin(hashMD5("123456"));
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);
230 logger.trace("Got message: {}",
231 new GsonBuilder().setPrettyPrinting().create().toJson(alarmSwitchResponseMessage));
233 if (alarmSwitchResponseMessage.getBody().getErrorMessage() != null) {
234 logger.debug("Could not register for {} messages: {}", type,
235 new String(alarmSwitchResponseMessage.getBody().getErrorMessage(), StandardCharsets.UTF_8));
237 logger.debug("Registered for {} messages", type);
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);
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();
260 public Collection<Class<? extends ThingHandlerService>> getServices() {
261 return Collections.singleton(VehicleDiscovery.class);
265 public String getUid() {
270 public String getToken() {
274 public Collection<VinInfo> getVinList() {
275 return Optional.ofNullable(vinList).orElse(Collections.emptyList());
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();
284 public void relogin() {
290 public void dispose() {
291 Future<?> job = pollingJob;