]> git.basschouten.com Git - openhab-addons.git/blob
30a1b706c8552375fbb19ce06f46638218b572c9
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2022 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.groupepsa.internal.things;
14
15 import static org.openhab.binding.groupepsa.internal.GroupePSABindingConstants.*;
16
17 import java.math.BigDecimal;
18 import java.text.MessageFormat;
19 import java.time.ZonedDateTime;
20 import java.util.List;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
23 import java.util.function.Function;
24
25 import javax.measure.Quantity;
26 import javax.measure.Unit;
27
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.groupepsa.internal.bridge.GroupePSABridgeHandler;
31 import org.openhab.binding.groupepsa.internal.rest.api.dto.Air;
32 import org.openhab.binding.groupepsa.internal.rest.api.dto.AirConditioning;
33 import org.openhab.binding.groupepsa.internal.rest.api.dto.Battery;
34 import org.openhab.binding.groupepsa.internal.rest.api.dto.BatteryStatus;
35 import org.openhab.binding.groupepsa.internal.rest.api.dto.Charging;
36 import org.openhab.binding.groupepsa.internal.rest.api.dto.DoorsState;
37 import org.openhab.binding.groupepsa.internal.rest.api.dto.Energy;
38 import org.openhab.binding.groupepsa.internal.rest.api.dto.Environment;
39 import org.openhab.binding.groupepsa.internal.rest.api.dto.Health;
40 import org.openhab.binding.groupepsa.internal.rest.api.dto.Ignition;
41 import org.openhab.binding.groupepsa.internal.rest.api.dto.Kinetic;
42 import org.openhab.binding.groupepsa.internal.rest.api.dto.Luminosity;
43 import org.openhab.binding.groupepsa.internal.rest.api.dto.Odometer;
44 import org.openhab.binding.groupepsa.internal.rest.api.dto.Opening;
45 import org.openhab.binding.groupepsa.internal.rest.api.dto.Position;
46 import org.openhab.binding.groupepsa.internal.rest.api.dto.Preconditionning;
47 import org.openhab.binding.groupepsa.internal.rest.api.dto.Privacy;
48 import org.openhab.binding.groupepsa.internal.rest.api.dto.Properties;
49 import org.openhab.binding.groupepsa.internal.rest.api.dto.Safety;
50 import org.openhab.binding.groupepsa.internal.rest.api.dto.Service;
51 import org.openhab.binding.groupepsa.internal.rest.api.dto.VehicleStatus;
52 import org.openhab.binding.groupepsa.internal.rest.exceptions.GroupePSACommunicationException;
53 import org.openhab.core.library.types.DateTimeType;
54 import org.openhab.core.library.types.DecimalType;
55 import org.openhab.core.library.types.OpenClosedType;
56 import org.openhab.core.library.types.PointType;
57 import org.openhab.core.library.types.QuantityType;
58 import org.openhab.core.library.types.StringType;
59 import org.openhab.core.library.unit.MetricPrefix;
60 import org.openhab.core.library.unit.SIUnits;
61 import org.openhab.core.library.unit.Units;
62 import org.openhab.core.thing.Bridge;
63 import org.openhab.core.thing.Channel;
64 import org.openhab.core.thing.ChannelUID;
65 import org.openhab.core.thing.Thing;
66 import org.openhab.core.thing.ThingStatus;
67 import org.openhab.core.thing.ThingStatusDetail;
68 import org.openhab.core.thing.binding.BaseThingHandler;
69 import org.openhab.core.thing.binding.ThingHandler;
70 import org.openhab.core.thing.binding.ThingHandlerCallback;
71 import org.openhab.core.thing.binding.builder.ThingBuilder;
72 import org.openhab.core.thing.type.ChannelTypeUID;
73 import org.openhab.core.types.Command;
74 import org.openhab.core.types.RefreshType;
75 import org.openhab.core.types.UnDefType;
76 import org.slf4j.Logger;
77 import org.slf4j.LoggerFactory;
78
79 import com.github.filosganga.geogson.model.Geometry;
80 import com.github.filosganga.geogson.model.positions.SinglePosition;
81
82 /**
83  * The {@link GroupePSAHandler} is responsible for handling commands, which are
84  * sent to one of the channels.
85  *
86  * @author Arjan Mels - Initial contribution
87  */
88 @NonNullByDefault
89 public class GroupePSAHandler extends BaseThingHandler {
90     private static final long DEFAULT_POLLING_INTERVAL_M = TimeUnit.MINUTES.toMinutes(1);
91     private static final long DEFAULT_ONLINE_INTERVAL_M = TimeUnit.MINUTES.toMinutes(60);
92
93     private final Logger logger = LoggerFactory.getLogger(GroupePSAHandler.class);
94
95     private @Nullable String id = null;
96     private long lastQueryTimeNs = 0L;
97
98     private @Nullable ScheduledFuture<?> groupepsaPollingJob;
99     private long maxQueryFrequencyNanos = TimeUnit.MINUTES.toNanos(1);
100     private long onlineIntervalM;
101
102     public GroupePSAHandler(Thing thing) {
103         super(thing);
104     }
105
106     @Override
107     protected @Nullable Bridge getBridge() {
108         return super.getBridge();
109     }
110
111     private void pollStatus() {
112         Bridge bridge = getBridge();
113         if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
114             updateGroupePSAState();
115         } else {
116             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
117         }
118     };
119
120     @Override
121     public void handleCommand(ChannelUID channelUID, Command command) {
122         if (command instanceof RefreshType) {
123             refreshChannels(channelUID);
124         }
125     }
126
127     private void refreshChannels(ChannelUID channelUID) {
128         updateGroupePSAState();
129     }
130
131     @Override
132     public void initialize() {
133         if (getBridgeHandler() != null) {
134             GroupePSAConfiguration currentConfig = getConfigAs(GroupePSAConfiguration.class);
135             final String id = currentConfig.getId();
136             final Integer pollingIntervalM = currentConfig.getPollingInterval();
137             final Integer onlineIntervalM = currentConfig.getOnlineInterval();
138
139             if (id == null) {
140                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
141                         "@text/conf-error-no-vehicle-id");
142             } else if (pollingIntervalM != null && pollingIntervalM < 1) {
143                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
144                         "@text/conf-error-invalid-polling-interval");
145             } else if (onlineIntervalM != null && onlineIntervalM < 1) {
146                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
147                         "@text/conf-error-invalid-online-interval");
148             } else {
149                 this.id = id;
150                 this.onlineIntervalM = onlineIntervalM != null ? onlineIntervalM : DEFAULT_ONLINE_INTERVAL_M;
151                 startGroupePSAPolling(pollingIntervalM);
152             }
153
154         } else {
155             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
156         }
157     }
158
159     @Nullable
160     public GroupePSABridgeHandler getBridgeHandler() {
161         Bridge bridge = getBridge();
162         if (bridge != null) {
163             ThingHandler handler = bridge.getHandler();
164             if (handler instanceof GroupePSABridgeHandler) {
165                 return (GroupePSABridgeHandler) handler;
166             }
167         }
168         return null;
169     }
170
171     @Override
172     public void dispose() {
173         stopGroupePSAPolling();
174         id = null;
175     }
176
177     private void startGroupePSAPolling(@Nullable Integer pollingIntervalM) {
178         if (groupepsaPollingJob == null) {
179             final long pollingIntervalToUse = pollingIntervalM == null ? DEFAULT_POLLING_INTERVAL_M : pollingIntervalM;
180             groupepsaPollingJob = scheduler.scheduleWithFixedDelay(() -> pollStatus(), 1, pollingIntervalToUse * 60,
181                     TimeUnit.SECONDS);
182         }
183     }
184
185     private void stopGroupePSAPolling() {
186         ScheduledFuture<?> job = groupepsaPollingJob;
187         if (job != null) {
188             job.cancel(true);
189             groupepsaPollingJob = null;
190         }
191     }
192
193     private boolean isValidResult(VehicleStatus vehicle) {
194         return vehicle.getUpdatedAt() != null;
195     }
196
197     private boolean isConnected(VehicleStatus vehicle) {
198         ZonedDateTime updatedAt = vehicle.getUpdatedAt();
199         if (updatedAt == null) {
200             return false;
201         }
202
203         return updatedAt.isAfter(ZonedDateTime.now().minusMinutes(onlineIntervalM));
204     }
205
206     private synchronized void updateGroupePSAState() {
207         if (System.nanoTime() - lastQueryTimeNs <= maxQueryFrequencyNanos) {
208             return;
209         }
210
211         lastQueryTimeNs = System.nanoTime();
212
213         String id = this.id;
214         if (id == null) {
215             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/conf-error-no-vehicle-id");
216             return;
217         }
218
219         GroupePSABridgeHandler groupepsaBridge = getBridgeHandler();
220         if (groupepsaBridge == null) {
221             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/conf-error-no-bridge");
222             return;
223         }
224
225         try {
226             VehicleStatus vehicle = groupepsaBridge.getVehicleStatus(id);
227
228             if (vehicle != null && isValidResult(vehicle)) {
229                 logger.trace("Vehicle: {}", vehicle.toString());
230
231                 logger.debug("Update vehicle state now: {}, lastupdate: {}", ZonedDateTime.now(),
232                         vehicle.getUpdatedAt());
233
234                 updateChannelState(vehicle);
235
236                 if (isConnected(vehicle)) {
237                     updateStatus(ThingStatus.ONLINE);
238                 } else {
239                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
240                             "@text/comm-error-vehicle-not-connected-to-cloud");
241                 }
242             } else {
243                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
244                         "@text/comm-error-query-vehicle-failed");
245             }
246         } catch (GroupePSACommunicationException e) {
247             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
248                     MessageFormat.format("@text/comm-error-query-vehicle-failed", e.getMessage()));
249         }
250     }
251
252     private void updateChannelState(VehicleStatus vehicle) {
253         final DoorsState doorsState = vehicle.getDoorsState();
254         if (doorsState != null) {
255             buildDoorChannels(doorsState);
256
257             List<Opening> openings = doorsState.getOpening();
258             if (openings != null) {
259                 for (Opening opening : openings) {
260                     String id = opening.getIdentifier();
261                     if (id != null) {
262                         ChannelUID channelUID = new ChannelUID(getThing().getUID(), CHANNEL_GROUP_DOORS,
263                                 id.toLowerCase());
264                         updateState(channelUID, "open".equalsIgnoreCase(opening.getState()) ? OpenClosedType.OPEN
265                                 : OpenClosedType.CLOSED);
266                     }
267                 }
268             }
269
270             List<String> lockedState = doorsState.getLockedState();
271             updateState(CHANNEL_DOORS_LOCK, lockedState, x -> x.get(0));
272         } else {
273             updateState(CHANNEL_DOORS_LOCK, UnDefType.UNDEF);
274         }
275
276         updateState(CHANNEL_BATTERY_CURRENT, vehicle.getBattery(), Battery::getCurrent, Units.AMPERE);
277         updateState(CHANNEL_BATTERY_VOLTAGE, vehicle.getBattery(), Battery::getVoltage, Units.VOLT);
278
279         updateState(CHANNEL_ENVIRONMENT_TEMPERATURE, vehicle.getEnvironment(), Environment::getAir, Air::getTemp,
280                 SIUnits.CELSIUS);
281         updateStateBoolean(CHANNEL_ENVIRONMENT_DAYTIME, vehicle.getEnvironment(), Environment::getLuminosity,
282                 Luminosity::isDay);
283
284         updateState(CHANNEL_MOTION_IGNITION, vehicle.getIgnition(), Ignition::getType);
285
286         updateStateBoolean(CHANNEL_MOTION_MOVING, vehicle.getKinetic(), Kinetic::isMoving);
287         updateState(CHANNEL_MOTION_ACCELERATION, vehicle.getKinetic(), Kinetic::getAcceleration,
288                 Units.METRE_PER_SQUARE_SECOND);
289         updateState(CHANNEL_MOTION_SPEED, vehicle.getKinetic(), Kinetic::getSpeed, SIUnits.KILOMETRE_PER_HOUR);
290
291         updateState(CHANNEL_MOTION_MILEAGE, vehicle.getOdometer(), Odometer::getMileage,
292                 MetricPrefix.KILO(SIUnits.METRE));
293
294         Position lastPosition = vehicle.getLastPosition();
295         if (lastPosition != null) {
296             Geometry<SinglePosition> geometry = lastPosition.getGeometry();
297             if (geometry != null) {
298                 SinglePosition position = (SinglePosition) geometry.positions();
299                 if (Double.isFinite(position.alt())) {
300                     updateState(CHANNEL_POSITION_POSITION, new PointType(new DecimalType(position.lat()),
301                             new DecimalType(position.lon()), new DecimalType(position.alt())));
302                 } else {
303                     updateState(CHANNEL_POSITION_POSITION,
304                             new PointType(new DecimalType(position.lat()), new DecimalType(position.lon())));
305                 }
306             } else {
307                 updateState(CHANNEL_POSITION_POSITION, UnDefType.UNDEF);
308             }
309             updateState(CHANNEL_POSITION_HEADING, lastPosition.getProperties(), Properties::getHeading,
310                     Units.DEGREE_ANGLE);
311             updateState(CHANNEL_POSITION_TYPE, lastPosition.getProperties(), Properties::getType);
312             updateState(CHANNEL_POSITION_SIGNALSTRENGTH, lastPosition.getProperties(), Properties::getSignalQuality,
313                     Units.PERCENT);
314         }
315
316         updateState(CHANNEL_VARIOUS_LAST_UPDATED, vehicle.getUpdatedAt());
317         updateState(CHANNEL_VARIOUS_PRIVACY, vehicle.getPrivacy(), Privacy::getState);
318         updateState(CHANNEL_VARIOUS_BELT, vehicle.getSafety(), Safety::getBeltWarning);
319         updateState(CHANNEL_VARIOUS_EMERGENCY, vehicle.getSafety(), Safety::getECallTriggeringRequest);
320         updateState(CHANNEL_VARIOUS_SERVICE, vehicle.getService(), Service::getType);
321         updateState(CHANNEL_VARIOUS_PRECONDITINING, vehicle.getPreconditionning(), Preconditionning::getAirConditioning,
322                 AirConditioning::getStatus);
323         updateState(CHANNEL_VARIOUS_PRECONDITINING_FAILURE, vehicle.getPreconditionning(),
324                 Preconditionning::getAirConditioning, AirConditioning::getFailureCause);
325
326         List<Energy> energies = vehicle.getEnergy();
327         if (energies != null) {
328             for (Energy energy : energies) {
329                 if ("Fuel".equalsIgnoreCase(energy.getType())) {
330                     updateState(CHANNEL_FUEL_AUTONOMY, energy, Energy::getAutonomy, MetricPrefix.KILO(SIUnits.METRE));
331                     updateState(CHANNEL_FUEL_CONSUMPTION, energy, Energy::getConsumption,
332                             Units.LITRE.divide(MetricPrefix.KILO(SIUnits.METRE)));
333                     updateState(CHANNEL_FUEL_LEVEL, energy, Energy::getLevel, Units.PERCENT);
334                 } else if ("Electric".equalsIgnoreCase(energy.getType())) {
335                     updateState(CHANNEL_ELECTRIC_AUTONOMY, energy, Energy::getAutonomy,
336                             MetricPrefix.KILO(SIUnits.METRE));
337                     updateState(CHANNEL_ELECTRIC_RESIDUAL, energy, Energy::getResidual, Units.KILOWATT_HOUR);
338                     updateState(CHANNEL_ELECTRIC_LEVEL, energy, Energy::getLevel, Units.PERCENT);
339
340                     updateState(CHANNEL_ELECTRIC_BATTERY_CAPACITY, energy, Energy::getBattery,
341                             BatteryStatus::getCapacity, Units.KILOWATT_HOUR);
342                     updateState(CHANNEL_ELECTRIC_BATTERY_HEALTH_CAPACITY, energy, Energy::getBattery,
343                             BatteryStatus::getHealth, Health::getCapacity, Units.PERCENT);
344                     updateState(CHANNEL_ELECTRIC_BATTERY_HEALTH_RESISTANCE, energy, Energy::getBattery,
345                             BatteryStatus::getHealth, Health::getResistance, Units.PERCENT);
346
347                     updateState(CHANNEL_ELECTRIC_CHARGING_STATUS, energy, Energy::getCharging, Charging::getStatus);
348                     updateState(CHANNEL_ELECTRIC_CHARGING_MODE, energy, Energy::getCharging, Charging::getChargingMode);
349                     updateStateBoolean(CHANNEL_ELECTRIC_CHARGING_PLUGGED, energy, Energy::getCharging,
350                             Charging::isPlugged);
351                     updateState(CHANNEL_ELECTRIC_CHARGING_RATE, energy, Energy::getCharging, Charging::getChargingRate,
352                             SIUnits.KILOMETRE_PER_HOUR);
353
354                     updateState(CHANNEL_ELECTRIC_CHARGING_REMAININGTIME, energy, Energy::getCharging,
355                             Charging::getRemainingTime, x -> new BigDecimal(x.getSeconds()), Units.SECOND);
356                     updateState(CHANNEL_ELECTRIC_CHARGING_NEXTDELAYEDTIME, energy, Energy::getCharging,
357                             Charging::getNextDelayedTime, x -> new BigDecimal(x.getSeconds()), Units.SECOND);
358
359                 }
360             }
361         }
362     }
363
364     void buildDoorChannels(final DoorsState doorsState) {
365         ThingHandlerCallback callback = getCallback();
366         if (callback == null) {
367             return;
368         }
369
370         ThingBuilder thingBuilder = editThing();
371         List<Channel> channels = getThing().getChannelsOfGroup(CHANNEL_GROUP_DOORS);
372         thingBuilder.withoutChannels(channels);
373
374         ChannelUID channelUID = new ChannelUID(getThing().getUID(), CHANNEL_DOORS_LOCK);
375         ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_DOORLOCK);
376         thingBuilder.withChannel(callback.createChannelBuilder(channelUID, channelTypeUID).build());
377
378         List<Opening> openings = doorsState.getOpening();
379         if (openings != null) {
380             for (Opening opening : openings) {
381                 String id = opening.getIdentifier();
382                 if (id != null) {
383                     channelUID = new ChannelUID(getThing().getUID(), CHANNEL_GROUP_DOORS, id.toLowerCase());
384                     channelTypeUID = new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_DOOROPEN);
385                     thingBuilder.withChannel(callback.createChannelBuilder(channelUID, channelTypeUID).build());
386                 }
387             }
388         }
389
390         updateThing(thingBuilder.build());
391     }
392
393     // Various update helper functions
394
395     protected <T extends Quantity<T>> void updateState(String channelID, @Nullable BigDecimal number, Unit<T> unit) {
396         if (number != null) {
397             updateState(channelID, new QuantityType<T>(number, unit));
398         } else {
399             updateState(channelID, UnDefType.UNDEF);
400         }
401     }
402
403     protected <T1, T2 extends Quantity<T2>> void updateState(String channelID, final @Nullable T1 object,
404             Function<? super T1, @Nullable BigDecimal> mapper, Unit<T2> unit) {
405         updateState(channelID, object != null ? mapper.apply(object) : null, unit);
406     }
407
408     protected <T1, T2, T3 extends Quantity<T3>> void updateState(String channelID, final @Nullable T1 object1,
409             Function<? super T1, @Nullable T2> mapper1, Function<? super T2, @Nullable BigDecimal> mapper2,
410             Unit<T3> unit) {
411         final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
412         updateState(channelID, object2 != null ? mapper2.apply(object2) : null, unit);
413     }
414
415     protected <T1, T2, T3, T4 extends Quantity<T4>> void updateState(String channelID, final @Nullable T1 object1,
416             Function<? super T1, @Nullable T2> mapper1, Function<? super T2, @Nullable T3> mapper2,
417             Function<? super T3, @Nullable BigDecimal> mapper3, Unit<T4> unit) {
418         final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
419         final @Nullable T3 object3 = object2 != null ? mapper2.apply(object2) : null;
420         updateState(channelID, object3 != null ? mapper3.apply(object3) : null, unit);
421     }
422
423     protected void updateState(String channelID, @Nullable ZonedDateTime date) {
424         if (date != null) {
425             updateState(channelID, new DateTimeType(date));
426         } else {
427             updateState(channelID, UnDefType.UNDEF);
428         }
429     }
430
431     protected <T1> void updateStateDate(String channelID, @Nullable T1 object,
432             Function<? super T1, @Nullable ZonedDateTime> mapper) {
433         updateState(channelID, object != null ? mapper.apply(object) : null);
434     }
435
436     protected <T1, T2> void updateStateDate(String channelID, @Nullable T1 object1,
437             Function<? super T1, @Nullable T2> mapper1, Function<? super T2, @Nullable ZonedDateTime> mapper2) {
438         final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
439         updateState(channelID, object2 != null ? mapper2.apply(object2) : null);
440     }
441
442     protected void updateState(String channelID, @Nullable String text) {
443         if (text != null) {
444             updateState(channelID, new StringType(text));
445         } else {
446             updateState(channelID, UnDefType.UNDEF);
447         }
448     }
449
450     protected <T1> void updateState(String channelID, @Nullable T1 object,
451             Function<? super T1, @Nullable String> mapper) {
452         updateState(channelID, object != null ? mapper.apply(object) : null);
453     }
454
455     protected <T1, T2> void updateState(String channelID, @Nullable T1 object1,
456             Function<? super T1, @Nullable T2> mapper1, Function<? super T2, @Nullable String> mapper2) {
457         final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
458         updateState(channelID, object2 != null ? mapper2.apply(object2) : null);
459     }
460
461     protected void updateState(String channelID, @Nullable Boolean value) {
462         if (value != null) {
463             updateState(channelID, value ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
464         } else {
465             updateState(channelID, UnDefType.UNDEF);
466         }
467     }
468
469     protected <T1> void updateStateBoolean(String channelID, @Nullable T1 object,
470             Function<? super T1, @Nullable Boolean> mapper) {
471         updateState(channelID, object != null ? mapper.apply(object) : null);
472     }
473
474     protected <T1, T2> void updateStateBoolean(String channelID, final @Nullable T1 object1,
475             Function<? super T1, @Nullable T2> mapper1, Function<? super T2, @Nullable Boolean> mapper2) {
476         final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
477         updateState(channelID, object2 != null ? mapper2.apply(object2) : null);
478     }
479 }