diff --git a/pom.xml b/pom.xml
index 32be76c..d72ab9b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -27,6 +27,8 @@
saic-java-client
saic-java-api-gateway
saic-java-api-cli
+ saic-java-rest-api
+ saic-java-rest-client
saic-java-mqtt-gateway
diff --git a/saic-java-rest-api/pom.xml b/saic-java-rest-api/pom.xml
new file mode 100644
index 0000000..7c32ae3
--- /dev/null
+++ b/saic-java-rest-api/pom.xml
@@ -0,0 +1,15 @@
+
+
+ 4.0.0
+
+
+ io.github.saic-ismart-api
+ saic-ismart-api-parent
+ 0.0.0-SNAPSHOT
+
+
+ saic-java-rest-api
+ SAIC Java REST API
+ Implementation of the SAIC Rest API in Java
+
+
diff --git a/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/AESUtils.java b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/AESUtils.java
new file mode 100644
index 0000000..1c85574
--- /dev/null
+++ b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/AESUtils.java
@@ -0,0 +1,45 @@
+package net.heberling.ismart.java.rest;
+
+import java.nio.charset.StandardCharsets;
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+public class AESUtils {
+
+ public static final String AES = "AES";
+
+ public static String decrypt(String cipherText, String hexKey, String hexIV) {
+ if (StringUtils.isEmpty(cipherText)
+ || StringUtils.isEmpty(hexKey)
+ || StringUtils.isEmpty(hexIV)) {
+ return null;
+ }
+ try {
+ SecretKeySpec secretKeySpec = new SecretKeySpec(HexUtils.hexToBytes(hexKey), AES);
+ IvParameterSpec ivParameterSpec = new IvParameterSpec(HexUtils.hexToBytes(hexIV));
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(2, secretKeySpec, ivParameterSpec);
+ return new String(cipher.doFinal(HexUtils.hexToBytes(cipherText)), StandardCharsets.UTF_8);
+ } catch (Exception e2) {
+ throw new RuntimeException(e2);
+ }
+ }
+
+ public static String encrypt(String plainText, String hexKey, String hexIV) {
+ if (StringUtils.isEmpty(plainText)
+ || StringUtils.isEmpty(hexKey)
+ || StringUtils.isEmpty(hexIV)) {
+ return null;
+ }
+ try {
+ SecretKeySpec secretKeySpec = new SecretKeySpec(HexUtils.hexToBytes(hexKey), AES);
+ IvParameterSpec ivParameterSpec = new IvParameterSpec(HexUtils.hexToBytes(hexIV));
+ Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
+ cipher.init(1, secretKeySpec, ivParameterSpec);
+ return HexUtils.bytesToHex(cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)));
+ } catch (Exception e2) {
+ throw new RuntimeException(e2);
+ }
+ }
+}
diff --git a/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/APIConfig.java b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/APIConfig.java
new file mode 100644
index 0000000..40423d3
--- /dev/null
+++ b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/APIConfig.java
@@ -0,0 +1,8 @@
+package net.heberling.ismart.java.rest;
+
+public final class APIConfig {
+ public static final String CONTENT_ENCRYPTED = "1";
+ public static final String PARAM_AUTHENTICATION = "Basic c3dvcmQ6c3dvcmRfc2VjcmV0";
+ public static final String TENANT_ID = "459771";
+ public static final String USER_TYPE = "app";
+}
diff --git a/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/EncryptionUtils.java b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/EncryptionUtils.java
new file mode 100644
index 0000000..c98660e
--- /dev/null
+++ b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/EncryptionUtils.java
@@ -0,0 +1,105 @@
+package net.heberling.ismart.java.rest;
+
+public class EncryptionUtils {
+ private EncryptionUtils() {}
+
+ public static String calculateRequestVerification(
+ String resourcePath,
+ long sendDate,
+ String tenant,
+ String contentType,
+ String bodyEncrypted,
+ String token) {
+ String str9 = resourcePath + tenant + token + APIConfig.USER_TYPE;
+ String a2 = HashUtils.md5(str9);
+ String str10 = sendDate + APIConfig.CONTENT_ENCRYPTED + contentType;
+ String a3 = HashUtils.md5(a2 + str10);
+ String str11 =
+ resourcePath
+ + tenant
+ + token
+ + APIConfig.USER_TYPE
+ + sendDate
+ + APIConfig.CONTENT_ENCRYPTED
+ + contentType
+ + bodyEncrypted;
+ String a5 = HashUtils.md5(a3 + sendDate);
+ if (!StringUtils.isEmpty(a5) && !StringUtils.isEmpty(str11)) {
+ return MacUtils.hmacSha256(a5.getBytes(), str11);
+ }
+ return "";
+ }
+
+ public static String decryptResponse(String timeStamp, String contentType, String cipherText) {
+ String str4 = timeStamp + APIConfig.CONTENT_ENCRYPTED + contentType;
+ String a2 = StringUtils.isEmpty(str4) ? "" : HashUtils.md5(str4);
+ String hashedTimeStamp = HashUtils.md5(timeStamp);
+ if (!StringUtils.isEmpty(cipherText)) {
+ return AESUtils.decrypt(cipherText, a2, hashedTimeStamp);
+ }
+ return "";
+ }
+
+ public static String calculateResponseVerification(String str, String str2, String str3) {
+ String str4 = str + APIConfig.CONTENT_ENCRYPTED + str2;
+ String a2 = StringUtils.isEmpty(str4) ? "" : HashUtils.md5(str4);
+ String str5 = str + APIConfig.CONTENT_ENCRYPTED + str2 + str3;
+ String a4 = HashUtils.md5(a2 + str);
+ if (!StringUtils.isEmpty(a4) && !StringUtils.isEmpty(str5)) {
+
+ return MacUtils.hmacSha256(a4.getBytes(), str5);
+ }
+
+ return "";
+ }
+
+ public static final String BASE_URL_P = "https://gateway-mg-eu.soimt.com/api.app/v1/";
+
+ public static String encryptRequest(
+ String url, long time, String tenant, String token, String body, String contentType) {
+
+ String sendDate = String.valueOf(time);
+ // tenant
+ String replace = !StringUtils.isEmpty(url) ? url.replace(BASE_URL_P, "/") : "";
+ String encryptedBody = "";
+ if (!StringUtils.isEmpty(body)) {
+ String sb3 =
+ HashUtils.md5(replace + tenant + token + APIConfig.USER_TYPE)
+ + sendDate
+ + APIConfig.CONTENT_ENCRYPTED
+ + contentType;
+ String a2 = HashUtils.md5(sb3);
+ String a3 = HashUtils.md5(sendDate);
+ if (!StringUtils.isEmpty(body) && !StringUtils.isEmpty(a2) && !StringUtils.isEmpty(a3)) {
+ encryptedBody = AESUtils.encrypt(body, a2, a3);
+ }
+ }
+
+ if (encryptedBody == null) {
+ encryptedBody = "";
+ }
+ return encryptedBody;
+ }
+
+ public static String decryptRequest(
+ String url, long time, String tenant, String token, String body, String contentType) {
+
+ String timeStamp = String.valueOf(time);
+ String resourcePath = !StringUtils.isEmpty(url) ? url.replace(BASE_URL_P, "/") : "";
+ if (!StringUtils.isEmpty(body)) {
+ String sb3 =
+ HashUtils.md5(resourcePath + tenant + token + APIConfig.USER_TYPE)
+ + timeStamp
+ + APIConfig.CONTENT_ENCRYPTED
+ + contentType;
+ String a2 = HashUtils.md5(sb3);
+ String timeStampHash = HashUtils.md5(timeStamp);
+ if (!StringUtils.isEmpty(body)
+ && !StringUtils.isEmpty(a2)
+ && !StringUtils.isEmpty(timeStampHash)) {
+ return AESUtils.decrypt(body, a2, timeStampHash);
+ }
+ }
+ return null;
+ }
+}
diff --git a/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/HashUtils.java b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/HashUtils.java
new file mode 100644
index 0000000..1277281
--- /dev/null
+++ b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/HashUtils.java
@@ -0,0 +1,42 @@
+package net.heberling.ismart.java.rest;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+public class HashUtils {
+ public static final String MD5 = "MD5";
+ public static final String SHA_1 = "SHA1";
+ public static final String SHA_256 = "SHA-256";
+
+ public static String md5(String str) {
+ try {
+ MessageDigest messageDigest = MessageDigest.getInstance(MD5);
+ messageDigest.update(str.getBytes(StandardCharsets.UTF_8));
+ byte[] digest = messageDigest.digest();
+ return HexUtils.bytesToHex(digest);
+ } catch (NoSuchAlgorithmException e2) {
+ throw new RuntimeException(e2);
+ }
+ }
+
+ public static String sha1(String str) {
+ try {
+ MessageDigest messageDigest = MessageDigest.getInstance(SHA_1);
+ messageDigest.update(str.getBytes(StandardCharsets.UTF_8));
+ return HexUtils.bytesToHex(messageDigest.digest());
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public static String sha256(String str) {
+ try {
+ MessageDigest messageDigest = MessageDigest.getInstance(SHA_256);
+ messageDigest.update(str.getBytes(StandardCharsets.UTF_8));
+ return HexUtils.bytesToHex(messageDigest.digest());
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/HexUtils.java b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/HexUtils.java
new file mode 100644
index 0000000..e654c39
--- /dev/null
+++ b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/HexUtils.java
@@ -0,0 +1,32 @@
+package net.heberling.ismart.java.rest;
+
+public class HexUtils {
+
+ public static String bytesToHex(byte[] byteArray) {
+ StringBuilder hexStringBuffer = new StringBuilder();
+ for (int i = 0; i < byteArray.length; i++) {
+ hexStringBuffer.append(byteToHex(byteArray[i]));
+ }
+ return hexStringBuffer.toString();
+ }
+
+ public static String byteToHex(byte num) {
+ return String.format("%02x", num);
+ }
+
+ public static byte[] hexToBytes(String hexString) {
+ if (hexString.length() % 2 == 1) {
+ throw new IllegalArgumentException("Invalid hexadecimal String supplied.");
+ }
+
+ byte[] bytes = new byte[hexString.length() / 2];
+ for (int i = 0; i < hexString.length(); i += 2) {
+ bytes[i / 2] = hexToByte(hexString.substring(i, i + 2));
+ }
+ return bytes;
+ }
+
+ public static byte hexToByte(String str) {
+ return (byte) Integer.parseInt(str, 16);
+ }
+}
diff --git a/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/MacUtils.java b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/MacUtils.java
new file mode 100644
index 0000000..df7a5dd
--- /dev/null
+++ b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/MacUtils.java
@@ -0,0 +1,21 @@
+package net.heberling.ismart.java.rest;
+
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+
+public class MacUtils {
+ private static final String HMAC_SHA_256 = "HmacSHA256";
+
+ public static String hmacSha256(byte[] bArr, String str) {
+ try {
+ Mac mac = Mac.getInstance(HMAC_SHA_256);
+ mac.init(new SecretKeySpec(bArr, HMAC_SHA_256));
+ return HexUtils.bytesToHex(mac.doFinal(str.getBytes(StandardCharsets.UTF_8)));
+ } catch (NoSuchAlgorithmException | InvalidKeyException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/StringUtils.java b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/StringUtils.java
new file mode 100644
index 0000000..a4b1e9c
--- /dev/null
+++ b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/StringUtils.java
@@ -0,0 +1,7 @@
+package net.heberling.ismart.java.rest;
+
+public class StringUtils {
+ public static boolean isEmpty(String str2) {
+ return str2 == null || str2.isEmpty();
+ }
+}
diff --git a/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/JsonResponseMessage.java b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/JsonResponseMessage.java
new file mode 100644
index 0000000..5356761
--- /dev/null
+++ b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/JsonResponseMessage.java
@@ -0,0 +1,54 @@
+package net.heberling.ismart.java.rest.api.v1;
+
+/**
+ * @author Doug Culnane
+ */
+public abstract class JsonResponseMessage {
+
+ public static final Integer CODE_SUCCESS = 0;
+
+ Integer code;
+ String message;
+ String eventId;
+
+ abstract Object getData();
+
+ public boolean hasData() {
+ return getData() != null;
+ }
+
+ public String getEventId() {
+ return eventId;
+ }
+
+ public void setEventId(String eventId) {
+ this.eventId = eventId;
+ }
+
+ public JsonResponseMessage() {}
+
+ public Integer getCode() {
+ return code;
+ }
+
+ public void setCode(Integer code) {
+ this.code = code;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
+
+ @Override
+ public String toString() {
+ return "[" + code + "] " + message;
+ }
+
+ public boolean isSuccess() {
+ return code == CODE_SUCCESS;
+ }
+}
diff --git a/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/MessageList.java b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/MessageList.java
new file mode 100644
index 0000000..1cdbb8e
--- /dev/null
+++ b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/MessageList.java
@@ -0,0 +1,17 @@
+package net.heberling.ismart.java.rest.api.v1;
+
+/**
+ * @author Doug Culnane
+ */
+public class MessageList extends JsonResponseMessage {
+
+ String data;
+
+ public String getData() {
+ return data;
+ }
+
+ public void setData(String data) {
+ this.data = data;
+ }
+}
diff --git a/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/MessageNotificationList.java b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/MessageNotificationList.java
new file mode 100644
index 0000000..03c5ceb
--- /dev/null
+++ b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/MessageNotificationList.java
@@ -0,0 +1,113 @@
+package net.heberling.ismart.java.rest.api.v1;
+
+/**
+ * @author Doug Culnane
+ */
+public class MessageNotificationList extends JsonResponseMessage {
+
+ MessageNotificationListData data;
+
+ public MessageNotificationListData getData() {
+ return data;
+ }
+
+ public void setData(MessageNotificationListData data) {
+ this.data = data;
+ }
+
+ public class MessageNotificationListData {
+ Long recordsNumber;
+ Notification[] notifications;
+
+ public Long getRecordsNumber() {
+ return recordsNumber;
+ }
+
+ public void setRecordsNumber(Long recordsNumber) {
+ this.recordsNumber = recordsNumber;
+ }
+
+ public Notification[] getNotifications() {
+ return notifications;
+ }
+
+ public void setNotifications(Notification[] notifications) {
+ this.notifications = notifications;
+ }
+ }
+
+ public class Notification {
+ Integer readStatus;
+ String messageTime;
+ String messageType;
+ String sender;
+ Long messageId;
+ String vin;
+ String title;
+ String content;
+
+ public Integer getReadStatus() {
+ return readStatus;
+ }
+
+ public void setReadStatus(Integer readStatus) {
+ this.readStatus = readStatus;
+ }
+
+ public String getMessageTime() {
+ return messageTime;
+ }
+
+ public void setMessageTime(String messageTime) {
+ this.messageTime = messageTime;
+ }
+
+ public String getMessageType() {
+ return messageType;
+ }
+
+ public void setMessageType(String messageType) {
+ this.messageType = messageType;
+ }
+
+ public String getSender() {
+ return sender;
+ }
+
+ public void setSender(String sender) {
+ this.sender = sender;
+ }
+
+ public Long getMessageId() {
+ return messageId;
+ }
+
+ public void setMessageId(Long messageId) {
+ this.messageId = messageId;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public String getVin() {
+ return vin;
+ }
+
+ public void setVin(String vin) {
+ this.vin = vin;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+ }
+}
diff --git a/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/OauthToken.java b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/OauthToken.java
new file mode 100644
index 0000000..a7778cf
--- /dev/null
+++ b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/OauthToken.java
@@ -0,0 +1,194 @@
+package net.heberling.ismart.java.rest.api.v1;
+
+/**
+ * @author Doug Culnane
+ */
+public class OauthToken extends JsonResponseMessage {
+
+ OauthTokenData data;
+ Boolean IS_CHANGE_PASSWORD;
+ String dept_id;
+ Long expires_in;
+ String account;
+
+ public OauthTokenData getData() {
+ return data;
+ }
+
+ public void setData(OauthTokenData data) {
+ this.data = data;
+ }
+
+ public Boolean getIS_CHANGE_PASSWORD() {
+ return IS_CHANGE_PASSWORD;
+ }
+
+ public void setIS_CHANGE_PASSWORD(Boolean iS_CHANGE_PASSWORD) {
+ IS_CHANGE_PASSWORD = iS_CHANGE_PASSWORD;
+ }
+
+ public String getDept_id() {
+ return dept_id;
+ }
+
+ public void setDept_id(String dept_id) {
+ this.dept_id = dept_id;
+ }
+
+ public Long getExpires_in() {
+ return expires_in;
+ }
+
+ public void setExpires_in(Long expires_in) {
+ this.expires_in = expires_in;
+ }
+
+ public String getAccount() {
+ return account;
+ }
+
+ public void setAccount(String account) {
+ this.account = account;
+ }
+
+ public class OauthTokenData {
+
+ public OauthTokenData() {}
+
+ String tenant_id;
+ String languageType;
+ String user_name;
+ String avatar;
+ String token_type;
+ String client_id;
+ String access_token;
+ String role_name;
+ String refresh_token;
+ String license;
+ String post_id;
+ String user_id;
+ String role_id;
+ String scope;
+ String oauth_id;
+
+ public String getTenant_id() {
+ return tenant_id;
+ }
+
+ public void setTenant_id(String tenant_id) {
+ this.tenant_id = tenant_id;
+ }
+
+ public String getLanguageType() {
+ return languageType;
+ }
+
+ public void setLanguageType(String languageType) {
+ this.languageType = languageType;
+ }
+
+ public String getUser_name() {
+ return user_name;
+ }
+
+ public void setUser_name(String user_name) {
+ this.user_name = user_name;
+ }
+
+ public String getAvatar() {
+ return avatar;
+ }
+
+ public void setAvatar(String avatar) {
+ this.avatar = avatar;
+ }
+
+ public String getToken_type() {
+ return token_type;
+ }
+
+ public void setToken_type(String token_type) {
+ this.token_type = token_type;
+ }
+
+ public String getClient_id() {
+ return client_id;
+ }
+
+ public void setClient_id(String client_id) {
+ this.client_id = client_id;
+ }
+
+ public String getAccess_token() {
+ return access_token;
+ }
+
+ public void setAccess_token(String access_token) {
+ this.access_token = access_token;
+ }
+
+ public String getRole_name() {
+ return role_name;
+ }
+
+ public void setRole_name(String role_name) {
+ this.role_name = role_name;
+ }
+
+ public String getRefresh_token() {
+ return refresh_token;
+ }
+
+ public void setRefresh_token(String refresh_token) {
+ this.refresh_token = refresh_token;
+ }
+
+ public String getLicense() {
+ return license;
+ }
+
+ public void setLicense(String license) {
+ this.license = license;
+ }
+
+ public String getPost_id() {
+ return post_id;
+ }
+
+ public void setPost_id(String post_id) {
+ this.post_id = post_id;
+ }
+
+ public String getUser_id() {
+ return user_id;
+ }
+
+ public void setUser_id(String user_id) {
+ this.user_id = user_id;
+ }
+
+ public String getRole_id() {
+ return role_id;
+ }
+
+ public void setRole_id(String role_id) {
+ this.role_id = role_id;
+ }
+
+ public String getScope() {
+ return scope;
+ }
+
+ public void setScope(String scope) {
+ this.scope = scope;
+ }
+
+ public String getOauth_id() {
+ return oauth_id;
+ }
+
+ public void setOauth_id(String oauth_id) {
+ this.oauth_id = oauth_id;
+ }
+ }
+}
diff --git a/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VechicleChargingMgmtData.java b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VechicleChargingMgmtData.java
new file mode 100644
index 0000000..3622662
--- /dev/null
+++ b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VechicleChargingMgmtData.java
@@ -0,0 +1,392 @@
+package net.heberling.ismart.java.rest.api.v1;
+
+/**
+ * @author Doug Culnane
+ */
+public class VechicleChargingMgmtData extends JsonResponseMessage {
+
+ ViechicleChargingMgmtDataData data;
+
+ public ViechicleChargingMgmtDataData getData() {
+ return data;
+ }
+
+ public void setData(ViechicleChargingMgmtDataData data) {
+ this.data = data;
+ }
+
+ public class ViechicleChargingMgmtDataData {
+ public RvsChargeStatus getRvsChargeStatus() {
+ return rvsChargeStatus;
+ }
+
+ public void setRvsChargeStatus(RvsChargeStatus rvsChargeStatus) {
+ this.rvsChargeStatus = rvsChargeStatus;
+ }
+
+ public ChrgMgmtData getChrgMgmtData() {
+ return chrgMgmtData;
+ }
+
+ public void setChrgMgmtData(ChrgMgmtData chrgMgmtData) {
+ this.chrgMgmtData = chrgMgmtData;
+ }
+
+ RvsChargeStatus rvsChargeStatus;
+ ChrgMgmtData chrgMgmtData;
+ }
+
+ public class RvsChargeStatus {
+
+ Integer mileageSinceLastCharge;
+ Integer totalBatteryCapacity;
+ Integer workingVoltage;
+ Integer chargingDuration;
+ Integer chargingType;
+ Integer lastChargeEndingPower;
+
+ /** Value / 10 = Range */
+ Integer fuelRangeElec;
+
+ Integer realtimePower;
+ Integer workingCurrent;
+
+ /** Gun connected 0 = Disconnected 1 = Connected */
+ Integer chargingGunState;
+
+ Integer mileageOfDay;
+ Long startTime;
+ Long endTime;
+ Integer powerUsageOfDay;
+ Integer powerUsageSinceLastCharge;
+
+ /** Odometer */
+ Integer mileage;
+
+ public Integer getMileageSinceLastCharge() {
+ return mileageSinceLastCharge;
+ }
+
+ public void setMileageSinceLastCharge(Integer mileageSinceLastCharge) {
+ this.mileageSinceLastCharge = mileageSinceLastCharge;
+ }
+
+ public Integer getTotalBatteryCapacity() {
+ return totalBatteryCapacity;
+ }
+
+ public void setTotalBatteryCapacity(Integer totalBatteryCapacity) {
+ this.totalBatteryCapacity = totalBatteryCapacity;
+ }
+
+ public Integer getWorkingVoltage() {
+ return workingVoltage;
+ }
+
+ public void setWorkingVoltage(Integer workingVoltage) {
+ this.workingVoltage = workingVoltage;
+ }
+
+ public Integer getChargingDuration() {
+ return chargingDuration;
+ }
+
+ public void setChargingDuration(Integer chargingDuration) {
+ this.chargingDuration = chargingDuration;
+ }
+
+ public Integer getChargingType() {
+ return chargingType;
+ }
+
+ public void setChargingType(Integer chargingType) {
+ this.chargingType = chargingType;
+ }
+
+ public Integer getLastChargeEndingPower() {
+ return lastChargeEndingPower;
+ }
+
+ public void setLastChargeEndingPower(Integer lastChargeEndingPower) {
+ this.lastChargeEndingPower = lastChargeEndingPower;
+ }
+
+ public Integer getFuelRangeElec() {
+ return fuelRangeElec;
+ }
+
+ public void setFuelRangeElec(Integer fuelRangeElec) {
+ this.fuelRangeElec = fuelRangeElec;
+ }
+
+ public Integer getRealtimePower() {
+ return realtimePower;
+ }
+
+ public void setRealtimePower(Integer realtimePower) {
+ this.realtimePower = realtimePower;
+ }
+
+ public Integer getWorkingCurrent() {
+ return workingCurrent;
+ }
+
+ public void setWorkingCurrent(Integer workingCurrent) {
+ this.workingCurrent = workingCurrent;
+ }
+
+ public Integer getChargingGunState() {
+ return chargingGunState;
+ }
+
+ public void setChargingGunState(Integer chargingGunState) {
+ this.chargingGunState = chargingGunState;
+ }
+
+ public Integer getMileageOfDay() {
+ return mileageOfDay;
+ }
+
+ public void setMileageOfDay(Integer mileageOfDay) {
+ this.mileageOfDay = mileageOfDay;
+ }
+
+ public Long getStartTime() {
+ return startTime;
+ }
+
+ public void setStartTime(Long startTime) {
+ this.startTime = startTime;
+ }
+
+ public Long getEndTime() {
+ return endTime;
+ }
+
+ public void setEndTime(Long endTime) {
+ this.endTime = endTime;
+ }
+
+ public Integer getPowerUsageOfDay() {
+ return powerUsageOfDay;
+ }
+
+ public void setPowerUsageOfDay(Integer powerUsageOfDay) {
+ this.powerUsageOfDay = powerUsageOfDay;
+ }
+
+ public Integer getPowerUsageSinceLastCharge() {
+ return powerUsageSinceLastCharge;
+ }
+
+ public void setPowerUsageSinceLastCharge(Integer powerUsageSinceLastCharge) {
+ this.powerUsageSinceLastCharge = powerUsageSinceLastCharge;
+ }
+
+ public Integer getMileage() {
+ return mileage;
+ }
+
+ public void setMileage(Integer mileage) {
+ this.mileage = mileage;
+ }
+ }
+
+ public class ChrgMgmtData {
+
+ Integer clstrElecRngToEPT;
+
+ /** BMS Charging Status 0 = Not charging. 1 = Charging 5 = Waiting for Charger */
+ Integer bmsChrgSts;
+
+ Integer bmsPackSOCDsp;
+ Integer bmsPTCHeatReqDspCmd;
+ Integer bmsPackCrnt;
+ Integer bmsChrgCtrlDspCmd;
+ Integer bmsReserStMintueDspCmd;
+ Integer bmsOnBdChrgTrgtSOCDspCmd;
+ Integer bmsEstdElecRng;
+ Integer bmsReserCtrlDspCmd;
+ Integer bmsChrgOtptCrntReq;
+ Integer bmsPTCHeatSpRsn;
+ Integer bmsReserStHourDspCmd;
+ Integer bmsPackVol;
+ Integer bmsReserSpHourDspCmd;
+ Integer bmsAdpPubChrgSttnDspCmd;
+ Integer chrgngRmnngTimeV;
+ Integer bmsReserSpMintueDspCmd;
+ Integer bmsAltngChrgCrntDspCmd;
+ Integer bmsChrgSpRsn;
+
+ /** Charging remaining time */
+ Integer chrgngRmnngTime;
+
+ public Integer getClstrElecRngToEPT() {
+ return clstrElecRngToEPT;
+ }
+
+ public void setClstrElecRngToEPT(Integer clstrElecRngToEPT) {
+ this.clstrElecRngToEPT = clstrElecRngToEPT;
+ }
+
+ public Integer getBmsChrgSts() {
+ return bmsChrgSts;
+ }
+
+ public void setBmsChrgSts(Integer bmsChrgSts) {
+ this.bmsChrgSts = bmsChrgSts;
+ }
+
+ public Integer getBmsPackSOCDsp() {
+ return bmsPackSOCDsp;
+ }
+
+ public void setBmsPackSOCDsp(Integer bmsPackSOCDsp) {
+ this.bmsPackSOCDsp = bmsPackSOCDsp;
+ }
+
+ public Integer getBmsPTCHeatReqDspCmd() {
+ return bmsPTCHeatReqDspCmd;
+ }
+
+ public void setBmsPTCHeatReqDspCmd(Integer bmsPTCHeatReqDspCmd) {
+ this.bmsPTCHeatReqDspCmd = bmsPTCHeatReqDspCmd;
+ }
+
+ public Integer getBmsPackCrnt() {
+ return bmsPackCrnt;
+ }
+
+ public void setBmsPackCrnt(Integer bmsPackCrnt) {
+ this.bmsPackCrnt = bmsPackCrnt;
+ }
+
+ public Integer getBmsChrgCtrlDspCmd() {
+ return bmsChrgCtrlDspCmd;
+ }
+
+ public void setBmsChrgCtrlDspCmd(Integer bmsChrgCtrlDspCmd) {
+ this.bmsChrgCtrlDspCmd = bmsChrgCtrlDspCmd;
+ }
+
+ public Integer getBmsReserStMintueDspCmd() {
+ return bmsReserStMintueDspCmd;
+ }
+
+ public void setBmsReserStMintueDspCmd(Integer bmsReserStMintueDspCmd) {
+ this.bmsReserStMintueDspCmd = bmsReserStMintueDspCmd;
+ }
+
+ public Integer getBmsOnBdChrgTrgtSOCDspCmd() {
+ return bmsOnBdChrgTrgtSOCDspCmd;
+ }
+
+ public void setBmsOnBdChrgTrgtSOCDspCmd(Integer bmsOnBdChrgTrgtSOCDspCmd) {
+ this.bmsOnBdChrgTrgtSOCDspCmd = bmsOnBdChrgTrgtSOCDspCmd;
+ }
+
+ public Integer getBmsEstdElecRng() {
+ return bmsEstdElecRng;
+ }
+
+ public void setBmsEstdElecRng(Integer bmsEstdElecRng) {
+ this.bmsEstdElecRng = bmsEstdElecRng;
+ }
+
+ public Integer getBmsReserCtrlDspCmd() {
+ return bmsReserCtrlDspCmd;
+ }
+
+ public void setBmsReserCtrlDspCmd(Integer bmsReserCtrlDspCmd) {
+ this.bmsReserCtrlDspCmd = bmsReserCtrlDspCmd;
+ }
+
+ public Integer getBmsChrgOtptCrntReq() {
+ return bmsChrgOtptCrntReq;
+ }
+
+ public void setBmsChrgOtptCrntReq(Integer bmsChrgOtptCrntReq) {
+ this.bmsChrgOtptCrntReq = bmsChrgOtptCrntReq;
+ }
+
+ public Integer getBmsPTCHeatSpRsn() {
+ return bmsPTCHeatSpRsn;
+ }
+
+ public void setBmsPTCHeatSpRsn(Integer bmsPTCHeatSpRsn) {
+ this.bmsPTCHeatSpRsn = bmsPTCHeatSpRsn;
+ }
+
+ public Integer getBmsReserStHourDspCmd() {
+ return bmsReserStHourDspCmd;
+ }
+
+ public void setBmsReserStHourDspCmd(Integer bmsReserStHourDspCmd) {
+ this.bmsReserStHourDspCmd = bmsReserStHourDspCmd;
+ }
+
+ public Integer getBmsPackVol() {
+ return bmsPackVol;
+ }
+
+ public void setBmsPackVol(Integer bmsPackVol) {
+ this.bmsPackVol = bmsPackVol;
+ }
+
+ public Integer getBmsReserSpHourDspCmd() {
+ return bmsReserSpHourDspCmd;
+ }
+
+ public void setBmsReserSpHourDspCmd(Integer bmsReserSpHourDspCmd) {
+ this.bmsReserSpHourDspCmd = bmsReserSpHourDspCmd;
+ }
+
+ public Integer getBmsAdpPubChrgSttnDspCmd() {
+ return bmsAdpPubChrgSttnDspCmd;
+ }
+
+ public void setBmsAdpPubChrgSttnDspCmd(Integer bmsAdpPubChrgSttnDspCmd) {
+ this.bmsAdpPubChrgSttnDspCmd = bmsAdpPubChrgSttnDspCmd;
+ }
+
+ public Integer getChrgngRmnngTimeV() {
+ return chrgngRmnngTimeV;
+ }
+
+ public void setChrgngRmnngTimeV(Integer chrgngRmnngTimeV) {
+ this.chrgngRmnngTimeV = chrgngRmnngTimeV;
+ }
+
+ public Integer getBmsReserSpMintueDspCmd() {
+ return bmsReserSpMintueDspCmd;
+ }
+
+ public void setBmsReserSpMintueDspCmd(Integer bmsReserSpMintueDspCmd) {
+ this.bmsReserSpMintueDspCmd = bmsReserSpMintueDspCmd;
+ }
+
+ public Integer getBmsAltngChrgCrntDspCmd() {
+ return bmsAltngChrgCrntDspCmd;
+ }
+
+ public void setBmsAltngChrgCrntDspCmd(Integer bmsAltngChrgCrntDspCmd) {
+ this.bmsAltngChrgCrntDspCmd = bmsAltngChrgCrntDspCmd;
+ }
+
+ public Integer getBmsChrgSpRsn() {
+ return bmsChrgSpRsn;
+ }
+
+ public void setBmsChrgSpRsn(Integer bmsChrgSpRsn) {
+ this.bmsChrgSpRsn = bmsChrgSpRsn;
+ }
+
+ public Integer getChrgngRmnngTime() {
+ return chrgngRmnngTime;
+ }
+
+ public void setChrgngRmnngTime(Integer chrgngRmnngTime) {
+ this.chrgngRmnngTime = chrgngRmnngTime;
+ }
+ }
+}
diff --git a/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VehicleCcInfo.java b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VehicleCcInfo.java
new file mode 100644
index 0000000..e086ec5
--- /dev/null
+++ b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VehicleCcInfo.java
@@ -0,0 +1,19 @@
+package net.heberling.ismart.java.rest.api.v1;
+
+/**
+ * Returns null...?
+ *
+ * @author Doug Culnane
+ */
+public class VehicleCcInfo extends JsonResponseMessage {
+
+ String data;
+
+ public String getData() {
+ return data;
+ }
+
+ public void setData(String data) {
+ this.data = data;
+ }
+}
diff --git a/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VehicleList.java b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VehicleList.java
new file mode 100644
index 0000000..7f98cb3
--- /dev/null
+++ b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VehicleList.java
@@ -0,0 +1,166 @@
+package net.heberling.ismart.java.rest.api.v1;
+
+/**
+ * @author Doug Culnane
+ */
+public class VehicleList extends JsonResponseMessage {
+
+ VinListData data;
+
+ public VinListData getData() {
+ return data;
+ }
+
+ public void setData(VinListData data) {
+ this.data = data;
+ }
+
+ public class VinListData {
+ public VinListData() {}
+
+ VinListItem[] vinList;
+
+ public VinListItem[] getVinList() {
+ return vinList;
+ }
+
+ public void setVinList(VinListItem[] vinList) {
+ this.vinList = vinList;
+ }
+ }
+
+ public class VinListItem {
+ Long bindTime;
+ String colorName;
+ String brandName;
+ String modelName;
+ String modelYear;
+ String series;
+ Boolean isActivate;
+ Boolean isCurrentVehicle;
+ Boolean isSubaccount;
+ String vin;
+ VehicleModelConfiguration[] vehicleModelConfiguration;
+
+ public VinListItem() {}
+
+ public Long getBindTime() {
+ return bindTime;
+ }
+
+ public void setBindTime(Long bindTime) {
+ this.bindTime = bindTime;
+ }
+
+ public String getColorName() {
+ return colorName;
+ }
+
+ public void setColorName(String colorName) {
+ this.colorName = colorName;
+ }
+
+ public String getBrandName() {
+ return brandName;
+ }
+
+ public void setBrandName(String brandName) {
+ this.brandName = brandName;
+ }
+
+ public String getModelName() {
+ return modelName;
+ }
+
+ public void setModelName(String modelName) {
+ this.modelName = modelName;
+ }
+
+ public String getModelYear() {
+ return modelYear;
+ }
+
+ public void setModelYear(String modelYear) {
+ this.modelYear = modelYear;
+ }
+
+ public String getSeries() {
+ return series;
+ }
+
+ public void setSeries(String series) {
+ this.series = series;
+ }
+
+ public Boolean getIsActivate() {
+ return isActivate;
+ }
+
+ public void setIsActivate(Boolean isActivate) {
+ this.isActivate = isActivate;
+ }
+
+ public Boolean getIsCurrentVehicle() {
+ return isCurrentVehicle;
+ }
+
+ public void setIsCurrentVehicle(Boolean isCurrentVehicle) {
+ this.isCurrentVehicle = isCurrentVehicle;
+ }
+
+ public Boolean getIsSubaccount() {
+ return isSubaccount;
+ }
+
+ public void setIsSubaccount(Boolean isSubaccount) {
+ this.isSubaccount = isSubaccount;
+ }
+
+ public String getVin() {
+ return vin;
+ }
+
+ public void setVin(String vin) {
+ this.vin = vin;
+ }
+
+ public VehicleModelConfiguration[] getVehicleModelConfiguration() {
+ return vehicleModelConfiguration;
+ }
+
+ public void setVehicleModelConfiguration(
+ VehicleModelConfiguration[] vehicleModelConfiguration) {
+ this.vehicleModelConfiguration = vehicleModelConfiguration;
+ }
+ }
+
+ public class VehicleModelConfiguration {
+ String itemName;
+ String itemCode;
+ String itemValue;
+
+ public String getItemName() {
+ return itemName;
+ }
+
+ public void setItemName(String itemName) {
+ this.itemName = itemName;
+ }
+
+ public String getItemCode() {
+ return itemCode;
+ }
+
+ public void setItemCode(String itemCode) {
+ this.itemCode = itemCode;
+ }
+
+ public String getItemValue() {
+ return itemValue;
+ }
+
+ public void setItemValue(String itemValue) {
+ this.itemValue = itemValue;
+ }
+ }
+}
diff --git a/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VehicleLocation.java b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VehicleLocation.java
new file mode 100644
index 0000000..ec691ff
--- /dev/null
+++ b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VehicleLocation.java
@@ -0,0 +1,17 @@
+package net.heberling.ismart.java.rest.api.v1;
+
+/**
+ * @author Doug Culnane
+ */
+public class VehicleLocation extends JsonResponseMessage {
+
+ String data;
+
+ public String getData() {
+ return data;
+ }
+
+ public void setData(String data) {
+ this.data = data;
+ }
+}
diff --git a/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VehicleStatisticsBasicInfo.java b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VehicleStatisticsBasicInfo.java
new file mode 100644
index 0000000..b3827b6
--- /dev/null
+++ b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VehicleStatisticsBasicInfo.java
@@ -0,0 +1,43 @@
+package net.heberling.ismart.java.rest.api.v1;
+
+/**
+ * @author Doug Culnane
+ */
+public class VehicleStatisticsBasicInfo extends JsonResponseMessage {
+
+ VehicleStatisticsBasicInfoData data;
+
+ public VehicleStatisticsBasicInfo() {}
+
+ public VehicleStatisticsBasicInfoData getData() {
+ return data;
+ }
+
+ public void setData(VehicleStatisticsBasicInfoData data) {
+ this.data = data;
+ }
+
+ public class VehicleStatisticsBasicInfoData {
+
+ String vin;
+ Long initialDate;
+
+ public VehicleStatisticsBasicInfoData() {}
+
+ public String getVin() {
+ return vin;
+ }
+
+ public void setVin(String vin) {
+ this.vin = vin;
+ }
+
+ public Long getInitialDate() {
+ return initialDate;
+ }
+
+ public void setInitialDate(Long initialDate) {
+ this.initialDate = initialDate;
+ }
+ }
+}
diff --git a/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VehicleStatisticsInfo.java b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VehicleStatisticsInfo.java
new file mode 100644
index 0000000..3a24d86
--- /dev/null
+++ b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VehicleStatisticsInfo.java
@@ -0,0 +1,19 @@
+package net.heberling.ismart.java.rest.api.v1;
+
+/**
+ * Returns null...?
+ *
+ * @author Doug Culnane
+ */
+public class VehicleStatisticsInfo extends JsonResponseMessage {
+
+ String data;
+
+ public String getData() {
+ return data;
+ }
+
+ public void setData(String data) {
+ this.data = data;
+ }
+}
diff --git a/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VehicleStatus.java b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VehicleStatus.java
new file mode 100644
index 0000000..5a4af35
--- /dev/null
+++ b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/api/v1/VehicleStatus.java
@@ -0,0 +1,17 @@
+package net.heberling.ismart.java.rest.api.v1;
+
+/**
+ * @author Doug Culnane
+ */
+public class VehicleStatus extends JsonResponseMessage {
+
+ String data;
+
+ public String getData() {
+ return data;
+ }
+
+ public void setData(String data) {
+ this.data = data;
+ }
+}
diff --git a/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/exceptions/ChargingStatusAPIException.java b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/exceptions/ChargingStatusAPIException.java
new file mode 100644
index 0000000..5f74c20
--- /dev/null
+++ b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/exceptions/ChargingStatusAPIException.java
@@ -0,0 +1,13 @@
+package net.heberling.ismart.java.rest.exceptions;
+
+import net.heberling.ismart.java.rest.api.v1.JsonResponseMessage;
+
+/**
+ * @author Doug Culnane - Initial contribution
+ */
+public class ChargingStatusAPIException extends Exception {
+
+ public ChargingStatusAPIException(JsonResponseMessage response) {
+ super("[" + response.getCode() + "] " + response.getMessage());
+ }
+}
diff --git a/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/exceptions/VehicleStatusAPIException.java b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/exceptions/VehicleStatusAPIException.java
new file mode 100644
index 0000000..6ebaed8
--- /dev/null
+++ b/saic-java-rest-api/src/main/java/net/heberling/ismart/java/rest/exceptions/VehicleStatusAPIException.java
@@ -0,0 +1,13 @@
+package net.heberling.ismart.java.rest.exceptions;
+
+import net.heberling.ismart.java.rest.api.v1.JsonResponseMessage;
+
+/**
+ * @author Doug Culnane - Initial contribution
+ */
+public class VehicleStatusAPIException extends Exception {
+
+ public VehicleStatusAPIException(JsonResponseMessage response) {
+ super("[" + response.getCode() + "] " + response.getMessage());
+ }
+}
diff --git a/saic-java-rest-client/pom.xml b/saic-java-rest-client/pom.xml
new file mode 100644
index 0000000..761806f
--- /dev/null
+++ b/saic-java-rest-client/pom.xml
@@ -0,0 +1,43 @@
+
+
+ 4.0.0
+
+
+ io.github.saic-ismart-api
+ saic-ismart-api-parent
+ 0.0.0-SNAPSHOT
+
+
+ saic-java-rest-client
+ SAIC Java REST API Client
+ Implementation of the SAIC Rest API Client in Java
+
+
+
+ io.github.saic-ismart-api
+ saic-java-rest-api
+ 0.0.0-SNAPSHOT
+
+
+ org.eclipse.jetty
+ jetty-client
+ 9.4.54.v20240208
+
+
+ org.slf4j
+ slf4j-api
+ 2.0.12
+
+
+ com.google.code.gson
+ gson
+ 2.8.9
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.10.0
+ test
+
+
+
diff --git a/saic-java-rest-client/src/main/java/net/heberling/ismart/java/rest/SaicApiClient.java b/saic-java-rest-client/src/main/java/net/heberling/ismart/java/rest/SaicApiClient.java
new file mode 100644
index 0000000..cac2ea7
--- /dev/null
+++ b/saic-java-rest-client/src/main/java/net/heberling/ismart/java/rest/SaicApiClient.java
@@ -0,0 +1,334 @@
+package net.heberling.ismart.java.rest;
+
+import com.google.gson.Gson;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Random;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeoutException;
+import net.heberling.ismart.java.rest.api.v1.JsonResponseMessage;
+import net.heberling.ismart.java.rest.api.v1.MessageList;
+import net.heberling.ismart.java.rest.api.v1.MessageNotificationList;
+import net.heberling.ismart.java.rest.api.v1.OauthToken;
+import net.heberling.ismart.java.rest.api.v1.VechicleChargingMgmtData;
+import net.heberling.ismart.java.rest.api.v1.VehicleCcInfo;
+import net.heberling.ismart.java.rest.api.v1.VehicleList;
+import net.heberling.ismart.java.rest.api.v1.VehicleLocation;
+import net.heberling.ismart.java.rest.api.v1.VehicleStatisticsBasicInfo;
+import net.heberling.ismart.java.rest.api.v1.VehicleStatus;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.api.ContentResponse;
+import org.eclipse.jetty.client.api.Request;
+import org.eclipse.jetty.client.util.StringContentProvider;
+import org.eclipse.jetty.http.HttpMethod;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * SIAC API HTTP Client implementation using org.eclipse.jetty.client.
+ *
+ * @author Doug Culnane
+ */
+public class SaicApiClient {
+
+ private HttpClient httpClient;
+
+ private byte[] deviceId;
+
+ private final Logger logger = LoggerFactory.getLogger(SaicApiClient.class);
+
+ private final String URL_ROOT_V1 = "https://gateway-mg-eu.soimt.com/api.app/v1/";
+
+ public SaicApiClient(HttpClient httpClient) {
+ this.httpClient = httpClient;
+ }
+
+ public OauthToken getOauthToken(String username, String password, String language)
+ throws IOException, InterruptedException, TimeoutException, ExecutionException {
+ if (deviceId == null) {
+ deviceId = new byte[64];
+ new Random().nextBytes(deviceId);
+ }
+ return sendRequest(
+ new OauthToken(),
+ URL_ROOT_V1 + "oauth/token",
+ HttpMethod.POST,
+ "grant_type=password&username="
+ + username
+ + "&password="
+ + HashUtils.sha1(password)
+ + "&scope=all&deviceId="
+ + HexUtils.bytesToHex(deviceId)
+ + "###europecar&deviceType=1&loginType="
+ + getLoginType(username)
+ + "&language="
+ + language,
+ "application/x-www-form-urlencoded",
+ "",
+ "");
+ }
+
+ /**
+ * "2" if username_is_email else "1",
+ *
+ * @param username
+ * @return
+ */
+ private int getLoginType(String username) {
+ if (username.contains("@")) {
+ return 2;
+ }
+ return 1;
+ }
+
+ public VehicleList getVehicleList(String token)
+ throws IOException, InterruptedException, TimeoutException, ExecutionException {
+ return sendRequest(
+ new VehicleList(),
+ URL_ROOT_V1 + "vehicle/list",
+ HttpMethod.GET,
+ "",
+ "application/json",
+ token,
+ "");
+ }
+
+ public VehicleStatus getVehicleStatus(String token, String vin)
+ throws IOException, InterruptedException, TimeoutException, ExecutionException {
+ return sendRequest(
+ new VehicleStatus(),
+ URL_ROOT_V1 + "vehicle/status?vin=" + HashUtils.sha256(vin) + "&vehStatusReqType=2",
+ HttpMethod.GET,
+ "",
+ "application/json",
+ token,
+ "");
+ }
+
+ public VechicleChargingMgmtData getVehicleChargingMgmtData(String token, String vin)
+ throws IOException, InterruptedException, TimeoutException, ExecutionException {
+ VechicleChargingMgmtData message1 =
+ sendRequest(
+ new VechicleChargingMgmtData(),
+ URL_ROOT_V1 + "vehicle/charging/mgmtData?vin=" + HashUtils.sha256(vin),
+ HttpMethod.GET,
+ "",
+ "application/json",
+ token,
+ "");
+ if (message1.isSuccess()) {
+ return sendRequest(
+ new VechicleChargingMgmtData(),
+ URL_ROOT_V1 + "vehicle/charging/mgmtData?vin=" + HashUtils.sha256(vin),
+ HttpMethod.GET,
+ "",
+ "application/json",
+ token,
+ message1.getEventId());
+ }
+ return message1;
+ }
+
+ public VehicleStatisticsBasicInfo getVehicleStatisticsBasicInfo(String token, String vin)
+ throws IOException, InterruptedException, TimeoutException, ExecutionException {
+ return sendRequest(
+ new VehicleStatisticsBasicInfo(),
+ URL_ROOT_V1 + "vehicle/statisticsBasicInfo?vin=" + HashUtils.sha256(vin),
+ HttpMethod.GET,
+ "",
+ "application/json",
+ token,
+ "");
+ }
+
+ public VehicleStatus getVehicleStatusClassified(String token, String vin)
+ throws IOException, InterruptedException, TimeoutException, ExecutionException {
+ return sendRequest(
+ new VehicleStatus(),
+ URL_ROOT_V1 + "vehicle/status/classified?vin=" + HashUtils.sha256(vin),
+ HttpMethod.GET,
+ "",
+ "application/json",
+ token,
+ "");
+ }
+
+ public VehicleCcInfo getVehicleCcInfo(String token, String vin)
+ throws IOException, InterruptedException, TimeoutException, ExecutionException {
+ VehicleCcInfo message1 =
+ sendRequest(
+ new VehicleCcInfo(),
+ URL_ROOT_V1 + "vehicle/vehicle/ccInfo?vin=" + HashUtils.sha256(vin),
+ HttpMethod.GET,
+ "",
+ "application/json",
+ token,
+ "");
+ if (message1.isSuccess()) {
+ return sendRequest(
+ new VehicleCcInfo(),
+ URL_ROOT_V1 + "vehicle/vehicle/ccInfo?vin=" + HashUtils.sha256(vin),
+ HttpMethod.GET,
+ "",
+ "application/json",
+ token,
+ message1.getEventId());
+ }
+ return message1;
+ }
+
+ public VehicleLocation getVehicleLocation(String token, String vin)
+ throws IOException, InterruptedException, TimeoutException, ExecutionException {
+ VehicleLocation message1 =
+ sendRequest(
+ new VehicleLocation(),
+ URL_ROOT_V1 + "vehicle/location?vin=" + HashUtils.sha256(vin),
+ HttpMethod.GET,
+ "",
+ "application/json",
+ token,
+ "");
+ if (message1.isSuccess()) {
+ return sendRequest(
+ new VehicleLocation(),
+ URL_ROOT_V1 + "vehicle/location?vin=" + HashUtils.sha256(vin),
+ HttpMethod.GET,
+ "",
+ "application/json",
+ token,
+ message1.getEventId());
+ }
+ return message1;
+ }
+
+ public MessageList getMessageList(String token, String vin)
+ throws IOException, InterruptedException, TimeoutException, ExecutionException {
+ return sendRequest(
+ new MessageList(),
+ URL_ROOT_V1 + "message/list?vin=" + HashUtils.sha256(vin),
+ HttpMethod.GET,
+ "",
+ "application/json",
+ token,
+ "");
+ }
+
+ public MessageList getVehicleInfo(String token, String vin)
+ throws IOException, InterruptedException, TimeoutException, ExecutionException {
+ return sendRequest(
+ new MessageList(),
+ URL_ROOT_V1 + "vehicle/info?vin=" + HashUtils.sha256(vin),
+ HttpMethod.GET,
+ "",
+ "application/json",
+ token,
+ "");
+ }
+
+ public JsonResponseMessage getVehicleGeoFence(String token, String vin)
+ throws IOException, InterruptedException, TimeoutException, ExecutionException {
+ return sendRequest(
+ new VehicleStatus(),
+ URL_ROOT_V1 + "vehicle/geoFence?vin=" + HashUtils.sha256(vin),
+ HttpMethod.GET,
+ "",
+ "application/json",
+ token,
+ "");
+ }
+
+ public MessageNotificationList getMessageNotificationList(String token)
+ throws IOException, InterruptedException, TimeoutException, ExecutionException {
+ return sendRequest(
+ new MessageNotificationList(),
+ URL_ROOT_V1 + "message/notificationList",
+ HttpMethod.GET,
+ "",
+ "application/json",
+ token,
+ "");
+ }
+
+ private T sendRequest(
+ T responseMessage,
+ String url,
+ HttpMethod httpMethod,
+ String requestBody,
+ String contentType,
+ String token,
+ String eventId)
+ throws IOException, InterruptedException, TimeoutException, ExecutionException {
+
+ Gson gson = new Gson();
+ URI endpoint = URI.create(url);
+
+ long appSendDate = System.currentTimeMillis();
+
+ Request request =
+ this.httpClient
+ .newRequest(endpoint)
+ .method(httpMethod)
+ .header("tenant-id", APIConfig.TENANT_ID)
+ .header("user-type", APIConfig.USER_TYPE)
+ .header("app-send-date", "" + appSendDate)
+ .header("app-content-encrypted", APIConfig.CONTENT_ENCRYPTED)
+ .header("Authorization", APIConfig.PARAM_AUTHENTICATION)
+ .header("original-content-type", contentType);
+
+ String encryptedBody = "";
+ if (requestBody != null && !requestBody.isBlank()) {
+ encryptedBody =
+ EncryptionUtils.encryptRequest(
+ endpoint.toString(),
+ appSendDate,
+ APIConfig.TENANT_ID,
+ token,
+ requestBody,
+ contentType);
+ request.content(new StringContentProvider(encryptedBody), contentType);
+ }
+
+ String replace =
+ !StringUtils.isEmpty(endpoint.toString())
+ ? endpoint.toString().replace(EncryptionUtils.BASE_URL_P, "/")
+ : "";
+ request.header(
+ "app-verification-string",
+ EncryptionUtils.calculateRequestVerification(
+ replace, appSendDate, APIConfig.TENANT_ID, contentType, encryptedBody, token));
+
+ if (token != null && !token.isBlank()) {
+ request.header("blade-auth", token);
+ }
+ if (eventId != null && !eventId.isBlank()) {
+ request.header("event-id", eventId);
+ }
+ if (logger.isDebugEnabled()) {
+ logger.debug("Request: {}", request);
+ }
+
+ ContentResponse response = request.send();
+ if (response.getHeaders().get("app-content-encrypted") != null
+ && response.getHeaders().get("app-content-encrypted").equals("1")) {
+ String responseEncripted = response.getContentAsString();
+
+ String decryptedString =
+ EncryptionUtils.decryptResponse(
+ response.getHeaders().get("app-send-date"),
+ response.getHeaders().get("original-content-type"),
+ responseEncripted);
+ if (logger.isDebugEnabled()) {
+ logger.debug("Response: {}", decryptedString);
+ }
+ responseMessage = (T) gson.fromJson(decryptedString, responseMessage.getClass());
+ } else {
+ logger.warn("Unexpected Response: {}", response.getContentAsString());
+ }
+
+ if (response.getHeaders().get("event-id") != null) {
+ responseMessage.setEventId(response.getHeaders().get("event-id"));
+ }
+
+ return responseMessage;
+ }
+}
diff --git a/saic-java-rest-client/src/test/java/net/heberling/ismart/java/rest/TestJsonResponseMessages.java b/saic-java-rest-client/src/test/java/net/heberling/ismart/java/rest/TestJsonResponseMessages.java
new file mode 100644
index 0000000..8250d6e
--- /dev/null
+++ b/saic-java-rest-client/src/test/java/net/heberling/ismart/java/rest/TestJsonResponseMessages.java
@@ -0,0 +1,59 @@
+package net.heberling.ismart.java.rest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.stream.Collectors;
+import net.heberling.ismart.java.rest.api.v1.OauthToken;
+import net.heberling.ismart.java.rest.api.v1.VehicleList;
+import org.junit.jupiter.api.Test;
+
+/**
+ * @author Doug Culnane
+ */
+public class TestJsonResponseMessages {
+
+ @Test
+ void testOauthToken() throws JsonSyntaxException, IOException {
+
+ Gson gson = new Gson();
+ OauthToken oathToken =
+ gson.fromJson(getResourceFileAsString("rest_v1/oauth_token_0.json"), OauthToken.class);
+
+ assertTrue(oathToken.isSuccess());
+ assertEquals("[0] success", oathToken.toString());
+ assertEquals("ABC123", oathToken.getData().getAccess_token());
+ }
+
+ @Test
+ void testVehicleList() throws JsonSyntaxException, IOException {
+
+ Gson gson = new Gson();
+ VehicleList vinList =
+ gson.fromJson(getResourceFileAsString("rest_v1/vehicle_list_0.json"), VehicleList.class);
+
+ assertTrue(vinList.isSuccess());
+ assertEquals("[0] success", vinList.toString());
+ assertEquals(1, vinList.getData().getVinList().length);
+ assertEquals("ABCD1234567890123", vinList.getData().getVinList()[0].getVin());
+ assertEquals("MG", vinList.getData().getVinList()[0].getBrandName());
+ assertEquals("MG5 Electric", vinList.getData().getVinList()[0].getModelName());
+ }
+
+ static String getResourceFileAsString(String fileName) throws IOException {
+ ClassLoader classLoader = ClassLoader.getSystemClassLoader();
+ try (InputStream is = classLoader.getResourceAsStream(fileName)) {
+ if (is == null) return null;
+ try (InputStreamReader isr = new InputStreamReader(is);
+ BufferedReader reader = new BufferedReader(isr)) {
+ return reader.lines().collect(Collectors.joining(System.lineSeparator()));
+ }
+ }
+ }
+}
diff --git a/saic-java-rest-client/src/test/java/net/heberling/ismart/java/rest/TestSaicApiClient.java b/saic-java-rest-client/src/test/java/net/heberling/ismart/java/rest/TestSaicApiClient.java
new file mode 100644
index 0000000..918b394
--- /dev/null
+++ b/saic-java-rest-client/src/test/java/net/heberling/ismart/java/rest/TestSaicApiClient.java
@@ -0,0 +1,61 @@
+package net.heberling.ismart.java.rest;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import net.heberling.ismart.java.rest.api.v1.OauthToken;
+import net.heberling.ismart.java.rest.api.v1.VehicleList;
+import org.eclipse.jetty.client.HttpClient;
+import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * @author Doug Culnane
+ */
+public class TestSaicApiClient {
+
+ private final Logger logger = LoggerFactory.getLogger(TestSaicApiClient.class);
+
+ @Test
+ public void testClient() throws Exception {
+
+ // Set the environment variables to run the test against the API
+ String username = System.getenv("SAIC_USERNAME");
+ String password = System.getenv("SAIC_PASSWORD");
+ if (username == null || password == null) {
+ logger.warn(
+ "To activate SaicApiClient tests set the SAIC_USERNAME and SAIC_PASSWORD environment"
+ + " variables.");
+ return;
+ }
+
+ HttpClient httpClient =
+ new HttpClient(new HttpClientTransportOverHTTP(), new SslContextFactory(true));
+ httpClient.start();
+
+ SaicApiClient client = new SaicApiClient(httpClient);
+ OauthToken oauthToken = client.getOauthToken(username, password, "EN");
+
+ String token = oauthToken.getData().getAccess_token();
+ VehicleList vehicleList = client.getVehicleList(token);
+ String vin = vehicleList.getData().getVinList()[0].getVin();
+
+ assertTrue(client.getVehicleStatus(token, vin).isSuccess());
+ assertTrue(client.getVehicleStatisticsBasicInfo(token, vin).isSuccess());
+ assertTrue(client.getVehicleChargingMgmtData(token, vin).isSuccess());
+ assertTrue(client.getMessageNotificationList(token).isSuccess());
+
+ // Not much useful info
+ // assertTrue(client.getVehicleGeoFence(token, vin).isSuccess());
+ // assertTrue(client.getVehicleStatisticsBasicInfo(token, vin).isSuccess());
+
+ // Failing
+ // assertTrue(client.getVehicleInfo(token, vin).isSuccess());
+ // assertTrue(client.getVehicleStatusClassified(token, vin).isSuccess());
+ // assertTrue(client.getMessageList(token, vin).isSuccess());
+ // assertTrue(client.getVehicleCcInfo(token, vin).isSuccess());
+ // assertTrue(client.getVehicleLocation(token, vin).isSuccess());
+ }
+}