2 * Copyright (c) 2010-2022 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.util.concurrent.ExecutionException;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
24 import java.util.concurrent.TimeoutException;
26 import javax.measure.quantity.Energy;
27 import javax.measure.quantity.Length;
28 import javax.measure.quantity.Temperature;
29 import javax.measure.quantity.Time;
31 import org.eclipse.jdt.annotation.NonNullByDefault;
32 import org.eclipse.jdt.annotation.Nullable;
33 import org.eclipse.jetty.client.HttpClient;
34 import org.openhab.binding.renault.internal.RenaultBindingConstants;
35 import org.openhab.binding.renault.internal.RenaultConfiguration;
36 import org.openhab.binding.renault.internal.api.Car;
37 import org.openhab.binding.renault.internal.api.Car.ChargingMode;
38 import org.openhab.binding.renault.internal.api.MyRenaultHttpSession;
39 import org.openhab.binding.renault.internal.api.exceptions.RenaultException;
40 import org.openhab.binding.renault.internal.api.exceptions.RenaultForbiddenException;
41 import org.openhab.binding.renault.internal.api.exceptions.RenaultNotImplementedException;
42 import org.openhab.binding.renault.internal.api.exceptions.RenaultUpdateException;
43 import org.openhab.core.library.types.DateTimeType;
44 import org.openhab.core.library.types.DecimalType;
45 import org.openhab.core.library.types.PointType;
46 import org.openhab.core.library.types.QuantityType;
47 import org.openhab.core.library.types.StringType;
48 import org.openhab.core.library.unit.SIUnits;
49 import org.openhab.core.thing.ChannelUID;
50 import org.openhab.core.thing.Thing;
51 import org.openhab.core.thing.ThingStatus;
52 import org.openhab.core.thing.ThingStatusDetail;
53 import org.openhab.core.thing.binding.BaseThingHandler;
54 import org.openhab.core.types.Command;
55 import org.openhab.core.types.RefreshType;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
60 * The {@link RenaultHandler} is responsible for handling commands, which are
61 * sent to one of the channels.
63 * @author Doug Culnane - Initial contribution
66 public class RenaultHandler extends BaseThingHandler {
68 private final Logger logger = LoggerFactory.getLogger(RenaultHandler.class);
70 private RenaultConfiguration config = new RenaultConfiguration();
72 private @Nullable ScheduledFuture<?> pollingJob;
74 private HttpClient httpClient;
78 public RenaultHandler(Thing thing, HttpClient httpClient) {
81 this.httpClient = httpClient;
85 public void initialize() {
86 // reset the car on initialize
88 this.config = getConfigAs(RenaultConfiguration.class);
90 // Validate configuration
91 if (this.config.myRenaultUsername.isBlank()) {
92 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "MyRenault Username is empty!");
95 if (this.config.myRenaultPassword.isBlank()) {
96 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "MyRenault Password is empty!");
99 if (this.config.locale.isBlank()) {
100 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Location is empty!");
103 if (this.config.vin.isBlank()) {
104 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "VIN is empty!");
107 if (this.config.refreshInterval < 1) {
108 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
109 "The refresh interval mush to be larger than 1");
112 updateStatus(ThingStatus.UNKNOWN);
114 updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
115 new QuantityType<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
117 // Background initialization:
118 ScheduledFuture<?> job = pollingJob;
119 if (job == null || job.isCancelled()) {
120 pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, 0, config.refreshInterval, TimeUnit.MINUTES);
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) {
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 // We can only trigger pre-conditioning of the car.
150 if (command instanceof StringType && command.toString().equals(Car.HVAC_STATUS_ON)
151 && !car.isDisableHvac()) {
152 final MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
154 updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_PENDING));
155 car.resetHVACStatus();
156 httpSession.initSesssion(car);
157 httpSession.actionHvacOn(car.getHvacTargetTemperature());
158 if (pollingJob != null) {
159 pollingJob.cancel(true);
161 pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, config.updateDelay,
162 config.refreshInterval * 60, TimeUnit.SECONDS);
163 } catch (InterruptedException e) {
164 logger.warn("Error My Renault Http Session.", e);
165 Thread.currentThread().interrupt();
166 } catch (RenaultException | RenaultForbiddenException | RenaultUpdateException
167 | RenaultNotImplementedException | ExecutionException | TimeoutException e) {
168 logger.warn("Error My Renault Http Session.", e);
169 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
173 case RenaultBindingConstants.CHANNEL_CHARGING_MODE:
174 if (command instanceof StringType) {
176 ChargingMode newMode = ChargingMode.valueOf(command.toString());
177 if (!ChargingMode.UNKNOWN.equals(newMode)) {
178 MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
180 httpSession.initSesssion(car);
181 httpSession.actionChargeMode(newMode);
182 car.setChargeMode(newMode);
183 updateState(CHANNEL_CHARGING_MODE, new StringType(newMode.toString()));
184 } catch (InterruptedException e) {
185 logger.warn("Error My Renault Http Session.", e);
186 Thread.currentThread().interrupt();
187 } catch (RenaultException | RenaultForbiddenException | RenaultUpdateException
188 | RenaultNotImplementedException | ExecutionException | TimeoutException e) {
189 logger.warn("Error My Renault Http Session.", e);
190 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
194 } catch (IllegalArgumentException e) {
195 logger.warn("Invalid ChargingMode {}.", command.toString());
205 public void dispose() {
206 ScheduledFuture<?> job = pollingJob;
214 private void getStatus() {
215 MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
217 httpSession.initSesssion(car);
218 updateStatus(ThingStatus.ONLINE);
219 } catch (InterruptedException e) {
220 logger.warn("Error My Renault Http Session.", e);
221 Thread.currentThread().interrupt();
222 } catch (Exception e) {
223 logger.warn("Error My Renault Http Session.", e);
224 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
226 if (httpSession != null) {
227 String imageURL = car.getImageURL();
228 if (imageURL != null && !imageURL.isEmpty()) {
229 updateState(CHANNEL_IMAGE, new StringType(imageURL));
231 updateHvacStatus(httpSession);
232 updateCockpit(httpSession);
233 updateLocation(httpSession);
234 updateBattery(httpSession);
238 private void updateHvacStatus(MyRenaultHttpSession httpSession) {
239 if (!car.isDisableHvac()) {
241 httpSession.getHvacStatus(car);
242 Boolean hvacstatus = car.getHvacstatus();
243 if (hvacstatus == null) {
244 updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_PENDING));
245 } else if (hvacstatus.booleanValue()) {
246 updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_ON));
248 updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_OFF));
250 Double externalTemperature = car.getExternalTemperature();
251 if (externalTemperature != null) {
252 updateState(CHANNEL_EXTERNAL_TEMPERATURE,
253 new QuantityType<Temperature>(externalTemperature.doubleValue(), SIUnits.CELSIUS));
255 } catch (RenaultNotImplementedException e) {
256 car.setDisableHvac(true);
257 } catch (RenaultForbiddenException | RenaultUpdateException e) {
262 private void updateLocation(MyRenaultHttpSession httpSession) {
263 if (!car.isDisableLocation()) {
265 httpSession.getLocation(car);
266 Double latitude = car.getGpsLatitude();
267 Double longitude = car.getGpsLongitude();
268 if (latitude != null && longitude != null) {
269 updateState(CHANNEL_LOCATION, new PointType(new DecimalType(latitude.doubleValue()),
270 new DecimalType(longitude.doubleValue())));
272 String locationUpdated = car.getLocationUpdated();
273 if (locationUpdated != null) {
274 updateState(CHANNEL_LOCATION_UPDATED, new DateTimeType(locationUpdated));
276 } catch (RenaultNotImplementedException e) {
277 car.setDisableLocation(true);
278 } catch (IllegalArgumentException | RenaultForbiddenException | RenaultUpdateException e) {
283 private void updateCockpit(MyRenaultHttpSession httpSession) {
284 if (!car.isDisableCockpit()) {
286 httpSession.getCockpit(car);
287 Double odometer = car.getOdometer();
288 if (odometer != null) {
289 updateState(CHANNEL_ODOMETER, new QuantityType<Length>(odometer.doubleValue(), KILO(METRE)));
291 } catch (RenaultNotImplementedException e) {
292 car.setDisableCockpit(true);
293 } catch (RenaultForbiddenException | RenaultUpdateException e) {
298 private void updateBattery(MyRenaultHttpSession httpSession) {
299 if (!car.isDisableBattery()) {
301 httpSession.getBatteryStatus(car);
302 updateState(CHANNEL_PLUG_STATUS, new StringType(car.getPlugStatus().name()));
303 updateState(CHANNEL_CHARGING_STATUS, new StringType(car.getChargingStatus().name()));
304 Double batteryLevel = car.getBatteryLevel();
305 if (batteryLevel != null) {
306 updateState(CHANNEL_BATTERY_LEVEL, new DecimalType(batteryLevel.doubleValue()));
308 Double estimatedRange = car.getEstimatedRange();
309 if (estimatedRange != null) {
310 updateState(CHANNEL_ESTIMATED_RANGE,
311 new QuantityType<Length>(estimatedRange.doubleValue(), KILO(METRE)));
313 Double batteryAvailableEnergy = car.getBatteryAvailableEnergy();
314 if (batteryAvailableEnergy != null) {
315 updateState(CHANNEL_BATTERY_AVAILABLE_ENERGY,
316 new QuantityType<Energy>(batteryAvailableEnergy.doubleValue(), KILOWATT_HOUR));
318 Integer chargingRemainingTime = car.getChargingRemainingTime();
319 if (chargingRemainingTime != null) {
320 updateState(CHANNEL_CHARGING_REMAINING_TIME,
321 new QuantityType<Time>(chargingRemainingTime.doubleValue(), MINUTE));
323 } catch (RenaultNotImplementedException e) {
324 car.setDisableBattery(true);
325 } catch (RenaultForbiddenException | RenaultUpdateException e) {