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) {
127 switch (channelUID.getId()) {
128 case RenaultBindingConstants.CHANNEL_HVAC_TARGET_TEMPERATURE:
129 if (!car.isDisableHvac()) {
130 if (command instanceof RefreshType) {
131 updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
132 new QuantityType<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
133 } else if (command instanceof DecimalType) {
134 car.setHvacTargetTemperature(((DecimalType) command).doubleValue());
135 updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
136 new QuantityType<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
137 } else if (command instanceof QuantityType) {
139 QuantityType<Temperature> celsius = ((QuantityType<Temperature>) command)
140 .toUnit(SIUnits.CELSIUS);
141 if (celsius != null) {
142 car.setHvacTargetTemperature(celsius.doubleValue());
144 updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
145 new QuantityType<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
149 case RenaultBindingConstants.CHANNEL_HVAC_STATUS:
150 if (!car.isDisableHvac()) {
151 if (command instanceof RefreshType) {
152 reschedulePollingJob();
153 } else if (command instanceof StringType && command.toString().equals(Car.HVAC_STATUS_ON)) {
154 // We can only trigger pre-conditioning of the car.
155 final MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
157 updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_PENDING));
158 car.resetHVACStatus();
159 httpSession.initSesssion(car);
160 httpSession.actionHvacOn(car.getHvacTargetTemperature());
161 ScheduledFuture<?> job = pollingJob;
165 pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, config.updateDelay,
166 config.refreshInterval * 60, TimeUnit.SECONDS);
167 } catch (InterruptedException e) {
168 logger.warn("Error My Renault Http Session.", e);
169 Thread.currentThread().interrupt();
170 } catch (RenaultException | RenaultForbiddenException | RenaultUpdateException
171 | RenaultActionException | RenaultNotImplementedException | ExecutionException
172 | TimeoutException e) {
173 logger.warn("Error during action HVAC on.", e);
174 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
179 case RenaultBindingConstants.CHANNEL_CHARGING_MODE:
180 if (command instanceof RefreshType) {
181 reschedulePollingJob();
182 } else if (command instanceof StringType) {
184 ChargingMode newMode = ChargingMode.valueOf(command.toString());
185 if (!ChargingMode.UNKNOWN.equals(newMode)) {
186 MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
188 httpSession.initSesssion(car);
189 httpSession.actionChargeMode(newMode);
190 car.setChargeMode(newMode);
191 updateState(CHANNEL_CHARGING_MODE, new StringType(newMode.toString()));
192 } catch (InterruptedException e) {
193 logger.warn("Error My Renault Http Session.", e);
194 Thread.currentThread().interrupt();
195 } catch (RenaultException | RenaultForbiddenException | RenaultUpdateException
196 | RenaultActionException | RenaultNotImplementedException | ExecutionException
197 | TimeoutException e) {
198 logger.warn("Error during action set charge mode.", e);
199 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
203 } catch (IllegalArgumentException e) {
204 logger.warn("Invalid ChargingMode {}.", command.toString());
209 case RenaultBindingConstants.CHANNEL_PAUSE:
210 if (command instanceof RefreshType) {
211 reschedulePollingJob();
212 } else if (command instanceof OnOffType) {
214 MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
216 boolean pause = OnOffType.ON == command;
217 httpSession.initSesssion(car);
218 httpSession.actionPause(pause);
219 car.setPauseMode(pause);
220 updateState(CHANNEL_PAUSE, OnOffType.from(command.toString()));
221 } catch (InterruptedException e) {
222 logger.warn("Error My Renault Http Session.", e);
223 Thread.currentThread().interrupt();
224 } catch (RenaultForbiddenException | RenaultNotImplementedException | RenaultActionException
225 | RenaultException | RenaultUpdateException | ExecutionException | TimeoutException e) {
226 logger.warn("Error during action set pause.", e);
227 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
229 } catch (IllegalArgumentException e) {
230 logger.warn("Invalid Pause Mode {}.", command.toString());
236 if (command instanceof RefreshType) {
237 reschedulePollingJob();
244 public void dispose() {
245 ScheduledFuture<?> job = pollingJob;
253 private void getStatus() {
254 MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
256 httpSession.initSesssion(car);
257 updateStatus(ThingStatus.ONLINE);
258 } catch (InterruptedException e) {
259 logger.warn("Error My Renault Http Session.", e);
260 Thread.currentThread().interrupt();
261 } catch (Exception e) {
262 logger.warn("Error My Renault Http Session.", e);
263 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
265 String imageURL = car.getImageURL();
266 if (imageURL != null && !imageURL.isEmpty()) {
267 updateState(CHANNEL_IMAGE, new StringType(imageURL));
269 updateHvacStatus(httpSession);
270 updateCockpit(httpSession);
271 updateLocation(httpSession);
272 updateBattery(httpSession);
273 updateLockStatus(httpSession);
276 private void updateHvacStatus(MyRenaultHttpSession httpSession) {
277 if (!car.isDisableHvac()) {
279 httpSession.getHvacStatus(car);
280 Boolean hvacstatus = car.getHvacstatus();
281 if (hvacstatus == null) {
282 updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_PENDING));
283 } else if (hvacstatus.booleanValue()) {
284 updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_ON));
286 updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_OFF));
288 Double externalTemperature = car.getExternalTemperature();
289 if (externalTemperature != null) {
290 updateState(CHANNEL_EXTERNAL_TEMPERATURE,
291 new QuantityType<Temperature>(externalTemperature.doubleValue(), SIUnits.CELSIUS));
293 } catch (RenaultNotImplementedException e) {
294 logger.warn("Disabling unsupported HVAC status update.");
295 car.setDisableHvac(true);
296 } catch (RenaultForbiddenException | RenaultUpdateException | RenaultAPIGatewayException e) {
297 logger.warn("Error updating HVAC status.", e);
302 private void updateLocation(MyRenaultHttpSession httpSession) {
303 if (!car.isDisableLocation()) {
305 httpSession.getLocation(car);
306 Double latitude = car.getGpsLatitude();
307 Double longitude = car.getGpsLongitude();
308 if (latitude != null && longitude != null) {
309 updateState(CHANNEL_LOCATION, new PointType(new DecimalType(latitude.doubleValue()),
310 new DecimalType(longitude.doubleValue())));
312 ZonedDateTime locationUpdated = car.getLocationUpdated();
313 if (locationUpdated != null) {
314 updateState(CHANNEL_LOCATION_UPDATED, new DateTimeType(locationUpdated));
316 } catch (RenaultNotImplementedException e) {
317 logger.warn("Disabling unsupported location update.");
318 car.setDisableLocation(true);
319 } catch (IllegalArgumentException | RenaultForbiddenException | RenaultUpdateException
320 | RenaultAPIGatewayException e) {
321 logger.warn("Error updating location.", e);
326 private void updateCockpit(MyRenaultHttpSession httpSession) {
327 if (!car.isDisableCockpit()) {
329 httpSession.getCockpit(car);
330 Double odometer = car.getOdometer();
331 if (odometer != null) {
332 updateState(CHANNEL_ODOMETER, new QuantityType<Length>(odometer.doubleValue(), KILO(METRE)));
334 } catch (RenaultNotImplementedException e) {
335 logger.warn("Disabling unsupported cockpit status update.");
336 car.setDisableCockpit(true);
337 } catch (RenaultForbiddenException | RenaultUpdateException | RenaultAPIGatewayException e) {
338 logger.warn("Error updating cockpit status.", e);
343 private void updateBattery(MyRenaultHttpSession httpSession) {
344 if (!car.isDisableBattery()) {
346 httpSession.getBatteryStatus(car);
347 updateState(CHANNEL_PLUG_STATUS, new StringType(car.getPlugStatus().name()));
348 updateState(CHANNEL_CHARGING_STATUS, new StringType(car.getChargingStatus().name()));
349 Double batteryLevel = car.getBatteryLevel();
350 if (batteryLevel != null) {
351 updateState(CHANNEL_BATTERY_LEVEL, new DecimalType(batteryLevel.doubleValue()));
353 Double estimatedRange = car.getEstimatedRange();
354 if (estimatedRange != null) {
355 updateState(CHANNEL_ESTIMATED_RANGE,
356 new QuantityType<Length>(estimatedRange.doubleValue(), KILO(METRE)));
358 Double batteryAvailableEnergy = car.getBatteryAvailableEnergy();
359 if (batteryAvailableEnergy != null) {
360 updateState(CHANNEL_BATTERY_AVAILABLE_ENERGY,
361 new QuantityType<Energy>(batteryAvailableEnergy.doubleValue(), KILOWATT_HOUR));
363 Integer chargingRemainingTime = car.getChargingRemainingTime();
364 if (chargingRemainingTime != null) {
365 updateState(CHANNEL_CHARGING_REMAINING_TIME,
366 new QuantityType<Time>(chargingRemainingTime.doubleValue(), MINUTE));
368 ZonedDateTime batteryStatusUpdated = car.getBatteryStatusUpdated();
369 if (batteryStatusUpdated != null) {
370 updateState(CHANNEL_BATTERY_STATUS_UPDATED, new DateTimeType(batteryStatusUpdated));
372 } catch (RenaultNotImplementedException e) {
373 logger.warn("Disabling unsupported battery update.");
374 car.setDisableBattery(true);
375 } catch (RenaultForbiddenException | RenaultUpdateException | RenaultAPIGatewayException e) {
376 logger.warn("Error updating battery status.", e);
381 private void updateLockStatus(MyRenaultHttpSession httpSession) {
382 if (!car.isDisableLockStatus()) {
384 httpSession.getLockStatus(car);
385 switch (car.getLockStatus()) {
387 updateState(CHANNEL_LOCKED, OnOffType.ON);
390 updateState(CHANNEL_LOCKED, OnOffType.OFF);
393 updateState(CHANNEL_LOCKED, UnDefType.UNDEF);
396 } catch (RenaultNotImplementedException | RenaultAPIGatewayException e) {
397 // If not supported API returns a Bad Gateway for this call.
398 updateState(CHANNEL_LOCKED, UnDefType.UNDEF);
399 logger.warn("Disabling unsupported lock status update.");
400 car.setDisableLockStatus(true);
401 } catch (RenaultForbiddenException | RenaultUpdateException e) {
402 logger.warn("Error updating lock status.", e);
407 private void reschedulePollingJob() {
408 ScheduledFuture<?> job = pollingJob;
412 pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, 0, config.refreshInterval, TimeUnit.MINUTES);