[renault] Add new channels 'batterystatusupdated' and 'locked' (#14076)
Signed-off-by: Doug Culnane <doug@culnane.net>
This commit is contained in:
parent
cb460657eb
commit
c7fa49bcda
|
@ -19,15 +19,15 @@ No discovery
|
|||
|
||||
You require your MyRenault credential, locale and VIN for your MyRenault registered car.
|
||||
|
||||
| Parameter | Description | Required |
|
||||
|-------------------|----------------------------------------------------------------------------|----------|
|
||||
| myRenaultUsername | MyRenault Username. | yes |
|
||||
| myRenaultPassword | MyRenault Password. | yes |
|
||||
| locale | MyRenault Location (language_country). | yes |
|
||||
| vin | Vehicle Identification Number. | yes |
|
||||
| refreshInterval | Interval the car is polled in minutes. | no |
|
||||
| updateDelay | How long to wait for commands to reach car and update to server in seconds.| no |
|
||||
| kamereonApiKey | Kamereon API Key. | no |
|
||||
| Parameter | Description | Default |
|
||||
|-------------------|----------------------------------------------------------------------------|----------------------------------|
|
||||
| myRenaultUsername | MyRenault Username. | |
|
||||
| myRenaultPassword | MyRenault Password. | |
|
||||
| locale | MyRenault Location (language_country). | |
|
||||
| vin | Vehicle Identification Number. | |
|
||||
| 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 |
|
||||
|
||||
## Channels
|
||||
|
||||
|
@ -35,6 +35,7 @@ You require your MyRenault credential, locale and VIN for your MyRenault registe
|
|||
|------------------------|--------------------|-------------------------------------------------|-----------|
|
||||
| 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 |
|
||||
| chargingstatus | String | Charging status | Yes |
|
||||
| chargingremainingtime | Number:Time | Charging time remaining | Yes |
|
||||
|
@ -47,6 +48,7 @@ You require your MyRenault credential, locale and VIN for your MyRenault registe
|
|||
| 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
|
||||
|
||||
|
@ -69,24 +71,24 @@ renaultcar.sitemap:
|
|||
sitemap renaultcar label="Renault Car" {
|
||||
Frame {
|
||||
Image item=RenaultCar_ImageURL
|
||||
Default item=RenaultCar_BatteryLevel icon="batterylevel"
|
||||
Default item=RenaultCar_BatteryEnergyAvailable icon="energy"
|
||||
Default item=RenaultCar_PlugStatus icon="poweroutlet"
|
||||
Default item=RenaultCar_ChargingStatus icon="switch"
|
||||
Selection item=RenaultCar_ChargingMode mappings=[SCHEDULE_MODE="Schedule mode",ALWAYS_CHARGING="Instant charge"] icon="switch"
|
||||
Default item=RenaultCar_ChargingTimeRemaining icon="time"
|
||||
Default item=RenaultCar_EstimatedRange
|
||||
Default item=RenaultCar_Odometer
|
||||
Selection item=RenaultCar_HVACStatus mappings=[ON="ON"] icon="switch"
|
||||
Setpoint item=RenaultCar_HVACTargetTemperature minValue=19 maxValue=21 step=1 icon="temperature"
|
||||
Default item=RenaultCar_LocationUpdate icon="time"
|
||||
Default item=RenaultCar_Location
|
||||
Default icon="batterylevel" item=RenaultCar_BatteryLevel
|
||||
Default item=RenaultCar_BatteryEnergyAvailable
|
||||
Default item=RenaultCar_BatteryStatusUpdated
|
||||
Default icon="poweroutlet" item=RenaultCar_PlugStatus
|
||||
Default icon="switch" item=RenaultCar_ChargingStatus
|
||||
Selection icon="switch" item=RenaultCar_ChargingMode mappings=[SCHEDULE_MODE="Schedule mode",ALWAYS_CHARGING="Instant charge"]
|
||||
Default item=RenaultCar_ChargingTimeRemaining
|
||||
Default icon="pressure" item=RenaultCar_EstimatedRange
|
||||
Default icon="pressure" item=RenaultCar_Odometer
|
||||
Selection icon="switch" item=RenaultCar_HVACStatus mappings=[ON="ON"]
|
||||
Setpoint icon="temperature" item=RenaultCar_HVACTargetTemperature maxValue=21 minValue=19 step=1
|
||||
Default icon="lock" item=RenaultCar_Locked
|
||||
Default item=RenaultCar_LocationUpdate
|
||||
Default icon="zoom" item=RenaultCar_Location
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
![Sitemap](doc/sitemap.png)
|
||||
|
||||
If you want to limit the charge of the car battery to less than 100%, this can be done as follows.
|
||||
|
||||
- Set up an active dummy charge schedule in the MyRenault App.
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 211 KiB |
|
@ -32,6 +32,7 @@ public class RenaultBindingConstants {
|
|||
// List of all Channel ids
|
||||
public static final String CHANNEL_BATTERY_AVAILABLE_ENERGY = "batteryavailableenergy";
|
||||
public static final String CHANNEL_BATTERY_LEVEL = "batterylevel";
|
||||
public static final String CHANNEL_BATTERY_STATUS_UPDATED = "batterystatusupdated";
|
||||
public static final String CHANNEL_CHARGING_MODE = "chargingmode";
|
||||
public static final String CHANNEL_CHARGING_STATUS = "chargingstatus";
|
||||
public static final String CHANNEL_CHARGING_REMAINING_TIME = "chargingremainingtime";
|
||||
|
@ -42,6 +43,7 @@ public class RenaultBindingConstants {
|
|||
public static final String CHANNEL_IMAGE = "image";
|
||||
public static final String CHANNEL_LOCATION = "location";
|
||||
public static final String CHANNEL_LOCATION_UPDATED = "locationupdated";
|
||||
public static final String CHANNEL_LOCKED = "locked";
|
||||
public static final String CHANNEL_ODOMETER = "odometer";
|
||||
public static final String CHANNEL_PLUG_STATUS = "plugstatus";
|
||||
}
|
||||
|
|
|
@ -12,6 +12,9 @@
|
|||
*/
|
||||
package org.openhab.binding.renault.internal.api;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeParseException;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
import org.eclipse.jdt.annotation.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
|
@ -40,6 +43,7 @@ public class Car {
|
|||
private boolean disableBattery = false;
|
||||
private boolean disableCockpit = false;
|
||||
private boolean disableHvac = false;
|
||||
private boolean disableLockStatus = false;
|
||||
|
||||
private ChargingStatus chargingStatus = ChargingStatus.UNKNOWN;
|
||||
private ChargingMode chargingMode = ChargingMode.UNKNOWN;
|
||||
|
@ -47,12 +51,14 @@ public class Car {
|
|||
private double hvacTargetTemperature = 20.0;
|
||||
private @Nullable Double batteryLevel;
|
||||
private @Nullable Double batteryAvailableEnergy;
|
||||
private @Nullable ZonedDateTime batteryStatusUpdated;
|
||||
private @Nullable Integer chargingRemainingTime;
|
||||
private @Nullable Boolean hvacstatus;
|
||||
private @Nullable Double odometer;
|
||||
private @Nullable Double estimatedRange;
|
||||
private @Nullable String imageURL;
|
||||
private @Nullable String locationUpdated;
|
||||
private @Nullable ZonedDateTime locationUpdated;
|
||||
private LockStatus lockStatus = LockStatus.UNKNOWN;
|
||||
private @Nullable Double gpsLatitude;
|
||||
private @Nullable Double gpsLongitude;
|
||||
private @Nullable Double externalTemperature;
|
||||
|
@ -63,14 +69,6 @@ public class Car {
|
|||
ALWAYS_CHARGING
|
||||
}
|
||||
|
||||
public enum PlugStatus {
|
||||
UNPLUGGED,
|
||||
PLUGGED,
|
||||
PLUG_ERROR,
|
||||
PLUG_UNKNOWN,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
public enum ChargingStatus {
|
||||
NOT_IN_CHARGE,
|
||||
WAITING_FOR_A_PLANNED_CHARGE,
|
||||
|
@ -83,6 +81,20 @@ public class Car {
|
|||
UNKNOWN
|
||||
}
|
||||
|
||||
public enum LockStatus {
|
||||
LOCKED,
|
||||
UNLOCKED,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
public enum PlugStatus {
|
||||
UNPLUGGED,
|
||||
PLUGGED,
|
||||
PLUG_ERROR,
|
||||
PLUG_UNKNOWN,
|
||||
UNKNOWN
|
||||
}
|
||||
|
||||
public void setBatteryStatus(JsonObject responseJson) {
|
||||
try {
|
||||
JsonObject attributes = getAttributes(responseJson);
|
||||
|
@ -105,6 +117,14 @@ public class Car {
|
|||
if (attributes.get("chargingRemainingTime") != null) {
|
||||
chargingRemainingTime = attributes.get("chargingRemainingTime").getAsInt();
|
||||
}
|
||||
if (attributes.get("timestamp") != null) {
|
||||
try {
|
||||
batteryStatusUpdated = ZonedDateTime.parse(attributes.get("timestamp").getAsString());
|
||||
} catch (DateTimeParseException e) {
|
||||
batteryStatusUpdated = null;
|
||||
logger.debug("Error updating battery status updated timestamp. {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IllegalStateException | ClassCastException e) {
|
||||
logger.warn("Error {} parsing Battery Status: {}", e.getMessage(), responseJson);
|
||||
|
@ -153,7 +173,25 @@ public class Car {
|
|||
gpsLongitude = attributes.get("gpsLongitude").getAsDouble();
|
||||
}
|
||||
if (attributes.get("lastUpdateTime") != null) {
|
||||
locationUpdated = attributes.get("lastUpdateTime").getAsString();
|
||||
try {
|
||||
locationUpdated = ZonedDateTime.parse(attributes.get("lastUpdateTime").getAsString());
|
||||
} catch (DateTimeParseException e) {
|
||||
locationUpdated = null;
|
||||
logger.debug("Error updating location updated timestamp. {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IllegalStateException | ClassCastException e) {
|
||||
logger.warn("Error {} parsing Location: {}", e.getMessage(), responseJson);
|
||||
}
|
||||
}
|
||||
|
||||
public void setLockStatus(JsonObject responseJson) {
|
||||
try {
|
||||
JsonObject attributes = getAttributes(responseJson);
|
||||
if (attributes != null) {
|
||||
if (attributes.get("lockStatus") != null) {
|
||||
lockStatus = mapLockStatus(attributes.get("lockStatus").getAsString());
|
||||
}
|
||||
}
|
||||
} catch (IllegalStateException | ClassCastException e) {
|
||||
|
@ -212,6 +250,10 @@ public class Car {
|
|||
return batteryLevel;
|
||||
}
|
||||
|
||||
public @Nullable ZonedDateTime getBatteryStatusUpdated() {
|
||||
return batteryStatusUpdated;
|
||||
}
|
||||
|
||||
public @Nullable Boolean getHvacstatus() {
|
||||
return hvacstatus;
|
||||
}
|
||||
|
@ -232,7 +274,7 @@ public class Car {
|
|||
return gpsLongitude;
|
||||
}
|
||||
|
||||
public @Nullable String getLocationUpdated() {
|
||||
public @Nullable ZonedDateTime getLocationUpdated() {
|
||||
return locationUpdated;
|
||||
}
|
||||
|
||||
|
@ -312,6 +354,17 @@ public class Car {
|
|||
return null;
|
||||
}
|
||||
|
||||
private LockStatus mapLockStatus(final String apiLockStatus) {
|
||||
switch (apiLockStatus) {
|
||||
case "locked":
|
||||
return LockStatus.LOCKED;
|
||||
case "unlocked":
|
||||
return LockStatus.UNLOCKED;
|
||||
default:
|
||||
return 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) {
|
||||
|
@ -351,4 +404,16 @@ public class Car {
|
|||
return ChargingStatus.UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
public LockStatus getLockStatus() {
|
||||
return lockStatus;
|
||||
}
|
||||
|
||||
public boolean isDisableLockStatus() {
|
||||
return disableLockStatus;
|
||||
}
|
||||
|
||||
public void setDisableLockStatus(boolean disableLockStatus) {
|
||||
this.disableLockStatus = disableLockStatus;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import org.eclipse.jetty.http.HttpStatus;
|
|||
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.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;
|
||||
|
@ -87,9 +88,13 @@ public class MyRenaultHttpSession {
|
|||
fields.add("ApiKey", this.constants.getGigyaApiKey());
|
||||
fields.add("loginID", config.myRenaultUsername);
|
||||
fields.add("password", config.myRenaultPassword);
|
||||
logger.debug("URL: {}/accounts.login", this.constants.getGigyaRootUrl());
|
||||
ContentResponse response = httpClient.FORM(this.constants.getGigyaRootUrl() + "/accounts.login", fields);
|
||||
final String url = this.constants.getGigyaRootUrl() + "/accounts.login";
|
||||
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");
|
||||
|
@ -104,11 +109,11 @@ public class MyRenaultHttpSession {
|
|||
throw new RenaultException("Login Error: cookie value not found in JSON response");
|
||||
}
|
||||
if (cookieValue == null) {
|
||||
logger.warn("Login Error: cookie value not found! Response: [{}] {}\n{}", response.getStatus(),
|
||||
response.getReason(), response.getContentAsString());
|
||||
logger.warn("Login Error: cookie value not found! Response: {}", response.getContentAsString());
|
||||
throw new RenaultException("Login Error: cookie value not found in JSON response");
|
||||
}
|
||||
} else {
|
||||
logger.warn("Response: [{}] {}\n{}", response.getStatus(), response.getReason(),
|
||||
logger.warn("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(), response.getReason(),
|
||||
response.getContentAsString());
|
||||
throw new RenaultException("Login Error: " + response.getReason());
|
||||
}
|
||||
|
@ -118,9 +123,13 @@ public class MyRenaultHttpSession {
|
|||
Fields fields = new Fields();
|
||||
fields.add("ApiKey", this.constants.getGigyaApiKey());
|
||||
fields.add("login_token", cookieValue);
|
||||
ContentResponse response = httpClient.FORM(this.constants.getGigyaRootUrl() + "/accounts.getAccountInfo",
|
||||
fields);
|
||||
final String url = this.constants.getGigyaRootUrl() + "/accounts.getAccountInfo";
|
||||
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 dataJson = responseJson.getAsJsonObject("data");
|
||||
|
@ -138,7 +147,7 @@ public class MyRenaultHttpSession {
|
|||
"Get Account Info Error: personId or gigyaDataCenter value not found in JSON response");
|
||||
}
|
||||
} else {
|
||||
logger.warn("Response: [{}] {}\n{}", response.getStatus(), response.getReason(),
|
||||
logger.warn("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(), response.getReason(),
|
||||
response.getContentAsString());
|
||||
throw new RenaultException("Get Account Info Error: " + response.getReason());
|
||||
}
|
||||
|
@ -151,20 +160,25 @@ public class MyRenaultHttpSession {
|
|||
fields.add("fields", "data.personId,data.gigyaDataCenter");
|
||||
fields.add("personId", personId);
|
||||
fields.add("gigyaDataCenter", gigyaDataCenter);
|
||||
ContentResponse response = this.httpClient.FORM(this.constants.getGigyaRootUrl() + "/accounts.getJWT", fields);
|
||||
final String url = this.constants.getGigyaRootUrl() + "/accounts.getJWT";
|
||||
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");
|
||||
if (element != null) {
|
||||
jwt = element.getAsString();
|
||||
logger.debug("jwt: {} ", jwt);
|
||||
logger.debug("GigyaApi jwt: {} ", jwt);
|
||||
}
|
||||
} catch (JsonParseException | ClassCastException | IllegalStateException e) {
|
||||
throw new RenaultException("Get JWT Error: jwt value not found in JSON response");
|
||||
}
|
||||
} else {
|
||||
logger.warn("Response: [{}] {}\n{}", response.getStatus(), response.getReason(),
|
||||
logger.warn("GigyaApi Request: {} Response: [{}] {}\n{}", url, response.getStatus(), response.getReason(),
|
||||
response.getContentAsString());
|
||||
throw new RenaultException("Get JWT Error: " + response.getReason());
|
||||
}
|
||||
|
@ -233,108 +247,101 @@ public class MyRenaultHttpSession {
|
|||
}
|
||||
}
|
||||
|
||||
public void actionHvacOn(double hvacTargetTemperature)
|
||||
throws RenaultForbiddenException, RenaultNotImplementedException {
|
||||
Request request = httpClient
|
||||
.newRequest(this.constants.getKamereonRootUrl() + "/commerce/v1/accounts/" + kamereonaccountId
|
||||
+ "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/actions/hvac-start?country="
|
||||
+ getCountry(config))
|
||||
.method(HttpMethod.POST).header("Content-type", "application/vnd.api+json")
|
||||
.header("apikey", this.config.kamereonApiKey)
|
||||
.header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt);
|
||||
request.content(new StringContentProvider(
|
||||
"{\"data\":{\"type\":\"HvacStart\",\"attributes\":{\"action\":\"start\",\"targetTemperature\":\""
|
||||
+ hvacTargetTemperature + "\"}}}",
|
||||
"utf-8"));
|
||||
try {
|
||||
ContentResponse response = request.send();
|
||||
logger.debug("Kamereon Response HVAC ON: {}", response.getContentAsString());
|
||||
if (HttpStatus.OK_200 != response.getStatus()) {
|
||||
logger.warn("Kamereon Response: [{}] {} {}", response.getStatus(), response.getReason(),
|
||||
response.getContentAsString());
|
||||
if (HttpStatus.FORBIDDEN_403 == response.getStatus()) {
|
||||
throw new RenaultForbiddenException(
|
||||
"Kamereon Response Forbidden! Ensure the car is paired in your MyRenault App.");
|
||||
} else if (HttpStatus.NOT_IMPLEMENTED_501 == response.getStatus()) {
|
||||
throw new RenaultNotImplementedException(
|
||||
"Kamereon Service Not Implemented: [" + response.getStatus() + "] " + response.getReason());
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (JsonParseException | TimeoutException | ExecutionException e) {
|
||||
logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
|
||||
public void getLockStatus(Car car)
|
||||
throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
|
||||
JsonObject responseJson = getKamereonResponse("/commerce/v1/accounts/" + kamereonaccountId
|
||||
+ "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/lock-status?country=" + getCountry(config));
|
||||
if (responseJson != null) {
|
||||
car.setLockStatus(responseJson);
|
||||
}
|
||||
}
|
||||
|
||||
public void actionChargeMode(ChargingMode mode) throws RenaultForbiddenException, RenaultNotImplementedException {
|
||||
Request request = httpClient
|
||||
.newRequest(this.constants.getKamereonRootUrl() + "/commerce/v1/accounts/" + kamereonaccountId
|
||||
+ "/kamereon/kca/car-adapter/v1/cars/" + config.vin + "/actions/charge-mode?country="
|
||||
+ getCountry(config))
|
||||
.method(HttpMethod.POST).header("Content-type", "application/vnd.api+json")
|
||||
.header("apikey", this.config.kamereonApiKey)
|
||||
.header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt);
|
||||
public void actionHvacOn(double hvacTargetTemperature)
|
||||
throws RenaultForbiddenException, RenaultNotImplementedException, RenaultActionException {
|
||||
final String path = "/commerce/v1/accounts/" + kamereonaccountId + "/kamereon/kca/car-adapter/v1/cars/"
|
||||
+ config.vin + "/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 {
|
||||
final String apiMode = ChargingMode.SCHEDULE_MODE.equals(mode) ? CHARGING_MODE_SCHEDULE : CHARGING_MODE_ALWAYS;
|
||||
request.content(new StringContentProvider(
|
||||
"{\"data\":{\"type\":\"ChargeMode\",\"attributes\":{\"action\":\"" + apiMode + "\"}}}", "utf-8"));
|
||||
final String path = "/commerce/v1/accounts/" + kamereonaccountId + "/kamereon/kca/car-adapter/v1/cars/"
|
||||
+ config.vin + "/actions/charge-mode?country=" + getCountry(config);
|
||||
postKamereonRequest(path,
|
||||
"{\"data\":{\"type\":\"ChargeMode\",\"attributes\":{\"action\":\"" + apiMode + "\"}}}");
|
||||
}
|
||||
|
||||
private void postKamereonRequest(final String path, final String content)
|
||||
throws RenaultForbiddenException, RenaultNotImplementedException, RenaultActionException {
|
||||
Request request = httpClient.newRequest(this.constants.getKamereonRootUrl() + path).method(HttpMethod.POST)
|
||||
.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();
|
||||
logger.debug("Kamereon Response set ChargeMode: {}", response.getContentAsString());
|
||||
if (HttpStatus.OK_200 != response.getStatus()) {
|
||||
logger.warn("Kamereon Response: [{}] {} {}", response.getStatus(), response.getReason(),
|
||||
response.getContentAsString());
|
||||
if (HttpStatus.FORBIDDEN_403 == response.getStatus()) {
|
||||
throw new RenaultForbiddenException(
|
||||
"Kamereon Response Forbidden! Ensure the car is paired in your MyRenault App.");
|
||||
} else if (HttpStatus.NOT_IMPLEMENTED_501 == response.getStatus()) {
|
||||
throw new RenaultNotImplementedException(
|
||||
"Kamereon Service Not Implemented: [" + response.getStatus() + "] " + response.getReason());
|
||||
}
|
||||
}
|
||||
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) {
|
||||
logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
|
||||
throw new RenaultActionException(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable JsonObject getKamereonResponse(String path)
|
||||
throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
|
||||
throws RenaultForbiddenException, RenaultNotImplementedException, RenaultUpdateException {
|
||||
Request request = httpClient.newRequest(this.constants.getKamereonRootUrl() + path).method(HttpMethod.GET)
|
||||
.header("Content-type", "application/vnd.api+json").header("apikey", this.config.kamereonApiKey)
|
||||
.header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt);
|
||||
try {
|
||||
ContentResponse response = request.send();
|
||||
logKamereonCall(request, response);
|
||||
if (HttpStatus.OK_200 == response.getStatus()) {
|
||||
logger.debug("Kamereon Response: {}", response.getContentAsString());
|
||||
return JsonParser.parseString(response.getContentAsString()).getAsJsonObject();
|
||||
} else {
|
||||
logger.warn("Kamereon Response: [{}] {} {}", response.getStatus(), response.getReason(),
|
||||
response.getContentAsString());
|
||||
if (HttpStatus.FORBIDDEN_403 == response.getStatus()) {
|
||||
throw new RenaultForbiddenException(
|
||||
"Kamereon Response Forbidden! Ensure the car is paired in your MyRenault App.");
|
||||
} else if (HttpStatus.NOT_IMPLEMENTED_501 == response.getStatus()) {
|
||||
throw new RenaultNotImplementedException(
|
||||
"Kamereon Service Not Implemented: [" + response.getStatus() + "] " + response.getReason());
|
||||
} else {
|
||||
throw new RenaultUpdateException(
|
||||
"Kamereon Response Failed! Error: [" + response.getStatus() + "] " + response.getReason());
|
||||
}
|
||||
}
|
||||
checkResponse(response);
|
||||
} catch (InterruptedException e) {
|
||||
logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (JsonParseException | TimeoutException | ExecutionException e) {
|
||||
logger.warn("Kamereon Request: {} threw exception: {} ", request.getURI().toString(), e.getMessage());
|
||||
throw new RenaultUpdateException(e.toString());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void logKamereonCall(Request request, ContentResponse response) {
|
||||
if (HttpStatus.OK_200 == response.getStatus()) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Kamereon Request: {} Response: [{}] {}\n{}", request.getURI().toString(),
|
||||
response.getStatus(), response.getReason(), response.getContentAsString());
|
||||
}
|
||||
} else {
|
||||
logger.warn("Kamereon Request: {} Response: [{}] {}\n{}", request.getURI().toString(), response.getStatus(),
|
||||
response.getReason(), response.getContentAsString());
|
||||
}
|
||||
}
|
||||
|
||||
private void checkResponse(ContentResponse response)
|
||||
throws RenaultForbiddenException, RenaultNotImplementedException {
|
||||
switch (response.getStatus()) {
|
||||
case HttpStatus.FORBIDDEN_403:
|
||||
throw new RenaultForbiddenException(
|
||||
"Kamereon request forbidden! Ensure the car is paired in your MyRenault App.");
|
||||
case HttpStatus.NOT_FOUND_404:
|
||||
throw new RenaultNotImplementedException("Kamereon service not found");
|
||||
case HttpStatus.NOT_IMPLEMENTED_501:
|
||||
throw new RenaultNotImplementedException("Kamereon request not implemented");
|
||||
case HttpStatus.BAD_GATEWAY_502:
|
||||
throw new RenaultNotImplementedException("Kamereon request failed");
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private String getCountry(RenaultConfiguration config) {
|
||||
String country = "XX";
|
||||
if (config.locale.length() == 5) {
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* Copyright (c) 2010-2022 Contributors to the openHAB project
|
||||
*
|
||||
* See the NOTICE file(s) distributed with this work for additional
|
||||
* information.
|
||||
*
|
||||
* This program and the accompanying materials are made available under the
|
||||
* terms of the Eclipse Public License 2.0 which is available at
|
||||
* http://www.eclipse.org/legal/epl-2.0
|
||||
*
|
||||
* SPDX-License-Identifier: EPL-2.0
|
||||
*/
|
||||
package org.openhab.binding.renault.internal.api.exceptions;
|
||||
|
||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||
|
||||
/**
|
||||
* Exception thrown while trying to perform action on the My Renault car.
|
||||
*
|
||||
* @author Doug Culnane - Initial contribution
|
||||
*/
|
||||
@NonNullByDefault
|
||||
public class RenaultActionException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public RenaultActionException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -18,6 +18,7 @@ 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 java.time.ZonedDateTime;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
@ -36,12 +37,14 @@ 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.MyRenaultHttpSession;
|
||||
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.library.types.DateTimeType;
|
||||
import org.openhab.core.library.types.DecimalType;
|
||||
import org.openhab.core.library.types.OnOffType;
|
||||
import org.openhab.core.library.types.PointType;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.types.StringType;
|
||||
|
@ -53,6 +56,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.UnDefType;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -110,19 +114,15 @@ public class RenaultHandler extends BaseThingHandler {
|
|||
return;
|
||||
}
|
||||
updateStatus(ThingStatus.UNKNOWN);
|
||||
|
||||
updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
|
||||
new QuantityType<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
|
||||
|
||||
// Background initialization:
|
||||
ScheduledFuture<?> job = pollingJob;
|
||||
if (job == null || job.isCancelled()) {
|
||||
pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, 0, config.refreshInterval, TimeUnit.MINUTES);
|
||||
}
|
||||
reschedulePollingJob();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||
|
||||
switch (channelUID.getId()) {
|
||||
case RenaultBindingConstants.CHANNEL_HVAC_TARGET_TEMPERATURE:
|
||||
if (!car.isDisableHvac()) {
|
||||
|
@ -146,32 +146,39 @@ public class RenaultHandler extends BaseThingHandler {
|
|||
}
|
||||
break;
|
||||
case RenaultBindingConstants.CHANNEL_HVAC_STATUS:
|
||||
// We can only trigger pre-conditioning of the car.
|
||||
if (command instanceof StringType && command.toString().equals(Car.HVAC_STATUS_ON)
|
||||
&& !car.isDisableHvac()) {
|
||||
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());
|
||||
if (pollingJob != null) {
|
||||
pollingJob.cancel(true);
|
||||
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());
|
||||
}
|
||||
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
|
||||
| RenaultNotImplementedException | ExecutionException | TimeoutException e) {
|
||||
logger.warn("Error My Renault Http Session.", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
break;
|
||||
case RenaultBindingConstants.CHANNEL_CHARGING_MODE:
|
||||
if (command instanceof StringType) {
|
||||
if (command instanceof RefreshType) {
|
||||
reschedulePollingJob();
|
||||
} else if (command instanceof StringType) {
|
||||
try {
|
||||
ChargingMode newMode = ChargingMode.valueOf(command.toString());
|
||||
if (!ChargingMode.UNKNOWN.equals(newMode)) {
|
||||
|
@ -185,8 +192,9 @@ public class RenaultHandler extends BaseThingHandler {
|
|||
logger.warn("Error My Renault Http Session.", e);
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (RenaultException | RenaultForbiddenException | RenaultUpdateException
|
||||
| RenaultNotImplementedException | ExecutionException | TimeoutException e) {
|
||||
logger.warn("Error My Renault Http Session.", e);
|
||||
| RenaultActionException | RenaultNotImplementedException | ExecutionException
|
||||
| TimeoutException e) {
|
||||
logger.warn("Error during action set charge mode.", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
|
||||
e.getMessage());
|
||||
}
|
||||
|
@ -196,7 +204,11 @@ public class RenaultHandler extends BaseThingHandler {
|
|||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (command instanceof RefreshType) {
|
||||
reschedulePollingJob();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -223,16 +235,15 @@ public class RenaultHandler extends BaseThingHandler {
|
|||
logger.warn("Error My Renault Http Session.", e);
|
||||
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
|
||||
}
|
||||
if (httpSession != null) {
|
||||
String imageURL = car.getImageURL();
|
||||
if (imageURL != null && !imageURL.isEmpty()) {
|
||||
updateState(CHANNEL_IMAGE, new StringType(imageURL));
|
||||
}
|
||||
updateHvacStatus(httpSession);
|
||||
updateCockpit(httpSession);
|
||||
updateLocation(httpSession);
|
||||
updateBattery(httpSession);
|
||||
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) {
|
||||
|
@ -253,8 +264,10 @@ public class RenaultHandler extends BaseThingHandler {
|
|||
new QuantityType<Temperature>(externalTemperature.doubleValue(), SIUnits.CELSIUS));
|
||||
}
|
||||
} catch (RenaultNotImplementedException e) {
|
||||
logger.warn("Disabling unsupported HVAC status update.");
|
||||
car.setDisableHvac(true);
|
||||
} catch (RenaultForbiddenException | RenaultUpdateException e) {
|
||||
logger.warn("Error updating HVAC status.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -269,13 +282,15 @@ public class RenaultHandler extends BaseThingHandler {
|
|||
updateState(CHANNEL_LOCATION, new PointType(new DecimalType(latitude.doubleValue()),
|
||||
new DecimalType(longitude.doubleValue())));
|
||||
}
|
||||
String locationUpdated = car.getLocationUpdated();
|
||||
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 e) {
|
||||
logger.warn("Error updating location.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -289,8 +304,10 @@ public class RenaultHandler extends BaseThingHandler {
|
|||
updateState(CHANNEL_ODOMETER, new QuantityType<Length>(odometer.doubleValue(), KILO(METRE)));
|
||||
}
|
||||
} catch (RenaultNotImplementedException e) {
|
||||
logger.warn("Disabling unsupported cockpit status update.");
|
||||
car.setDisableCockpit(true);
|
||||
} catch (RenaultForbiddenException | RenaultUpdateException e) {
|
||||
logger.warn("Error updating cockpit status.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -320,10 +337,49 @@ public class RenaultHandler extends BaseThingHandler {
|
|||
updateState(CHANNEL_CHARGING_REMAINING_TIME,
|
||||
new QuantityType<Time>(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 e) {
|
||||
logger.warn("Error updating battery status.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 e) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void reschedulePollingJob() {
|
||||
ScheduledFuture<?> job = pollingJob;
|
||||
if (job != null) {
|
||||
job.cancel(true);
|
||||
}
|
||||
pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, 0, config.refreshInterval, TimeUnit.MINUTES);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,6 +55,9 @@ thing-type.config.renault.car.vin.description = Vehicle Identification Number
|
|||
# channel types
|
||||
|
||||
channel-type.renault.batteryavailableenergy.label = Battery Energy Available
|
||||
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
|
||||
|
@ -85,6 +88,8 @@ channel-type.renault.image.description = Image URL of MyRenault
|
|||
channel-type.renault.locationupdated.label = Location Update
|
||||
channel-type.renault.locationupdated.description = Timestamp of the last location update
|
||||
channel-type.renault.locationupdated.state.pattern = %1$tH:%1$tM %1$td.%1$tm.%1$tY
|
||||
channel-type.renault.locked.label = Locked
|
||||
channel-type.renault.locked.description = Locked status of the car
|
||||
channel-type.renault.odometer.label = Odometer
|
||||
channel-type.renault.odometer.description = Total distance travelled
|
||||
channel-type.renault.plugstatus.label = Plug Status
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
<channels>
|
||||
<channel id="batterylevel" typeId="system.battery-level"/>
|
||||
<channel id="batteryavailableenergy" typeId="batteryavailableenergy"/>
|
||||
<channel id="batterystatusupdated" typeId="batterystatusupdated"/>
|
||||
<channel id="plugstatus" typeId="plugstatus"/>
|
||||
<channel id="chargingstatus" typeId="chargingstatus"/>
|
||||
<channel id="chargingmode" typeId="chargingmode"/>
|
||||
|
@ -24,6 +25,7 @@
|
|||
<channel id="image" typeId="image"/>
|
||||
<channel id="location" typeId="system.location"/>
|
||||
<channel id="locationupdated" typeId="locationupdated"/>
|
||||
<channel id="locked" typeId="locked"/>
|
||||
</channels>
|
||||
<config-description>
|
||||
|
||||
|
@ -94,8 +96,16 @@
|
|||
<channel-type id="batteryavailableenergy">
|
||||
<item-type>Number:Energy</item-type>
|
||||
<label>Battery Energy Available</label>
|
||||
<category>Energy</category>
|
||||
<state pattern="%.1f %unit%" readOnly="true"></state>
|
||||
</channel-type>
|
||||
<channel-type id="batterystatusupdated">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Battery Status Updated</label>
|
||||
<description>Timestamp of the last battery status update</description>
|
||||
<category>Time</category>
|
||||
<state pattern="%1$tH:%1$tM %1$td.%1$tm.%1$tY" readOnly="true"></state>
|
||||
</channel-type>
|
||||
<channel-type id="plugstatus">
|
||||
<item-type>String</item-type>
|
||||
<label>Plug Status</label>
|
||||
|
@ -189,9 +199,17 @@
|
|||
</channel-type>
|
||||
<channel-type id="locationupdated">
|
||||
<item-type>DateTime</item-type>
|
||||
<label>Location Update</label>
|
||||
<label>Location Updated</label>
|
||||
<description>Timestamp of the last location update</description>
|
||||
<category>Time</category>
|
||||
<state pattern="%1$tH:%1$tM %1$td.%1$tm.%1$tY" readOnly="true"></state>
|
||||
</channel-type>
|
||||
<channel-type id="locked">
|
||||
<item-type>Switch</item-type>
|
||||
<label>Locked</label>
|
||||
<description>Locked status of the car</description>
|
||||
<category>Lock</category>
|
||||
<state readOnly="true"/>
|
||||
</channel-type>
|
||||
|
||||
</thing:thing-descriptions>
|
||||
|
|
Loading…
Reference in New Issue