diff --git a/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/GatewayMqttClient.java b/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/GatewayMqttClient.java new file mode 100644 index 00000000..3ff10a3c --- /dev/null +++ b/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/GatewayMqttClient.java @@ -0,0 +1,61 @@ +package net.heberling.ismart.mqtt; + +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.UUID; +import org.eclipse.paho.client.mqttv3.*; + +public class GatewayMqttClient { + + private final IMqttClient client; + + public GatewayMqttClient(URI mqttUri, String mqttUser, char[] mqttPassword) { + String publisherId = UUID.randomUUID().toString(); + try { + client = + new MqttClient(mqttUri.toString(), publisherId, null) { + @Override + public void close() throws MqttException { + Thread.dumpStack(); + disconnect(); + super.close(true); + } + }; + MqttConnectOptions options = new MqttConnectOptions(); + options.setAutomaticReconnect(true); + options.setCleanSession(true); + options.setConnectionTimeout(10); + if (mqttUser != null) { + options.setUserName(mqttUser); + } + if (mqttPassword != null) { + options.setPassword(mqttPassword); + } + client.connect(options); + } catch (MqttException e) { + throw new MqttGatewayException("Error initializing mqtt client.", e); + } + } + + public void publish(String topic, String message) throws MqttException { + MqttMessage msg = new MqttMessage(message.getBytes(StandardCharsets.UTF_8)); + msg.setQos(0); + msg.setRetained(false); + client.publish(topic, msg); + } + + public void publishRetained(String topic, String message) throws MqttException { + MqttMessage msg = new MqttMessage(message.getBytes(StandardCharsets.UTF_8)); + msg.setQos(0); + msg.setRetained(true); + client.publish(topic, msg); + } + + public void setCallback(MqttCallback callback) { + client.setCallback(callback); + } + + public void subscribe(String topic) throws MqttException { + client.subscribe(topic); + } +} diff --git a/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/MqttGatewayTopics.java b/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/MqttGatewayTopics.java index 4da2c4cc..0825c630 100644 --- a/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/MqttGatewayTopics.java +++ b/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/MqttGatewayTopics.java @@ -6,6 +6,7 @@ public class MqttGatewayTopics { public static final String CLIMATE_EXTERIOR_TEMPERATURE = CLIMATE + "/exteriorTemperature"; public static final String CLIMATE_INTERIOR_TEMPERATURE = CLIMATE + "/interiorTemperature"; public static final String CLIMATE_REMOTE_CLIMATE_STATE = CLIMATE + "/remoteClimateState"; + public static final String CLIMATE_REMOTE_TEMPERATURE = CLIMATE + "/remoteTemperature"; public static final String DOORS = "doors"; public static final String DOORS_BONNET = DOORS + "/bonnet"; public static final String DOORS_BOOT = DOORS + "/boot"; diff --git a/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/SaicMqttGateway.java b/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/SaicMqttGateway.java index 578dc4e8..387914c1 100644 --- a/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/SaicMqttGateway.java +++ b/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/SaicMqttGateway.java @@ -27,7 +27,6 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -53,11 +52,8 @@ import org.bn.annotations.ASN1Enum; import org.bn.annotations.ASN1Sequence; import org.bn.coders.IASN1PreparedElement; -import org.eclipse.paho.client.mqttv3.IMqttClient; import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken; import org.eclipse.paho.client.mqttv3.MqttCallback; -import org.eclipse.paho.client.mqttv3.MqttClient; -import org.eclipse.paho.client.mqttv3.MqttConnectOptions; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; import org.slf4j.Logger; @@ -165,7 +161,7 @@ public File convert(String value) throws Exception { split = ",") private Map vinAbrpTokenMap = new HashMap<>(); - private IMqttClient client; + private GatewayMqttClient client; private final Map vehicleHandlerMap = new HashMap<>(); @@ -174,149 +170,129 @@ public File convert(String value) throws Exception { @Override public Integer call() throws Exception { // your business logic goes here... - String publisherId = UUID.randomUUID().toString(); - try (IMqttClient client = - new MqttClient(mqttUri.toString(), publisherId, null) { + + var mqttAccountPrefix = "saic/" + saicUser; + + this.client = new GatewayMqttClient(mqttUri, mqttUser, mqttPassword); + + client.setCallback( + new MqttCallback() { @Override - public void close() throws MqttException { - disconnect(); - super.close(true); - } - }) { - this.client = client; - MqttConnectOptions options = new MqttConnectOptions(); - options.setAutomaticReconnect(true); - options.setCleanSession(true); - options.setConnectionTimeout(10); - if (mqttUser != null) { - options.setUserName(mqttUser); - } - if (mqttPassword != null) { - options.setPassword(mqttPassword); - } - client.connect(options); - - var mqttAccountPrefix = "saic/" + saicUser; - - client.setCallback( - new MqttCallback() { - @Override - public void connectionLost(Throwable cause) {} - - @Override - public void messageArrived(String topic, MqttMessage message) throws Exception { - LOGGER.info("Got message for topic {}: {}", topic, message); - final Matcher setValueMatcher = - Pattern.compile(".*/vehicles/([^/]*)/(.*)/set").matcher(topic); - final Matcher configurationValueMatcher = - Pattern.compile(".*/vehicles/([^/]*)/(.*)").matcher(topic); - if (setValueMatcher.matches()) { - new Thread( - () -> { - try { - vehicleHandlerMap - .get(setValueMatcher.group(1)) - .handleMQTTCommand(setValueMatcher.group(2), message); - } catch (MqttException e) { - LOGGER.error( - "Could not handle message for topic {}: {}", topic, message, e); - } - }) - .start(); - } else if (configurationValueMatcher.matches()) { - VehicleState vehicleState = - getVehicleState(mqttAccountPrefix, configurationValueMatcher.group(1)); - if (!vehicleState.isComplete()) { - vehicleState.configure(configurationValueMatcher.group(2), message); - } + public void connectionLost(Throwable cause) {} + + @Override + public void messageArrived(String topic, MqttMessage message) throws Exception { + LOGGER.info("Got message for topic {}: {}", topic, message); + final Matcher setValueMatcher = + Pattern.compile(".*/vehicles/([^/]*)/(.*)/set").matcher(topic); + final Matcher configurationValueMatcher = + Pattern.compile(".*/vehicles/([^/]*)/(.*)").matcher(topic); + if (setValueMatcher.matches()) { + new Thread( + () -> { + try { + vehicleHandlerMap + .get(setValueMatcher.group(1)) + .handleMQTTCommand(setValueMatcher.group(2), message); + } catch (MqttException e) { + LOGGER.error( + "Could not handle message for topic {}: {}", topic, message, e); + } + }) + .start(); + } else if (configurationValueMatcher.matches()) { + VehicleState vehicleState = + getVehicleState(mqttAccountPrefix, configurationValueMatcher.group(1)); + if (!vehicleState.isComplete()) { + vehicleState.configure(configurationValueMatcher.group(2), message); } } + } - @Override - public void deliveryComplete(IMqttDeliveryToken token) {} - }); + @Override + public void deliveryComplete(IMqttDeliveryToken token) {} + }); - client.subscribe(mqttAccountPrefix + "/vehicles/+/+/+/set"); - client.subscribe(mqttAccountPrefix + "/vehicles/+/+/+/+/set"); - client.subscribe(mqttAccountPrefix + "/vehicles/+/" + REFRESH_MODE); - client.subscribe(mqttAccountPrefix + "/vehicles/+/" + REFRESH_PERIOD + "/+"); + client.subscribe(mqttAccountPrefix + "/vehicles/+/+/+/set"); + client.subscribe(mqttAccountPrefix + "/vehicles/+/+/+/+/set"); + client.subscribe(mqttAccountPrefix + "/vehicles/+/" + REFRESH_MODE); + client.subscribe(mqttAccountPrefix + "/vehicles/+/" + REFRESH_PERIOD + "/+"); + + MessageCoder loginRequestMessageCoder = + new MessageCoder<>(MP_UserLoggingInReq.class); + + MP_UserLoggingInReq applicationData = new MP_UserLoggingInReq(); + applicationData.setPassword(saicPassword); + Message loginRequestMessage = + loginRequestMessageCoder.initializeMessage( + "0000000000000000000000000000000000000000000000000#".substring(saicUser.length()) + + saicUser, + null, + null, + "501", + 513, + 1, + applicationData); - MessageCoder loginRequestMessageCoder = - new MessageCoder<>(MP_UserLoggingInReq.class); + String loginRequest = loginRequestMessageCoder.encodeRequest(loginRequestMessage); - MP_UserLoggingInReq applicationData = new MP_UserLoggingInReq(); - applicationData.setPassword(saicPassword); - Message loginRequestMessage = - loginRequestMessageCoder.initializeMessage( - "0000000000000000000000000000000000000000000000000#".substring(saicUser.length()) - + saicUser, - null, - null, - "501", - 513, - 1, - applicationData); + LOGGER.debug(toJSON(anonymized(loginRequestMessageCoder, loginRequestMessage))); - String loginRequest = loginRequestMessageCoder.encodeRequest(loginRequestMessage); + String loginResponse = Client.sendRequest(saicUri.resolve("/TAP.Web/ota.mp"), loginRequest); - LOGGER.debug(toJSON(anonymized(loginRequestMessageCoder, loginRequestMessage))); + Message loginResponseMessage = + new MessageCoder<>(MP_UserLoggingInResp.class).decodeResponse(loginResponse); - String loginResponse = Client.sendRequest(saicUri.resolve("/TAP.Web/ota.mp"), loginRequest); + // register for all known alarm types (not all might be actually delivered) + for (MP_AlarmSettingType.EnumType type : MP_AlarmSettingType.EnumType.values()) { + registerAlarmMessage( + loginResponseMessage.getBody().getUid(), + loginResponseMessage.getApplicationData().getToken(), + type); + } - Message loginResponseMessage = - new MessageCoder<>(MP_UserLoggingInResp.class).decodeResponse(loginResponse); + LOGGER.debug( + toJSON(anonymized(new MessageCoder<>(MP_UserLoggingInResp.class), loginResponseMessage))); + List> futures = + loginResponseMessage.getApplicationData().getVinList().stream() + .map( + vin -> { + VehicleHandler handler = + new VehicleHandler( + this, + client, + saicUri, + loginResponseMessage.getBody().getUid(), + loginResponseMessage.getApplicationData().getToken(), + mqttAccountPrefix, + vin, + getVehicleState(mqttAccountPrefix, vin.getVin())); + vehicleHandlerMap.put(vin.getVin(), handler); + return handler; + }) + .map( + handler -> + (Callable) + () -> { + handler.handleVehicle(); + return null; + }) + .map(Executors.newSingleThreadExecutor()::submit) + .collect(Collectors.toList()); - // register for all known alarm types (not all might be actually delivered) - for (MP_AlarmSettingType.EnumType type : MP_AlarmSettingType.EnumType.values()) { - registerAlarmMessage( + ScheduledFuture pollingJob = + createMessagePoller( loginResponseMessage.getBody().getUid(), loginResponseMessage.getApplicationData().getToken(), - type); - } + mqttAccountPrefix); - LOGGER.debug( - toJSON(anonymized(new MessageCoder<>(MP_UserLoggingInResp.class), loginResponseMessage))); - List> futures = - loginResponseMessage.getApplicationData().getVinList().stream() - .map( - vin -> { - VehicleHandler handler = - new VehicleHandler( - this, - client, - saicUri, - loginResponseMessage.getBody().getUid(), - loginResponseMessage.getApplicationData().getToken(), - mqttAccountPrefix, - vin, - getVehicleState(mqttAccountPrefix, vin.getVin())); - vehicleHandlerMap.put(vin.getVin(), handler); - return handler; - }) - .map( - handler -> - (Callable) - () -> { - handler.handleVehicle(); - return null; - }) - .map(Executors.newSingleThreadExecutor()::submit) - .collect(Collectors.toList()); - - ScheduledFuture pollingJob = - createMessagePoller( - loginResponseMessage.getBody().getUid(), - loginResponseMessage.getApplicationData().getToken(), - mqttAccountPrefix); - - futures.add(pollingJob); - - for (Future future : futures) { - // make sure we wait on all futures before exiting - future.get(); - } - return 0; + futures.add(pollingJob); + + for (Future future : futures) { + // make sure we wait on all futures before exiting + future.get(); } + return 0; } private VehicleState getVehicleState(String mqttAccountPrefix, String vin) { @@ -526,14 +502,8 @@ public String getAbrpUserToken(String vin) { } public void notifyMessage(String mqttMessagePrefix, SaicMessage message) throws MqttException { - MqttMessage msg = - new MqttMessage(SaicMqttGateway.toJSON(message).getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - // Don't retain, so deleted messages are removed - // automatically from the broker - msg.setRetained(false); - client.publish(mqttMessagePrefix + "/" + message.getMessageId(), msg); - + client.publish( + mqttMessagePrefix + "/" + message.getMessageId(), SaicMqttGateway.toJSON(message)); if (message.getVin() != null) { vehicleHandlerMap.get(message.getVin()).notifyMessage(message); } diff --git a/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/SaicService.java b/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/SaicService.java new file mode 100644 index 00000000..4fd968d5 --- /dev/null +++ b/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/SaicService.java @@ -0,0 +1,500 @@ +package net.heberling.ismart.mqtt; + +import static net.heberling.ismart.mqtt.MqttGatewayTopics.*; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.time.OffsetDateTime; +import java.util.function.Supplier; +import net.heberling.ismart.Client; +import net.heberling.ismart.asn1.v2_1.Message; +import net.heberling.ismart.asn1.v2_1.MessageCoder; +import net.heberling.ismart.asn1.v2_1.entity.OTA_RVMVehicleStatusReq; +import net.heberling.ismart.asn1.v2_1.entity.OTA_RVMVehicleStatusResp25857; +import net.heberling.ismart.asn1.v3_0.entity.OTA_ChrgMangDataResp; +import org.bn.coders.IASN1PreparedElement; +import org.eclipse.paho.client.mqttv3.MqttException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SaicService { + + private static final Logger LOGGER = LoggerFactory.getLogger(SaicService.class); + private final URI saicUri; + private final String mqttVINPrefix; + private final VehicleState vehicleState; + private final GatewayMqttClient client; + private final Supplier clockSupplier; + + public SaicService( + URI saicUri, String mqttVINPrefix, VehicleState vehicleState, GatewayMqttClient client) { + this(saicUri, mqttVINPrefix, vehicleState, client, Clock::systemDefaultZone); + } + + protected SaicService( + URI saicUri, + String mqttVINPrefix, + VehicleState vehicleState, + GatewayMqttClient client, + Supplier clockSupplier) { + this.saicUri = saicUri; + this.mqttVINPrefix = mqttVINPrefix; + this.vehicleState = vehicleState; + this.client = client; + this.clockSupplier = clockSupplier; + } + + public OTA_RVMVehicleStatusResp25857 updateVehicleStatus( + String uid, String token, String vin, VehicleHandler vehicleHandler) + throws IOException, MqttException { + MessageCoder otaRvmVehicleStatusReqMessageCoder = + new MessageCoder<>(OTA_RVMVehicleStatusReq.class); + + OTA_RVMVehicleStatusReq otaRvmVehicleStatusReq = new OTA_RVMVehicleStatusReq(); + otaRvmVehicleStatusReq.setVehStatusReqType(2); + net.heberling.ismart.asn1.v2_1.Message vehicleStatusRequestMessage = + otaRvmVehicleStatusReqMessageCoder.initializeMessage( + uid, token, vin, "511", 25857, 1, otaRvmVehicleStatusReq); + + String vehicleStatusRequest = + otaRvmVehicleStatusReqMessageCoder.encodeRequest(vehicleStatusRequestMessage); + + String vehicleStatusResponse = + Client.sendRequest(saicUri.resolve("/TAP.Web/ota.mpv21"), vehicleStatusRequest); + + net.heberling.ismart.asn1.v2_1.Message + vehicleStatusResponseMessage = + new MessageCoder<>(OTA_RVMVehicleStatusResp25857.class) + .decodeResponse(vehicleStatusResponse); + + // we get an eventId back... + vehicleStatusRequestMessage + .getBody() + .setEventID(vehicleStatusResponseMessage.getBody().getEventID()); + // ... use that to request the data again, until we have it + while (vehicleStatusResponseMessage.getApplicationData() == null) { + + if (vehicleStatusResponseMessage.getBody().isErrorMessagePresent()) { + + if (vehicleStatusResponseMessage.getBody().getResult() == 2) { + // TODO: relogn + } + + throw new MqttGatewayException( + "Refreshing Vehicle State from SAIC API failed with message: " + + new String( + vehicleStatusResponseMessage.getBody().getErrorMessage(), + StandardCharsets.UTF_8)); + } + + vehicleStatusRequestMessage.getBody().setUid(uid); + vehicleStatusRequestMessage.getBody().setToken(token); + + SaicMqttGateway.fillReserved(vehicleStatusRequestMessage.getReserved()); + + vehicleStatusRequest = + otaRvmVehicleStatusReqMessageCoder.encodeRequest(vehicleStatusRequestMessage); + + vehicleStatusResponse = + Client.sendRequest(saicUri.resolve("/TAP.Web/ota.mpv21"), vehicleStatusRequest); + + vehicleStatusResponseMessage = + new MessageCoder<>(OTA_RVMVehicleStatusResp25857.class) + .decodeResponse(vehicleStatusResponse); + + LOGGER.debug( + SaicMqttGateway.toJSON( + SaicMqttGateway.anonymized( + new MessageCoder<>(OTA_RVMVehicleStatusResp25857.class), + vehicleStatusResponseMessage))); + } + + handleVehicleStatusMessage(vehicleStatusResponseMessage); + return vehicleStatusResponseMessage.getApplicationData(); + } + + private void handleVehicleStatusMessage( + Message vehicleStatusResponseMessage) throws MqttException { + boolean engineRunning = + vehicleStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getEngineStatus() + == 1; + boolean isCharging = + vehicleStatusResponseMessage + .getApplicationData() + .getBasicVehicleStatus() + .isExtendedData2Present() + && vehicleStatusResponseMessage + .getApplicationData() + .getBasicVehicleStatus() + .getExtendedData2() + >= 1; + + Integer remoteClimateStatus = + vehicleStatusResponseMessage + .getApplicationData() + .getBasicVehicleStatus() + .getRemoteClimateStatus(); + + vehicleState.setRemoteClimateStatus(remoteClimateStatus); + vehicleState.setHVBatteryActive(isCharging || engineRunning || remoteClimateStatus > 0); + + client.publishRetained( + mqttVINPrefix + + "/" + + INTERNAL + + "/" + + vehicleStatusResponseMessage.getBody().getApplicationID() + + "_" + + vehicleStatusResponseMessage.getBody().getApplicationDataProtocolVersion() + + "/json", + SaicMqttGateway.toJSON(vehicleStatusResponseMessage)); + + client.publishRetained(mqttVINPrefix + "/" + DRIVETRAIN_RUNNING, String.valueOf(engineRunning)); + + client.publishRetained(mqttVINPrefix + "/" + DRIVETRAIN_CHARGING, String.valueOf(isCharging)); + + Integer interiorTemperature = + vehicleStatusResponseMessage + .getApplicationData() + .getBasicVehicleStatus() + .getInteriorTemperature(); + if (interiorTemperature > -128) { + client.publishRetained( + mqttVINPrefix + "/" + CLIMATE_INTERIOR_TEMPERATURE, String.valueOf(interiorTemperature)); + } + + Integer exteriorTemperature = + vehicleStatusResponseMessage + .getApplicationData() + .getBasicVehicleStatus() + .getExteriorTemperature(); + if (exteriorTemperature > -128) { + client.publishRetained( + mqttVINPrefix + "/" + CLIMATE_EXTERIOR_TEMPERATURE, String.valueOf(exteriorTemperature)); + } + + client.publishRetained( + mqttVINPrefix + "/" + DRIVETRAIN_AUXILIARY_BATTERY_VOLTAGE, + String.valueOf( + vehicleStatusResponseMessage + .getApplicationData() + .getBasicVehicleStatus() + .getBatteryVoltage() + / 10.d)); + + client.publishRetained( + mqttVINPrefix + "/" + LOCATION_POSITION, + SaicMqttGateway.toJSON( + vehicleStatusResponseMessage + .getApplicationData() + .getGpsPosition() + .getWayPoint() + .getPosition())); + + client.publishRetained( + mqttVINPrefix + "/" + LOCATION_SPEED, + String.valueOf( + vehicleStatusResponseMessage + .getApplicationData() + .getGpsPosition() + .getWayPoint() + .getSpeed() + / 10d)); + + client.publishRetained( + mqttVINPrefix + "/" + LOCATION_HEADING, + String.valueOf( + vehicleStatusResponseMessage + .getApplicationData() + .getGpsPosition() + .getWayPoint() + .getHeading() + / 10d)); + + client.publishRetained( + mqttVINPrefix + "/" + DOORS_LOCKED, + String.valueOf( + vehicleStatusResponseMessage + .getApplicationData() + .getBasicVehicleStatus() + .getLockStatus())); + + // todo check configuration for available doors + + client.publishRetained( + mqttVINPrefix + "/" + DOORS_DRIVER, + String.valueOf( + vehicleStatusResponseMessage + .getApplicationData() + .getBasicVehicleStatus() + .getDriverDoor())); + + client.publishRetained( + mqttVINPrefix + "/" + DOORS_PASSENGER, + String.valueOf( + vehicleStatusResponseMessage + .getApplicationData() + .getBasicVehicleStatus() + .getPassengerDoor())); + + client.publishRetained( + mqttVINPrefix + "/" + DOORS_REAR_LEFT, + String.valueOf( + vehicleStatusResponseMessage + .getApplicationData() + .getBasicVehicleStatus() + .getRearLeftDoor())); + + client.publishRetained( + mqttVINPrefix + "/" + DOORS_REAR_RIGHT, + String.valueOf( + vehicleStatusResponseMessage + .getApplicationData() + .getBasicVehicleStatus() + .getRearRightDoor())); + + client.publishRetained( + mqttVINPrefix + "/" + DOORS_BOOT, + String.valueOf( + vehicleStatusResponseMessage + .getApplicationData() + .getBasicVehicleStatus() + .getBootStatus())); + + client.publishRetained( + mqttVINPrefix + "/" + DOORS_BONNET, + String.valueOf( + vehicleStatusResponseMessage + .getApplicationData() + .getBasicVehicleStatus() + .getBonnetStatus())); + + client.publishRetained( + mqttVINPrefix + "/" + TYRES_FRONT_LEFT_PRESSURE, + String.valueOf( + vehicleStatusResponseMessage + .getApplicationData() + .getBasicVehicleStatus() + .getFrontLeftTyrePressure() + * 4 + / 100d)); + + client.publishRetained( + mqttVINPrefix + "/" + TYRES_FRONT_RIGHT_PRESSURE, + String.valueOf( + vehicleStatusResponseMessage + .getApplicationData() + .getBasicVehicleStatus() + .getFrontRrightTyrePressure() + * 4 + / 100d)); + + client.publishRetained( + mqttVINPrefix + "/" + TYRES_REAR_LEFT_PRESSURE, + String.valueOf( + vehicleStatusResponseMessage + .getApplicationData() + .getBasicVehicleStatus() + .getRearLeftTyrePressure() + * 4 + / 100d)); + client.publishRetained( + mqttVINPrefix + "/" + TYRES_REAR_RIGHT_PRESSURE, + String.valueOf( + vehicleStatusResponseMessage + .getApplicationData() + .getBasicVehicleStatus() + .getRearRightTyrePressure() + * 4 + / 100d)); + + client.publishRetained( + mqttVINPrefix + "/" + TYRES_REAR_RIGHT_PRESSURE, + String.valueOf( + vehicleStatusResponseMessage + .getApplicationData() + .getBasicVehicleStatus() + .getRearRightTyrePressure() + * 4 + / 100d)); + + client.publishRetained( + mqttVINPrefix + "/" + CLIMATE_REMOTE_CLIMATE_STATE, + vehicleState.getHvacSettings().toRemoteClimate(remoteClimateStatus)); + + client.publishRetained( + mqttVINPrefix + "/" + CLIMATE_BACK_WINDOW_HEAT, + String.valueOf( + vehicleStatusResponseMessage + .getApplicationData() + .getBasicVehicleStatus() + .getRmtHtdRrWndSt())); + + if (vehicleStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getMileage() + > 0) { + // sometimes mileage is 0, ignore such values + client.publishRetained( + mqttVINPrefix + "/" + DRIVETRAIN_MILEAGE, + String.valueOf( + vehicleStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getMileage() + / 10.d)); + + // if the milage is 0, the electric range is also 0 + + client.publishRetained( + mqttVINPrefix + "/" + DRIVETRAIN_RANGE, + String.valueOf( + vehicleStatusResponseMessage + .getApplicationData() + .getBasicVehicleStatus() + .getFuelRangeElec() + / 10.d)); + } + + client.publishRetained( + mqttVINPrefix + "/" + REFRESH_LAST_VEHICLE_STATE, + OffsetDateTime.now(clockSupplier.get()).toString()); + } + + OTA_ChrgMangDataResp updateChargeStatus( + String uid, String token, String vin, VehicleHandler vehicleHandler) + throws IOException, MqttException { + net.heberling.ismart.asn1.v3_0.MessageCoder + chargingStatusRequestMessageEncoder = + new net.heberling.ismart.asn1.v3_0.MessageCoder<>(IASN1PreparedElement.class); + + net.heberling.ismart.asn1.v3_0.Message chargingStatusMessage = + chargingStatusRequestMessageEncoder.initializeMessage(uid, token, vin, "516", 768, 5, null); + + String chargingStatusRequestMessage = + chargingStatusRequestMessageEncoder.encodeRequest(chargingStatusMessage); + + LOGGER.debug( + SaicMqttGateway.toJSON( + SaicMqttGateway.anonymized( + chargingStatusRequestMessageEncoder, chargingStatusMessage))); + + String chargingStatusResponse = + Client.sendRequest(saicUri.resolve("/TAP.Web/ota.mpv30"), chargingStatusRequestMessage); + + net.heberling.ismart.asn1.v3_0.Message chargingStatusResponseMessage = + new net.heberling.ismart.asn1.v3_0.MessageCoder<>(OTA_ChrgMangDataResp.class) + .decodeResponse(chargingStatusResponse); + + LOGGER.debug( + SaicMqttGateway.toJSON( + SaicMqttGateway.anonymized( + new net.heberling.ismart.asn1.v3_0.MessageCoder<>(OTA_ChrgMangDataResp.class), + chargingStatusResponseMessage))); + + // we get an eventId back... + chargingStatusMessage + .getBody() + .setEventID(chargingStatusResponseMessage.getBody().getEventID()); + // ... use that to request the data again, until we have it + while (chargingStatusResponseMessage.getApplicationData() == null) { + + if (chargingStatusResponseMessage.getBody().isErrorMessagePresent()) { + if (chargingStatusResponseMessage.getBody().getResult() == 2) { + // TODO: relogn + } + LOGGER.error( + "Refreshing Charging State from SAIC API failed with message: {}", + new String( + chargingStatusResponseMessage.getBody().getErrorMessage(), StandardCharsets.UTF_8)); + return null; + } + + SaicMqttGateway.fillReserved(chargingStatusMessage.getReserved()); + + LOGGER.debug( + SaicMqttGateway.toJSON( + SaicMqttGateway.anonymized( + chargingStatusRequestMessageEncoder, chargingStatusMessage))); + + chargingStatusRequestMessage = + chargingStatusRequestMessageEncoder.encodeRequest(chargingStatusMessage); + + chargingStatusResponse = + Client.sendRequest(saicUri.resolve("/TAP.Web/ota.mpv30"), chargingStatusRequestMessage); + + chargingStatusResponseMessage = + new net.heberling.ismart.asn1.v3_0.MessageCoder<>(OTA_ChrgMangDataResp.class) + .decodeResponse(chargingStatusResponse); + + LOGGER.debug( + SaicMqttGateway.toJSON( + SaicMqttGateway.anonymized( + new net.heberling.ismart.asn1.v3_0.MessageCoder<>(OTA_ChrgMangDataResp.class), + chargingStatusResponseMessage))); + } + handleChargeStatusMessage(chargingStatusResponseMessage); + + return chargingStatusResponseMessage.getApplicationData(); + } + + private void handleChargeStatusMessage( + net.heberling.ismart.asn1.v3_0.Message chargingStatusResponseMessage) + throws MqttException { + + client.publishRetained( + mqttVINPrefix + + "/" + + INTERNAL + + "/" + + chargingStatusResponseMessage.getBody().getApplicationID() + + "_" + + chargingStatusResponseMessage.getBody().getApplicationDataProtocolVersion() + + "/json", + SaicMqttGateway.toJSON(chargingStatusResponseMessage)); + + double current = + chargingStatusResponseMessage.getApplicationData().getBmsPackCrnt() * 0.05d - 1000.0d; + client.publishRetained(mqttVINPrefix + "/" + DRIVETRAIN_CURRENT, String.valueOf(current)); + + double voltage = + (double) chargingStatusResponseMessage.getApplicationData().getBmsPackVol() * 0.25d; + + client.publishRetained(mqttVINPrefix + "/" + DRIVETRAIN_VOLTAGE, String.valueOf(voltage)); + + int remainingChargingTime = 0; + if (chargingStatusResponseMessage.getApplicationData().getChargeStatus().getChargingGunState() + && current < 0) { + remainingChargingTime = + chargingStatusResponseMessage.getApplicationData().getChrgngRmnngTime() * 60; + } + client.publishRetained( + mqttVINPrefix + "/" + DRIVETRAIN_REMAINING_CHARGING_TIME, + String.valueOf(remainingChargingTime)); + + double power = current * voltage / 1000d; + client.publishRetained(mqttVINPrefix + "/" + DRIVETRAIN_POWER, String.valueOf(power)); + + client.publishRetained( + mqttVINPrefix + "/" + DRIVETRAIN_CHARGER_CONNECTED, + String.valueOf( + chargingStatusResponseMessage + .getApplicationData() + .getChargeStatus() + .getChargingGunState())); + + client.publishRetained( + mqttVINPrefix + "/" + DRIVETRAIN_CHARGING_TYPE, + String.valueOf( + chargingStatusResponseMessage + .getApplicationData() + .getChargeStatus() + .getChargingType())); + + client.publishRetained( + mqttVINPrefix + "/" + DRIVETRAIN_SOC, + String.valueOf( + chargingStatusResponseMessage.getApplicationData().getBmsPackSOCDsp() / 10d)); + + client.publishRetained( + mqttVINPrefix + "/" + REFRESH_LAST_CHARGE_STATE, + OffsetDateTime.now(clockSupplier.get()).toString()); + } +} diff --git a/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/VehicleHandler.java b/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/VehicleHandler.java index a2084fea..41127345 100644 --- a/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/VehicleHandler.java +++ b/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/VehicleHandler.java @@ -6,7 +6,6 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; -import java.nio.charset.StandardCharsets; import java.time.OffsetDateTime; import java.util.*; import java.util.concurrent.ExecutionException; @@ -17,15 +16,13 @@ import net.heberling.ismart.asn1.v2_1.MessageCoder; import net.heberling.ismart.asn1.v2_1.entity.OTA_RVCReq; import net.heberling.ismart.asn1.v2_1.entity.OTA_RVCStatus25857; -import net.heberling.ismart.asn1.v2_1.entity.OTA_RVMVehicleStatusReq; import net.heberling.ismart.asn1.v2_1.entity.OTA_RVMVehicleStatusResp25857; import net.heberling.ismart.asn1.v2_1.entity.RvcReqParam; import net.heberling.ismart.asn1.v3_0.Message; import net.heberling.ismart.asn1.v3_0.entity.OTA_ChrgCtrlReq; import net.heberling.ismart.asn1.v3_0.entity.OTA_ChrgCtrlStsResp; import net.heberling.ismart.asn1.v3_0.entity.OTA_ChrgMangDataResp; -import org.bn.coders.IASN1PreparedElement; -import org.eclipse.paho.client.mqttv3.IMqttClient; +import net.heberling.ismart.mqtt.carconfig.DefaultHVACSettings; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; import org.slf4j.Logger; @@ -39,13 +36,16 @@ public class VehicleHandler { private final String token; private final VinInfo vinInfo; private final SaicMqttGateway saicMqttGateway; - private final IMqttClient client; + private final GatewayMqttClient client; private final VehicleState vehicleState; + private final DefaultHVACSettings hvacSettings; + + private final SaicService saicService; public VehicleHandler( SaicMqttGateway saicMqttGateway, - IMqttClient client, + GatewayMqttClient client, URI saicUri, String uid, String token, @@ -60,6 +60,13 @@ public VehicleHandler( this.token = token; this.vinInfo = vinInfo; this.vehicleState = vehicleState; + switch (vinInfo.getSeries()) { + // TODO which extra cases do we need? + default: + this.hvacSettings = new DefaultHVACSettings(); + } + vehicleState.setHvacSettings(hvacSettings); + saicService = new SaicService(saicUri, mqttAccountPrefix, vehicleState, client); } void handleVehicle() throws MqttException, IOException { @@ -76,18 +83,17 @@ void handleVehicle() throws MqttException, IOException { try { OTA_RVMVehicleStatusResp25857 vehicleStatus = - updateVehicleStatus(uid, token, vinInfo.getVin()); + saicService.updateVehicleStatus(uid, token, vinInfo.getVin(), this); - OTA_ChrgMangDataResp chargeStatus = updateChargeStatus(uid, token, vinInfo.getVin()); + OTA_ChrgMangDataResp chargeStatus = + saicService.updateChargeStatus(uid, token, vinInfo.getVin(), this); final String abrpApiKey = saicMqttGateway.getAbrpApiKey(); final String abrpUserToken = saicMqttGateway.getAbrpUserToken(vinInfo.getVin()); if (abrpApiKey != null && abrpUserToken != null && vehicleStatus != null) { String abrpResponse = ABRP.updateAbrp(abrpApiKey, abrpUserToken, vehicleStatus, chargeStatus); - MqttMessage msg = new MqttMessage(abrpResponse.getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(vehicleState.getMqttVINPrefix() + "/" + INTERNAL_ABRP, msg); + client.publishRetained( + vehicleState.getMqttVINPrefix() + "/" + INTERNAL_ABRP, abrpResponse); } if (Objects.isNull(chargeStatus)) { updateFallbackChargeStateData(vehicleStatus); @@ -113,161 +119,10 @@ void handleVehicle() throws MqttException, IOException { private void updateFallbackChargeStateData(OTA_RVMVehicleStatusResp25857 vehicleStatus) throws MqttException { LOGGER.warn("Extracting SOC from vehicle status as charge state update failed..."); - MqttMessage msg = - new MqttMessage( - vehicleStatus - .getBasicVehicleStatus() - .getExtendedData1() - .toString() - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(vehicleState.getMqttVINPrefix() + "/" + DRIVETRAIN_SOC, msg); - } - - private OTA_RVMVehicleStatusResp25857 updateVehicleStatus(String uid, String token, String vin) - throws IOException, MqttException { - MessageCoder otaRvmVehicleStatusReqMessageCoder = - new MessageCoder<>(OTA_RVMVehicleStatusReq.class); - - OTA_RVMVehicleStatusReq otaRvmVehicleStatusReq = new OTA_RVMVehicleStatusReq(); - otaRvmVehicleStatusReq.setVehStatusReqType(2); - net.heberling.ismart.asn1.v2_1.Message vehicleStatusRequestMessage = - otaRvmVehicleStatusReqMessageCoder.initializeMessage( - uid, token, vin, "511", 25857, 1, otaRvmVehicleStatusReq); - - String vehicleStatusRequest = - otaRvmVehicleStatusReqMessageCoder.encodeRequest(vehicleStatusRequestMessage); - - String vehicleStatusResponse = - Client.sendRequest(saicUri.resolve("/TAP.Web/ota.mpv21"), vehicleStatusRequest); - - net.heberling.ismart.asn1.v2_1.Message - vehicleStatusResponseMessage = - new net.heberling.ismart.asn1.v2_1.MessageCoder<>(OTA_RVMVehicleStatusResp25857.class) - .decodeResponse(vehicleStatusResponse); - - // we get an eventId back... - vehicleStatusRequestMessage - .getBody() - .setEventID(vehicleStatusResponseMessage.getBody().getEventID()); - // ... use that to request the data again, until we have it - while (vehicleStatusResponseMessage.getApplicationData() == null) { - - if (vehicleStatusResponseMessage.getBody().isErrorMessagePresent()) { - - if (vehicleStatusResponseMessage.getBody().getResult() == 2) { - // TODO: relogn - } - - throw new MqttGatewayException( - "Refreshing Vehicle State from SAIC API failed with message: " - + new String( - vehicleStatusResponseMessage.getBody().getErrorMessage(), - StandardCharsets.UTF_8)); - } - - vehicleStatusRequestMessage.getBody().setUid(uid); - vehicleStatusRequestMessage.getBody().setToken(token); - - SaicMqttGateway.fillReserved(vehicleStatusRequestMessage.getReserved()); - - vehicleStatusRequest = - otaRvmVehicleStatusReqMessageCoder.encodeRequest(vehicleStatusRequestMessage); - - vehicleStatusResponse = - Client.sendRequest(saicUri.resolve("/TAP.Web/ota.mpv21"), vehicleStatusRequest); - - vehicleStatusResponseMessage = - new net.heberling.ismart.asn1.v2_1.MessageCoder<>(OTA_RVMVehicleStatusResp25857.class) - .decodeResponse(vehicleStatusResponse); - - LOGGER.debug( - SaicMqttGateway.toJSON( - SaicMqttGateway.anonymized( - new net.heberling.ismart.asn1.v2_1.MessageCoder<>( - OTA_RVMVehicleStatusResp25857.class), - vehicleStatusResponseMessage))); - } - - vehicleState.handleVehicleStatusMessage(vehicleStatusResponseMessage); - return vehicleStatusResponseMessage.getApplicationData(); - } - private OTA_ChrgMangDataResp updateChargeStatus(String uid, String token, String vin) - throws IOException, MqttException { - net.heberling.ismart.asn1.v3_0.MessageCoder - chargingStatusRequestMessageEncoder = - new net.heberling.ismart.asn1.v3_0.MessageCoder<>(IASN1PreparedElement.class); - - net.heberling.ismart.asn1.v3_0.Message chargingStatusMessage = - chargingStatusRequestMessageEncoder.initializeMessage(uid, token, vin, "516", 768, 5, null); - - String chargingStatusRequestMessage = - chargingStatusRequestMessageEncoder.encodeRequest(chargingStatusMessage); - - LOGGER.debug( - SaicMqttGateway.toJSON( - SaicMqttGateway.anonymized( - chargingStatusRequestMessageEncoder, chargingStatusMessage))); - - String chargingStatusResponse = - Client.sendRequest(saicUri.resolve("/TAP.Web/ota.mpv30"), chargingStatusRequestMessage); - - net.heberling.ismart.asn1.v3_0.Message chargingStatusResponseMessage = - new net.heberling.ismart.asn1.v3_0.MessageCoder<>(OTA_ChrgMangDataResp.class) - .decodeResponse(chargingStatusResponse); - - LOGGER.debug( - SaicMqttGateway.toJSON( - SaicMqttGateway.anonymized( - new net.heberling.ismart.asn1.v3_0.MessageCoder<>(OTA_ChrgMangDataResp.class), - chargingStatusResponseMessage))); - - // we get an eventId back... - chargingStatusMessage - .getBody() - .setEventID(chargingStatusResponseMessage.getBody().getEventID()); - // ... use that to request the data again, until we have it - while (chargingStatusResponseMessage.getApplicationData() == null) { - - if (chargingStatusResponseMessage.getBody().isErrorMessagePresent()) { - if (chargingStatusResponseMessage.getBody().getResult() == 2) { - // TODO: relogn - } - LOGGER.error( - "Refreshing Charging State from SAIC API failed with message: {}", - new String( - chargingStatusResponseMessage.getBody().getErrorMessage(), StandardCharsets.UTF_8)); - return null; - } - - SaicMqttGateway.fillReserved(chargingStatusMessage.getReserved()); - - LOGGER.debug( - SaicMqttGateway.toJSON( - SaicMqttGateway.anonymized( - chargingStatusRequestMessageEncoder, chargingStatusMessage))); - - chargingStatusRequestMessage = - chargingStatusRequestMessageEncoder.encodeRequest(chargingStatusMessage); - - chargingStatusResponse = - Client.sendRequest(saicUri.resolve("/TAP.Web/ota.mpv30"), chargingStatusRequestMessage); - - chargingStatusResponseMessage = - new net.heberling.ismart.asn1.v3_0.MessageCoder<>(OTA_ChrgMangDataResp.class) - .decodeResponse(chargingStatusResponse); - - LOGGER.debug( - SaicMqttGateway.toJSON( - SaicMqttGateway.anonymized( - new net.heberling.ismart.asn1.v3_0.MessageCoder<>(OTA_ChrgMangDataResp.class), - chargingStatusResponseMessage))); - } - vehicleState.handleChargeStatusMessage(chargingStatusResponseMessage); - - return chargingStatusResponseMessage.getApplicationData(); + client.publishRetained( + vehicleState.getMqttVINPrefix() + "/" + DRIVETRAIN_SOC, + vehicleStatus.getBasicVehicleStatus().getExtendedData1().toString()); } public void notifyMessage(SaicMessage message) throws MqttException { @@ -488,10 +343,13 @@ public void handleMQTTCommand(String topic, MqttMessage message) throws MqttExce sendACCommand((byte) 0, (byte) 0); break; case "on": - sendACCommand((byte) 2, (byte) 8); + sendACCommand( + (byte) 2, hvacSettings.mapTempToSaicApi(vehicleState.getRemoteTemperature())); break; case "front": - sendACCommand((byte) 5, (byte) 8); + sendACCommand( + (byte) 5, + (byte) hvacSettings.mapTempToSaicApi(vehicleState.getRemoteTemperature())); case "blowingOnly": sendACBlowingCommand(true); break; @@ -527,11 +385,7 @@ public void handleMQTTCommand(String topic, MqttMessage message) throws MqttExce default: vehicleState.configure(topic, message); } - MqttMessage msg = new MqttMessage("Success".getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(false); - client.publish(vehicleState.getMqttVINPrefix() + "/" + topic + "/result", msg); - + client.publish(vehicleState.getMqttVINPrefix() + "/" + topic + "/result", "Success"); vehicleState.setRefreshMode(FORCE); } catch (URISyntaxException @@ -541,11 +395,10 @@ public void handleMQTTCommand(String topic, MqttMessage message) throws MqttExce | IOException | MqttGatewayException e) { LOGGER.error("Command {} failed with {}.", topic, message, e); - MqttMessage msg = - new MqttMessage(("Command failed. " + e.getMessage()).getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(false); - client.publish(vehicleState.getMqttVINPrefix() + "/" + topic + "/result", msg); + + client.publish( + vehicleState.getMqttVINPrefix() + "/" + topic + "/result", + "Command failed. " + e.getMessage()); } } } diff --git a/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/VehicleState.java b/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/VehicleState.java index 1188c38c..987ea54b 100644 --- a/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/VehicleState.java +++ b/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/VehicleState.java @@ -4,18 +4,15 @@ import static net.heberling.ismart.mqtt.RefreshMode.FORCE; import static net.heberling.ismart.mqtt.RefreshMode.PERIODIC; -import java.nio.charset.StandardCharsets; import java.time.Clock; import java.time.OffsetDateTime; import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.Map; +import java.util.Objects; import java.util.function.Supplier; import net.heberling.ismart.asn1.v1_1.entity.VinInfo; -import net.heberling.ismart.asn1.v2_1.Message; -import net.heberling.ismart.asn1.v2_1.entity.OTA_RVMVehicleStatusResp25857; -import net.heberling.ismart.asn1.v3_0.entity.OTA_ChrgMangDataResp; -import org.eclipse.paho.client.mqttv3.IMqttClient; +import net.heberling.ismart.mqtt.carconfig.HVACSettings; import org.eclipse.paho.client.mqttv3.MqttException; import org.eclipse.paho.client.mqttv3.MqttMessage; import org.slf4j.Logger; @@ -24,7 +21,7 @@ public class VehicleState { private static final Logger LOGGER = LoggerFactory.getLogger(VehicleState.class); - private final IMqttClient client; + private final GatewayMqttClient client; private final String mqttVINPrefix; private final Supplier clockSupplier; private OffsetDateTime lastCarActivity; @@ -38,13 +35,33 @@ public class VehicleState { private Long refreshPeriodAfterShutdown; private RefreshMode refreshMode; private RefreshMode previousRefreshMode; + private Integer remoteTemperature; - public VehicleState(IMqttClient client, String mqttAccountPrefix, String vin) { + public HVACSettings getHvacSettings() { + return hvacSettings; + } + + public Integer getRemoteClimateStatus() { + return remoteClimateStatus; + } + + private HVACSettings hvacSettings; + + public void setRemoteClimateStatus(Integer remoteClimateStatus) { + this.remoteClimateStatus = remoteClimateStatus; + } + + private Integer remoteClimateStatus; + + public VehicleState(GatewayMqttClient client, String mqttAccountPrefix, String vin) { this(client, mqttAccountPrefix, vin, Clock::systemDefaultZone); } protected VehicleState( - IMqttClient client, String mqttAccountPrefix, String vin, Supplier clockSupplier) { + GatewayMqttClient client, + String mqttAccountPrefix, + String vin, + Supplier clockSupplier) { this.client = client; this.mqttVINPrefix = mqttAccountPrefix + "/" + VEHICLES + "/" + vin; this.clockSupplier = clockSupplier; @@ -55,455 +72,20 @@ public String getMqttVINPrefix() { return mqttVINPrefix; } - public void handleVehicleStatusMessage( - Message vehicleStatusResponseMessage) throws MqttException { - boolean engineRunning = - vehicleStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getEngineStatus() - == 1; - boolean isCharging = - vehicleStatusResponseMessage - .getApplicationData() - .getBasicVehicleStatus() - .isExtendedData2Present() - && vehicleStatusResponseMessage - .getApplicationData() - .getBasicVehicleStatus() - .getExtendedData2() - >= 1; - - final Integer remoteClimateStatus = - vehicleStatusResponseMessage - .getApplicationData() - .getBasicVehicleStatus() - .getRemoteClimateStatus(); - - setHVBatteryActive(isCharging || engineRunning || remoteClimateStatus > 0); - - MqttMessage msg = - new MqttMessage( - SaicMqttGateway.toJSON(vehicleStatusResponseMessage).getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish( - mqttVINPrefix - + "/" - + INTERNAL - + "/" - + vehicleStatusResponseMessage.getBody().getApplicationID() - + "_" - + vehicleStatusResponseMessage.getBody().getApplicationDataProtocolVersion() - + "/json", - msg); - - msg = new MqttMessage(String.valueOf(engineRunning).getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + DRIVETRAIN_RUNNING, msg); - - msg = new MqttMessage(String.valueOf(isCharging).getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + DRIVETRAIN_CHARGING, msg); - - Integer interiorTemperature = - vehicleStatusResponseMessage - .getApplicationData() - .getBasicVehicleStatus() - .getInteriorTemperature(); - if (interiorTemperature > -128) { - msg = new MqttMessage(String.valueOf(interiorTemperature).getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + CLIMATE_INTERIOR_TEMPERATURE, msg); - } - - Integer exteriorTemperature = - vehicleStatusResponseMessage - .getApplicationData() - .getBasicVehicleStatus() - .getExteriorTemperature(); - if (exteriorTemperature > -128) { - msg = new MqttMessage(String.valueOf(exteriorTemperature).getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + CLIMATE_EXTERIOR_TEMPERATURE, msg); - } - - msg = - new MqttMessage( - String.valueOf( - vehicleStatusResponseMessage - .getApplicationData() - .getBasicVehicleStatus() - .getBatteryVoltage() - / 10.d) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + DRIVETRAIN_AUXILIARY_BATTERY_VOLTAGE, msg); - - msg = - new MqttMessage( - SaicMqttGateway.toJSON( - vehicleStatusResponseMessage - .getApplicationData() - .getGpsPosition() - .getWayPoint() - .getPosition()) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + LOCATION_POSITION, msg); - - msg = - new MqttMessage( - String.valueOf( - vehicleStatusResponseMessage - .getApplicationData() - .getGpsPosition() - .getWayPoint() - .getSpeed() - / 10d) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + LOCATION_SPEED, msg); - - msg = - new MqttMessage( - String.valueOf( - vehicleStatusResponseMessage - .getApplicationData() - .getGpsPosition() - .getWayPoint() - .getHeading() - / 10d) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + LOCATION_HEADING, msg); - - msg = - new MqttMessage( - String.valueOf( - vehicleStatusResponseMessage - .getApplicationData() - .getBasicVehicleStatus() - .getLockStatus()) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + DOORS_LOCKED, msg); - - // todo check configuration for available doors - msg = - new MqttMessage( - String.valueOf( - vehicleStatusResponseMessage - .getApplicationData() - .getBasicVehicleStatus() - .getDriverDoor()) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + DOORS_DRIVER, msg); - - msg = - new MqttMessage( - String.valueOf( - vehicleStatusResponseMessage - .getApplicationData() - .getBasicVehicleStatus() - .getPassengerDoor()) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + DOORS_PASSENGER, msg); - - msg = - new MqttMessage( - String.valueOf( - vehicleStatusResponseMessage - .getApplicationData() - .getBasicVehicleStatus() - .getRearLeftDoor()) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + DOORS_REAR_LEFT, msg); - - msg = - new MqttMessage( - String.valueOf( - vehicleStatusResponseMessage - .getApplicationData() - .getBasicVehicleStatus() - .getRearRightDoor()) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + DOORS_REAR_RIGHT, msg); - - msg = - new MqttMessage( - String.valueOf( - vehicleStatusResponseMessage - .getApplicationData() - .getBasicVehicleStatus() - .getBootStatus()) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + DOORS_BOOT, msg); - - msg = - new MqttMessage( - String.valueOf( - vehicleStatusResponseMessage - .getApplicationData() - .getBasicVehicleStatus() - .getBonnetStatus()) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + DOORS_BONNET, msg); - - msg = - new MqttMessage( - String.valueOf( - vehicleStatusResponseMessage - .getApplicationData() - .getBasicVehicleStatus() - .getFrontLeftTyrePressure() - * 4 - / 100d) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + TYRES_FRONT_LEFT_PRESSURE, msg); - - msg = - new MqttMessage( - String.valueOf( - vehicleStatusResponseMessage - .getApplicationData() - .getBasicVehicleStatus() - .getFrontRrightTyrePressure() - * 4 - / 100d) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + TYRES_FRONT_RIGHT_PRESSURE, msg); - - msg = - new MqttMessage( - String.valueOf( - vehicleStatusResponseMessage - .getApplicationData() - .getBasicVehicleStatus() - .getRearLeftTyrePressure() - * 4 - / 100d) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + TYRES_REAR_LEFT_PRESSURE, msg); - - msg = - new MqttMessage( - String.valueOf( - vehicleStatusResponseMessage - .getApplicationData() - .getBasicVehicleStatus() - .getRearRightTyrePressure() - * 4 - / 100d) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + TYRES_REAR_RIGHT_PRESSURE, msg); - - msg = new MqttMessage(toRemoteClimate(remoteClimateStatus).getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + CLIMATE_REMOTE_CLIMATE_STATE, msg); - - msg = - new MqttMessage( - String.valueOf( - vehicleStatusResponseMessage - .getApplicationData() - .getBasicVehicleStatus() - .getRmtHtdRrWndSt()) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + CLIMATE_BACK_WINDOW_HEAT, msg); - - if (vehicleStatusResponseMessage.getApplicationData().getBasicVehicleStatus().getMileage() - > 0) { - // sometimes mileage is 0, ignore such values - msg = - new MqttMessage( - String.valueOf( - vehicleStatusResponseMessage - .getApplicationData() - .getBasicVehicleStatus() - .getMileage() - / 10.d) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + DRIVETRAIN_MILEAGE, msg); - - // if the milage is 0, the electric range is also 0 - msg = - new MqttMessage( - String.valueOf( - vehicleStatusResponseMessage - .getApplicationData() - .getBasicVehicleStatus() - .getFuelRangeElec() - / 10.d) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + DRIVETRAIN_RANGE, msg); - } - - msg = - new MqttMessage(OffsetDateTime.now(getClock()).toString().getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + REFRESH_LAST_VEHICLE_STATE, msg); - } - - private static String toRemoteClimate(Integer remoteClimateStatus) { - switch (remoteClimateStatus) { - case 0: - return "off"; - case 1: - return "blowingOnly"; - case 2: - return "on"; - case 5: - return "front"; - default: - return "unknown (" + remoteClimateStatus + ")"; - } - } - - public void handleChargeStatusMessage( - net.heberling.ismart.asn1.v3_0.Message chargingStatusResponseMessage) - throws MqttException { - MqttMessage msg = - new MqttMessage( - SaicMqttGateway.toJSON(chargingStatusResponseMessage).getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish( - mqttVINPrefix - + "/" - + INTERNAL - + "/" - + chargingStatusResponseMessage.getBody().getApplicationID() - + "_" - + chargingStatusResponseMessage.getBody().getApplicationDataProtocolVersion() - + "/json", - msg); - - double current = - chargingStatusResponseMessage.getApplicationData().getBmsPackCrnt() * 0.05d - 1000.0d; - msg = new MqttMessage((String.valueOf(current)).getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + DRIVETRAIN_CURRENT, msg); - - double voltage = - (double) chargingStatusResponseMessage.getApplicationData().getBmsPackVol() * 0.25d; - msg = new MqttMessage((String.valueOf(voltage)).getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + DRIVETRAIN_VOLTAGE, msg); - - int remainingChargingTime = 0; - if (chargingStatusResponseMessage.getApplicationData().getChargeStatus().getChargingGunState() - && current < 0) { - remainingChargingTime = - chargingStatusResponseMessage.getApplicationData().getChrgngRmnngTime() * 60; - } - msg = new MqttMessage((String.valueOf(remainingChargingTime)).getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + DRIVETRAIN_REMAINING_CHARGING_TIME, msg); - - double power = current * voltage / 1000d; - msg = new MqttMessage((String.valueOf(power)).getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + DRIVETRAIN_POWER, msg); - - msg = - new MqttMessage( - (String.valueOf( - chargingStatusResponseMessage - .getApplicationData() - .getChargeStatus() - .getChargingGunState())) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + DRIVETRAIN_CHARGER_CONNECTED, msg); - - msg = - new MqttMessage( - (String.valueOf( - chargingStatusResponseMessage - .getApplicationData() - .getChargeStatus() - .getChargingType())) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + DRIVETRAIN_CHARGING_TYPE, msg); - - msg = - new MqttMessage( - (String.valueOf( - chargingStatusResponseMessage.getApplicationData().getBmsPackSOCDsp() / 10d)) - .getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + DRIVETRAIN_SOC, msg); - - msg = - new MqttMessage(OffsetDateTime.now(getClock()).toString().getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + REFRESH_LAST_CHARGE_STATE, msg); - } - public void notifyCarActivityTime(OffsetDateTime now, boolean force) throws MqttException { // if the car activity changed, notify the channel if (lastCarActivity == null || force || lastCarActivity.isBefore(now)) { lastCarActivity = now; - MqttMessage msg = - new MqttMessage(lastCarActivity.toString().getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + REFRESH_LAST_ACTIVITY, msg); + client.publishRetained( + mqttVINPrefix + "/" + REFRESH_LAST_ACTIVITY, lastCarActivity.toString()); } } public void notifyMessage(SaicMessage message) throws MqttException { if (lastVehicleMessage == null || message.getMessageTime().isAfter(lastVehicleMessage)) { // only publish the latest message - MqttMessage msg = - new MqttMessage(SaicMqttGateway.toJSON(message).getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + INFO_LAST_MESSAGE, msg); + client.publishRetained( + mqttVINPrefix + "/" + INFO_LAST_MESSAGE, SaicMqttGateway.toJSON(message)); lastVehicleMessage = message.getMessageTime(); } // something happened, better check the vehicle state @@ -533,94 +115,86 @@ public boolean shouldRefresh() { if (hvBatteryActive || lastCarShutdown .plus(refreshPeriodAfterShutdown, ChronoUnit.SECONDS) - .isAfter(OffsetDateTime.now(getClock()))) { + .isAfter(OffsetDateTime.now(clockSupplier.get()))) { return lastSuccessfulRefresh.isBefore( - OffsetDateTime.now(getClock()).minus(refreshPeriodActive, ChronoUnit.SECONDS)); + OffsetDateTime.now(clockSupplier.get()) + .minus(refreshPeriodActive, ChronoUnit.SECONDS)); } else { return lastSuccessfulRefresh.isBefore( - OffsetDateTime.now(getClock()).minus(refreshPeriodInactive, ChronoUnit.SECONDS)); + OffsetDateTime.now(clockSupplier.get()) + .minus(refreshPeriodInactive, ChronoUnit.SECONDS)); } } } public void setHVBatteryActive(boolean hvBatteryActive) throws MqttException { if (!hvBatteryActive && this.hvBatteryActive) { - this.lastCarShutdown = OffsetDateTime.now(getClock()); + this.lastCarShutdown = OffsetDateTime.now(clockSupplier.get()); } this.hvBatteryActive = hvBatteryActive; - MqttMessage msg = - new MqttMessage(SaicMqttGateway.toJSON(hvBatteryActive).getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + DRIVETRAIN_HV_BATTERY_ACTIVE, msg); + client.publishRetained( + mqttVINPrefix + "/" + DRIVETRAIN_HV_BATTERY_ACTIVE, + SaicMqttGateway.toJSON(hvBatteryActive)); if (hvBatteryActive) { - notifyCarActivityTime(OffsetDateTime.now(getClock()), true); + notifyCarActivityTime(OffsetDateTime.now(clockSupplier.get()), true); } } public void configure(VinInfo vinInfo) throws MqttException { - MqttMessage msg = - new MqttMessage(vinInfo.getModelConfigurationJsonStr().getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + INTERNAL_CONFIGURATION_RAW, msg); + + client.publishRetained( + mqttVINPrefix + "/" + INTERNAL_CONFIGURATION_RAW, vinInfo.getModelConfigurationJsonStr()); for (String c : vinInfo.getModelConfigurationJsonStr().split(";")) { Map map = new HashMap<>(); for (String e : c.split(",")) { map.put(e.split(":")[0], e.split(":")[1]); } - msg = new MqttMessage(map.get("value").getBytes(StandardCharsets.UTF_8)); - msg.setQos(0); - msg.setRetained(true); - client.publish(mqttVINPrefix + "/" + INFO_CONFIGURATION + "/" + map.get("code"), msg); + client.publishRetained( + mqttVINPrefix + "/" + INFO_CONFIGURATION + "/" + map.get("code"), map.get("value")); } } public void setRefreshPeriodActive(long refreshPeriodActive) { - if (this.refreshPeriodActive == null || this.refreshPeriodActive != refreshPeriodActive) { - MqttMessage mqttMessage = - new MqttMessage(String.valueOf(refreshPeriodActive).getBytes(StandardCharsets.UTF_8)); + if (this.refreshPeriodActive != null && this.refreshPeriodActive != refreshPeriodActive) { + try { - mqttMessage.setRetained(true); - this.client.publish(this.mqttVINPrefix + "/" + REFRESH_PERIOD_ACTIVE, mqttMessage); + client.publishRetained( + mqttVINPrefix + "/" + REFRESH_PERIOD_ACTIVE, String.valueOf(refreshPeriodActive)); } catch (MqttException e) { - throw new MqttGatewayException("Error publishing message: " + mqttMessage, e); + throw new MqttGatewayException("Error publishing message.", e); } } this.refreshPeriodActive = refreshPeriodActive; } public void setRefreshPeriodInactive(long refreshPeriodInactive) { - if (this.refreshPeriodInactive == null || this.refreshPeriodInactive != refreshPeriodInactive) { - MqttMessage mqttMessage = - new MqttMessage(String.valueOf(refreshPeriodInactive).getBytes(StandardCharsets.UTF_8)); + if (this.refreshPeriodInactive != null && this.refreshPeriodInactive != refreshPeriodInactive) { + try { - mqttMessage.setRetained(true); - this.client.publish(this.mqttVINPrefix + "/" + REFRESH_PERIOD_INACTIVE, mqttMessage); + client.publishRetained( + mqttVINPrefix + "/" + REFRESH_PERIOD_INACTIVE, String.valueOf(refreshPeriodInactive)); + } catch (MqttException e) { - throw new MqttGatewayException("Error publishing message: " + mqttMessage, e); + throw new MqttGatewayException("Error publishing message.", e); } } this.refreshPeriodInactive = refreshPeriodInactive; } public void setRefreshMode(RefreshMode refreshMode) { - if (this.refreshMode == null || this.refreshMode != refreshMode) { + if (this.refreshMode != null && this.refreshMode != refreshMode) { LOGGER.info("Setting refresh mode to {}", refreshMode.getStringValue()); if (refreshMode != FORCE) { // never send force mode to MQTT. // If we get restarted while force mode is active, the configuration from MQTT // feature would enable force mode permanently and polling of the car never stops - MqttMessage mqttMessage = - new MqttMessage(refreshMode.getStringValue().getBytes(StandardCharsets.UTF_8)); try { - mqttMessage.setRetained(true); - this.client.publish(this.mqttVINPrefix + "/" + REFRESH_MODE, mqttMessage); + client.publishRetained(mqttVINPrefix + "/" + REFRESH_MODE, refreshMode.getStringValue()); } catch (MqttException e) { - throw new MqttGatewayException("Error publishing message: " + mqttMessage, e); + throw new MqttGatewayException("Error publishing message. ", e); } } } @@ -633,31 +207,43 @@ public RefreshMode getRefreshMode() { } public void markSuccessfulRefresh() { - this.lastSuccessfulRefresh = OffsetDateTime.now(getClock()); + this.lastSuccessfulRefresh = OffsetDateTime.now(clockSupplier.get()); } public void setRefreshPeriodAfterShutdown(long refreshPeriodAfterShutdown) { - if (this.refreshPeriodAfterShutdown == null - || this.refreshPeriodAfterShutdown != refreshPeriodAfterShutdown) { + if (this.refreshPeriodAfterShutdown != null + && this.refreshPeriodAfterShutdown != refreshPeriodAfterShutdown) { - MqttMessage mqttMessage = - new MqttMessage( - String.valueOf(refreshPeriodAfterShutdown).getBytes(StandardCharsets.UTF_8)); try { - mqttMessage.setRetained(true); - this.client.publish(this.mqttVINPrefix + "/" + REFRESH_PERIOD_INACTIVE_GRACE, mqttMessage); + client.publishRetained( + mqttVINPrefix + "/" + REFRESH_PERIOD_INACTIVE_GRACE, + String.valueOf(refreshPeriodAfterShutdown)); } catch (MqttException e) { - throw new MqttGatewayException("Error publishing message: " + mqttMessage, e); + throw new MqttGatewayException("Error publishing message.", e); } } this.refreshPeriodAfterShutdown = refreshPeriodAfterShutdown; } + public void setRemoteTemperature(Integer remoteTemperature) { + remoteTemperature = hvacSettings.normalizeTemperature(remoteTemperature); + if (this.remoteTemperature != null && this.remoteTemperature != remoteTemperature) { + try { + client.publishRetained( + mqttVINPrefix + "/" + CLIMATE_REMOTE_TEMPERATURE, String.valueOf(remoteTemperature)); + } catch (MqttException e) { + throw new MqttGatewayException("Error publishing message.", e); + } + } + this.remoteTemperature = remoteTemperature; + } + public boolean isComplete() { return refreshPeriodActive != null && refreshPeriodInactive != null && refreshPeriodAfterShutdown != null - && refreshMode != null; + && refreshMode != null + && remoteTemperature != null; } public void configureMissing() { @@ -673,6 +259,9 @@ public void configureMissing() { if (refreshMode == null) { setRefreshMode(PERIODIC); } + if (remoteTemperature == null) { + setRemoteTemperature(22); + } } public void configure(String topic, MqttMessage message) { @@ -709,12 +298,27 @@ public void configure(String topic, MqttMessage message) { throw new MqttGatewayException("Error setting value for payload: " + message); } break; + case CLIMATE_REMOTE_TEMPERATURE: + try { + int temperature = Integer.parseInt(message.toString()); + setRemoteTemperature(temperature); + } catch (NumberFormatException e) { + throw new MqttGatewayException("Error setting value for payload: " + message); + } + if (Objects.nonNull(remoteClimateStatus) && remoteClimateStatus > 0) { + // TODO send ACC command to car + } + break; default: throw new MqttGatewayException("Unsupported topic " + topic); } } - private Clock getClock() { - return clockSupplier.get(); + public int getRemoteTemperature() { + return remoteTemperature; + } + + public void setHvacSettings(HVACSettings hvacSettings) { + this.hvacSettings = hvacSettings; } } diff --git a/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/carconfig/DefaultHVACSettings.java b/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/carconfig/DefaultHVACSettings.java new file mode 100644 index 00000000..1c0481e7 --- /dev/null +++ b/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/carconfig/DefaultHVACSettings.java @@ -0,0 +1,23 @@ +package net.heberling.ismart.mqtt.carconfig; + +public class DefaultHVACSettings implements HVACSettings { + @Override + public Integer getMinAllowedTemp() { + return 16; + } + + @Override + public Integer getMaxAllowedTemp() { + return 28; + } + + @Override + public Integer normalizeTemperature(Integer temp) { + return Math.min(Math.max(temp, getMinAllowedTemp()), getMaxAllowedTemp()); + } + + @Override + public byte mapTempToSaicApi(Integer temp) { + return (byte) (normalizeTemperature(temp) - 14); + } +} diff --git a/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/carconfig/HVACSettings.java b/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/carconfig/HVACSettings.java new file mode 100644 index 00000000..a8d9dedd --- /dev/null +++ b/saic-java-mqtt-gateway/src/main/java/net/heberling/ismart/mqtt/carconfig/HVACSettings.java @@ -0,0 +1,35 @@ +package net.heberling.ismart.mqtt.carconfig; + +public interface HVACSettings { + + default Integer getMinAllowedTemp() { + return 16; + } + + default Integer getMaxAllowedTemp() { + return 28; + } + + default Integer normalizeTemperature(Integer temp) { + return Math.min(Math.max(temp, getMinAllowedTemp()), getMaxAllowedTemp()); + } + + default byte mapTempToSaicApi(Integer temp) { + return (byte) (normalizeTemperature(temp) - 14); + } + + default String toRemoteClimate(Integer remoteClimateStatus) { + switch (remoteClimateStatus) { + case 0: + return "off"; + case 1: + return "blowingOnly"; + case 2: + return "on"; + case 5: + return "front"; + default: + return "unknown (" + remoteClimateStatus + ")"; + } + } +} diff --git a/saic-java-mqtt-gateway/src/test/java/net/heberling/ismart/mqtt/VehicleStateTest.java b/saic-java-mqtt-gateway/src/test/java/net/heberling/ismart/mqtt/VehicleStateTest.java index 5e6c5ce9..79dab2e5 100644 --- a/saic-java-mqtt-gateway/src/test/java/net/heberling/ismart/mqtt/VehicleStateTest.java +++ b/saic-java-mqtt-gateway/src/test/java/net/heberling/ismart/mqtt/VehicleStateTest.java @@ -7,7 +7,7 @@ import java.time.Instant; import java.time.OffsetDateTime; import java.time.ZoneId; -import org.eclipse.paho.client.mqttv3.MqttClient; +import net.heberling.ismart.mqtt.carconfig.DefaultHVACSettings; import org.eclipse.paho.client.mqttv3.MqttException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -19,7 +19,7 @@ public class VehicleStateTest { public static final int REFERENCE_TIME = 1684273208; - @Mock private MqttClient mqttClient; + @Mock private GatewayMqttClient mqttClient; private Clock clock; private VehicleState vehicleState; @@ -28,6 +28,7 @@ public class VehicleStateTest { public void setUp() throws MqttException { clock = Clock.fixed(Instant.ofEpochSecond(REFERENCE_TIME), ZoneId.systemDefault()); vehicleState = new VehicleState(mqttClient, "test/topic", "test", () -> this.clock); + vehicleState.setHvacSettings(new DefaultHVACSettings()); vehicleState.configureMissing(); vehicleState.notifyCarActivityTime(OffsetDateTime.now(clock), true); }