]> git.basschouten.com Git - openhab-addons.git/blob
d8f584f25c1a2cca24a6ae4baa4ee6a74cc6492b
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.renault.internal.handler;
14
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;
20
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;
26
27 import javax.measure.quantity.Energy;
28 import javax.measure.quantity.Length;
29 import javax.measure.quantity.Temperature;
30 import javax.measure.quantity.Time;
31
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;
63
64 /**
65  * The {@link RenaultHandler} is responsible for handling commands, which are
66  * sent to one of the channels.
67  *
68  * @author Doug Culnane - Initial contribution
69  */
70 @NonNullByDefault
71 public class RenaultHandler extends BaseThingHandler {
72
73     private final Logger logger = LoggerFactory.getLogger(RenaultHandler.class);
74
75     private RenaultConfiguration config = new RenaultConfiguration();
76
77     private @Nullable ScheduledFuture<?> pollingJob;
78
79     private HttpClient httpClient;
80
81     private Car car;
82
83     public RenaultHandler(Thing thing, HttpClient httpClient) {
84         super(thing);
85         this.car = new Car();
86         this.httpClient = httpClient;
87     }
88
89     @Override
90     public void initialize() {
91         // reset the car on initialize
92         this.car = new Car();
93         this.config = getConfigAs(RenaultConfiguration.class);
94
95         // Validate configuration
96         if (this.config.myRenaultUsername.isBlank()) {
97             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "MyRenault Username is empty!");
98             return;
99         }
100         if (this.config.myRenaultPassword.isBlank()) {
101             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "MyRenault Password is empty!");
102             return;
103         }
104         if (this.config.locale.isBlank()) {
105             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Location is empty!");
106             return;
107         }
108         if (this.config.vin.isBlank()) {
109             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "VIN is empty!");
110             return;
111         }
112         if (this.config.refreshInterval < 1) {
113             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
114                     "The refresh interval mush to be larger than 1");
115             return;
116         }
117         updateStatus(ThingStatus.UNKNOWN);
118         updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
119                 new QuantityType<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
120
121         reschedulePollingJob();
122     }
123
124     @Override
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) {
137                         @Nullable
138                         QuantityType<Temperature> celsius = ((QuantityType<Temperature>) command)
139                                 .toUnit(SIUnits.CELSIUS);
140                         if (celsius != null) {
141                             car.setHvacTargetTemperature(celsius.doubleValue());
142                         }
143                         updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
144                                 new QuantityType<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
145                     }
146                 }
147                 break;
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);
155                         try {
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;
161                             if (job != null) {
162                                 job.cancel(true);
163                             }
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());
174                         }
175                     }
176                 }
177                 break;
178             case RenaultBindingConstants.CHANNEL_CHARGING_MODE:
179                 if (command instanceof RefreshType) {
180                     reschedulePollingJob();
181                 } else if (command instanceof StringType) {
182                     try {
183                         ChargingMode newMode = ChargingMode.valueOf(command.toString());
184                         if (!ChargingMode.UNKNOWN.equals(newMode)) {
185                             MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
186                             try {
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,
199                                         e.getMessage());
200                             }
201                         }
202                     } catch (IllegalArgumentException e) {
203                         logger.warn("Invalid ChargingMode {}.", command.toString());
204                         return;
205                     }
206                 }
207                 break;
208             case RenaultBindingConstants.CHANNEL_PAUSE:
209                 if (command instanceof RefreshType) {
210                     reschedulePollingJob();
211                 } else if (command instanceof OnOffType) {
212                     try {
213                         MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
214                         try {
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());
227                         }
228                     } catch (IllegalArgumentException e) {
229                         logger.warn("Invalid Pause Mode {}.", command.toString());
230                         return;
231                     }
232                 }
233                 break;
234             default:
235                 if (command instanceof RefreshType) {
236                     reschedulePollingJob();
237                 }
238                 break;
239         }
240     }
241
242     @Override
243     public void dispose() {
244         ScheduledFuture<?> job = pollingJob;
245         if (job != null) {
246             job.cancel(true);
247             pollingJob = null;
248         }
249         super.dispose();
250     }
251
252     private void getStatus() {
253         MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
254         try {
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());
263         }
264         String imageURL = car.getImageURL();
265         if (imageURL != null && !imageURL.isEmpty()) {
266             updateState(CHANNEL_IMAGE, new StringType(imageURL));
267         }
268         updateHvacStatus(httpSession);
269         updateCockpit(httpSession);
270         updateLocation(httpSession);
271         updateBattery(httpSession);
272         updateLockStatus(httpSession);
273     }
274
275     private void updateHvacStatus(MyRenaultHttpSession httpSession) {
276         if (!car.isDisableHvac()) {
277             try {
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));
284                 } else {
285                     updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_OFF));
286                 }
287                 Double externalTemperature = car.getExternalTemperature();
288                 if (externalTemperature != null) {
289                     updateState(CHANNEL_EXTERNAL_TEMPERATURE,
290                             new QuantityType<Temperature>(externalTemperature.doubleValue(), SIUnits.CELSIUS));
291                 }
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);
297             }
298         }
299     }
300
301     private void updateLocation(MyRenaultHttpSession httpSession) {
302         if (!car.isDisableLocation()) {
303             try {
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())));
310                 }
311                 ZonedDateTime locationUpdated = car.getLocationUpdated();
312                 if (locationUpdated != null) {
313                     updateState(CHANNEL_LOCATION_UPDATED, new DateTimeType(locationUpdated));
314                 }
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);
321             }
322         }
323     }
324
325     private void updateCockpit(MyRenaultHttpSession httpSession) {
326         if (!car.isDisableCockpit()) {
327             try {
328                 httpSession.getCockpit(car);
329                 Double odometer = car.getOdometer();
330                 if (odometer != null) {
331                     updateState(CHANNEL_ODOMETER, new QuantityType<Length>(odometer.doubleValue(), KILO(METRE)));
332                 }
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);
338             }
339         }
340     }
341
342     private void updateBattery(MyRenaultHttpSession httpSession) {
343         if (!car.isDisableBattery()) {
344             try {
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()));
351                 }
352                 Double estimatedRange = car.getEstimatedRange();
353                 if (estimatedRange != null) {
354                     updateState(CHANNEL_ESTIMATED_RANGE,
355                             new QuantityType<Length>(estimatedRange.doubleValue(), KILO(METRE)));
356                 }
357                 Double batteryAvailableEnergy = car.getBatteryAvailableEnergy();
358                 if (batteryAvailableEnergy != null) {
359                     updateState(CHANNEL_BATTERY_AVAILABLE_ENERGY,
360                             new QuantityType<Energy>(batteryAvailableEnergy.doubleValue(), KILOWATT_HOUR));
361                 }
362                 Integer chargingRemainingTime = car.getChargingRemainingTime();
363                 if (chargingRemainingTime != null) {
364                     updateState(CHANNEL_CHARGING_REMAINING_TIME,
365                             new QuantityType<Time>(chargingRemainingTime.doubleValue(), MINUTE));
366                 }
367                 ZonedDateTime batteryStatusUpdated = car.getBatteryStatusUpdated();
368                 if (batteryStatusUpdated != null) {
369                     updateState(CHANNEL_BATTERY_STATUS_UPDATED, new DateTimeType(batteryStatusUpdated));
370                 }
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);
376             }
377         }
378     }
379
380     private void updateLockStatus(MyRenaultHttpSession httpSession) {
381         if (!car.isDisableLockStatus()) {
382             try {
383                 httpSession.getLockStatus(car);
384                 switch (car.getLockStatus()) {
385                     case LOCKED:
386                         updateState(CHANNEL_LOCKED, OnOffType.ON);
387                         break;
388                     case UNLOCKED:
389                         updateState(CHANNEL_LOCKED, OnOffType.OFF);
390                         break;
391                     default:
392                         updateState(CHANNEL_LOCKED, UnDefType.UNDEF);
393                         break;
394                 }
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);
402             }
403         }
404     }
405
406     private void reschedulePollingJob() {
407         ScheduledFuture<?> job = pollingJob;
408         if (job != null) {
409             job.cancel(true);
410         }
411         pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, 0, config.refreshInterval, TimeUnit.MINUTES);
412     }
413 }