[renault] Renault more channels and HVAC ON / toggle charge mode (#12095)

* [renault] Add more channels to Renault Binding.
* [renault] Improve example rule to reduce API use.

Signed-off-by: Culnane Douglas <douglas.culnane@extern.a1.at>
This commit is contained in:
Doug Culnane 2022-02-12 20:22:35 +01:00 committed by GitHub
parent 223eb5e24d
commit 605c06107f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 660 additions and 83 deletions

View File

@ -1,43 +1,132 @@
# Renault Binding
This binding allow MyRenault App. users to get battery status and other data from their cars.
This binding allows MyRenault App. users to get battery status and other data from their cars.
They can also heat their cars by turning ON the HVAC status and toggle the car's charging mode.
A binding that translates the [python based renault-api](https://renault-api.readthedocs.io/en/latest/) in an easy to use binding.
The binding translates the [python based renault-api](https://renault-api.readthedocs.io/en/latest/) in an easy to use openHAB java binding.
## Supported Things
Supports MyRenault registered cars with an active Connected-Services account.
This binding can only retrieve information that is available in the the MyRenault App.
This binding can only retrieve information that is available in the MyRenault App.
## Discovery
No discovery
## Thing Configuration
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 |
| 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 |
## Channels
Currently all available channels are read only:
| Channel ID | Type | Description |
|--------------|---------------|---------------------------------|
| batterylevel | Number | State of the battery in % |
| hvacstatus | Switch | HVAC status switch |
| image | String | Image URL of MyRenault |
| location | Location | The GPS position of the vehicle |
| odometer | Number:Length | Total distance travelled |
| Channel ID | Type | Description | Read Only |
|------------------------|--------------------|-------------------------------------------------|-----------|
| batteryavailableEnergy | Number:Energy | Battery Energy Available | Yes |
| batterylevel | Number | State of the battery in % | Yes |
| chargingmode | String | Charging mode. always_charging or schedule_mode | 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 |
## Limitations
Some channels may not work depending on your car and MyRenault account.
The "externaltemperature" only works on a few cars.
The "hvactargettemperature" is used by the hvacstatus ON command for pre-conditioning the car.
This seams to only allow values 19, 20 and 21 or else the pre-conditioning command will not work.
## Example
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
}
}
```
![Sitemap](doc/sitemap.png)
If you do not have a smart charger and want to limit the charge of the battery you can set up an active 15 minute charge schedule in the MyRenault App.
Then create a Dimmer item "RenaultCar_ChargeLimit" and set it to 80% for example. This rule will change the RenaultCar_ChargingMode to schedule_mode when the limit is reached.
This stops charging after the battery level reaches the charge limit.
ChargeRenaultCarLimit Code
```
configuration: {}
triggers:
- id: "1"
configuration:
itemName: RenaultCar_BatteryLevel
type: core.ItemStateUpdateTrigger
- id: "3"
configuration:
itemName: RenaultCar_ChargeLimit
type: core.ItemStateUpdateTrigger
- id: "4"
configuration:
itemName: RenaultCar_PlugStatus
type: core.ItemStateUpdateTrigger
conditions: []
actions:
- inputs: {}
id: "2"
configuration:
type: application/vnd.openhab.dsl.rule
script: >-
if ( RenaultCar_PlugStatus.state.toString == 'PLUGGED' ) {
if ( RenaultCar_BatteryLevel.state as Number >= RenaultCar_ChargeLimit.state as Number ) {
if (RenaultCar_ChargingMode.state.toString == 'ALWAYS_CHARGING' ) {
RenaultCar_ChargingMode.sendCommand("SCHEDULE_MODE")
}
} else {
if (RenaultCar_ChargingMode.state.toString == 'SCHEDULE_MODE' ) {
RenaultCar_ChargingMode.sendCommand("ALWAYS_CHARGING")
}
}
}
type: script.ScriptAction
```

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

View File

@ -30,9 +30,18 @@ public class RenaultBindingConstants {
public static final ThingTypeUID THING_TYPE_CAR = new ThingTypeUID(BINDING_ID, "car");
// 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_CHARGING_MODE = "chargingmode";
public static final String CHANNEL_CHARGING_STATUS = "chargingstatus";
public static final String CHANNEL_CHARGING_REMAINING_TIME = "chargingremainingtime";
public static final String CHANNEL_ESTIMATED_RANGE = "estimatedrange";
public static final String CHANNEL_EXTERNAL_TEMPERATURE = "externaltemperature";
public static final String CHANNEL_HVAC_STATUS = "hvacstatus";
public static final String CHANNEL_HVAC_TARGET_TEMPERATURE = "hvactargettemperature";
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_ODOMETER = "odometer";
public static final String CHANNEL_PLUG_STATUS = "plugstatus";
}

View File

@ -27,4 +27,5 @@ public class RenaultConfiguration {
public String locale = "";
public String vin = "";
public int refreshInterval = 10;
public int updateDelay = 30;
}

View File

@ -30,6 +30,10 @@ import com.google.gson.JsonObject;
@NonNullByDefault
public class Car {
public static final String HVAC_STATUS_ON = "ON";
public static final String HVAC_STATUS_OFF = "OFF";
public static final String HVAC_STATUS_PENDING = "PENDING";
private final Logger logger = LoggerFactory.getLogger(Car.class);
private boolean disableLocation = false;
@ -37,29 +41,90 @@ public class Car {
private boolean disableCockpit = false;
private boolean disableHvac = false;
private ChargingStatus chargingStatus = ChargingStatus.UNKNOWN;
private ChargingMode chargingMode = ChargingMode.UNKNOWN;
private PlugStatus plugStatus = PlugStatus.UNKNOWN;
private double hvacTargetTemperature = 20.0;
private @Nullable Double batteryLevel;
private @Nullable Double batteryAvailableEnergy;
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 Double gpsLatitude;
private @Nullable Double gpsLongitude;
private @Nullable Double externalTemperature;
public enum ChargingMode {
UNKNOWN,
SCHEDULE_MODE,
ALWAYS_CHARGING
}
public enum PlugStatus {
UNPLUGGED,
PLUGGED,
PLUG_ERROR,
PLUG_UNKNOWN,
UNKNOWN
}
public enum ChargingStatus {
NOT_IN_CHARGE,
WAITING_FOR_A_PLANNED_CHARGE,
CHARGE_ENDED,
WAITING_FOR_CURRENT_CHARGE,
ENERGY_FLAP_OPENED,
CHARGE_IN_PROGRESS,
CHARGE_ERROR,
UNAVAILABLE,
UNKNOWN
}
public void setBatteryStatus(JsonObject responseJson) {
try {
JsonObject attributes = getAttributes(responseJson);
if (attributes != null && attributes.get("batteryLevel") != null) {
batteryLevel = attributes.get("batteryLevel").getAsDouble();
if (attributes != null) {
if (attributes.get("batteryLevel") != null) {
batteryLevel = attributes.get("batteryLevel").getAsDouble();
}
if (attributes.get("batteryAutonomy") != null) {
estimatedRange = attributes.get("batteryAutonomy").getAsDouble();
}
if (attributes.get("plugStatus") != null) {
plugStatus = mapPlugStatus(attributes.get("plugStatus").getAsString());
}
if (attributes.get("chargingStatus") != null) {
chargingStatus = mapChargingStatus(attributes.get("chargingStatus").getAsString());
}
if (attributes.get("batteryAvailableEnergy") != null) {
batteryAvailableEnergy = attributes.get("batteryAvailableEnergy").getAsDouble();
}
if (attributes.get("chargingRemainingTime") != null) {
chargingRemainingTime = attributes.get("chargingRemainingTime").getAsInt();
}
}
} catch (IllegalStateException | ClassCastException e) {
logger.warn("Error {} parsing Battery Status: {}", e.getMessage(), responseJson);
}
}
public void resetHVACStatus() {
this.hvacstatus = null;
}
public void setHVACStatus(JsonObject responseJson) {
try {
JsonObject attributes = getAttributes(responseJson);
if (attributes != null && attributes.get("hvacStatus") != null) {
hvacstatus = attributes.get("hvacStatus").getAsString().equals("on");
if (attributes != null) {
if (attributes.get("hvacStatus") != null) {
hvacstatus = attributes.get("hvacStatus").getAsString().equals("on");
}
if (attributes.get("externalTemperature") != null) {
externalTemperature = attributes.get("externalTemperature").getAsDouble();
}
}
} catch (IllegalStateException | ClassCastException e) {
logger.warn("Error {} parsing HVAC Status: {}", e.getMessage(), responseJson);
@ -87,6 +152,9 @@ public class Car {
if (attributes.get("gpsLongitude") != null) {
gpsLongitude = attributes.get("gpsLongitude").getAsDouble();
}
if (attributes.get("lastUpdateTime") != null) {
locationUpdated = attributes.get("lastUpdateTime").getAsString();
}
}
} catch (IllegalStateException | ClassCastException e) {
logger.warn("Error {} parsing Location: {}", e.getMessage(), responseJson);
@ -128,80 +196,112 @@ public class Car {
return disableLocation;
}
public void setDisableLocation(boolean disableLocation) {
this.disableLocation = disableLocation;
}
public boolean isDisableBattery() {
return disableBattery;
}
public void setDisableBattery(boolean disableBattery) {
this.disableBattery = disableBattery;
}
public boolean isDisableCockpit() {
return disableCockpit;
}
public void setDisableCockpit(boolean disableCockpit) {
this.disableCockpit = disableCockpit;
}
public boolean isDisableHvac() {
return disableHvac;
}
public void setDisableHvac(boolean disableHvac) {
this.disableHvac = disableHvac;
}
public @Nullable Double getBatteryLevel() {
return batteryLevel;
}
public void setBatteryLevel(Double batteryLevel) {
this.batteryLevel = batteryLevel;
}
public @Nullable Boolean getHvacstatus() {
return hvacstatus;
}
public void setHvacstatus(Boolean hvacstatus) {
this.hvacstatus = hvacstatus;
}
public @Nullable Double getOdometer() {
return odometer;
}
public void setOdometer(Double odometer) {
this.odometer = odometer;
}
public @Nullable String getImageURL() {
return imageURL;
}
public void setImageURL(String imageURL) {
this.imageURL = imageURL;
}
public @Nullable Double getGpsLatitude() {
return gpsLatitude;
}
public void setGpsLatitude(Double gpsLatitude) {
this.gpsLatitude = gpsLatitude;
}
public @Nullable Double getGpsLongitude() {
return gpsLongitude;
}
public void setGpsLongitude(Double gpsLongitude) {
this.gpsLongitude = gpsLongitude;
public @Nullable String getLocationUpdated() {
return locationUpdated;
}
public @Nullable Double getExternalTemperature() {
return externalTemperature;
}
public @Nullable Double getEstimatedRange() {
return estimatedRange;
}
public PlugStatus getPlugStatus() {
return plugStatus;
}
public ChargingStatus getChargingStatus() {
return chargingStatus;
}
public ChargingMode getChargingMode() {
return chargingMode;
}
public @Nullable Integer getChargingRemainingTime() {
return chargingRemainingTime;
}
public @Nullable Double getBatteryAvailableEnergy() {
return batteryAvailableEnergy;
}
public double getHvacTargetTemperature() {
return hvacTargetTemperature;
}
public void setHvacTargetTemperature(double hvacTargetTemperature) {
this.hvacTargetTemperature = hvacTargetTemperature;
}
public void setDisableLocation(boolean disableLocation) {
this.disableLocation = disableLocation;
}
public void setDisableBattery(boolean disableBattery) {
this.disableBattery = disableBattery;
}
public void setDisableCockpit(boolean disableCockpit) {
this.disableCockpit = disableCockpit;
}
public void setDisableHvac(boolean disableHvac) {
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;
}
}
private @Nullable JsonObject getAttributes(JsonObject responseJson)
@ -211,4 +311,44 @@ public class Car {
}
return null;
}
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;
}
}
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;
}
}
}

View File

@ -20,10 +20,12 @@ import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpMethod;
import org.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.RenaultException;
import org.openhab.binding.renault.internal.api.exceptions.RenaultForbiddenException;
import org.openhab.binding.renault.internal.api.exceptions.RenaultNotImplementedException;
@ -46,6 +48,9 @@ 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 RenaultConfiguration config;
private HttpClient httpClient;
private Constants constants;
@ -98,6 +103,10 @@ public class MyRenaultHttpSession {
} catch (JsonParseException | ClassCastException | IllegalStateException e) {
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());
}
} else {
logger.warn("Response: [{}] {}\n{}", response.getStatus(), response.getReason(),
response.getContentAsString());
@ -224,6 +233,75 @@ 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.constants.getKamereonApiKey())
.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 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.constants.getKamereonApiKey())
.header("x-kamereon-authorization", "Bearer " + kamereonToken).header("x-gigya-id_token", jwt);
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"));
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());
}
}
} 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());
}
}
private @Nullable JsonObject getKamereonResponse(String path)
throws RenaultForbiddenException, RenaultUpdateException, RenaultNotImplementedException {
Request request = httpClient.newRequest(this.constants.getKamereonRootUrl() + path).method(HttpMethod.GET)
@ -248,7 +326,10 @@ public class MyRenaultHttpSession {
"Kamereon Response Failed! Error: [" + response.getStatus() + "] " + response.getReason());
}
}
} catch (JsonParseException | InterruptedException | TimeoutException | ExecutionException e) {
} 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());
}
return null;

View File

@ -15,32 +15,44 @@ 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 java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.measure.quantity.Energy;
import javax.measure.quantity.Length;
import javax.measure.quantity.Temperature;
import javax.measure.quantity.Time;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
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.MyRenaultHttpSession;
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;
import org.openhab.core.library.unit.SIUnits;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -69,11 +81,6 @@ public class RenaultHandler extends BaseThingHandler {
this.httpClient = httpClient;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// This binding only polls status data automatically.
}
@Override
public void initialize() {
// reset the car on initialize
@ -104,6 +111,9 @@ public class RenaultHandler extends BaseThingHandler {
}
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()) {
@ -111,6 +121,86 @@ public class RenaultHandler extends BaseThingHandler {
}
}
@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<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
} else if (command instanceof DecimalType) {
car.setHvacTargetTemperature(((DecimalType) command).doubleValue());
updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
new QuantityType<Temperature>(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<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
}
}
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);
}
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) {
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
| RenaultNotImplementedException | ExecutionException | TimeoutException e) {
logger.warn("Error My Renault Http Session.", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
e.getMessage());
}
}
} catch (IllegalArgumentException e) {
logger.warn("Invalid ChargingMode {}.", command.toString());
return;
}
}
default:
break;
}
}
@Override
public void dispose() {
ScheduledFuture<?> job = pollingJob;
@ -126,8 +216,10 @@ public class RenaultHandler extends BaseThingHandler {
try {
httpSession.initSesssion(car);
updateStatus(ThingStatus.ONLINE);
} catch (InterruptedException e) {
logger.warn("Error My Renault Http Session.", e);
Thread.currentThread().interrupt();
} catch (Exception e) {
httpSession = null;
logger.warn("Error My Renault Http Session.", e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
@ -148,8 +240,17 @@ public class RenaultHandler extends BaseThingHandler {
try {
httpSession.getHvacStatus(car);
Boolean hvacstatus = car.getHvacstatus();
if (hvacstatus != null) {
updateState(CHANNEL_HVAC_STATUS, OnOffType.from(hvacstatus.booleanValue()));
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<Temperature>(externalTemperature.doubleValue(), SIUnits.CELSIUS));
}
} catch (RenaultNotImplementedException e) {
car.setDisableHvac(true);
@ -168,9 +269,13 @@ public class RenaultHandler extends BaseThingHandler {
updateState(CHANNEL_LOCATION, new PointType(new DecimalType(latitude.doubleValue()),
new DecimalType(longitude.doubleValue())));
}
String locationUpdated = car.getLocationUpdated();
if (locationUpdated != null) {
updateState(CHANNEL_LOCATION_UPDATED, new DateTimeType(locationUpdated));
}
} catch (RenaultNotImplementedException e) {
car.setDisableLocation(true);
} catch (RenaultForbiddenException | RenaultUpdateException e) {
} catch (IllegalArgumentException | RenaultForbiddenException | RenaultUpdateException e) {
}
}
}
@ -194,10 +299,27 @@ public class RenaultHandler extends BaseThingHandler {
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<Length>(estimatedRange.doubleValue(), KILO(METRE)));
}
Double batteryAvailableEnergy = car.getBatteryAvailableEnergy();
if (batteryAvailableEnergy != null) {
updateState(CHANNEL_BATTERY_AVAILABLE_ENERGY,
new QuantityType<Energy>(batteryAvailableEnergy.doubleValue(), KILOWATT_HOUR));
}
Integer chargingRemainingTime = car.getChargingRemainingTime();
if (chargingRemainingTime != null) {
updateState(CHANNEL_CHARGING_REMAINING_TIME,
new QuantityType<Time>(chargingRemainingTime.doubleValue(), MINUTE));
}
} catch (RenaultNotImplementedException e) {
car.setDisableBattery(true);
} catch (RenaultForbiddenException | RenaultUpdateException e) {

View File

@ -45,13 +45,50 @@ thing-type.config.renault.car.myRenaultPassword.label = MyRenault Password
thing-type.config.renault.car.myRenaultUsername.label = MyRenault Username
thing-type.config.renault.car.refreshInterval.label = Refresh Interval
thing-type.config.renault.car.refreshInterval.description = Interval the car is polled in minutes.
thing-type.config.renault.car.updateDelay.label = Update Delay
thing-type.config.renault.car.updateDelay.description = How long to wait for commands to reach car and update to server in seconds.
thing-type.config.renault.car.vin.label = VIN
thing-type.config.renault.car.vin.description = Vehicle Identification Number
# channel types
channel-type.renault.hvacstatus.label = HVAC Status
channel-type.renault.batteryavailableenergy.label = Battery Energy Available
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
channel-type.renault.chargingstatus.label = Charging Status
channel-type.renault.chargingstatus.state.option.NOT_IN_CHARGE = Not charging
channel-type.renault.chargingstatus.state.option.WAITING_FOR_A_PLANNED_CHARGE = Waiting for schedule
channel-type.renault.chargingstatus.state.option.CHARGE_ENDED = Charge ended
channel-type.renault.chargingstatus.state.option.WAITING_FOR_CURRENT_CHARGE = Waiting for charge
channel-type.renault.chargingstatus.state.option.ENERGY_FLAP_OPENED = Plug flap opened
channel-type.renault.chargingstatus.state.option.CHARGE_IN_PROGRESS = Charge in progress
channel-type.renault.chargingstatus.state.option.CHARGE_ERROR = Charge error
channel-type.renault.chargingstatus.state.option.UNAVAILABLE = Unavailable
channel-type.renault.chargingstatus.state.option.UNKNOWN = Unknown
channel-type.renault.estimatedrange.label = Estimated Range
channel-type.renault.estimatedrange.description = Estimated range of the car.
channel-type.renault.externaltemperature.label = External Temperature
channel-type.renault.externaltemperature.description = Temperature outside of the car
channel-type.renault.hvacstatus.label = HVAC Status (ON | OFF | PENDING)
channel-type.renault.hvacstatus.state.option.ON = On
channel-type.renault.hvacstatus.state.option.PENDING = Pending
channel-type.renault.hvacstatus.state.option.OFF = Off
channel-type.renault.hvactargettemperature.label = HVAC Target Temperature
channel-type.renault.hvactargettemperature.description = HVAC target temperature (19 to 21)
channel-type.renault.image.label = Image URL
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.odometer.label = Odometer
channel-type.renault.odometer.description = Total distance travelled
channel-type.renault.plugstatus.label = Plug Status
channel-type.renault.plugstatus.description = Status of charging plug.
channel-type.renault.plugstatus.state.option.UNPLUGGED = Unplugged
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

View File

@ -11,10 +11,19 @@
<channels>
<channel id="batterylevel" typeId="system.battery-level"/>
<channel id="batteryavailableenergy" typeId="batteryavailableenergy"/>
<channel id="plugstatus" typeId="plugstatus"/>
<channel id="chargingstatus" typeId="chargingstatus"/>
<channel id="chargingmode" typeId="chargingmode"/>
<channel id="chargingremainingtime" typeId="chargingremainingtime"/>
<channel id="estimatedrange" typeId="estimatedrange"/>
<channel id="odometer" typeId="odometer"/>
<channel id="hvacstatus" typeId="hvacstatus"/>
<channel id="hvactargettemperature" typeId="hvactargettemperature"/>
<channel id="externaltemperature" typeId="externaltemperature"/>
<channel id="image" typeId="image"/>
<channel id="location" typeId="system.location"/>
<channel id="odometer" typeId="odometer"/>
<channel id="locationupdated" typeId="locationupdated"/>
</channels>
<config-description>
@ -69,20 +78,72 @@
<description>Interval the car is polled in minutes.</description>
<default>10</default>
</parameter>
<parameter name="updateDelay" type="integer" unit="s" min="5" max="120">
<label>Update Delay</label>
<description>How long to wait for commands to reach car and update to server in seconds.</description>
<default>30</default>
</parameter>
</config-description>
</thing-type>
<!-- Sample Channel Type -->
<channel-type id="hvacstatus">
<item-type>Switch</item-type>
<label>HVAC Status</label>
<state readOnly="true"></state>
<channel-type id="batteryavailableenergy">
<item-type>Number:Energy</item-type>
<label>Battery Energy Available</label>
<state pattern="%.1f %unit%" readOnly="true"></state>
</channel-type>
<channel-type id="image">
<channel-type id="plugstatus">
<item-type>String</item-type>
<label>Image URL</label>
<description>Image URL of MyRenault</description>
<state readOnly="true"></state>
<label>Plug Status</label>
<description>Status of charging plug.</description>
<state readOnly="true">
<options>
<option value="UNPLUGGED">Unplugged</option>
<option value="PLUGGED">Plugged</option>
<option value="PLUG_ERROR">Plug error</option>
<option value="PLUG_UNKNOWN">Plug unknown</option>
<option value="UNKNOWN">Unknown</option>
</options>
</state>
</channel-type>
<channel-type id="chargingstatus">
<item-type>String</item-type>
<label>Charging Status</label>
<state readOnly="true">
<options>
<option value="NOT_IN_CHARGE">Not charging</option>
<option value="WAITING_FOR_A_PLANNED_CHARGE">Waiting for schedule</option>
<option value="CHARGE_ENDED">Charge ended</option>
<option value="WAITING_FOR_CURRENT_CHARGE">Waiting for charge</option>
<option value="ENERGY_FLAP_OPENED">Plug flap opened</option>
<option value="CHARGE_IN_PROGRESS">Charge in progress</option>
<option value="CHARGE_ERROR">Charge error</option>
<option value="UNAVAILABLE">Unavailable</option>
<option value="UNKNOWN">Unknown</option>
</options>
</state>
</channel-type>
<channel-type id="chargingmode">
<item-type>String</item-type>
<label>Charging Mode</label>
<state readOnly="false">
<options>
<option value="UNKNOWN">Unknown</option>
<option value="SCHEDULE_MODE">Schedule mode</option>
<option value="ALWAYS_CHARGING">Instant charge</option>
</options>
</state>
</channel-type>
<channel-type id="chargingremainingtime">
<item-type>Number:Time</item-type>
<label>Charging Time Remaining</label>
<category>Time</category>
<state pattern="%d %unit%" readOnly="true"></state>
</channel-type>
<channel-type id="estimatedrange">
<item-type>Number:Length</item-type>
<label>Estimated Range</label>
<description>Estimated range of the car.</description>
<state pattern="%.1f %unit%" readOnly="true"></state>
</channel-type>
<channel-type id="odometer">
<item-type>Number:Length</item-type>
@ -90,5 +151,42 @@
<description>Total distance travelled</description>
<state pattern="%.1f %unit%" readOnly="true"></state>
</channel-type>
<channel-type id="hvacstatus">
<item-type>String</item-type>
<label>HVAC Status (ON | OFF | PENDING)</label>
<state readOnly="false">
<options>
<option value="ON">On</option>
<option value="PENDING">Pending</option>
<option value="OFF">Off</option>
</options>
</state>
</channel-type>
<channel-type id="hvactargettemperature" advanced="true">
<item-type>Number:Temperature</item-type>
<label>HVAC Target Temperature</label>
<description>HVAC target temperature (19 to 21)</description>
<category>Temperature</category>
<state pattern="%.1f %unit%"></state>
</channel-type>
<channel-type id="externaltemperature" advanced="true">
<item-type>Number:Temperature</item-type>
<label>External Temperature</label>
<description>Temperature outside of the car</description>
<category>Temperature</category>
<state pattern="%.1f %unit%" readOnly="true"></state>
</channel-type>
<channel-type id="image">
<item-type>String</item-type>
<label>Image URL</label>
<description>Image URL of MyRenault</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="locationupdated">
<item-type>DateTime</item-type>
<label>Location Update</label>
<description>Timestamp of the last location update</description>
<state pattern="%1$tH:%1$tM %1$td.%1$tm.%1$tY" readOnly="true"></state>
</channel-type>
</thing:thing-descriptions>