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.RenaultActionException;
41 import org.openhab.binding.renault.internal.api.exceptions.RenaultException;
42 import org.openhab.binding.renault.internal.api.exceptions.RenaultForbiddenException;
43 import org.openhab.binding.renault.internal.api.exceptions.RenaultNotImplementedException;
44 import org.openhab.binding.renault.internal.api.exceptions.RenaultUpdateException;
45 import org.openhab.core.library.types.DateTimeType;
46 import org.openhab.core.library.types.DecimalType;
47 import org.openhab.core.library.types.OnOffType;
48 import org.openhab.core.library.types.PointType;
49 import org.openhab.core.library.types.QuantityType;
50 import org.openhab.core.library.types.StringType;
51 import org.openhab.core.library.unit.SIUnits;
52 import org.openhab.core.thing.ChannelUID;
53 import org.openhab.core.thing.Thing;
54 import org.openhab.core.thing.ThingStatus;
55 import org.openhab.core.thing.ThingStatusDetail;
56 import org.openhab.core.thing.binding.BaseThingHandler;
57 import org.openhab.core.types.Command;
58 import org.openhab.core.types.RefreshType;
59 import org.openhab.core.types.UnDefType;
60 import org.slf4j.Logger;
61 import org.slf4j.LoggerFactory;
64 * The {@link RenaultHandler} is responsible for handling commands, which are
65 * sent to one of the channels.
67 * @author Doug Culnane - Initial contribution
70 public class RenaultHandler extends BaseThingHandler {
72 private final Logger logger = LoggerFactory.getLogger(RenaultHandler.class);
74 private RenaultConfiguration config = new RenaultConfiguration();
76 private @Nullable ScheduledFuture<?> pollingJob;
78 private HttpClient httpClient;
82 public RenaultHandler(Thing thing, HttpClient httpClient) {
85 this.httpClient = httpClient;
89 public void initialize() {
90 // reset the car on initialize
92 this.config = getConfigAs(RenaultConfiguration.class);
94 // Validate configuration
95 if (this.config.myRenaultUsername.isBlank()) {
96 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "MyRenault Username is empty!");
99 if (this.config.myRenaultPassword.isBlank()) {
100 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "MyRenault Password is empty!");
103 if (this.config.locale.isBlank()) {
104 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Location is empty!");
107 if (this.config.vin.isBlank()) {
108 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "VIN is empty!");
111 if (this.config.refreshInterval < 1) {
112 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
113 "The refresh interval mush to be larger than 1");
116 updateStatus(ThingStatus.UNKNOWN);
117 updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
118 new QuantityType<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
120 reschedulePollingJob();
124 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) {
133 car.setHvacTargetTemperature(((DecimalType) command).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());
209 if (command instanceof RefreshType) {
210 reschedulePollingJob();
217 public void dispose() {
218 ScheduledFuture<?> job = pollingJob;
226 private void getStatus() {
227 MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
229 httpSession.initSesssion(car);
230 updateStatus(ThingStatus.ONLINE);
231 } catch (InterruptedException e) {
232 logger.warn("Error My Renault Http Session.", e);
233 Thread.currentThread().interrupt();
234 } catch (Exception e) {
235 logger.warn("Error My Renault Http Session.", e);
236 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
238 String imageURL = car.getImageURL();
239 if (imageURL != null && !imageURL.isEmpty()) {
240 updateState(CHANNEL_IMAGE, new StringType(imageURL));
242 updateHvacStatus(httpSession);
243 updateCockpit(httpSession);
244 updateLocation(httpSession);
245 updateBattery(httpSession);
246 updateLockStatus(httpSession);
249 private void updateHvacStatus(MyRenaultHttpSession httpSession) {
250 if (!car.isDisableHvac()) {
252 httpSession.getHvacStatus(car);
253 Boolean hvacstatus = car.getHvacstatus();
254 if (hvacstatus == null) {
255 updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_PENDING));
256 } else if (hvacstatus.booleanValue()) {
257 updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_ON));
259 updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_OFF));
261 Double externalTemperature = car.getExternalTemperature();
262 if (externalTemperature != null) {
263 updateState(CHANNEL_EXTERNAL_TEMPERATURE,
264 new QuantityType<Temperature>(externalTemperature.doubleValue(), SIUnits.CELSIUS));
266 } catch (RenaultNotImplementedException e) {
267 logger.warn("Disabling unsupported HVAC status update.");
268 car.setDisableHvac(true);
269 } catch (RenaultForbiddenException | RenaultUpdateException e) {
270 logger.warn("Error updating HVAC status.", e);
275 private void updateLocation(MyRenaultHttpSession httpSession) {
276 if (!car.isDisableLocation()) {
278 httpSession.getLocation(car);
279 Double latitude = car.getGpsLatitude();
280 Double longitude = car.getGpsLongitude();
281 if (latitude != null && longitude != null) {
282 updateState(CHANNEL_LOCATION, new PointType(new DecimalType(latitude.doubleValue()),
283 new DecimalType(longitude.doubleValue())));
285 ZonedDateTime locationUpdated = car.getLocationUpdated();
286 if (locationUpdated != null) {
287 updateState(CHANNEL_LOCATION_UPDATED, new DateTimeType(locationUpdated));
289 } catch (RenaultNotImplementedException e) {
290 logger.warn("Disabling unsupported location update.");
291 car.setDisableLocation(true);
292 } catch (IllegalArgumentException | RenaultForbiddenException | RenaultUpdateException e) {
293 logger.warn("Error updating location.", e);
298 private void updateCockpit(MyRenaultHttpSession httpSession) {
299 if (!car.isDisableCockpit()) {
301 httpSession.getCockpit(car);
302 Double odometer = car.getOdometer();
303 if (odometer != null) {
304 updateState(CHANNEL_ODOMETER, new QuantityType<Length>(odometer.doubleValue(), KILO(METRE)));
306 } catch (RenaultNotImplementedException e) {
307 logger.warn("Disabling unsupported cockpit status update.");
308 car.setDisableCockpit(true);
309 } catch (RenaultForbiddenException | RenaultUpdateException e) {
310 logger.warn("Error updating cockpit status.", e);
315 private void updateBattery(MyRenaultHttpSession httpSession) {
316 if (!car.isDisableBattery()) {
318 httpSession.getBatteryStatus(car);
319 updateState(CHANNEL_PLUG_STATUS, new StringType(car.getPlugStatus().name()));
320 updateState(CHANNEL_CHARGING_STATUS, new StringType(car.getChargingStatus().name()));
321 Double batteryLevel = car.getBatteryLevel();
322 if (batteryLevel != null) {
323 updateState(CHANNEL_BATTERY_LEVEL, new DecimalType(batteryLevel.doubleValue()));
325 Double estimatedRange = car.getEstimatedRange();
326 if (estimatedRange != null) {
327 updateState(CHANNEL_ESTIMATED_RANGE,
328 new QuantityType<Length>(estimatedRange.doubleValue(), KILO(METRE)));
330 Double batteryAvailableEnergy = car.getBatteryAvailableEnergy();
331 if (batteryAvailableEnergy != null) {
332 updateState(CHANNEL_BATTERY_AVAILABLE_ENERGY,
333 new QuantityType<Energy>(batteryAvailableEnergy.doubleValue(), KILOWATT_HOUR));
335 Integer chargingRemainingTime = car.getChargingRemainingTime();
336 if (chargingRemainingTime != null) {
337 updateState(CHANNEL_CHARGING_REMAINING_TIME,
338 new QuantityType<Time>(chargingRemainingTime.doubleValue(), MINUTE));
340 ZonedDateTime batteryStatusUpdated = car.getBatteryStatusUpdated();
341 if (batteryStatusUpdated != null) {
342 updateState(CHANNEL_BATTERY_STATUS_UPDATED, new DateTimeType(batteryStatusUpdated));
344 } catch (RenaultNotImplementedException e) {
345 logger.warn("Disabling unsupported battery update.");
346 car.setDisableBattery(true);
347 } catch (RenaultForbiddenException | RenaultUpdateException e) {
348 logger.warn("Error updating battery status.", e);
353 private void updateLockStatus(MyRenaultHttpSession httpSession) {
354 if (!car.isDisableLockStatus()) {
356 httpSession.getLockStatus(car);
357 switch (car.getLockStatus()) {
359 updateState(CHANNEL_LOCKED, OnOffType.ON);
362 updateState(CHANNEL_LOCKED, OnOffType.OFF);
365 updateState(CHANNEL_LOCKED, UnDefType.UNDEF);
368 } catch (RenaultNotImplementedException e) {
369 updateState(CHANNEL_LOCKED, UnDefType.UNDEF);
370 logger.warn("Disabling unsupported lock status update.");
371 car.setDisableLockStatus(true);
372 } catch (RenaultForbiddenException | RenaultUpdateException e) {
373 logger.warn("Error updating lock status.", e);
378 private void reschedulePollingJob() {
379 ScheduledFuture<?> job = pollingJob;
383 pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, 0, config.refreshInterval, TimeUnit.MINUTES);