]> git.basschouten.com Git - openhab-addons.git/blob
7484c173feb34189412a8f88fa8c9a89cafd4463
[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             default:
209                 if (command instanceof RefreshType) {
210                     reschedulePollingJob();
211                 }
212                 break;
213         }
214     }
215
216     @Override
217     public void dispose() {
218         ScheduledFuture<?> job = pollingJob;
219         if (job != null) {
220             job.cancel(true);
221             pollingJob = null;
222         }
223         super.dispose();
224     }
225
226     private void getStatus() {
227         MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
228         try {
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());
237         }
238         String imageURL = car.getImageURL();
239         if (imageURL != null && !imageURL.isEmpty()) {
240             updateState(CHANNEL_IMAGE, new StringType(imageURL));
241         }
242         updateHvacStatus(httpSession);
243         updateCockpit(httpSession);
244         updateLocation(httpSession);
245         updateBattery(httpSession);
246         updateLockStatus(httpSession);
247     }
248
249     private void updateHvacStatus(MyRenaultHttpSession httpSession) {
250         if (!car.isDisableHvac()) {
251             try {
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));
258                 } else {
259                     updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_OFF));
260                 }
261                 Double externalTemperature = car.getExternalTemperature();
262                 if (externalTemperature != null) {
263                     updateState(CHANNEL_EXTERNAL_TEMPERATURE,
264                             new QuantityType<Temperature>(externalTemperature.doubleValue(), SIUnits.CELSIUS));
265                 }
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);
271             }
272         }
273     }
274
275     private void updateLocation(MyRenaultHttpSession httpSession) {
276         if (!car.isDisableLocation()) {
277             try {
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())));
284                 }
285                 ZonedDateTime locationUpdated = car.getLocationUpdated();
286                 if (locationUpdated != null) {
287                     updateState(CHANNEL_LOCATION_UPDATED, new DateTimeType(locationUpdated));
288                 }
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);
294             }
295         }
296     }
297
298     private void updateCockpit(MyRenaultHttpSession httpSession) {
299         if (!car.isDisableCockpit()) {
300             try {
301                 httpSession.getCockpit(car);
302                 Double odometer = car.getOdometer();
303                 if (odometer != null) {
304                     updateState(CHANNEL_ODOMETER, new QuantityType<Length>(odometer.doubleValue(), KILO(METRE)));
305                 }
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);
311             }
312         }
313     }
314
315     private void updateBattery(MyRenaultHttpSession httpSession) {
316         if (!car.isDisableBattery()) {
317             try {
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()));
324                 }
325                 Double estimatedRange = car.getEstimatedRange();
326                 if (estimatedRange != null) {
327                     updateState(CHANNEL_ESTIMATED_RANGE,
328                             new QuantityType<Length>(estimatedRange.doubleValue(), KILO(METRE)));
329                 }
330                 Double batteryAvailableEnergy = car.getBatteryAvailableEnergy();
331                 if (batteryAvailableEnergy != null) {
332                     updateState(CHANNEL_BATTERY_AVAILABLE_ENERGY,
333                             new QuantityType<Energy>(batteryAvailableEnergy.doubleValue(), KILOWATT_HOUR));
334                 }
335                 Integer chargingRemainingTime = car.getChargingRemainingTime();
336                 if (chargingRemainingTime != null) {
337                     updateState(CHANNEL_CHARGING_REMAINING_TIME,
338                             new QuantityType<Time>(chargingRemainingTime.doubleValue(), MINUTE));
339                 }
340                 ZonedDateTime batteryStatusUpdated = car.getBatteryStatusUpdated();
341                 if (batteryStatusUpdated != null) {
342                     updateState(CHANNEL_BATTERY_STATUS_UPDATED, new DateTimeType(batteryStatusUpdated));
343                 }
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);
349             }
350         }
351     }
352
353     private void updateLockStatus(MyRenaultHttpSession httpSession) {
354         if (!car.isDisableLockStatus()) {
355             try {
356                 httpSession.getLockStatus(car);
357                 switch (car.getLockStatus()) {
358                     case LOCKED:
359                         updateState(CHANNEL_LOCKED, OnOffType.ON);
360                         break;
361                     case UNLOCKED:
362                         updateState(CHANNEL_LOCKED, OnOffType.OFF);
363                         break;
364                     default:
365                         updateState(CHANNEL_LOCKED, UnDefType.UNDEF);
366                         break;
367                 }
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);
374             }
375         }
376     }
377
378     private void reschedulePollingJob() {
379         ScheduledFuture<?> job = pollingJob;
380         if (job != null) {
381             job.cancel(true);
382         }
383         pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, 0, config.refreshInterval, TimeUnit.MINUTES);
384     }
385 }