* [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>
# 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 |
-
-## Channels
+| 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 |
-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 |
+## Channels
+| 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
+ }
+}
+```
+
+
+
+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
+
+```
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";
}
public String locale = "";
public String vin = "";
public int refreshInterval = 10;
+ public int updateDelay = 30;
}
@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;
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);
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);
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)
}
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;
+ }
+ }
}
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;
@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;
} 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());
}
}
+ 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)
"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;
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;
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
}
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()) {
}
}
+ @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;
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());
}
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);
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) {
}
}
}
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) {
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
<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>
<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>
<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>