]> git.basschouten.com Git - openhab-addons.git/blob
5f2420a70bd22a1eb7dcfe923d0b907a5d25778e
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.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;
62
63 /**
64  * The {@link RenaultHandler} is responsible for handling commands, which are
65  * sent to one of the channels.
66  *
67  * @author Doug Culnane - Initial contribution
68  */
69 @NonNullByDefault
70 public class RenaultHandler extends BaseThingHandler {
71
72     private final Logger logger = LoggerFactory.getLogger(RenaultHandler.class);
73
74     private RenaultConfiguration config = new RenaultConfiguration();
75
76     private @Nullable ScheduledFuture<?> pollingJob;
77
78     private HttpClient httpClient;
79
80     private Car car;
81
82     public RenaultHandler(Thing thing, HttpClient httpClient) {
83         super(thing);
84         this.car = new Car();
85         this.httpClient = httpClient;
86     }
87
88     @Override
89     public void initialize() {
90         // reset the car on initialize
91         this.car = new Car();
92         this.config = getConfigAs(RenaultConfiguration.class);
93
94         // Validate configuration
95         if (this.config.myRenaultUsername.isBlank()) {
96             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "MyRenault Username is empty!");
97             return;
98         }
99         if (this.config.myRenaultPassword.isBlank()) {
100             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "MyRenault Password is empty!");
101             return;
102         }
103         if (this.config.locale.isBlank()) {
104             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Location is empty!");
105             return;
106         }
107         if (this.config.vin.isBlank()) {
108             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "VIN is empty!");
109             return;
110         }
111         if (this.config.refreshInterval < 1) {
112             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
113                     "The refresh interval mush to be larger than 1");
114             return;
115         }
116         updateStatus(ThingStatus.UNKNOWN);
117         updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
118                 new QuantityType<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
119
120         reschedulePollingJob();
121     }
122
123     @Override
124     public void handleCommand(ChannelUID channelUID, Command command) {
125
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) {
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 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 e) {
319                 logger.warn("Error updating location.", e);
320             }
321         }
322     }
323
324     private void updateCockpit(MyRenaultHttpSession httpSession) {
325         if (!car.isDisableCockpit()) {
326             try {
327                 httpSession.getCockpit(car);
328                 Double odometer = car.getOdometer();
329                 if (odometer != null) {
330                     updateState(CHANNEL_ODOMETER, new QuantityType<Length>(odometer.doubleValue(), KILO(METRE)));
331                 }
332             } catch (RenaultNotImplementedException e) {
333                 logger.warn("Disabling unsupported cockpit status update.");
334                 car.setDisableCockpit(true);
335             } catch (RenaultForbiddenException | RenaultUpdateException e) {
336                 logger.warn("Error updating cockpit status.", e);
337             }
338         }
339     }
340
341     private void updateBattery(MyRenaultHttpSession httpSession) {
342         if (!car.isDisableBattery()) {
343             try {
344                 httpSession.getBatteryStatus(car);
345                 updateState(CHANNEL_PLUG_STATUS, new StringType(car.getPlugStatus().name()));
346                 updateState(CHANNEL_CHARGING_STATUS, new StringType(car.getChargingStatus().name()));
347                 Double batteryLevel = car.getBatteryLevel();
348                 if (batteryLevel != null) {
349                     updateState(CHANNEL_BATTERY_LEVEL, new DecimalType(batteryLevel.doubleValue()));
350                 }
351                 Double estimatedRange = car.getEstimatedRange();
352                 if (estimatedRange != null) {
353                     updateState(CHANNEL_ESTIMATED_RANGE,
354                             new QuantityType<Length>(estimatedRange.doubleValue(), KILO(METRE)));
355                 }
356                 Double batteryAvailableEnergy = car.getBatteryAvailableEnergy();
357                 if (batteryAvailableEnergy != null) {
358                     updateState(CHANNEL_BATTERY_AVAILABLE_ENERGY,
359                             new QuantityType<Energy>(batteryAvailableEnergy.doubleValue(), KILOWATT_HOUR));
360                 }
361                 Integer chargingRemainingTime = car.getChargingRemainingTime();
362                 if (chargingRemainingTime != null) {
363                     updateState(CHANNEL_CHARGING_REMAINING_TIME,
364                             new QuantityType<Time>(chargingRemainingTime.doubleValue(), MINUTE));
365                 }
366                 ZonedDateTime batteryStatusUpdated = car.getBatteryStatusUpdated();
367                 if (batteryStatusUpdated != null) {
368                     updateState(CHANNEL_BATTERY_STATUS_UPDATED, new DateTimeType(batteryStatusUpdated));
369                 }
370             } catch (RenaultNotImplementedException e) {
371                 logger.warn("Disabling unsupported battery update.");
372                 car.setDisableBattery(true);
373             } catch (RenaultForbiddenException | RenaultUpdateException e) {
374                 logger.warn("Error updating battery status.", e);
375             }
376         }
377     }
378
379     private void updateLockStatus(MyRenaultHttpSession httpSession) {
380         if (!car.isDisableLockStatus()) {
381             try {
382                 httpSession.getLockStatus(car);
383                 switch (car.getLockStatus()) {
384                     case LOCKED:
385                         updateState(CHANNEL_LOCKED, OnOffType.ON);
386                         break;
387                     case UNLOCKED:
388                         updateState(CHANNEL_LOCKED, OnOffType.OFF);
389                         break;
390                     default:
391                         updateState(CHANNEL_LOCKED, UnDefType.UNDEF);
392                         break;
393                 }
394             } catch (RenaultNotImplementedException e) {
395                 updateState(CHANNEL_LOCKED, UnDefType.UNDEF);
396                 logger.warn("Disabling unsupported lock status update.");
397                 car.setDisableLockStatus(true);
398             } catch (RenaultForbiddenException | RenaultUpdateException e) {
399                 logger.warn("Error updating lock status.", e);
400             }
401         }
402     }
403
404     private void reschedulePollingJob() {
405         ScheduledFuture<?> job = pollingJob;
406         if (job != null) {
407             job.cancel(true);
408         }
409         pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, 0, config.refreshInterval, TimeUnit.MINUTES);
410     }
411 }