mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-29 12:34:21 +02:00
[renault] Several improvements related to updating channels/documentation (#20773)
* [renault] Several improvements related to updating channels/documentation Signed-off-by: Hilbrand Bouwkamp <hilbrand@h72.nl>
This commit is contained in:
@@ -17,6 +17,7 @@ No discovery
|
||||
|
||||
## Thing Configuration
|
||||
|
||||
There is a single thing `car`.
|
||||
You require your MyRenault credential, locale and VIN for your MyRenault registered car.
|
||||
|
||||
| Parameter | Description | Default |
|
||||
@@ -29,28 +30,29 @@ You require your MyRenault credential, locale and VIN for your MyRenault registe
|
||||
| refreshInterval | Interval the car is polled in minutes. | 10 |
|
||||
| updateDelay | How long to wait for commands to reach car and update to server in seconds.| 30 |
|
||||
| kamereonApiKey | Kamereon API Key. | VAX7XYKGfa92yMvXculCkEFyfZbuM7Ss |
|
||||
| gigyaApiKey | Gigya API Key. Configure to override the hard coded value for your region. | Hard coded region value |
|
||||
|
||||
## Channels
|
||||
|
||||
| Channel ID | Type | Description | Read Only |
|
||||
|------------------------|--------------------|-------------------------------------------------|-----------|
|
||||
| batteryavailableEnergy | Number:Energy | Battery Energy Available | Yes |
|
||||
| batterylevel | Number | State of the battery in % | Yes |
|
||||
| batterystatusupdated | DateTime | Timestamp of the last battery status update | Yes |
|
||||
| chargingmode | String | Charging mode. always_charging or schedule_mode | No |
|
||||
| pause | Switch | Pause the charge. | No |
|
||||
| chargingstatus | String | Charging status | Yes |
|
||||
| chargingremainingtime | Number:Time | Charging time remaining | Yes |
|
||||
| plugstatus | String | Status of charging plug | Yes |
|
||||
| estimatedrange | Number:Length | Estimated range of the car | Yes |
|
||||
| odometer | Number:Length | Total distance travelled | Yes |
|
||||
| hvacstatus | String | HVAC status HVAC Status (ON, OFF, PENDING) | No |
|
||||
| hvactargettemperature | Number:Temperature | HVAC target temperature (19 to 21) | No |
|
||||
| externaltemperature | Number:Temperature | Temperature outside of the car | Yes |
|
||||
| image | String | Image URL of MyRenault | Yes |
|
||||
| location | Location | The GPS position of the vehicle | Yes |
|
||||
| locationupdated | DateTime | Timestamp of the last location update | Yes |
|
||||
| locked | Switch | Locked status of the car | Yes |
|
||||
| Channel ID | Type | Description | Read Only |
|
||||
|------------------------|--------------------|-----------------------------------------------------|-----------|
|
||||
| batteryavailableenergy | Number:Energy | Battery Energy Available | Yes |
|
||||
| batterylevel | Number | State of the battery in % | Yes |
|
||||
| batterystatusupdated | DateTime | Timestamp of the last battery status update | Yes |
|
||||
| chargingmode | String | Charging mode. `ALWAYS_CHARGING` or `SCHEDULE_MODE` | No |
|
||||
| pause | Switch | Pause the charge | No |
|
||||
| chargingstatus | String | Charging status | Yes |
|
||||
| chargingremainingtime | Number:Time | Charging time remaining | Yes |
|
||||
| plugstatus | String | Status of charging plug | Yes |
|
||||
| estimatedrange | Number:Length | Estimated range of the car | Yes |
|
||||
| odometer | Number:Length | Total distance travelled | Yes |
|
||||
| hvacstatus | String | HVAC status HVAC Status (ON, OFF, PENDING) | No |
|
||||
| hvactargettemperature | Number:Temperature | HVAC target temperature (19 to 21) | No |
|
||||
| externaltemperature | Number:Temperature | Temperature outside of the car | Yes |
|
||||
| image | String | Image URL of MyRenault | Yes |
|
||||
| location | Location | The GPS position of the vehicle | Yes |
|
||||
| locationupdated | DateTime | Timestamp of the last location update | Yes |
|
||||
| locked | Switch | Locked status of the car | Yes |
|
||||
|
||||
## Limitations
|
||||
|
||||
@@ -64,12 +66,64 @@ This seams to only allow values 19, 20 and 21 or else the pre-conditioning comma
|
||||
The 'pause' and 'chargingmode' may not work on some cars.
|
||||
As an example, 'chargingmode' does not work on Dacia Spring cars.
|
||||
|
||||
The `odometer` may not work on some cars.
|
||||
|
||||
The Kamereon API Key changes periodically, which causes a communication error.
|
||||
To fix this error update the API Key in the bindings configuration.
|
||||
The new key value can hopefully be found in the renault-api project: [KAMEREON_APIKEY value](https://github.com/hacf-fr/renault-api/blob/main/src/renault_api/const.py) or in the openHAB forums.
|
||||
|
||||
## Example
|
||||
|
||||
renaultcar.things:
|
||||
|
||||
```java
|
||||
renault:car:renault "Renault" [
|
||||
accountType="MYRENAULT",
|
||||
locale="<your locale>",
|
||||
myRenaultUsername="<my renault username>",
|
||||
myRenaultPassword="<my renault password>",
|
||||
kamereonApiKey="YjkKtHmGfaceeuExUDKGxrLZGGvtVS0J",
|
||||
vin="<your vin>",
|
||||
gigyaApiKey="",
|
||||
updateDelay=60,
|
||||
refreshInterval=10
|
||||
]
|
||||
```
|
||||
|
||||
renaultcar.items:
|
||||
|
||||
```java
|
||||
Number:Energy RenaultCar_BatteryEnergyAvailable "Battery Energy Available [%.1f %unit%]" <batterylevel> { channel="renault:car:renault:batteryavailableenergy", unit="kWh" }
|
||||
Number RenaultCar_BatteryLevel "Battery Level [%d %%]" <batterylevel> { channel="renault:car:renault:batterylevel" }
|
||||
DateTime RenaultCar_BatteryStatusUpdated "Battery Status Updated [%1$tH:%1$tM]" <time> { channel="renault:car:renault:batterystatusupdated" }
|
||||
String RenaultCar_ChargingMode "Charging Mode [%s]" <switch> { channel="renault:car:renault:chargingmode" }
|
||||
String RenaultCar_Pause "Pause/Resume Charge [%s]" <switch> { channel="renault:car:renault:pause" }
|
||||
String RenaultCar_ChargingStatus "Charging Status [%s]" <switch> { channel="renault:car:renault:chargingstatus" }
|
||||
Number:Time RenaultCar_ChargingTimeRemaining "Charging Time Remaining [%d %unit%]" <time> { channel="renault:car:renault:chargingremainingtime", unit="min" }
|
||||
String RenaultCar_PlugStatus "Plug Status [%s]" <poweroutlet> { channel="renault:car:renault:plugstatus" }
|
||||
|
||||
Number:Length RenaultCar_EstimatedRange "Estimated Range [%d %unit%]" { channel="renault:car:renault:estimatedrange", unit="km" }
|
||||
Number:Length RenaultCar_Odometer "Odometer [%d %unit%]" { channel="renault:car:renault:odometer", unit="km" }
|
||||
|
||||
String RenaultCar_HVACStatus "HVAC Status [%s]" <heating> { channel="renault:car:renault:hvacstatus" }
|
||||
Number:Temperature RenaultCar_HVACTargetTemperature "HVAC Target Temperature [%.0f %unit%]" <temperature> { channel="renault:car:renault:hvactargettemperature" }
|
||||
Number:Temperature RenaultCar_ExternalTemperature "External Temperature [%.1f %unit%]" <temperature> { channel="renault:car:renault:externaltemperature" }
|
||||
|
||||
String RenaultCar_ImageURL "Car Image URL [%s]" <image> { channel="renault:car:renault:image" }
|
||||
Location RenaultCar_Location "Location Car" <zoom> { channel="renault:car:renault:location" }
|
||||
DateTime RenaultCar_LocationUpdate "Location Updated [%1$tH:%1$tM]" <time> { channel="renault:car:renault:locationupdated" }
|
||||
Switch RenaultCar_Locked "Locked [%s]" <lock> { channel="renault:car:renault:locked" }
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Additional – Charge limit via Dimmer-item
|
||||
// Use together with the ChargeRenaultCarLimit rule
|
||||
// (see binding-documentation for rule code)
|
||||
// ------------------------------------------------------------
|
||||
|
||||
// Maximum load percentage (e.g. 80 is 80%)
|
||||
Dimmer RenaultCar_ChargeLimit "Charge Limit [%d %%]" <batterylevel>
|
||||
```
|
||||
|
||||
renaultcar.sitemap:
|
||||
|
||||
```perl
|
||||
|
||||
+12
@@ -12,6 +12,8 @@
|
||||
*/
|
||||
package org.openhab.binding.renault.internal;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.openhab.core.thing.ThingTypeUID;
|
||||
|
||||
@@ -47,4 +49,14 @@ public class RenaultBindingConstants {
|
||||
public static final String CHANNEL_LOCKED = "locked";
|
||||
public static final String CHANNEL_ODOMETER = "odometer";
|
||||
public static final String CHANNEL_PLUG_STATUS = "plugstatus";
|
||||
|
||||
public static final List<String> HVAC_CHANNELS = List.of(CHANNEL_HVAC_STATUS, CHANNEL_HVAC_TARGET_TEMPERATURE);
|
||||
public static final List<String> BATTERY_CHANNELS = List.of(CHANNEL_BATTERY_AVAILABLE_ENERGY, CHANNEL_BATTERY_LEVEL,
|
||||
CHANNEL_BATTERY_STATUS_UPDATED, CHANNEL_CHARGING_MODE, CHANNEL_CHARGING_STATUS,
|
||||
CHANNEL_CHARGING_REMAINING_TIME);
|
||||
public static final List<String> ALL_CHANNELS = List.of(CHANNEL_IMAGE, CHANNEL_LOCATION, CHANNEL_LOCATION_UPDATED,
|
||||
CHANNEL_ODOMETER, CHANNEL_LOCKED, CHANNEL_HVAC_STATUS, CHANNEL_HVAC_TARGET_TEMPERATURE,
|
||||
CHANNEL_EXTERNAL_TEMPERATURE, CHANNEL_PLUG_STATUS, CHANNEL_CHARGING_STATUS, CHANNEL_BATTERY_LEVEL,
|
||||
CHANNEL_ESTIMATED_RANGE, CHANNEL_BATTERY_AVAILABLE_ENERGY, CHANNEL_CHARGING_REMAINING_TIME,
|
||||
CHANNEL_BATTERY_STATUS_UPDATED, CHANNEL_CHARGING_MODE);
|
||||
}
|
||||
|
||||
+1
@@ -30,4 +30,5 @@ public class RenaultConfiguration {
|
||||
public int refreshInterval = 10;
|
||||
public int updateDelay = 30;
|
||||
public String kamereonApiKey = "YjkKtHmGfaceeuExUDKGxrLZGGvtVS0J";
|
||||
public String gigyaApiKey = "";
|
||||
}
|
||||
|
||||
+47
-68
@@ -39,15 +39,16 @@ public class Car {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(Car.class);
|
||||
|
||||
private boolean disableVehicle = false;
|
||||
private boolean disableLocation = false;
|
||||
private boolean disableBattery = false;
|
||||
private boolean disableChargeMode = false;
|
||||
private boolean disableCockpit = false;
|
||||
private boolean disableHvac = false;
|
||||
private boolean disableLockStatus = false;
|
||||
|
||||
private boolean pausemode = false;
|
||||
private ChargingStatus chargingStatus = ChargingStatus.UNKNOWN;
|
||||
private ChargingMode chargingMode = ChargingMode.UNKNOWN;
|
||||
private @Nullable ChargingMode chargingMode;
|
||||
private PlugStatus plugStatus = PlugStatus.UNKNOWN;
|
||||
private double hvacTargetTemperature = 20.0;
|
||||
private @Nullable Double batteryLevel;
|
||||
@@ -65,7 +66,6 @@ public class Car {
|
||||
private @Nullable Double externalTemperature;
|
||||
|
||||
public enum ChargingMode {
|
||||
UNKNOWN,
|
||||
SCHEDULE_MODE,
|
||||
ALWAYS_CHARGING
|
||||
}
|
||||
@@ -132,6 +132,10 @@ public class Car {
|
||||
}
|
||||
}
|
||||
|
||||
public void setChargeMode(ChargingMode chargeMode) {
|
||||
this.chargingMode = chargeMode;
|
||||
}
|
||||
|
||||
public void resetHVACStatus() {
|
||||
this.hvacstatus = null;
|
||||
}
|
||||
@@ -230,6 +234,10 @@ public class Car {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isDisableVehicle() {
|
||||
return disableVehicle;
|
||||
}
|
||||
|
||||
public boolean isDisableLocation() {
|
||||
return disableLocation;
|
||||
}
|
||||
@@ -294,14 +302,10 @@ public class Car {
|
||||
return chargingStatus;
|
||||
}
|
||||
|
||||
public ChargingMode getChargingMode() {
|
||||
public @Nullable ChargingMode getChargingMode() {
|
||||
return chargingMode;
|
||||
}
|
||||
|
||||
public boolean getPauseMode() {
|
||||
return pausemode;
|
||||
}
|
||||
|
||||
public @Nullable Integer getChargingRemainingTime() {
|
||||
return chargingRemainingTime;
|
||||
}
|
||||
@@ -318,6 +322,10 @@ public class Car {
|
||||
this.hvacTargetTemperature = hvacTargetTemperature;
|
||||
}
|
||||
|
||||
public void setDisableVehicle(boolean disableVehicle) {
|
||||
this.disableVehicle = disableVehicle;
|
||||
}
|
||||
|
||||
public void setDisableLocation(boolean disableLocation) {
|
||||
this.disableLocation = disableLocation;
|
||||
}
|
||||
@@ -334,26 +342,6 @@ public class Car {
|
||||
this.disableHvac = disableHvac;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the charging mode to a known mode.
|
||||
*
|
||||
* @param mode
|
||||
*/
|
||||
public void setChargeMode(ChargingMode mode) {
|
||||
switch (mode) {
|
||||
case SCHEDULE_MODE:
|
||||
case ALWAYS_CHARGING:
|
||||
chargingMode = mode;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void setPauseMode(boolean pausemode) {
|
||||
this.pausemode = pausemode;
|
||||
}
|
||||
|
||||
private @Nullable JsonObject getAttributes(JsonObject responseJson)
|
||||
throws IllegalStateException, ClassCastException {
|
||||
if (responseJson.get("data") != null && responseJson.get("data").getAsJsonObject().get("attributes") != null) {
|
||||
@@ -363,54 +351,37 @@ public class Car {
|
||||
}
|
||||
|
||||
private LockStatus mapLockStatus(final String apiLockStatus) {
|
||||
switch (apiLockStatus) {
|
||||
case "locked":
|
||||
return LockStatus.LOCKED;
|
||||
case "unlocked":
|
||||
return LockStatus.UNLOCKED;
|
||||
default:
|
||||
return LockStatus.UNKNOWN;
|
||||
}
|
||||
return switch (apiLockStatus) {
|
||||
case "locked" -> LockStatus.LOCKED;
|
||||
case "unlocked" -> LockStatus.UNLOCKED;
|
||||
default -> LockStatus.UNKNOWN;
|
||||
};
|
||||
}
|
||||
|
||||
private PlugStatus mapPlugStatus(final String apiPlugState) {
|
||||
// https://github.com/hacf-fr/renault-api/blob/main/src/renault_api/kamereon/enums.py
|
||||
switch (apiPlugState) {
|
||||
case "0":
|
||||
return PlugStatus.UNPLUGGED;
|
||||
case "1":
|
||||
return PlugStatus.PLUGGED;
|
||||
case "-1":
|
||||
return PlugStatus.PLUG_ERROR;
|
||||
case "-2147483648":
|
||||
return PlugStatus.PLUG_UNKNOWN;
|
||||
default:
|
||||
return PlugStatus.UNKNOWN;
|
||||
}
|
||||
return switch (apiPlugState) {
|
||||
case "0" -> PlugStatus.UNPLUGGED;
|
||||
case "1" -> PlugStatus.PLUGGED;
|
||||
case "-1" -> PlugStatus.PLUG_ERROR;
|
||||
case "-2147483648" -> PlugStatus.PLUG_UNKNOWN;
|
||||
default -> PlugStatus.UNKNOWN;
|
||||
};
|
||||
}
|
||||
|
||||
private ChargingStatus mapChargingStatus(final String apiChargeState) {
|
||||
// https://github.com/hacf-fr/renault-api/blob/main/src/renault_api/kamereon/enums.py
|
||||
switch (apiChargeState) {
|
||||
case "0.0":
|
||||
return ChargingStatus.NOT_IN_CHARGE;
|
||||
case "0.1":
|
||||
return ChargingStatus.WAITING_FOR_A_PLANNED_CHARGE;
|
||||
case "0.2":
|
||||
return ChargingStatus.CHARGE_ENDED;
|
||||
case "0.3":
|
||||
return ChargingStatus.WAITING_FOR_CURRENT_CHARGE;
|
||||
case "0.4":
|
||||
return ChargingStatus.ENERGY_FLAP_OPENED;
|
||||
case "1.0":
|
||||
return ChargingStatus.CHARGE_IN_PROGRESS;
|
||||
case "-1.0":
|
||||
return ChargingStatus.CHARGE_ERROR;
|
||||
case "-1.1":
|
||||
return ChargingStatus.UNAVAILABLE;
|
||||
default:
|
||||
return ChargingStatus.UNKNOWN;
|
||||
}
|
||||
return switch (apiChargeState) {
|
||||
case "0.0" -> ChargingStatus.NOT_IN_CHARGE;
|
||||
case "0.1" -> ChargingStatus.WAITING_FOR_A_PLANNED_CHARGE;
|
||||
case "0.2" -> ChargingStatus.CHARGE_ENDED;
|
||||
case "0.3" -> ChargingStatus.WAITING_FOR_CURRENT_CHARGE;
|
||||
case "0.4" -> ChargingStatus.ENERGY_FLAP_OPENED;
|
||||
case "1.0" -> ChargingStatus.CHARGE_IN_PROGRESS;
|
||||
case "-1.0" -> ChargingStatus.CHARGE_ERROR;
|
||||
case "-1.1" -> ChargingStatus.UNAVAILABLE;
|
||||
default -> ChargingStatus.UNKNOWN;
|
||||
};
|
||||
}
|
||||
|
||||
public LockStatus getLockStatus() {
|
||||
@@ -424,4 +395,12 @@ public class Car {
|
||||
public void setDisableLockStatus(boolean disableLockStatus) {
|
||||
this.disableLockStatus = disableLockStatus;
|
||||
}
|
||||
|
||||
public boolean isDisableChargeMode() {
|
||||
return disableChargeMode;
|
||||
}
|
||||
|
||||
public void setDisableChargeMode(boolean disableChargeMode) {
|
||||
this.disableChargeMode = disableChargeMode;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ public class Constants {
|
||||
break;
|
||||
case "de_AT":
|
||||
gigyaRootUrl = GIGYA_URL_EU;
|
||||
gigyaApiKey = "3__B4KghyeUb0GlpU62ZXKrjSfb7CPzwBS368wioftJUL5qXE0Z_sSy0rX69klXuHy";
|
||||
gigyaApiKey = GIGYA_KEY_EU;
|
||||
kamereonRootUrl = KAMEREON_URL_EU;
|
||||
break;
|
||||
case "de_CH":
|
||||
|
||||
+146
-108
@@ -12,6 +12,9 @@
|
||||
*/
|
||||
package org.openhab.binding.renault.internal.api;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
@@ -28,11 +31,11 @@ import org.eclipse.jetty.util.Fields;
|
||||
import org.openhab.binding.renault.internal.RenaultConfiguration;
|
||||
import org.openhab.binding.renault.internal.api.Car.ChargingMode;
|
||||
import org.openhab.binding.renault.internal.api.exceptions.RenaultAPIGatewayException;
|
||||
import org.openhab.binding.renault.internal.api.exceptions.RenaultActionException;
|
||||
import org.openhab.binding.renault.internal.api.exceptions.RenaultException;
|
||||
import org.openhab.binding.renault.internal.api.exceptions.RenaultForbiddenException;
|
||||
import org.openhab.binding.renault.internal.api.exceptions.RenaultNotImplementedException;
|
||||
import org.openhab.binding.renault.internal.api.exceptions.RenaultUpdateException;
|
||||
import org.openhab.core.cache.ExpiringCache;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -51,80 +54,84 @@ import com.google.gson.JsonParser;
|
||||
@NonNullByDefault
|
||||
public class MyRenaultHttpSession {
|
||||
|
||||
private static final String CHARGING_MODE_SCHEDULE = "schedule_mode";
|
||||
private static final String CHARGING_MODE_ALWAYS = "always_charging";
|
||||
private static final int REQUEST_TIMEOUT_MS = 10_000;
|
||||
private static final String NOT_BE_THERE = "you should not be there but well done for the effort";
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(MyRenaultHttpSession.class);
|
||||
// Use a expiring cache to not login again if initSession is called within 3
|
||||
// seconds of the previous call.
|
||||
private final ExpiringCache<Boolean> initCache = new ExpiringCache<>(Duration.ofSeconds(3), () -> true);
|
||||
private final Object lock = new Object();
|
||||
|
||||
private RenaultConfiguration config;
|
||||
private HttpClient httpClient;
|
||||
private Constants constants;
|
||||
private @Nullable String kamereonToken;
|
||||
private @Nullable String kamereonaccountId;
|
||||
private @Nullable String cookieValue;
|
||||
private @Nullable String personId;
|
||||
private @Nullable String gigyaDataCenter;
|
||||
private @Nullable String jwt;
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(MyRenaultHttpSession.class);
|
||||
|
||||
public MyRenaultHttpSession(RenaultConfiguration config, HttpClient httpClient) {
|
||||
this.config = config;
|
||||
this.httpClient = httpClient;
|
||||
this.constants = new Constants(config.locale);
|
||||
}
|
||||
|
||||
public void initSesssion(Car car) throws RenaultException, RenaultForbiddenException, RenaultUpdateException,
|
||||
RenaultNotImplementedException, InterruptedException, ExecutionException, TimeoutException {
|
||||
login();
|
||||
getAccountInfo();
|
||||
getJWT();
|
||||
getAccountID();
|
||||
|
||||
final String imageURL = car.getImageURL();
|
||||
if (imageURL == null) {
|
||||
getVehicle(car);
|
||||
public void initSesssion() throws RenaultException, InterruptedException, ExecutionException, TimeoutException {
|
||||
synchronized (lock) {
|
||||
if (initCache.isExpired()) {
|
||||
login();
|
||||
getAccountInfo();
|
||||
getJWT();
|
||||
getAccountID();
|
||||
initCache.refreshValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void login() throws RenaultException, InterruptedException, ExecutionException, TimeoutException {
|
||||
Fields fields = new Fields();
|
||||
fields.add("ApiKey", this.constants.getGigyaApiKey());
|
||||
final Fields fields = new Fields();
|
||||
fields.add("ApiKey", getGigyaApiKey());
|
||||
fields.add("loginID", config.myRenaultUsername);
|
||||
fields.add("password", config.myRenaultPassword);
|
||||
final String url = this.constants.getGigyaRootUrl() + "/accounts.login";
|
||||
ContentResponse response = httpClient.FORM(url, fields);
|
||||
final ContentResponse response = httpClient.FORM(url, fields);
|
||||
if (HttpStatus.OK_200 == response.getStatus()) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(),
|
||||
response.getReason(), response.getContentAsString());
|
||||
}
|
||||
try {
|
||||
JsonObject responseJson = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
|
||||
JsonObject sessionInfoJson = responseJson.getAsJsonObject("sessionInfo");
|
||||
if (sessionInfoJson != null) {
|
||||
JsonElement element = sessionInfoJson.get("cookieValue");
|
||||
if (element != null) {
|
||||
cookieValue = element.getAsString();
|
||||
logger.debug("Cookie: {}", cookieValue);
|
||||
}
|
||||
final JsonObject responseJson = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
|
||||
final JsonObject sessionInfoJson = responseJson.getAsJsonObject("sessionInfo");
|
||||
if (sessionInfoJson == null) {
|
||||
throw new RenaultException("@text/error.renault.session.login.no_session_info");
|
||||
}
|
||||
JsonElement element = sessionInfoJson.get("cookieValue");
|
||||
if (element == null) {
|
||||
throw new RenaultException("@text/error.renault.session.login.no_cookie");
|
||||
}
|
||||
cookieValue = element.getAsString();
|
||||
logger.debug("Cookie: {}", cookieValue);
|
||||
} catch (JsonParseException | ClassCastException | IllegalStateException e) {
|
||||
throw new RenaultException("Login Error: cookie value not found in JSON response");
|
||||
throw new RenaultException("@text/error.renault.session.login.no_cookie");
|
||||
}
|
||||
if (cookieValue == null) {
|
||||
logger.warn("Login Error: cookie value not found! Response: {}", response.getContentAsString());
|
||||
throw new RenaultException("Login Error: cookie value not found in JSON response");
|
||||
throw new RenaultException("@text/error.renault.session.login.no_cookie");
|
||||
}
|
||||
} else {
|
||||
logger.warn("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(), response.getReason(),
|
||||
response.getContentAsString());
|
||||
throw new RenaultException("Login Error: " + response.getReason());
|
||||
throw new RenaultException(
|
||||
String.format("@text/error.renault.session.login.error[\"%s\"]", response.getReason()));
|
||||
}
|
||||
}
|
||||
|
||||
private void getAccountInfo() throws RenaultException, InterruptedException, ExecutionException, TimeoutException {
|
||||
Fields fields = new Fields();
|
||||
fields.add("ApiKey", this.constants.getGigyaApiKey());
|
||||
fields.add("ApiKey", getGigyaApiKey());
|
||||
fields.add("login_token", cookieValue);
|
||||
final String url = this.constants.getGigyaRootUrl() + "/accounts.getAccountInfo";
|
||||
ContentResponse response = httpClient.FORM(url, fields);
|
||||
@@ -146,50 +153,64 @@ public class MyRenaultHttpSession {
|
||||
}
|
||||
}
|
||||
} catch (JsonParseException | ClassCastException | IllegalStateException e) {
|
||||
throw new RenaultException(
|
||||
"Get Account Info Error: personId or gigyaDataCenter value not found in JSON response");
|
||||
throw new RenaultException("@text/error.renault.session.gigya.no_data");
|
||||
}
|
||||
} else {
|
||||
logger.warn("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(), response.getReason(),
|
||||
response.getContentAsString());
|
||||
throw new RenaultException("Get Account Info Error: " + response.getReason());
|
||||
throw new RenaultException(
|
||||
String.format("@text/error.renault.session.gigya.error[\"%s\"]", response.getReason()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the gigyaApiKey from configuration if it is used to override the
|
||||
* default hard-coded constant.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
private String getGigyaApiKey() {
|
||||
if (!this.config.gigyaApiKey.isBlank()) {
|
||||
return this.config.gigyaApiKey;
|
||||
}
|
||||
return this.constants.getGigyaApiKey();
|
||||
}
|
||||
|
||||
private void getJWT() throws RenaultException, InterruptedException, ExecutionException, TimeoutException {
|
||||
Fields fields = new Fields();
|
||||
fields.add("ApiKey", this.constants.getGigyaApiKey());
|
||||
final Fields fields = new Fields();
|
||||
fields.add("ApiKey", getGigyaApiKey());
|
||||
fields.add("login_token", cookieValue);
|
||||
fields.add("fields", "data.personId,data.gigyaDataCenter");
|
||||
fields.add("personId", personId);
|
||||
fields.add("gigyaDataCenter", gigyaDataCenter);
|
||||
final String url = this.constants.getGigyaRootUrl() + "/accounts.getJWT";
|
||||
ContentResponse response = this.httpClient.FORM(url, fields);
|
||||
final ContentResponse response = this.httpClient.FORM(url, fields);
|
||||
if (HttpStatus.OK_200 == response.getStatus()) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(),
|
||||
response.getReason(), response.getContentAsString());
|
||||
}
|
||||
try {
|
||||
JsonObject responseJson = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
|
||||
JsonElement element = responseJson.get("id_token");
|
||||
final JsonObject responseJson = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
|
||||
logger.trace("GigyaApi response: {}", responseJson);
|
||||
final JsonElement element = responseJson.get("id_token");
|
||||
if (element != null) {
|
||||
jwt = element.getAsString();
|
||||
logger.debug("GigyaApi jwt: {} ", jwt);
|
||||
}
|
||||
} catch (JsonParseException | ClassCastException | IllegalStateException e) {
|
||||
throw new RenaultException("Get JWT Error: jwt value not found in JSON response");
|
||||
throw new RenaultException("@text/error.renault.session.jwt.no_jwt");
|
||||
}
|
||||
} else {
|
||||
logger.warn("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(), response.getReason(),
|
||||
response.getContentAsString());
|
||||
throw new RenaultException("Get JWT Error: " + response.getReason());
|
||||
throw new RenaultException(
|
||||
String.format("@text/error.renault.session.jwt.error[\"%s\"]", response.getReason()));
|
||||
}
|
||||
}
|
||||
|
||||
private void getAccountID()
|
||||
throws RenaultException, RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
|
||||
JsonObject responseJson = getKamereonResponse(
|
||||
private void getAccountID() throws RenaultException {
|
||||
final JsonObject responseJson = getKamereonResponse(
|
||||
"/commerce/v1/persons/" + personId + "?country=" + getCountry(config));
|
||||
if (responseJson != null) {
|
||||
JsonArray accounts = responseJson.getAsJsonArray("accounts");
|
||||
@@ -201,84 +222,69 @@ public class MyRenaultHttpSession {
|
||||
}
|
||||
}
|
||||
if (kamereonaccountId == null) {
|
||||
throw new RenaultException("Can not get Kamereon " + config.accountType + " Account ID!");
|
||||
throw new RenaultException(String.format("@text/error.renault.session.kamereon_cant_get_account_id[\"%s\"]",
|
||||
config.accountType));
|
||||
}
|
||||
}
|
||||
|
||||
public void getVehicle(Car car) throws RenaultForbiddenException, RenaultUpdateException,
|
||||
RenaultNotImplementedException, RenaultAPIGatewayException {
|
||||
JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId + "/vehicles/"
|
||||
public void getVehicle(Car car) throws RenaultException {
|
||||
final JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId + "/vehicles/"
|
||||
+ config.vin + "/details?country=" + getCountry(config));
|
||||
if (responseJson != null) {
|
||||
car.setDetails(responseJson);
|
||||
}
|
||||
}
|
||||
|
||||
public void getBatteryStatus(Car car) throws RenaultForbiddenException, RenaultUpdateException,
|
||||
RenaultNotImplementedException, RenaultAPIGatewayException {
|
||||
JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId
|
||||
+ "/kamereon/kca/car-adapter/v2/cars/" + config.vin + "/battery-status?country=" + getCountry(config));
|
||||
public void getBatteryStatus(Car car) throws RenaultException {
|
||||
final JsonObject responseJson = getKamereonResponse(kcav2() + "battery-status?country=" + getCountry(config));
|
||||
if (responseJson != null) {
|
||||
car.setBatteryStatus(responseJson);
|
||||
}
|
||||
}
|
||||
|
||||
public void getHvacStatus(Car car) throws RenaultForbiddenException, RenaultUpdateException,
|
||||
RenaultNotImplementedException, RenaultAPIGatewayException {
|
||||
JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId
|
||||
+ "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/hvac-status?country=" + getCountry(config));
|
||||
public void getHvacStatus(Car car) throws RenaultException {
|
||||
final JsonObject responseJson = getKamereonResponse(kcav1() + "hvac-status?country=" + getCountry(config));
|
||||
if (responseJson != null) {
|
||||
car.setHVACStatus(responseJson);
|
||||
}
|
||||
}
|
||||
|
||||
public void getCockpit(Car car) throws RenaultForbiddenException, RenaultUpdateException,
|
||||
RenaultNotImplementedException, RenaultAPIGatewayException {
|
||||
JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId
|
||||
+ "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/cockpit?country=" + getCountry(config));
|
||||
public void getCockpit(Car car) throws RenaultException {
|
||||
final JsonObject responseJson = getKamereonResponse(kcav1() + "cockpit?country=" + getCountry(config));
|
||||
if (responseJson != null) {
|
||||
car.setCockpit(responseJson);
|
||||
}
|
||||
}
|
||||
|
||||
public void getLocation(Car car) throws RenaultForbiddenException, RenaultUpdateException,
|
||||
RenaultNotImplementedException, RenaultAPIGatewayException {
|
||||
JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId
|
||||
+ "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/location?country=" + getCountry(config));
|
||||
public void getLocation(Car car) throws RenaultException {
|
||||
final JsonObject responseJson = getKamereonResponse(kcav1() + "location?country=" + getCountry(config));
|
||||
if (responseJson != null) {
|
||||
car.setLocation(responseJson);
|
||||
}
|
||||
}
|
||||
|
||||
public void getLockStatus(Car car) throws RenaultForbiddenException, RenaultUpdateException,
|
||||
RenaultNotImplementedException, RenaultAPIGatewayException {
|
||||
JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId
|
||||
+ "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/lock-status?country=" + getCountry(config));
|
||||
public void getLockStatus(Car car) throws RenaultException {
|
||||
final JsonObject responseJson = getKamereonResponse(kcav1() + "lock-status?country=" + getCountry(config));
|
||||
if (responseJson != null) {
|
||||
car.setLockStatus(responseJson);
|
||||
}
|
||||
}
|
||||
|
||||
public void actionHvacOn(double hvacTargetTemperature) throws RenaultForbiddenException,
|
||||
RenaultNotImplementedException, RenaultActionException, RenaultAPIGatewayException {
|
||||
final String path = "/commerce/v1/accounts/" + kamereonaccountId + "/kamereon/kca/car-adapter/v1/cars/"
|
||||
+ config.vin + "/actions/hvac-start?country=" + getCountry(config);
|
||||
public void actionHvacOn(double hvacTargetTemperature) throws RenaultException {
|
||||
final String path = kcav1() + "actions/hvac-start?country=" + getCountry(config);
|
||||
postKamereonRequest(path,
|
||||
"{\"data\":{\"type\":\"HvacStart\",\"attributes\":{\"action\":\"start\",\"targetTemperature\":"
|
||||
+ hvacTargetTemperature + "}}}");
|
||||
}
|
||||
|
||||
public void actionChargeMode(ChargingMode mode) throws RenaultForbiddenException, RenaultNotImplementedException,
|
||||
RenaultActionException, RenaultAPIGatewayException {
|
||||
final String apiMode = ChargingMode.SCHEDULE_MODE.equals(mode) ? CHARGING_MODE_SCHEDULE : CHARGING_MODE_ALWAYS;
|
||||
final String path = "/commerce/v1/accounts/" + kamereonaccountId + "/kamereon/kca/car-adapter/v1/cars/"
|
||||
+ config.vin + "/actions/charge-mode?country=" + getCountry(config);
|
||||
public void actionChargeMode(ChargingMode mode) throws RenaultException {
|
||||
final String apiMode = mode.name().toLowerCase(Locale.ROOT);
|
||||
final String path = kcav1() + "actions/charge-mode?country=" + getCountry(config);
|
||||
postKamereonRequest(path,
|
||||
"{\"data\":{\"type\":\"ChargeMode\",\"attributes\":{\"action\":\"" + apiMode + "\"}}}");
|
||||
}
|
||||
|
||||
public void actionPause(boolean mode) throws RenaultForbiddenException, RenaultNotImplementedException,
|
||||
RenaultActionException, RenaultAPIGatewayException {
|
||||
public void actionPause(boolean mode) throws RenaultException {
|
||||
final String apiMode = mode ? "pause" : "resume";
|
||||
final String path = "/commerce/v1/accounts/" + kamereonaccountId + "/kamereon/kcm/v1/vehicles/" + config.vin
|
||||
+ "/charge/pause-resume?country=" + getCountry(config);
|
||||
@@ -286,36 +292,28 @@ public class MyRenaultHttpSession {
|
||||
"{\"data\":{\"type\":\"ChargePauseResume\",\"attributes\":{\"action\":\"" + apiMode + "\"}}}");
|
||||
}
|
||||
|
||||
private void postKamereonRequest(final String path, final String content) throws RenaultForbiddenException,
|
||||
RenaultNotImplementedException, RenaultActionException, RenaultAPIGatewayException {
|
||||
Request request = httpClient.newRequest(this.constants.getKamereonRootUrl() + path).method(HttpMethod.POST)
|
||||
.timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS).header("Content-type", "application/vnd.api+json")
|
||||
.header("apikey", this.config.kamereonApiKey)
|
||||
.header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt)
|
||||
.content(new StringContentProvider(content, "utf-8"));
|
||||
try {
|
||||
ContentResponse response = request.send();
|
||||
logKamereonCall(request, response);
|
||||
checkResponse(response);
|
||||
} catch (InterruptedException e) {
|
||||
logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (JsonParseException | TimeoutException | ExecutionException e) {
|
||||
throw new RenaultActionException(e.toString());
|
||||
}
|
||||
private void postKamereonRequest(final String path, final String content) throws RenaultException {
|
||||
requestKamereonResponse(HttpMethod.POST, path, new StringContentProvider(content, "utf-8"));
|
||||
}
|
||||
|
||||
private @Nullable JsonObject getKamereonResponse(String path) throws RenaultForbiddenException,
|
||||
RenaultNotImplementedException, RenaultUpdateException, RenaultAPIGatewayException {
|
||||
Request request = httpClient.newRequest(this.constants.getKamereonRootUrl() + path).method(HttpMethod.GET)
|
||||
private @Nullable JsonObject getKamereonResponse(String path) throws RenaultException {
|
||||
return requestKamereonResponse(HttpMethod.GET, path, null);
|
||||
}
|
||||
|
||||
private @Nullable JsonObject requestKamereonResponse(HttpMethod httpMethod, String path,
|
||||
@Nullable StringContentProvider content) throws RenaultException {
|
||||
Request request = httpClient.newRequest(this.constants.getKamereonRootUrl() + path).method(httpMethod)
|
||||
.timeout(REQUEST_TIMEOUT_MS, TimeUnit.MILLISECONDS).header("Content-type", "application/vnd.api+json")
|
||||
.header("apikey", this.config.kamereonApiKey)
|
||||
.header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt);
|
||||
.header("apikey", this.config.kamereonApiKey).header("x-gigya-id_token", jwt).content(content);
|
||||
try {
|
||||
ContentResponse response = request.send();
|
||||
logKamereonCall(request, response);
|
||||
if (HttpStatus.OK_200 == response.getStatus()) {
|
||||
return JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
|
||||
if (httpMethod == HttpMethod.GET) {
|
||||
if (HttpStatus.OK_200 == response.getStatus()) {
|
||||
final JsonObject json = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
|
||||
checkNotSupported(json);
|
||||
return json;
|
||||
}
|
||||
}
|
||||
checkResponse(response);
|
||||
} catch (InterruptedException e) {
|
||||
@@ -327,6 +325,24 @@ public class MyRenaultHttpSession {
|
||||
return null;
|
||||
}
|
||||
|
||||
private String kcav1() {
|
||||
return String.format("/commerce/v1/accounts/%s/kamereon/kca/car-adapter/v1/cars/%s/", kamereonaccountId,
|
||||
config.vin);
|
||||
}
|
||||
|
||||
private String kcav2() {
|
||||
return String.format("/commerce/v1/accounts/%s/kamereon/kca/car-adapter/v2/cars/%s/", kamereonaccountId,
|
||||
config.vin);
|
||||
}
|
||||
|
||||
private void checkNotSupported(JsonObject json) throws RenaultNotImplementedException {
|
||||
if (Optional.ofNullable(json.get("message")).map(JsonElement::getAsString).filter(NOT_BE_THERE::equals)
|
||||
.isPresent()) {
|
||||
logger.debug("Kamereon response indicates unsupported operation: {}", NOT_BE_THERE);
|
||||
throw new RenaultNotImplementedException("@text/error.renault.session.kamereon_request_not_implemented");
|
||||
}
|
||||
}
|
||||
|
||||
private void logKamereonCall(Request request, ContentResponse response) {
|
||||
if (HttpStatus.OK_200 == response.getStatus()) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
@@ -343,14 +359,22 @@ public class MyRenaultHttpSession {
|
||||
throws RenaultForbiddenException, RenaultNotImplementedException, RenaultAPIGatewayException {
|
||||
switch (response.getStatus()) {
|
||||
case HttpStatus.FORBIDDEN_403:
|
||||
throw new RenaultForbiddenException(
|
||||
"Kamereon request forbidden! Ensure the car is paired in your MyRenault App.");
|
||||
try {
|
||||
final JsonObject json = JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
|
||||
if ("err.func.privacy.on".equals(getErrorCode(json))) {
|
||||
throw new RenaultForbiddenException("@text/error.renault.session.kamereon_privacy_on");
|
||||
}
|
||||
} catch (JsonParseException e) {
|
||||
logger.debug("Could not parse 403 message: {}", response.getContentAsString());
|
||||
}
|
||||
throw new RenaultForbiddenException("@text/error.renault.session.kamereon_request_forbidden");
|
||||
case HttpStatus.NOT_FOUND_404:
|
||||
throw new RenaultNotImplementedException("Kamereon service not found");
|
||||
throw new RenaultNotImplementedException("@text/error.renault.session.kamereon_service_not_found");
|
||||
case HttpStatus.NOT_IMPLEMENTED_501:
|
||||
throw new RenaultNotImplementedException("Kamereon request not implemented");
|
||||
throw new RenaultNotImplementedException(
|
||||
"@text/error.renault.session.kamereon_request_not_implemented");
|
||||
case HttpStatus.BAD_GATEWAY_502:
|
||||
throw new RenaultAPIGatewayException("Kamereon request failed");
|
||||
throw new RenaultAPIGatewayException("@text/error.renault.session.kamereon_request_failed");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -363,4 +387,18 @@ public class MyRenaultHttpSession {
|
||||
}
|
||||
return country;
|
||||
}
|
||||
|
||||
private static String getErrorCode(JsonObject responseJson) {
|
||||
// @formatter:off
|
||||
final @Nullable String errorCode = Optional.ofNullable(responseJson.get("messages"))
|
||||
.map(m -> m.getAsJsonArray())
|
||||
.map(m -> m.asList())
|
||||
.map(m -> m.get(0))
|
||||
.map(m -> m.getAsJsonObject())
|
||||
.map(m -> m.get("code"))
|
||||
.map(m -> m.getAsString())
|
||||
.orElse("");
|
||||
// @formatter:on
|
||||
return errorCode == null ? "" : errorCode;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
* @author Doug Culnane - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RenaultActionException extends Exception {
|
||||
public class RenaultActionException extends RenaultException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
* @author Doug Culnane - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RenaultForbiddenException extends Exception {
|
||||
public class RenaultForbiddenException extends RenaultException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
* @author Doug Culnane - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RenaultNotImplementedException extends Exception {
|
||||
public class RenaultNotImplementedException extends RenaultException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
* @author Doug Culnane - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RenaultUpdateException extends Exception {
|
||||
public class RenaultUpdateException extends RenaultException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
|
||||
+312
-270
@@ -15,15 +15,20 @@ package org.openhab.binding.renault.internal.handler;
|
||||
import static org.openhab.binding.renault.internal.RenaultBindingConstants.*;
|
||||
import static org.openhab.core.library.unit.MetricPrefix.KILO;
|
||||
import static org.openhab.core.library.unit.SIUnits.METRE;
|
||||
import static org.openhab.core.library.unit.Units.KILOWATT_HOUR;
|
||||
import static org.openhab.core.library.unit.Units.MINUTE;
|
||||
import static org.openhab.core.library.unit.Units.*;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.measure.Quantity;
|
||||
import javax.measure.Unit;
|
||||
import javax.measure.quantity.Temperature;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
@@ -33,13 +38,11 @@ import org.openhab.binding.renault.internal.RenaultBindingConstants;
|
||||
import org.openhab.binding.renault.internal.RenaultConfiguration;
|
||||
import org.openhab.binding.renault.internal.api.Car;
|
||||
import org.openhab.binding.renault.internal.api.Car.ChargingMode;
|
||||
import org.openhab.binding.renault.internal.api.Car.LockStatus;
|
||||
import org.openhab.binding.renault.internal.api.MyRenaultHttpSession;
|
||||
import org.openhab.binding.renault.internal.api.exceptions.RenaultAPIGatewayException;
|
||||
import org.openhab.binding.renault.internal.api.exceptions.RenaultActionException;
|
||||
import org.openhab.binding.renault.internal.api.exceptions.RenaultException;
|
||||
import org.openhab.binding.renault.internal.api.exceptions.RenaultForbiddenException;
|
||||
import org.openhab.binding.renault.internal.api.exceptions.RenaultNotImplementedException;
|
||||
import org.openhab.binding.renault.internal.api.exceptions.RenaultUpdateException;
|
||||
import org.openhab.core.cache.ExpiringCache;
|
||||
import org.openhab.core.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
@@ -54,6 +57,7 @@ import org.openhab.core.thing.ThingStatusDetail;
|
||||
import org.openhab.core.thing.binding.BaseThingHandler;
|
||||
import org.openhab.core.types.Command;
|
||||
import org.openhab.core.types.RefreshType;
|
||||
import org.openhab.core.types.State;
|
||||
import org.openhab.core.types.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -67,178 +71,160 @@ import org.slf4j.LoggerFactory;
|
||||
@NonNullByDefault
|
||||
public class RenaultHandler extends BaseThingHandler {
|
||||
|
||||
private interface RenaultRunner {
|
||||
void run() throws RenaultException, InterruptedException, ExecutionException, TimeoutException;
|
||||
}
|
||||
|
||||
private interface RenaultConsumer<T> {
|
||||
void accept(T value) throws RenaultException, InterruptedException, ExecutionException, TimeoutException;
|
||||
}
|
||||
|
||||
private static final Duration CACHE_INVALIDATION_TIMEOUT_SECONDS = Duration.ofSeconds(10);
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(RenaultHandler.class);
|
||||
|
||||
private final ExpireCarCache carCache = new ExpireCarCache(this::refreshCar);
|
||||
private final Object lockObject = new Object();
|
||||
|
||||
private final HttpClient httpClient;
|
||||
|
||||
private RenaultConfiguration config = new RenaultConfiguration();
|
||||
|
||||
private @Nullable ScheduledFuture<?> pollingJob;
|
||||
|
||||
private HttpClient httpClient;
|
||||
|
||||
private Car car;
|
||||
private @Nullable MyRenaultHttpSession httpSession;
|
||||
|
||||
public RenaultHandler(Thing thing, HttpClient httpClient) {
|
||||
super(thing);
|
||||
this.car = new Car();
|
||||
this.httpClient = httpClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
// reset the car on initialize
|
||||
this.car = new Car();
|
||||
this.config = getConfigAs(RenaultConfiguration.class);
|
||||
|
||||
// Validate configuration
|
||||
if (this.config.myRenaultUsername.isBlank()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "MyRenault Username is empty!");
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/thing-type.config.renault.car.error.username_empty");
|
||||
return;
|
||||
}
|
||||
if (this.config.myRenaultPassword.isBlank()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "MyRenault Password is empty!");
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/thing-type.config.renault.car.error.password_empty");
|
||||
return;
|
||||
}
|
||||
if (this.config.locale.isBlank()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Location is empty!");
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/thing-type.config.renault.car.error.location_empty");
|
||||
return;
|
||||
}
|
||||
if (this.config.vin.isBlank()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "VIN is empty!");
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"@text/thing-type.config.renault.car.error.vin_empty");
|
||||
return;
|
||||
}
|
||||
if (this.config.refreshInterval < 1) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
|
||||
"The refresh interval mush to be larger than 1");
|
||||
"@text/thing-type.config.renault.car.error.refresh_interval_to_low");
|
||||
return;
|
||||
}
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
|
||||
new QuantityType<>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
|
||||
|
||||
this.httpSession = new MyRenaultHttpSession(this.config, httpClient);
|
||||
reschedulePollingJob();
|
||||
}
|
||||
|
||||
private void reschedulePollingJob() {
|
||||
synchronized (lockObject) {
|
||||
final ScheduledFuture<?> job = pollingJob;
|
||||
if (job != null) {
|
||||
job.cancel(true);
|
||||
}
|
||||
carCache.reset();
|
||||
pollingJob = scheduler.scheduleWithFixedDelay(carCache::getCar, 0, config.refreshInterval,
|
||||
TimeUnit.MINUTES);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
switch (channelUID.getId()) {
|
||||
case RenaultBindingConstants.CHANNEL_HVAC_TARGET_TEMPERATURE:
|
||||
if (!car.isDisableHvac()) {
|
||||
if (command instanceof RefreshType) {
|
||||
updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
|
||||
new QuantityType<>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
|
||||
} else if (command instanceof DecimalType decimalCommand) {
|
||||
car.setHvacTargetTemperature(decimalCommand.doubleValue());
|
||||
updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
|
||||
new QuantityType<>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
|
||||
} else if (command instanceof QuantityType) {
|
||||
@Nullable
|
||||
QuantityType<Temperature> celsius = ((QuantityType<Temperature>) command)
|
||||
.toUnit(SIUnits.CELSIUS);
|
||||
if (celsius != null) {
|
||||
car.setHvacTargetTemperature(celsius.doubleValue());
|
||||
}
|
||||
updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
|
||||
new QuantityType<>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case RenaultBindingConstants.CHANNEL_HVAC_STATUS:
|
||||
if (!car.isDisableHvac()) {
|
||||
if (command instanceof RefreshType) {
|
||||
reschedulePollingJob();
|
||||
} else if (command instanceof StringType && command.toString().equals(Car.HVAC_STATUS_ON)) {
|
||||
// We can only trigger pre-conditioning of the car.
|
||||
final MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
|
||||
try {
|
||||
updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_PENDING));
|
||||
car.resetHVACStatus();
|
||||
httpSession.initSesssion(car);
|
||||
httpSession.actionHvacOn(car.getHvacTargetTemperature());
|
||||
ScheduledFuture<?> job = pollingJob;
|
||||
if (job != null) {
|
||||
job.cancel(true);
|
||||
}
|
||||
pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, config.updateDelay,
|
||||
config.refreshInterval * 60, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
logger.warn("Error My Renault Http Session.", e);
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (RenaultException | RenaultForbiddenException | RenaultUpdateException
|
||||
| RenaultActionException | RenaultNotImplementedException | ExecutionException
|
||||
| TimeoutException e) {
|
||||
logger.warn("Error during action HVAC on.", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
final MyRenaultHttpSession httpSession = this.httpSession;
|
||||
if (httpSession == null) {
|
||||
return;
|
||||
}
|
||||
synchronized (lockObject) {
|
||||
final Car car = carCache.getCar();
|
||||
if (command instanceof RefreshType) {
|
||||
updateChannel(channelUID.getId(), car);
|
||||
return;
|
||||
}
|
||||
switch (channelUID.getId()) {
|
||||
case RenaultBindingConstants.CHANNEL_HVAC_TARGET_TEMPERATURE:
|
||||
if (!car.isDisableHvac()) {
|
||||
if (command instanceof DecimalType decimalCommand) {
|
||||
car.setHvacTargetTemperature(decimalCommand.doubleValue());
|
||||
} else if (command instanceof QuantityType) {
|
||||
Optional.ofNullable(((QuantityType<Temperature>) command).toUnit(SIUnits.CELSIUS))
|
||||
.ifPresent(celsius -> car.setHvacTargetTemperature(celsius.doubleValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case RenaultBindingConstants.CHANNEL_CHARGING_MODE:
|
||||
if (command instanceof RefreshType) {
|
||||
reschedulePollingJob();
|
||||
} else if (command instanceof StringType) {
|
||||
try {
|
||||
ChargingMode newMode = ChargingMode.valueOf(command.toString());
|
||||
if (!ChargingMode.UNKNOWN.equals(newMode)) {
|
||||
MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
|
||||
try {
|
||||
httpSession.initSesssion(car);
|
||||
httpSession.actionChargeMode(newMode);
|
||||
car.setChargeMode(newMode);
|
||||
updateState(CHANNEL_CHARGING_MODE, new StringType(newMode.toString()));
|
||||
} catch (InterruptedException e) {
|
||||
logger.warn("Error My Renault Http Session.", e);
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (RenaultException | RenaultForbiddenException | RenaultUpdateException
|
||||
| RenaultActionException | RenaultNotImplementedException | ExecutionException
|
||||
| TimeoutException e) {
|
||||
logger.warn("Error during action set charge mode.", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
e.getMessage());
|
||||
break;
|
||||
case RenaultBindingConstants.CHANNEL_HVAC_STATUS:
|
||||
if (!car.isDisableHvac()) {
|
||||
if (command instanceof StringType && command.toString().equals(Car.HVAC_STATUS_ON)) {
|
||||
// We can only trigger pre-conditioning of the car.
|
||||
if (run(() -> {
|
||||
updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_PENDING));
|
||||
car.resetHVACStatus();
|
||||
httpSession.initSesssion();
|
||||
httpSession.actionHvacOn(car.getHvacTargetTemperature());
|
||||
}, "Error during action HVAC on.", true)) {
|
||||
refreshHvac(car);
|
||||
}
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Invalid ChargingMode {}.", command.toString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case RenaultBindingConstants.CHANNEL_PAUSE:
|
||||
if (command instanceof RefreshType) {
|
||||
reschedulePollingJob();
|
||||
} else if (command instanceof OnOffType) {
|
||||
try {
|
||||
MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
|
||||
break;
|
||||
case RenaultBindingConstants.CHANNEL_CHARGING_MODE:
|
||||
if (command instanceof StringType) {
|
||||
try {
|
||||
boolean pause = OnOffType.ON == command;
|
||||
httpSession.initSesssion(car);
|
||||
httpSession.actionPause(pause);
|
||||
car.setPauseMode(pause);
|
||||
updateState(CHANNEL_PAUSE, OnOffType.from(command.toString()));
|
||||
} catch (InterruptedException e) {
|
||||
logger.warn("Error My Renault Http Session.", e);
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (RenaultForbiddenException | RenaultNotImplementedException | RenaultActionException
|
||||
| RenaultException | RenaultUpdateException | ExecutionException | TimeoutException e) {
|
||||
logger.warn("Error during action set pause.", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
final ChargingMode newMode = ChargingMode.valueOf(command.toString());
|
||||
if ((newMode == ChargingMode.ALWAYS_CHARGING || newMode == ChargingMode.SCHEDULE_MODE)
|
||||
&& run(() -> {
|
||||
httpSession.initSesssion();
|
||||
httpSession.actionChargeMode(newMode);
|
||||
car.setChargeMode(newMode);
|
||||
}, "Error during action set charge mode.", true)) {
|
||||
refreshBattery(car);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Invalid ChargingMode {}.", command.toString());
|
||||
return;
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Invalid Pause Mode {}.", command.toString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (command instanceof RefreshType) {
|
||||
reschedulePollingJob();
|
||||
}
|
||||
break;
|
||||
break;
|
||||
case RenaultBindingConstants.CHANNEL_PAUSE:
|
||||
if (command instanceof OnOffType) {
|
||||
try {
|
||||
if (run(() -> {
|
||||
httpSession.initSesssion();
|
||||
httpSession.actionPause(OnOffType.ON == command);
|
||||
}, "Error during action set pause.", true)) {
|
||||
refreshBattery(car);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
logger.warn("Invalid Pause Mode {}.", command.toString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
ScheduledFuture<?> job = pollingJob;
|
||||
final ScheduledFuture<?> job = pollingJob;
|
||||
if (job != null) {
|
||||
job.cancel(true);
|
||||
pollingJob = null;
|
||||
@@ -246,164 +232,220 @@ public class RenaultHandler extends BaseThingHandler {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private void getStatus() {
|
||||
MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
|
||||
try {
|
||||
httpSession.initSesssion(car);
|
||||
private void refreshBattery(Car car) {
|
||||
runWithHttpSession(httpSession -> {
|
||||
updateBattery(httpSession, car);
|
||||
BATTERY_CHANNELS.forEach(c -> updateChannel(c, car));
|
||||
}, "Battery");
|
||||
}
|
||||
|
||||
private void refreshHvac(Car car) {
|
||||
runWithHttpSession(httpSession -> {
|
||||
updateHvac(httpSession, car);
|
||||
HVAC_CHANNELS.forEach(c -> updateChannel(c, car));
|
||||
}, "HVAC.");
|
||||
}
|
||||
|
||||
private Car refreshCar(Car car) {
|
||||
synchronized (lockObject) {
|
||||
runWithHttpSession(httpSession -> {
|
||||
httpSession.initSesssion();
|
||||
// Only get vehicle image once. Not other data from Vehicle is used.
|
||||
runIfNotDisabled(car.isDisableVehicle() || car.getImageURL() != null, () -> httpSession.getVehicle(car),
|
||||
() -> car.setDisableVehicle(true), "imageURL");
|
||||
updateHvac(httpSession, car);
|
||||
runIfNotDisabled(car.isDisableLocation(), () -> httpSession.getLocation(car),
|
||||
() -> car.setDisableLocation(true), "location");
|
||||
runIfNotDisabled(car.isDisableCockpit(), () -> httpSession.getCockpit(car),
|
||||
() -> car.setDisableCockpit(true), "cockpit");
|
||||
updateBattery(httpSession, car);
|
||||
runIfNotDisabled(car.isDisableLockStatus(), () -> httpSession.getLockStatus(car),
|
||||
() -> car.setDisableLockStatus(true), "lock");
|
||||
|
||||
ALL_CHANNELS.forEach(c -> updateChannel(c, car));
|
||||
updateStatus(car);
|
||||
}, "all");
|
||||
return car;
|
||||
}
|
||||
}
|
||||
|
||||
private void runWithHttpSession(RenaultConsumer<MyRenaultHttpSession> supplier, String type) {
|
||||
final MyRenaultHttpSession httpSession = this.httpSession;
|
||||
if (httpSession == null) {
|
||||
return;
|
||||
}
|
||||
logger.debug("Get Car {} data from cloud service and update channels.", type);
|
||||
run(() -> supplier.accept(httpSession), "Error My Renault Http Session.", true);
|
||||
}
|
||||
|
||||
private void updateHvac(final MyRenaultHttpSession httpSession, Car car) {
|
||||
runIfNotDisabled(car.isDisableHvac(), () -> httpSession.getHvacStatus(car), () -> car.setDisableHvac(true),
|
||||
"HVAC");
|
||||
}
|
||||
|
||||
private void updateBattery(final MyRenaultHttpSession httpSession, Car car) {
|
||||
runIfNotDisabled(car.isDisableBattery(), () -> httpSession.getBatteryStatus(car),
|
||||
() -> car.setDisableBattery(true), "battery");
|
||||
}
|
||||
|
||||
private void updateStatus(Car car) {
|
||||
if (car.getImageURL() == null && car.isDisableHvac() && car.isDisableLocation() && car.isDisableCockpit()
|
||||
&& car.isDisableBattery() && car.isDisableLockStatus()) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
"@text/thing-type.renault.car.error.no_data");
|
||||
} else {
|
||||
updateStatus(ThingStatus.ONLINE);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateChannel(String channelId, Car car) {
|
||||
if (CHANNEL_PAUSE.equals(channelId)) {
|
||||
return; // channel pause is command channel therefore no state set.
|
||||
}
|
||||
final @Nullable State state = switch (channelId) {
|
||||
case CHANNEL_IMAGE -> stringNotBlank(car.getImageURL());
|
||||
// ── Location ─────────────────────────────────────────────────────────
|
||||
case CHANNEL_LOCATION ->
|
||||
getIfNotDisabled(car.isDisableLocation(), () -> point(car.getGpsLatitude(), car.getGpsLongitude()));
|
||||
case CHANNEL_LOCATION_UPDATED ->
|
||||
getIfNotDisabled(car.isDisableLocation(), () -> dateTime(car.getLocationUpdated()));
|
||||
// ── Cockpit ──────────────────────────────────────────────────────────
|
||||
case CHANNEL_ODOMETER ->
|
||||
getIfNotDisabled(car.isDisableCockpit(), () -> quantity(car.getOdometer(), KILO(METRE)));
|
||||
// ── Lock ──────────────────────────────────────────────────────────
|
||||
case CHANNEL_LOCKED -> getIfNotDisabled(car.isDisableLockStatus(), () -> lock(car.getLockStatus()));
|
||||
// ── HVAC ─────────────────────────────────────────────────────────────
|
||||
case CHANNEL_HVAC_STATUS -> getIfNotDisabled(car.isDisableHvac(), () -> hvacStatus(car.getHvacstatus()));
|
||||
case CHANNEL_HVAC_TARGET_TEMPERATURE ->
|
||||
getIfNotDisabled(car.isDisableHvac(), () -> quantity(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
|
||||
case CHANNEL_EXTERNAL_TEMPERATURE ->
|
||||
getIfNotDisabled(car.isDisableHvac(), () -> quantity(car.getExternalTemperature(), SIUnits.CELSIUS));
|
||||
// ── Battery ──────────────────────────────────────────────────────────
|
||||
case CHANNEL_PLUG_STATUS ->
|
||||
getIfNotDisabled(car.isDisableBattery(), () -> new StringType(car.getPlugStatus().name()));
|
||||
case CHANNEL_CHARGING_STATUS ->
|
||||
getIfNotDisabled(car.isDisableBattery(), () -> new StringType(car.getChargingStatus().name()));
|
||||
case CHANNEL_BATTERY_LEVEL ->
|
||||
getIfNotDisabled(car.isDisableBattery(), () -> decimal(car.getBatteryLevel()));
|
||||
case CHANNEL_ESTIMATED_RANGE ->
|
||||
getIfNotDisabled(car.isDisableBattery(), () -> quantity(car.getEstimatedRange(), KILO(METRE)));
|
||||
case CHANNEL_BATTERY_AVAILABLE_ENERGY -> getIfNotDisabled(car.isDisableBattery(),
|
||||
() -> quantity(car.getBatteryAvailableEnergy(), KILOWATT_HOUR));
|
||||
case CHANNEL_CHARGING_REMAINING_TIME ->
|
||||
getIfNotDisabled(car.isDisableBattery(), () -> quantity(car.getChargingRemainingTime(), MINUTE));
|
||||
case CHANNEL_BATTERY_STATUS_UPDATED ->
|
||||
getIfNotDisabled(car.isDisableBattery(), () -> dateTime(car.getBatteryStatusUpdated()));
|
||||
case CHANNEL_CHARGING_MODE ->
|
||||
getIfNotDisabled(car.isDisableChargeMode(), () -> stringEnum(car.getChargingMode()));
|
||||
default -> null;
|
||||
};
|
||||
if (state == null) {
|
||||
logger.debug("updateChannel: unhandled channel '{}'", channelId);
|
||||
}
|
||||
updateState(channelId, state == null ? UnDefType.NULL : state);
|
||||
}
|
||||
|
||||
private static State getIfNotDisabled(boolean disabled, Supplier<State> supplier) {
|
||||
return disabled ? UnDefType.UNDEF : supplier.get();
|
||||
}
|
||||
|
||||
private State hvacStatus(@Nullable Boolean hvacStatus) {
|
||||
final String hvacString;
|
||||
if (hvacStatus == null) {
|
||||
hvacString = Car.HVAC_STATUS_PENDING;
|
||||
} else if (hvacStatus.booleanValue()) {
|
||||
hvacString = Car.HVAC_STATUS_ON;
|
||||
} else {
|
||||
hvacString = Car.HVAC_STATUS_OFF;
|
||||
}
|
||||
return new StringType(hvacString);
|
||||
}
|
||||
|
||||
private static State lock(LockStatus lockStatus) {
|
||||
return switch (lockStatus) {
|
||||
case LOCKED -> OnOffType.ON;
|
||||
case UNLOCKED -> OnOffType.OFF;
|
||||
default -> UnDefType.UNDEF;
|
||||
};
|
||||
}
|
||||
|
||||
private static State stringNotBlank(@Nullable String value) {
|
||||
return value == null || value.isBlank() ? UnDefType.UNDEF : new StringType(value);
|
||||
}
|
||||
|
||||
private static State stringEnum(@Nullable Enum<?> value) {
|
||||
return value == null ? UnDefType.UNDEF : new StringType(value.name());
|
||||
}
|
||||
|
||||
private static State point(@Nullable Double latitude, @Nullable Double longitude) {
|
||||
return latitude == null || longitude == null ? UnDefType.UNDEF
|
||||
: new PointType(new DecimalType(latitude.doubleValue()), new DecimalType(longitude.doubleValue()));
|
||||
}
|
||||
|
||||
private static State dateTime(@Nullable ZonedDateTime value) {
|
||||
return value == null ? UnDefType.UNDEF : new DateTimeType(value);
|
||||
}
|
||||
|
||||
private static <Q extends Quantity<Q>> State quantity(@Nullable Number value, Unit<Q> unit) {
|
||||
return value == null ? UnDefType.UNDEF : new QuantityType<>(value, unit);
|
||||
}
|
||||
|
||||
private static State decimal(@Nullable Number value) {
|
||||
return value == null ? UnDefType.UNDEF : new DecimalType(value);
|
||||
}
|
||||
|
||||
private void runIfNotDisabled(boolean disabled, RenaultRunner handler, Runnable disabler, String type) {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
run(() -> {
|
||||
try {
|
||||
handler.run();
|
||||
} catch (RenaultNotImplementedException e) {
|
||||
logger.debug("Disabling unsupported {} status update", type, e);
|
||||
disabler.run();
|
||||
}
|
||||
}, String.format("Error updating %s status.", type), false);
|
||||
}
|
||||
|
||||
private boolean run(RenaultRunner handler, String logWarnMessage, boolean setOffline) {
|
||||
try {
|
||||
handler.run();
|
||||
return true;
|
||||
} catch (InterruptedException e) {
|
||||
logger.warn("Error My Renault Http Session.", e);
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (Exception e) {
|
||||
logger.warn("Error My Renault Http Session.", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
String imageURL = car.getImageURL();
|
||||
if (imageURL != null && !imageURL.isEmpty()) {
|
||||
updateState(CHANNEL_IMAGE, new StringType(imageURL));
|
||||
}
|
||||
updateHvacStatus(httpSession);
|
||||
updateCockpit(httpSession);
|
||||
updateLocation(httpSession);
|
||||
updateBattery(httpSession);
|
||||
updateLockStatus(httpSession);
|
||||
}
|
||||
|
||||
private void updateHvacStatus(MyRenaultHttpSession httpSession) {
|
||||
if (!car.isDisableHvac()) {
|
||||
try {
|
||||
httpSession.getHvacStatus(car);
|
||||
Boolean hvacstatus = car.getHvacstatus();
|
||||
if (hvacstatus == null) {
|
||||
updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_PENDING));
|
||||
} else if (hvacstatus.booleanValue()) {
|
||||
updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_ON));
|
||||
} else {
|
||||
updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_OFF));
|
||||
}
|
||||
Double externalTemperature = car.getExternalTemperature();
|
||||
if (externalTemperature != null) {
|
||||
updateState(CHANNEL_EXTERNAL_TEMPERATURE,
|
||||
new QuantityType<>(externalTemperature.doubleValue(), SIUnits.CELSIUS));
|
||||
}
|
||||
} catch (RenaultNotImplementedException e) {
|
||||
logger.warn("Disabling unsupported HVAC status update.");
|
||||
car.setDisableHvac(true);
|
||||
} catch (RenaultForbiddenException | RenaultUpdateException | RenaultAPIGatewayException e) {
|
||||
logger.warn("Error updating HVAC status.", e);
|
||||
} catch (RenaultException | ExecutionException | TimeoutException e) {
|
||||
logger.warn("{}", logWarnMessage, e);
|
||||
if (setOffline) {
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void updateLocation(MyRenaultHttpSession httpSession) {
|
||||
if (!car.isDisableLocation()) {
|
||||
try {
|
||||
httpSession.getLocation(car);
|
||||
Double latitude = car.getGpsLatitude();
|
||||
Double longitude = car.getGpsLongitude();
|
||||
if (latitude != null && longitude != null) {
|
||||
updateState(CHANNEL_LOCATION, new PointType(new DecimalType(latitude.doubleValue()),
|
||||
new DecimalType(longitude.doubleValue())));
|
||||
}
|
||||
ZonedDateTime locationUpdated = car.getLocationUpdated();
|
||||
if (locationUpdated != null) {
|
||||
updateState(CHANNEL_LOCATION_UPDATED, new DateTimeType(locationUpdated));
|
||||
}
|
||||
} catch (RenaultNotImplementedException e) {
|
||||
logger.warn("Disabling unsupported location update.");
|
||||
car.setDisableLocation(true);
|
||||
} catch (IllegalArgumentException | RenaultForbiddenException | RenaultUpdateException
|
||||
| RenaultAPIGatewayException e) {
|
||||
logger.warn("Error updating location.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
private static class ExpireCarCache {
|
||||
private final ExpiringCache<Car> carCache;
|
||||
private Car car = new Car();
|
||||
|
||||
private void updateCockpit(MyRenaultHttpSession httpSession) {
|
||||
if (!car.isDisableCockpit()) {
|
||||
try {
|
||||
httpSession.getCockpit(car);
|
||||
Double odometer = car.getOdometer();
|
||||
if (odometer != null) {
|
||||
updateState(CHANNEL_ODOMETER, new QuantityType<>(odometer.doubleValue(), KILO(METRE)));
|
||||
}
|
||||
} catch (RenaultNotImplementedException e) {
|
||||
logger.warn("Disabling unsupported cockpit status update.");
|
||||
car.setDisableCockpit(true);
|
||||
} catch (RenaultForbiddenException | RenaultUpdateException | RenaultAPIGatewayException e) {
|
||||
logger.warn("Error updating cockpit status.", e);
|
||||
}
|
||||
ExpireCarCache(Function<Car, Car> updateCar) {
|
||||
carCache = new ExpiringCache<>(CACHE_INVALIDATION_TIMEOUT_SECONDS, () -> updateCar.apply(car));
|
||||
}
|
||||
}
|
||||
|
||||
private void updateBattery(MyRenaultHttpSession httpSession) {
|
||||
if (!car.isDisableBattery()) {
|
||||
try {
|
||||
httpSession.getBatteryStatus(car);
|
||||
updateState(CHANNEL_PLUG_STATUS, new StringType(car.getPlugStatus().name()));
|
||||
updateState(CHANNEL_CHARGING_STATUS, new StringType(car.getChargingStatus().name()));
|
||||
Double batteryLevel = car.getBatteryLevel();
|
||||
if (batteryLevel != null) {
|
||||
updateState(CHANNEL_BATTERY_LEVEL, new DecimalType(batteryLevel.doubleValue()));
|
||||
}
|
||||
Double estimatedRange = car.getEstimatedRange();
|
||||
if (estimatedRange != null) {
|
||||
updateState(CHANNEL_ESTIMATED_RANGE, new QuantityType<>(estimatedRange.doubleValue(), KILO(METRE)));
|
||||
}
|
||||
Double batteryAvailableEnergy = car.getBatteryAvailableEnergy();
|
||||
if (batteryAvailableEnergy != null) {
|
||||
updateState(CHANNEL_BATTERY_AVAILABLE_ENERGY,
|
||||
new QuantityType<>(batteryAvailableEnergy.doubleValue(), KILOWATT_HOUR));
|
||||
}
|
||||
Integer chargingRemainingTime = car.getChargingRemainingTime();
|
||||
if (chargingRemainingTime != null) {
|
||||
updateState(CHANNEL_CHARGING_REMAINING_TIME,
|
||||
new QuantityType<>(chargingRemainingTime.doubleValue(), MINUTE));
|
||||
}
|
||||
ZonedDateTime batteryStatusUpdated = car.getBatteryStatusUpdated();
|
||||
if (batteryStatusUpdated != null) {
|
||||
updateState(CHANNEL_BATTERY_STATUS_UPDATED, new DateTimeType(batteryStatusUpdated));
|
||||
}
|
||||
} catch (RenaultNotImplementedException e) {
|
||||
logger.warn("Disabling unsupported battery update.");
|
||||
car.setDisableBattery(true);
|
||||
} catch (RenaultForbiddenException | RenaultUpdateException | RenaultAPIGatewayException e) {
|
||||
logger.warn("Error updating battery status.", e);
|
||||
}
|
||||
/**
|
||||
* Resets the Car object. Only call when binding (re)started.
|
||||
*/
|
||||
public void reset() {
|
||||
car = new Car();
|
||||
carCache.invalidateValue();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateLockStatus(MyRenaultHttpSession httpSession) {
|
||||
if (!car.isDisableLockStatus()) {
|
||||
try {
|
||||
httpSession.getLockStatus(car);
|
||||
switch (car.getLockStatus()) {
|
||||
case LOCKED:
|
||||
updateState(CHANNEL_LOCKED, OnOffType.ON);
|
||||
break;
|
||||
case UNLOCKED:
|
||||
updateState(CHANNEL_LOCKED, OnOffType.OFF);
|
||||
break;
|
||||
default:
|
||||
updateState(CHANNEL_LOCKED, UnDefType.UNDEF);
|
||||
break;
|
||||
}
|
||||
} catch (RenaultNotImplementedException | RenaultAPIGatewayException e) {
|
||||
// If not supported API returns a Bad Gateway for this call.
|
||||
updateState(CHANNEL_LOCKED, UnDefType.UNDEF);
|
||||
logger.warn("Disabling unsupported lock status update.");
|
||||
car.setDisableLockStatus(true);
|
||||
} catch (RenaultForbiddenException | RenaultUpdateException e) {
|
||||
logger.warn("Error updating lock status.", e);
|
||||
}
|
||||
/**
|
||||
* @return Returns an updated Car object when the expire timeout has been
|
||||
* triggered.
|
||||
*/
|
||||
public Car getCar() {
|
||||
return Optional.ofNullable(carCache.getValue()).orElseGet(() -> car);
|
||||
}
|
||||
}
|
||||
|
||||
private void reschedulePollingJob() {
|
||||
ScheduledFuture<?> job = pollingJob;
|
||||
if (job != null) {
|
||||
job.cancel(true);
|
||||
}
|
||||
pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, 0, config.refreshInterval, TimeUnit.MINUTES);
|
||||
}
|
||||
}
|
||||
|
||||
+27
-1
@@ -15,6 +15,8 @@ thing-type.config.renault.car.accountType.option.MYDACIA = MyDacia
|
||||
thing-type.config.renault.car.accountType.option.MYRENAULT = MyRenault
|
||||
thing-type.config.renault.car.kamereonApiKey.label = Kamereon API Key
|
||||
thing-type.config.renault.car.kamereonApiKey.description = Access code for MyRenault Services.
|
||||
thing-type.config.renault.car.gigyaApiKey.label = Gigya API Key
|
||||
thing-type.config.renault.car.gigyaApiKey.description = Configure a Gigya API Key to override the hard coded value for your region.
|
||||
thing-type.config.renault.car.locale.label = MyRenault Location
|
||||
thing-type.config.renault.car.locale.description = The country (and language combination) that best fits with your MyRenault registered car.
|
||||
thing-type.config.renault.car.locale.option.de_AT = Austria
|
||||
@@ -62,7 +64,6 @@ channel-type.renault.batterystatusupdated.label = Battery Status Updated
|
||||
channel-type.renault.batterystatusupdated.description = Timestamp of the last battery status update
|
||||
channel-type.renault.batterystatusupdated.state.pattern = %1$tH:%1$tM %1$td.%1$tm.%1$tY
|
||||
channel-type.renault.chargingmode.label = Charging Mode
|
||||
channel-type.renault.chargingmode.state.option.UNKNOWN = Unknown
|
||||
channel-type.renault.chargingmode.state.option.SCHEDULE_MODE = Schedule mode
|
||||
channel-type.renault.chargingmode.state.option.ALWAYS_CHARGING = Instant charge
|
||||
channel-type.renault.chargingremainingtime.label = Charging Time Remaining
|
||||
@@ -104,3 +105,28 @@ channel-type.renault.plugstatus.state.option.PLUGGED = Plugged
|
||||
channel-type.renault.plugstatus.state.option.PLUG_ERROR = Plug error
|
||||
channel-type.renault.plugstatus.state.option.PLUG_UNKNOWN = Plug unknown
|
||||
channel-type.renault.plugstatus.state.option.UNKNOWN = Unknown
|
||||
|
||||
# thing type error messages
|
||||
|
||||
thing-type.config.renault.car.error.username_empty = MyRenault Username is empty!
|
||||
thing-type.config.renault.car.error.password_empty = MyRenault Password is empty!
|
||||
thing-type.config.renault.car.error.location_empty = Location is empty!
|
||||
thing-type.config.renault.car.error.vin_empty = VIN is empty!
|
||||
thing-type.config.renault.car.error.refresh_interval_to_low = The refresh interval must be at least 1
|
||||
thing-type.renault.car.error.no_data = No data at all. Is your VIN correct?
|
||||
|
||||
# error messages
|
||||
|
||||
error.renault.session.gigya.no_data = Get Account Info Error: personId or gigyaDataCenter value not found in JSON response.
|
||||
error.renault.session.gigya.error = Get Account Info Error: {0}.
|
||||
error.renault.session.jwt.error = Get JWT Error: {0}.
|
||||
error.renault.session.jwt.no_jwt = Get JWT Error: jwt value not found in JSON response.
|
||||
error.renault.session.kamereon_cant_get_account_id = Can not get Kamereon {0} Account ID.
|
||||
error.renault.session.kamereon_request_forbidden = Kamereon request forbidden! Ensure the car is paired in your MyRenault App.
|
||||
error.renault.session.kamereon_privacy_on = Privacy mode currently ON. Change your privacy settings in your Car.
|
||||
error.renault.session.kamereon_request_not_implemented = Kamereon request not implemented.
|
||||
error.renault.session.kamereon_request_failed = Kamereon request failed.
|
||||
error.renault.session.kamereon_service_not_found = Kamereon service not found.
|
||||
error.renault.session.login.error = Login Error: {0}.
|
||||
error.renault.session.login.no_cookie = Login Error: cookie value not found in JSON response.
|
||||
error.renault.session.login.no_session_info = Login Error: Could not find session info.
|
||||
|
||||
@@ -104,6 +104,11 @@
|
||||
<description>Access code for MyRenault Services.</description>
|
||||
<default>YjkKtHmGfaceeuExUDKGxrLZGGvtVS0J</default>
|
||||
</parameter>
|
||||
<parameter name="gigyaApiKey" type="text">
|
||||
<label>Gigya API Key</label>
|
||||
<description>Configure a Gigya API Key to override the hard coded value for your region.</description>
|
||||
<default></default>
|
||||
</parameter>
|
||||
</config-description>
|
||||
</thing-type>
|
||||
|
||||
@@ -172,7 +177,6 @@
|
||||
</tags>
|
||||
<state readOnly="false">
|
||||
<options>
|
||||
<option value="UNKNOWN">Unknown</option>
|
||||
<option value="SCHEDULE_MODE">Schedule mode</option>
|
||||
<option value="ALWAYS_CHARGING">Instant charge</option>
|
||||
</options>
|
||||
|
||||
Reference in New Issue
Block a user