2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.renault.internal.handler;
15 import static org.openhab.binding.renault.internal.RenaultBindingConstants.*;
16 import static org.openhab.core.library.unit.MetricPrefix.KILO;
17 import static org.openhab.core.library.unit.SIUnits.METRE;
18 import static org.openhab.core.library.unit.Units.KILOWATT_HOUR;
19 import static org.openhab.core.library.unit.Units.MINUTE;
21 import java.time.ZonedDateTime;
22 import java.util.concurrent.ExecutionException;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25 import java.util.concurrent.TimeoutException;
27 import javax.measure.quantity.Energy;
28 import javax.measure.quantity.Length;
29 import javax.measure.quantity.Temperature;
30 import javax.measure.quantity.Time;
32 import org.eclipse.jdt.annotation.NonNullByDefault;
33 import org.eclipse.jdt.annotation.Nullable;
34 import org.eclipse.jetty.client.HttpClient;
35 import org.openhab.binding.renault.internal.RenaultBindingConstants;
36 import org.openhab.binding.renault.internal.RenaultConfiguration;
37 import org.openhab.binding.renault.internal.api.Car;
38 import org.openhab.binding.renault.internal.api.Car.ChargingMode;
39 import org.openhab.binding.renault.internal.api.MyRenaultHttpSession;
40 import org.openhab.binding.renault.internal.api.exceptions.RenaultAPIGatewayException;
41 import org.openhab.binding.renault.internal.api.exceptions.RenaultActionException;
42 import org.openhab.binding.renault.internal.api.exceptions.RenaultException;
43 import org.openhab.binding.renault.internal.api.exceptions.RenaultForbiddenException;
44 import org.openhab.binding.renault.internal.api.exceptions.RenaultNotImplementedException;
45 import org.openhab.binding.renault.internal.api.exceptions.RenaultUpdateException;
46 import org.openhab.core.library.types.DateTimeType;
47 import org.openhab.core.library.types.DecimalType;
48 import org.openhab.core.library.types.OnOffType;
49 import org.openhab.core.library.types.PointType;
50 import org.openhab.core.library.types.QuantityType;
51 import org.openhab.core.library.types.StringType;
52 import org.openhab.core.library.unit.SIUnits;
53 import org.openhab.core.thing.ChannelUID;
54 import org.openhab.core.thing.Thing;
55 import org.openhab.core.thing.ThingStatus;
56 import org.openhab.core.thing.ThingStatusDetail;
57 import org.openhab.core.thing.binding.BaseThingHandler;
58 import org.openhab.core.types.Command;
59 import org.openhab.core.types.RefreshType;
60 import org.openhab.core.types.UnDefType;
61 import org.slf4j.Logger;
62 import org.slf4j.LoggerFactory;
65 * The {@link RenaultHandler} is responsible for handling commands, which are
66 * sent to one of the channels.
68 * @author Doug Culnane - Initial contribution
71 public class RenaultHandler extends BaseThingHandler {
73 private final Logger logger = LoggerFactory.getLogger(RenaultHandler.class);
75 private RenaultConfiguration config = new RenaultConfiguration();
77 private @Nullable ScheduledFuture<?> pollingJob;
79 private HttpClient httpClient;
83 public RenaultHandler(Thing thing, HttpClient httpClient) {
86 this.httpClient = httpClient;
90 public void initialize() {
91 // reset the car on initialize
93 this.config = getConfigAs(RenaultConfiguration.class);
95 // Validate configuration
96 if (this.config.myRenaultUsername.isBlank()) {
97 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "MyRenault Username is empty!");
100 if (this.config.myRenaultPassword.isBlank()) {
101 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "MyRenault Password is empty!");
104 if (this.config.locale.isBlank()) {
105 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Location is empty!");
108 if (this.config.vin.isBlank()) {
109 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "VIN is empty!");
112 if (this.config.refreshInterval < 1) {
113 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
114 "The refresh interval mush to be larger than 1");
117 updateStatus(ThingStatus.UNKNOWN);
118 updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
119 new QuantityType<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
121 reschedulePollingJob();
125 public void handleCommand(ChannelUID channelUID, Command command) {
126 switch (channelUID.getId()) {
127 case RenaultBindingConstants.CHANNEL_HVAC_TARGET_TEMPERATURE:
128 if (!car.isDisableHvac()) {
129 if (command instanceof RefreshType) {
130 updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
131 new QuantityType<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
132 } else if (command instanceof DecimalType decimalCommand) {
133 car.setHvacTargetTemperature(decimalCommand.doubleValue());
134 updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
135 new QuantityType<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
136 } else if (command instanceof QuantityType) {
138 QuantityType<Temperature> celsius = ((QuantityType<Temperature>) command)
139 .toUnit(SIUnits.CELSIUS);
140 if (celsius != null) {
141 car.setHvacTargetTemperature(celsius.doubleValue());
143 updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
144 new QuantityType<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
148 case RenaultBindingConstants.CHANNEL_HVAC_STATUS:
149 if (!car.isDisableHvac()) {
150 if (command instanceof RefreshType) {
151 reschedulePollingJob();
152 } else if (command instanceof StringType && command.toString().equals(Car.HVAC_STATUS_ON)) {
153 // We can only trigger pre-conditioning of the car.
154 final MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
156 updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_PENDING));
157 car.resetHVACStatus();
158 httpSession.initSesssion(car);
159 httpSession.actionHvacOn(car.getHvacTargetTemperature());
160 ScheduledFuture<?> job = pollingJob;
164 pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, config.updateDelay,
165 config.refreshInterval * 60, TimeUnit.SECONDS);
166 } catch (InterruptedException e) {
167 logger.warn("Error My Renault Http Session.", e);
168 Thread.currentThread().interrupt();
169 } catch (RenaultException | RenaultForbiddenException | RenaultUpdateException
170 | RenaultActionException | RenaultNotImplementedException | ExecutionException
171 | TimeoutException e) {
172 logger.warn("Error during action HVAC on.", e);
173 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
178 case RenaultBindingConstants.CHANNEL_CHARGING_MODE:
179 if (command instanceof RefreshType) {
180 reschedulePollingJob();
181 } else if (command instanceof StringType) {
183 ChargingMode newMode = ChargingMode.valueOf(command.toString());
184 if (!ChargingMode.UNKNOWN.equals(newMode)) {
185 MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
187 httpSession.initSesssion(car);
188 httpSession.actionChargeMode(newMode);
189 car.setChargeMode(newMode);
190 updateState(CHANNEL_CHARGING_MODE, new StringType(newMode.toString()));
191 } catch (InterruptedException e) {
192 logger.warn("Error My Renault Http Session.", e);
193 Thread.currentThread().interrupt();
194 } catch (RenaultException | RenaultForbiddenException | RenaultUpdateException
195 | RenaultActionException | RenaultNotImplementedException | ExecutionException
196 | TimeoutException e) {
197 logger.warn("Error during action set charge mode.", e);
198 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
202 } catch (IllegalArgumentException e) {
203 logger.warn("Invalid ChargingMode {}.", command.toString());
208 case RenaultBindingConstants.CHANNEL_PAUSE:
209 if (command instanceof RefreshType) {
210 reschedulePollingJob();
211 } else if (command instanceof OnOffType) {
213 MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
215 boolean pause = OnOffType.ON == command;
216 httpSession.initSesssion(car);
217 httpSession.actionPause(pause);
218 car.setPauseMode(pause);
219 updateState(CHANNEL_PAUSE, OnOffType.from(command.toString()));
220 } catch (InterruptedException e) {
221 logger.warn("Error My Renault Http Session.", e);
222 Thread.currentThread().interrupt();
223 } catch (RenaultForbiddenException | RenaultNotImplementedException | RenaultActionException
224 | RenaultException | RenaultUpdateException | ExecutionException | TimeoutException e) {
225 logger.warn("Error during action set pause.", e);
226 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
228 } catch (IllegalArgumentException e) {
229 logger.warn("Invalid Pause Mode {}.", command.toString());
235 if (command instanceof RefreshType) {
236 reschedulePollingJob();
243 public void dispose() {
244 ScheduledFuture<?> job = pollingJob;
252 private void getStatus() {
253 MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
255 httpSession.initSesssion(car);
256 updateStatus(ThingStatus.ONLINE);
257 } catch (InterruptedException e) {
258 logger.warn("Error My Renault Http Session.", e);
259 Thread.currentThread().interrupt();
260 } catch (Exception e) {
261 logger.warn("Error My Renault Http Session.", e);
262 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
264 String imageURL = car.getImageURL();
265 if (imageURL != null && !imageURL.isEmpty()) {
266 updateState(CHANNEL_IMAGE, new StringType(imageURL));
268 updateHvacStatus(httpSession);
269 updateCockpit(httpSession);
270 updateLocation(httpSession);
271 updateBattery(httpSession);
272 updateLockStatus(httpSession);
275 private void updateHvacStatus(MyRenaultHttpSession httpSession) {
276 if (!car.isDisableHvac()) {
278 httpSession.getHvacStatus(car);
279 Boolean hvacstatus = car.getHvacstatus();
280 if (hvacstatus == null) {
281 updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_PENDING));
282 } else if (hvacstatus.booleanValue()) {
283 updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_ON));
285 updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_OFF));
287 Double externalTemperature = car.getExternalTemperature();
288 if (externalTemperature != null) {
289 updateState(CHANNEL_EXTERNAL_TEMPERATURE,
290 new QuantityType<Temperature>(externalTemperature.doubleValue(), SIUnits.CELSIUS));
292 } catch (RenaultNotImplementedException e) {
293 logger.warn("Disabling unsupported HVAC status update.");
294 car.setDisableHvac(true);
295 } catch (RenaultForbiddenException | RenaultUpdateException | RenaultAPIGatewayException e) {
296 logger.warn("Error updating HVAC status.", e);
301 private void updateLocation(MyRenaultHttpSession httpSession) {
302 if (!car.isDisableLocation()) {
304 httpSession.getLocation(car);
305 Double latitude = car.getGpsLatitude();
306 Double longitude = car.getGpsLongitude();
307 if (latitude != null && longitude != null) {
308 updateState(CHANNEL_LOCATION, new PointType(new DecimalType(latitude.doubleValue()),
309 new DecimalType(longitude.doubleValue())));
311 ZonedDateTime locationUpdated = car.getLocationUpdated();
312 if (locationUpdated != null) {
313 updateState(CHANNEL_LOCATION_UPDATED, new DateTimeType(locationUpdated));
315 } catch (RenaultNotImplementedException e) {
316 logger.warn("Disabling unsupported location update.");
317 car.setDisableLocation(true);
318 } catch (IllegalArgumentException | RenaultForbiddenException | RenaultUpdateException
319 | RenaultAPIGatewayException e) {
320 logger.warn("Error updating location.", e);
325 private void updateCockpit(MyRenaultHttpSession httpSession) {
326 if (!car.isDisableCockpit()) {
328 httpSession.getCockpit(car);
329 Double odometer = car.getOdometer();
330 if (odometer != null) {
331 updateState(CHANNEL_ODOMETER, new QuantityType<Length>(odometer.doubleValue(), KILO(METRE)));
333 } catch (RenaultNotImplementedException e) {
334 logger.warn("Disabling unsupported cockpit status update.");
335 car.setDisableCockpit(true);
336 } catch (RenaultForbiddenException | RenaultUpdateException | RenaultAPIGatewayException e) {
337 logger.warn("Error updating cockpit status.", e);
342 private void updateBattery(MyRenaultHttpSession httpSession) {
343 if (!car.isDisableBattery()) {
345 httpSession.getBatteryStatus(car);
346 updateState(CHANNEL_PLUG_STATUS, new StringType(car.getPlugStatus().name()));
347 updateState(CHANNEL_CHARGING_STATUS, new StringType(car.getChargingStatus().name()));
348 Double batteryLevel = car.getBatteryLevel();
349 if (batteryLevel != null) {
350 updateState(CHANNEL_BATTERY_LEVEL, new DecimalType(batteryLevel.doubleValue()));
352 Double estimatedRange = car.getEstimatedRange();
353 if (estimatedRange != null) {
354 updateState(CHANNEL_ESTIMATED_RANGE,
355 new QuantityType<Length>(estimatedRange.doubleValue(), KILO(METRE)));
357 Double batteryAvailableEnergy = car.getBatteryAvailableEnergy();
358 if (batteryAvailableEnergy != null) {
359 updateState(CHANNEL_BATTERY_AVAILABLE_ENERGY,
360 new QuantityType<Energy>(batteryAvailableEnergy.doubleValue(), KILOWATT_HOUR));
362 Integer chargingRemainingTime = car.getChargingRemainingTime();
363 if (chargingRemainingTime != null) {
364 updateState(CHANNEL_CHARGING_REMAINING_TIME,
365 new QuantityType<Time>(chargingRemainingTime.doubleValue(), MINUTE));
367 ZonedDateTime batteryStatusUpdated = car.getBatteryStatusUpdated();
368 if (batteryStatusUpdated != null) {
369 updateState(CHANNEL_BATTERY_STATUS_UPDATED, new DateTimeType(batteryStatusUpdated));
371 } catch (RenaultNotImplementedException e) {
372 logger.warn("Disabling unsupported battery update.");
373 car.setDisableBattery(true);
374 } catch (RenaultForbiddenException | RenaultUpdateException | RenaultAPIGatewayException e) {
375 logger.warn("Error updating battery status.", e);
380 private void updateLockStatus(MyRenaultHttpSession httpSession) {
381 if (!car.isDisableLockStatus()) {
383 httpSession.getLockStatus(car);
384 switch (car.getLockStatus()) {
386 updateState(CHANNEL_LOCKED, OnOffType.ON);
389 updateState(CHANNEL_LOCKED, OnOffType.OFF);
392 updateState(CHANNEL_LOCKED, UnDefType.UNDEF);
395 } catch (RenaultNotImplementedException | RenaultAPIGatewayException e) {
396 // If not supported API returns a Bad Gateway for this call.
397 updateState(CHANNEL_LOCKED, UnDefType.UNDEF);
398 logger.warn("Disabling unsupported lock status update.");
399 car.setDisableLockStatus(true);
400 } catch (RenaultForbiddenException | RenaultUpdateException e) {
401 logger.warn("Error updating lock status.", e);
406 private void reschedulePollingJob() {
407 ScheduledFuture<?> job = pollingJob;
411 pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, 0, config.refreshInterval, TimeUnit.MINUTES);