]> git.basschouten.com Git - openhab-addons.git/blob
bfc76b719b5d47658f9c4b7c65be87b0be887c27
[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.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
127         switch (channelUID.getId()) {
128             case RenaultBindingConstants.CHANNEL_HVAC_TARGET_TEMPERATURE:
129                 if (!car.isDisableHvac()) {
130                     if (command instanceof RefreshType) {
131                         updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
132                                 new QuantityType<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
133                     } else if (command instanceof DecimalType) {
134                         car.setHvacTargetTemperature(((DecimalType) command).doubleValue());
135                         updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
136                                 new QuantityType<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
137                     } else if (command instanceof QuantityType) {
138                         @Nullable
139                         QuantityType<Temperature> celsius = ((QuantityType<Temperature>) command)
140                                 .toUnit(SIUnits.CELSIUS);
141                         if (celsius != null) {
142                             car.setHvacTargetTemperature(celsius.doubleValue());
143                         }
144                         updateState(CHANNEL_HVAC_TARGET_TEMPERATURE,
145                                 new QuantityType<Temperature>(car.getHvacTargetTemperature(), SIUnits.CELSIUS));
146                     }
147                 }
148                 break;
149             case RenaultBindingConstants.CHANNEL_HVAC_STATUS:
150                 if (!car.isDisableHvac()) {
151                     if (command instanceof RefreshType) {
152                         reschedulePollingJob();
153                     } else if (command instanceof StringType && command.toString().equals(Car.HVAC_STATUS_ON)) {
154                         // We can only trigger pre-conditioning of the car.
155                         final MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
156                         try {
157                             updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_PENDING));
158                             car.resetHVACStatus();
159                             httpSession.initSesssion(car);
160                             httpSession.actionHvacOn(car.getHvacTargetTemperature());
161                             ScheduledFuture<?> job = pollingJob;
162                             if (job != null) {
163                                 job.cancel(true);
164                             }
165                             pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, config.updateDelay,
166                                     config.refreshInterval * 60, TimeUnit.SECONDS);
167                         } catch (InterruptedException e) {
168                             logger.warn("Error My Renault Http Session.", e);
169                             Thread.currentThread().interrupt();
170                         } catch (RenaultException | RenaultForbiddenException | RenaultUpdateException
171                                 | RenaultActionException | RenaultNotImplementedException | ExecutionException
172                                 | TimeoutException e) {
173                             logger.warn("Error during action HVAC on.", e);
174                             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
175                         }
176                     }
177                 }
178                 break;
179             case RenaultBindingConstants.CHANNEL_CHARGING_MODE:
180                 if (command instanceof RefreshType) {
181                     reschedulePollingJob();
182                 } else if (command instanceof StringType) {
183                     try {
184                         ChargingMode newMode = ChargingMode.valueOf(command.toString());
185                         if (!ChargingMode.UNKNOWN.equals(newMode)) {
186                             MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
187                             try {
188                                 httpSession.initSesssion(car);
189                                 httpSession.actionChargeMode(newMode);
190                                 car.setChargeMode(newMode);
191                                 updateState(CHANNEL_CHARGING_MODE, new StringType(newMode.toString()));
192                             } catch (InterruptedException e) {
193                                 logger.warn("Error My Renault Http Session.", e);
194                                 Thread.currentThread().interrupt();
195                             } catch (RenaultException | RenaultForbiddenException | RenaultUpdateException
196                                     | RenaultActionException | RenaultNotImplementedException | ExecutionException
197                                     | TimeoutException e) {
198                                 logger.warn("Error during action set charge mode.", e);
199                                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
200                                         e.getMessage());
201                             }
202                         }
203                     } catch (IllegalArgumentException e) {
204                         logger.warn("Invalid ChargingMode {}.", command.toString());
205                         return;
206                     }
207                 }
208                 break;
209             case RenaultBindingConstants.CHANNEL_PAUSE:
210                 if (command instanceof RefreshType) {
211                     reschedulePollingJob();
212                 } else if (command instanceof OnOffType) {
213                     try {
214                         MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
215                         try {
216                             boolean pause = OnOffType.ON == command;
217                             httpSession.initSesssion(car);
218                             httpSession.actionPause(pause);
219                             car.setPauseMode(pause);
220                             updateState(CHANNEL_PAUSE, OnOffType.from(command.toString()));
221                         } catch (InterruptedException e) {
222                             logger.warn("Error My Renault Http Session.", e);
223                             Thread.currentThread().interrupt();
224                         } catch (RenaultForbiddenException | RenaultNotImplementedException | RenaultActionException
225                                 | RenaultException | RenaultUpdateException | ExecutionException | TimeoutException e) {
226                             logger.warn("Error during action set pause.", e);
227                             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
228                         }
229                     } catch (IllegalArgumentException e) {
230                         logger.warn("Invalid Pause Mode {}.", command.toString());
231                         return;
232                     }
233                 }
234                 break;
235             default:
236                 if (command instanceof RefreshType) {
237                     reschedulePollingJob();
238                 }
239                 break;
240         }
241     }
242
243     @Override
244     public void dispose() {
245         ScheduledFuture<?> job = pollingJob;
246         if (job != null) {
247             job.cancel(true);
248             pollingJob = null;
249         }
250         super.dispose();
251     }
252
253     private void getStatus() {
254         MyRenaultHttpSession httpSession = new MyRenaultHttpSession(this.config, httpClient);
255         try {
256             httpSession.initSesssion(car);
257             updateStatus(ThingStatus.ONLINE);
258         } catch (InterruptedException e) {
259             logger.warn("Error My Renault Http Session.", e);
260             Thread.currentThread().interrupt();
261         } catch (Exception e) {
262             logger.warn("Error My Renault Http Session.", e);
263             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
264         }
265         String imageURL = car.getImageURL();
266         if (imageURL != null && !imageURL.isEmpty()) {
267             updateState(CHANNEL_IMAGE, new StringType(imageURL));
268         }
269         updateHvacStatus(httpSession);
270         updateCockpit(httpSession);
271         updateLocation(httpSession);
272         updateBattery(httpSession);
273         updateLockStatus(httpSession);
274     }
275
276     private void updateHvacStatus(MyRenaultHttpSession httpSession) {
277         if (!car.isDisableHvac()) {
278             try {
279                 httpSession.getHvacStatus(car);
280                 Boolean hvacstatus = car.getHvacstatus();
281                 if (hvacstatus == null) {
282                     updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_PENDING));
283                 } else if (hvacstatus.booleanValue()) {
284                     updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_ON));
285                 } else {
286                     updateState(CHANNEL_HVAC_STATUS, new StringType(Car.HVAC_STATUS_OFF));
287                 }
288                 Double externalTemperature = car.getExternalTemperature();
289                 if (externalTemperature != null) {
290                     updateState(CHANNEL_EXTERNAL_TEMPERATURE,
291                             new QuantityType<Temperature>(externalTemperature.doubleValue(), SIUnits.CELSIUS));
292                 }
293             } catch (RenaultNotImplementedException e) {
294                 logger.warn("Disabling unsupported HVAC status update.");
295                 car.setDisableHvac(true);
296             } catch (RenaultForbiddenException | RenaultUpdateException | RenaultAPIGatewayException e) {
297                 logger.warn("Error updating HVAC status.", e);
298             }
299         }
300     }
301
302     private void updateLocation(MyRenaultHttpSession httpSession) {
303         if (!car.isDisableLocation()) {
304             try {
305                 httpSession.getLocation(car);
306                 Double latitude = car.getGpsLatitude();
307                 Double longitude = car.getGpsLongitude();
308                 if (latitude != null && longitude != null) {
309                     updateState(CHANNEL_LOCATION, new PointType(new DecimalType(latitude.doubleValue()),
310                             new DecimalType(longitude.doubleValue())));
311                 }
312                 ZonedDateTime locationUpdated = car.getLocationUpdated();
313                 if (locationUpdated != null) {
314                     updateState(CHANNEL_LOCATION_UPDATED, new DateTimeType(locationUpdated));
315                 }
316             } catch (RenaultNotImplementedException e) {
317                 logger.warn("Disabling unsupported location update.");
318                 car.setDisableLocation(true);
319             } catch (IllegalArgumentException | RenaultForbiddenException | RenaultUpdateException
320                     | RenaultAPIGatewayException e) {
321                 logger.warn("Error updating location.", e);
322             }
323         }
324     }
325
326     private void updateCockpit(MyRenaultHttpSession httpSession) {
327         if (!car.isDisableCockpit()) {
328             try {
329                 httpSession.getCockpit(car);
330                 Double odometer = car.getOdometer();
331                 if (odometer != null) {
332                     updateState(CHANNEL_ODOMETER, new QuantityType<Length>(odometer.doubleValue(), KILO(METRE)));
333                 }
334             } catch (RenaultNotImplementedException e) {
335                 logger.warn("Disabling unsupported cockpit status update.");
336                 car.setDisableCockpit(true);
337             } catch (RenaultForbiddenException | RenaultUpdateException | RenaultAPIGatewayException e) {
338                 logger.warn("Error updating cockpit status.", e);
339             }
340         }
341     }
342
343     private void updateBattery(MyRenaultHttpSession httpSession) {
344         if (!car.isDisableBattery()) {
345             try {
346                 httpSession.getBatteryStatus(car);
347                 updateState(CHANNEL_PLUG_STATUS, new StringType(car.getPlugStatus().name()));
348                 updateState(CHANNEL_CHARGING_STATUS, new StringType(car.getChargingStatus().name()));
349                 Double batteryLevel = car.getBatteryLevel();
350                 if (batteryLevel != null) {
351                     updateState(CHANNEL_BATTERY_LEVEL, new DecimalType(batteryLevel.doubleValue()));
352                 }
353                 Double estimatedRange = car.getEstimatedRange();
354                 if (estimatedRange != null) {
355                     updateState(CHANNEL_ESTIMATED_RANGE,
356                             new QuantityType<Length>(estimatedRange.doubleValue(), KILO(METRE)));
357                 }
358                 Double batteryAvailableEnergy = car.getBatteryAvailableEnergy();
359                 if (batteryAvailableEnergy != null) {
360                     updateState(CHANNEL_BATTERY_AVAILABLE_ENERGY,
361                             new QuantityType<Energy>(batteryAvailableEnergy.doubleValue(), KILOWATT_HOUR));
362                 }
363                 Integer chargingRemainingTime = car.getChargingRemainingTime();
364                 if (chargingRemainingTime != null) {
365                     updateState(CHANNEL_CHARGING_REMAINING_TIME,
366                             new QuantityType<Time>(chargingRemainingTime.doubleValue(), MINUTE));
367                 }
368                 ZonedDateTime batteryStatusUpdated = car.getBatteryStatusUpdated();
369                 if (batteryStatusUpdated != null) {
370                     updateState(CHANNEL_BATTERY_STATUS_UPDATED, new DateTimeType(batteryStatusUpdated));
371                 }
372             } catch (RenaultNotImplementedException e) {
373                 logger.warn("Disabling unsupported battery update.");
374                 car.setDisableBattery(true);
375             } catch (RenaultForbiddenException | RenaultUpdateException | RenaultAPIGatewayException e) {
376                 logger.warn("Error updating battery status.", e);
377             }
378         }
379     }
380
381     private void updateLockStatus(MyRenaultHttpSession httpSession) {
382         if (!car.isDisableLockStatus()) {
383             try {
384                 httpSession.getLockStatus(car);
385                 switch (car.getLockStatus()) {
386                     case LOCKED:
387                         updateState(CHANNEL_LOCKED, OnOffType.ON);
388                         break;
389                     case UNLOCKED:
390                         updateState(CHANNEL_LOCKED, OnOffType.OFF);
391                         break;
392                     default:
393                         updateState(CHANNEL_LOCKED, UnDefType.UNDEF);
394                         break;
395                 }
396             } catch (RenaultNotImplementedException | RenaultAPIGatewayException e) {
397                 // If not supported API returns a Bad Gateway for this call.
398                 updateState(CHANNEL_LOCKED, UnDefType.UNDEF);
399                 logger.warn("Disabling unsupported lock status update.");
400                 car.setDisableLockStatus(true);
401             } catch (RenaultForbiddenException | RenaultUpdateException e) {
402                 logger.warn("Error updating lock status.", e);
403             }
404         }
405     }
406
407     private void reschedulePollingJob() {
408         ScheduledFuture<?> job = pollingJob;
409         if (job != null) {
410             job.cancel(true);
411         }
412         pollingJob = scheduler.scheduleWithFixedDelay(this::getStatus, 0, config.refreshInterval, TimeUnit.MINUTES);
413     }
414 }