2 * Copyright (c) 2010-2022 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.groupepsa.internal.things;
15 import static org.openhab.binding.groupepsa.internal.GroupePSABindingConstants.*;
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;
25 import javax.measure.Quantity;
26 import javax.measure.Unit;
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;
79 import com.github.filosganga.geogson.model.Geometry;
80 import com.github.filosganga.geogson.model.positions.SinglePosition;
83 * The {@link GroupePSAHandler} is responsible for handling commands, which are
84 * sent to one of the channels.
86 * @author Arjan Mels - Initial contribution
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);
93 private final Logger logger = LoggerFactory.getLogger(GroupePSAHandler.class);
95 private @Nullable String id = null;
96 private long lastQueryTimeNs = 0L;
98 private @Nullable ScheduledFuture<?> groupepsaPollingJob;
99 private long maxQueryFrequencyNanos = TimeUnit.MINUTES.toNanos(1);
100 private long onlineIntervalM;
102 public GroupePSAHandler(Thing thing) {
107 protected @Nullable Bridge getBridge() {
108 return super.getBridge();
111 private void pollStatus() {
112 Bridge bridge = getBridge();
113 if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
114 updateGroupePSAState();
116 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
121 public void handleCommand(ChannelUID channelUID, Command command) {
122 if (command instanceof RefreshType) {
123 refreshChannels(channelUID);
127 private void refreshChannels(ChannelUID channelUID) {
128 updateGroupePSAState();
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();
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");
150 this.onlineIntervalM = onlineIntervalM != null ? onlineIntervalM : DEFAULT_ONLINE_INTERVAL_M;
151 startGroupePSAPolling(pollingIntervalM);
155 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
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;
172 public void dispose() {
173 stopGroupePSAPolling();
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,
185 private void stopGroupePSAPolling() {
186 ScheduledFuture<?> job = groupepsaPollingJob;
189 groupepsaPollingJob = null;
193 private boolean isValidResult(VehicleStatus vehicle) {
194 return vehicle.getUpdatedAt() != null;
197 private boolean isConnected(VehicleStatus vehicle) {
198 ZonedDateTime updatedAt = vehicle.getUpdatedAt();
199 if (updatedAt == null) {
203 return updatedAt.isAfter(ZonedDateTime.now().minusMinutes(onlineIntervalM));
206 private synchronized void updateGroupePSAState() {
207 if (System.nanoTime() - lastQueryTimeNs <= maxQueryFrequencyNanos) {
211 lastQueryTimeNs = System.nanoTime();
215 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/conf-error-no-vehicle-id");
219 GroupePSABridgeHandler groupepsaBridge = getBridgeHandler();
220 if (groupepsaBridge == null) {
221 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/conf-error-no-bridge");
226 VehicleStatus vehicle = groupepsaBridge.getVehicleStatus(id);
228 if (vehicle != null && isValidResult(vehicle)) {
229 logger.trace("Vehicle: {}", vehicle.toString());
231 logger.debug("Update vehicle state now: {}, lastupdate: {}", ZonedDateTime.now(),
232 vehicle.getUpdatedAt());
234 updateChannelState(vehicle);
236 if (isConnected(vehicle)) {
237 updateStatus(ThingStatus.ONLINE);
239 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
240 "@text/comm-error-vehicle-not-connected-to-cloud");
243 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
244 "@text/comm-error-query-vehicle-failed");
246 } catch (GroupePSACommunicationException e) {
247 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
248 MessageFormat.format("@text/comm-error-query-vehicle-failed", e.getMessage()));
252 private void updateChannelState(VehicleStatus vehicle) {
253 final DoorsState doorsState = vehicle.getDoorsState();
254 if (doorsState != null) {
255 buildDoorChannels(doorsState);
257 List<Opening> openings = doorsState.getOpening();
258 if (openings != null) {
259 for (Opening opening : openings) {
260 String id = opening.getIdentifier();
262 ChannelUID channelUID = new ChannelUID(getThing().getUID(), CHANNEL_GROUP_DOORS,
264 updateState(channelUID, "open".equalsIgnoreCase(opening.getState()) ? OpenClosedType.OPEN
265 : OpenClosedType.CLOSED);
270 List<String> lockedState = doorsState.getLockedState();
271 updateState(CHANNEL_DOORS_LOCK, lockedState, x -> x.get(0));
273 updateState(CHANNEL_DOORS_LOCK, UnDefType.UNDEF);
276 updateState(CHANNEL_BATTERY_CURRENT, vehicle.getBattery(), Battery::getCurrent, Units.AMPERE);
277 updateState(CHANNEL_BATTERY_VOLTAGE, vehicle.getBattery(), Battery::getVoltage, Units.VOLT);
279 updateState(CHANNEL_ENVIRONMENT_TEMPERATURE, vehicle.getEnvironment(), Environment::getAir, Air::getTemp,
281 updateStateBoolean(CHANNEL_ENVIRONMENT_DAYTIME, vehicle.getEnvironment(), Environment::getLuminosity,
284 updateState(CHANNEL_MOTION_IGNITION, vehicle.getIgnition(), Ignition::getType);
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);
291 updateState(CHANNEL_MOTION_MILEAGE, vehicle.getOdometer(), Odometer::getMileage,
292 MetricPrefix.KILO(SIUnits.METRE));
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())));
303 updateState(CHANNEL_POSITION_POSITION,
304 new PointType(new DecimalType(position.lat()), new DecimalType(position.lon())));
307 updateState(CHANNEL_POSITION_POSITION, UnDefType.UNDEF);
309 updateState(CHANNEL_POSITION_HEADING, lastPosition.getProperties(), Properties::getHeading,
311 updateState(CHANNEL_POSITION_TYPE, lastPosition.getProperties(), Properties::getType);
312 updateState(CHANNEL_POSITION_SIGNALSTRENGTH, lastPosition.getProperties(), Properties::getSignalQuality,
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);
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);
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);
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);
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);
364 void buildDoorChannels(final DoorsState doorsState) {
365 ThingHandlerCallback callback = getCallback();
366 if (callback == null) {
370 ThingBuilder thingBuilder = editThing();
371 List<Channel> channels = getThing().getChannelsOfGroup(CHANNEL_GROUP_DOORS);
372 thingBuilder.withoutChannels(channels);
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());
378 List<Opening> openings = doorsState.getOpening();
379 if (openings != null) {
380 for (Opening opening : openings) {
381 String id = opening.getIdentifier();
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());
390 updateThing(thingBuilder.build());
393 // Various update helper functions
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));
399 updateState(channelID, UnDefType.UNDEF);
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);
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,
411 final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
412 updateState(channelID, object2 != null ? mapper2.apply(object2) : null, unit);
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);
423 protected void updateState(String channelID, @Nullable ZonedDateTime date) {
425 updateState(channelID, new DateTimeType(date));
427 updateState(channelID, UnDefType.UNDEF);
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);
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);
442 protected void updateState(String channelID, @Nullable String text) {
444 updateState(channelID, new StringType(text));
446 updateState(channelID, UnDefType.UNDEF);
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);
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);
461 protected void updateState(String channelID, @Nullable Boolean value) {
463 updateState(channelID, value ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
465 updateState(channelID, UnDefType.UNDEF);
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);
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);