2 * Copyright (c) 2010-2024 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.i18n.TimeZoneProvider;
54 import org.openhab.core.library.types.DateTimeType;
55 import org.openhab.core.library.types.DecimalType;
56 import org.openhab.core.library.types.OpenClosedType;
57 import org.openhab.core.library.types.PointType;
58 import org.openhab.core.library.types.QuantityType;
59 import org.openhab.core.library.types.StringType;
60 import org.openhab.core.library.unit.MetricPrefix;
61 import org.openhab.core.library.unit.SIUnits;
62 import org.openhab.core.library.unit.Units;
63 import org.openhab.core.thing.Bridge;
64 import org.openhab.core.thing.Channel;
65 import org.openhab.core.thing.ChannelUID;
66 import org.openhab.core.thing.Thing;
67 import org.openhab.core.thing.ThingStatus;
68 import org.openhab.core.thing.ThingStatusDetail;
69 import org.openhab.core.thing.binding.BaseThingHandler;
70 import org.openhab.core.thing.binding.ThingHandler;
71 import org.openhab.core.thing.binding.ThingHandlerCallback;
72 import org.openhab.core.thing.binding.builder.ThingBuilder;
73 import org.openhab.core.thing.type.ChannelTypeUID;
74 import org.openhab.core.types.Command;
75 import org.openhab.core.types.RefreshType;
76 import org.openhab.core.types.UnDefType;
77 import org.slf4j.Logger;
78 import org.slf4j.LoggerFactory;
80 import com.github.filosganga.geogson.model.Geometry;
81 import com.github.filosganga.geogson.model.positions.SinglePosition;
84 * The {@link GroupePSAHandler} is responsible for handling commands, which are
85 * sent to one of the channels.
87 * @author Arjan Mels - Initial contribution
90 public class GroupePSAHandler extends BaseThingHandler {
91 private static final long DEFAULT_POLLING_INTERVAL_M = TimeUnit.MINUTES.toMinutes(1);
92 private static final long DEFAULT_ONLINE_INTERVAL_M = TimeUnit.MINUTES.toMinutes(60);
94 private final Logger logger = LoggerFactory.getLogger(GroupePSAHandler.class);
96 private final TimeZoneProvider timeZoneProvider;
98 private @Nullable String id = null;
99 private long lastQueryTimeNs = 0L;
101 private @Nullable ScheduledFuture<?> groupepsaPollingJob;
102 private long maxQueryFrequencyNanos = TimeUnit.MINUTES.toNanos(1);
103 private long onlineIntervalM;
105 public GroupePSAHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
107 this.timeZoneProvider = timeZoneProvider;
111 protected @Nullable Bridge getBridge() {
112 return super.getBridge();
115 private void pollStatus() {
116 Bridge bridge = getBridge();
117 if (bridge != null && bridge.getStatus() == ThingStatus.ONLINE) {
118 updateGroupePSAState();
120 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
125 public void handleCommand(ChannelUID channelUID, Command command) {
126 if (command instanceof RefreshType) {
127 refreshChannels(channelUID);
131 private void refreshChannels(ChannelUID channelUID) {
132 updateGroupePSAState();
136 public void initialize() {
137 if (getBridgeHandler() != null) {
138 GroupePSAConfiguration currentConfig = getConfigAs(GroupePSAConfiguration.class);
139 final String id = currentConfig.getId();
140 final Integer pollingIntervalM = currentConfig.getPollingInterval();
141 final Integer onlineIntervalM = currentConfig.getOnlineInterval();
144 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
145 "@text/conf-error-no-vehicle-id");
146 } else if (pollingIntervalM != null && pollingIntervalM < 1) {
147 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
148 "@text/conf-error-invalid-polling-interval");
149 } else if (onlineIntervalM != null && onlineIntervalM < 1) {
150 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
151 "@text/conf-error-invalid-online-interval");
154 this.onlineIntervalM = onlineIntervalM != null ? onlineIntervalM : DEFAULT_ONLINE_INTERVAL_M;
155 startGroupePSAPolling(pollingIntervalM);
159 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
164 public GroupePSABridgeHandler getBridgeHandler() {
165 Bridge bridge = getBridge();
166 if (bridge != null) {
167 ThingHandler handler = bridge.getHandler();
168 if (handler instanceof GroupePSABridgeHandler bridgeHandler) {
169 return bridgeHandler;
176 public void dispose() {
177 stopGroupePSAPolling();
181 private void startGroupePSAPolling(@Nullable Integer pollingIntervalM) {
182 if (groupepsaPollingJob == null) {
183 final long pollingIntervalToUse = pollingIntervalM == null ? DEFAULT_POLLING_INTERVAL_M : pollingIntervalM;
184 groupepsaPollingJob = scheduler.scheduleWithFixedDelay(() -> pollStatus(), 1, pollingIntervalToUse * 60,
189 private void stopGroupePSAPolling() {
190 ScheduledFuture<?> job = groupepsaPollingJob;
193 groupepsaPollingJob = null;
197 private boolean isValidResult(VehicleStatus vehicle) {
198 return vehicle.getUpdatedAt() != null;
201 private boolean isConnected(VehicleStatus vehicle) {
202 ZonedDateTime updatedAt = vehicle.getUpdatedAt();
203 if (updatedAt == null) {
207 return updatedAt.isAfter(ZonedDateTime.now().minusMinutes(onlineIntervalM));
210 private synchronized void updateGroupePSAState() {
211 if (System.nanoTime() - lastQueryTimeNs <= maxQueryFrequencyNanos) {
215 lastQueryTimeNs = System.nanoTime();
219 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/conf-error-no-vehicle-id");
223 GroupePSABridgeHandler groupepsaBridge = getBridgeHandler();
224 if (groupepsaBridge == null) {
225 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "@text/conf-error-no-bridge");
230 VehicleStatus vehicle = groupepsaBridge.getVehicleStatus(id);
232 if (vehicle != null && isValidResult(vehicle)) {
233 logger.trace("Vehicle: {}", vehicle.toString());
235 logger.debug("Update vehicle state now: {}, lastupdate: {}", ZonedDateTime.now(),
236 vehicle.getUpdatedAt());
238 updateChannelState(vehicle);
240 if (isConnected(vehicle)) {
241 updateStatus(ThingStatus.ONLINE);
243 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.NONE,
244 "@text/comm-error-vehicle-not-connected-to-cloud");
247 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
248 "@text/comm-error-query-vehicle-failed");
250 } catch (GroupePSACommunicationException e) {
251 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
252 MessageFormat.format("@text/comm-error-query-vehicle-failed", e.getMessage()));
256 private void updateChannelState(VehicleStatus vehicle) {
257 final DoorsState doorsState = vehicle.getDoorsState();
258 if (doorsState != null) {
259 buildDoorChannels(doorsState);
261 List<Opening> openings = doorsState.getOpening();
262 if (openings != null) {
263 for (Opening opening : openings) {
264 String id = opening.getIdentifier();
266 ChannelUID channelUID = new ChannelUID(getThing().getUID(), CHANNEL_GROUP_DOORS,
268 updateState(channelUID, "open".equalsIgnoreCase(opening.getState()) ? OpenClosedType.OPEN
269 : OpenClosedType.CLOSED);
274 List<String> lockedState = doorsState.getLockedState();
275 updateState(CHANNEL_DOORS_LOCK, lockedState, x -> x.get(0));
277 updateState(CHANNEL_DOORS_LOCK, UnDefType.UNDEF);
280 updateState(CHANNEL_BATTERY_CURRENT, vehicle.getBattery(), Battery::getCurrent, Units.AMPERE);
281 updateState(CHANNEL_BATTERY_VOLTAGE, vehicle.getBattery(), Battery::getVoltage, Units.VOLT);
283 updateState(CHANNEL_ENVIRONMENT_TEMPERATURE, vehicle.getEnvironment(), Environment::getAir, Air::getTemp,
285 updateStateBoolean(CHANNEL_ENVIRONMENT_DAYTIME, vehicle.getEnvironment(), Environment::getLuminosity,
288 updateState(CHANNEL_MOTION_IGNITION, vehicle.getIgnition(), Ignition::getType);
290 updateStateBoolean(CHANNEL_MOTION_MOVING, vehicle.getKinetic(), Kinetic::isMoving);
291 updateState(CHANNEL_MOTION_ACCELERATION, vehicle.getKinetic(), Kinetic::getAcceleration,
292 Units.METRE_PER_SQUARE_SECOND);
293 updateState(CHANNEL_MOTION_SPEED, vehicle.getKinetic(), Kinetic::getSpeed, SIUnits.KILOMETRE_PER_HOUR);
295 updateState(CHANNEL_MOTION_MILEAGE, vehicle.getOdometer(), Odometer::getMileage,
296 MetricPrefix.KILO(SIUnits.METRE));
298 Position lastPosition = vehicle.getLastPosition();
299 if (lastPosition != null) {
300 Geometry<SinglePosition> geometry = lastPosition.getGeometry();
301 if (geometry != null) {
302 SinglePosition position = geometry.positions();
303 if (Double.isFinite(position.alt())) {
304 updateState(CHANNEL_POSITION_POSITION, new PointType(new DecimalType(position.lat()),
305 new DecimalType(position.lon()), new DecimalType(position.alt())));
307 updateState(CHANNEL_POSITION_POSITION,
308 new PointType(new DecimalType(position.lat()), new DecimalType(position.lon())));
311 updateState(CHANNEL_POSITION_POSITION, UnDefType.UNDEF);
313 updateState(CHANNEL_POSITION_HEADING, lastPosition.getProperties(), Properties::getHeading,
315 updateState(CHANNEL_POSITION_TYPE, lastPosition.getProperties(), Properties::getType);
316 updateState(CHANNEL_POSITION_SIGNALSTRENGTH, lastPosition.getProperties(), Properties::getSignalQuality,
320 updateState(CHANNEL_VARIOUS_LAST_UPDATED, vehicle.getUpdatedAt());
321 updateState(CHANNEL_VARIOUS_PRIVACY, vehicle.getPrivacy(), Privacy::getState);
322 updateState(CHANNEL_VARIOUS_BELT, vehicle.getSafety(), Safety::getBeltWarning);
323 updateState(CHANNEL_VARIOUS_EMERGENCY, vehicle.getSafety(), Safety::getECallTriggeringRequest);
324 updateState(CHANNEL_VARIOUS_SERVICE, vehicle.getService(), Service::getType);
325 updateState(CHANNEL_VARIOUS_PRECONDITINING, vehicle.getPreconditionning(), Preconditionning::getAirConditioning,
326 AirConditioning::getStatus);
327 updateState(CHANNEL_VARIOUS_PRECONDITINING_FAILURE, vehicle.getPreconditionning(),
328 Preconditionning::getAirConditioning, AirConditioning::getFailureCause);
330 List<Energy> energies = vehicle.getEnergy();
331 if (energies != null) {
332 for (Energy energy : energies) {
333 if ("Fuel".equalsIgnoreCase(energy.getType())) {
334 updateState(CHANNEL_FUEL_AUTONOMY, energy, Energy::getAutonomy, MetricPrefix.KILO(SIUnits.METRE));
335 updateState(CHANNEL_FUEL_CONSUMPTION, energy, Energy::getConsumption,
336 Units.LITRE.divide(MetricPrefix.KILO(SIUnits.METRE)));
337 updateState(CHANNEL_FUEL_LEVEL, energy, Energy::getLevel, Units.PERCENT);
338 } else if ("Electric".equalsIgnoreCase(energy.getType())) {
339 updateState(CHANNEL_ELECTRIC_AUTONOMY, energy, Energy::getAutonomy,
340 MetricPrefix.KILO(SIUnits.METRE));
341 updateState(CHANNEL_ELECTRIC_RESIDUAL, energy, Energy::getResidual, Units.KILOWATT_HOUR);
342 updateState(CHANNEL_ELECTRIC_LEVEL, energy, Energy::getLevel, Units.PERCENT);
344 updateState(CHANNEL_ELECTRIC_BATTERY_CAPACITY, energy, Energy::getBattery,
345 BatteryStatus::getCapacity, Units.KILOWATT_HOUR);
346 updateState(CHANNEL_ELECTRIC_BATTERY_HEALTH_CAPACITY, energy, Energy::getBattery,
347 BatteryStatus::getHealth, Health::getCapacity, Units.PERCENT);
348 updateState(CHANNEL_ELECTRIC_BATTERY_HEALTH_RESISTANCE, energy, Energy::getBattery,
349 BatteryStatus::getHealth, Health::getResistance, Units.PERCENT);
351 updateState(CHANNEL_ELECTRIC_CHARGING_STATUS, energy, Energy::getCharging, Charging::getStatus);
352 updateState(CHANNEL_ELECTRIC_CHARGING_MODE, energy, Energy::getCharging, Charging::getChargingMode);
353 updateStateBoolean(CHANNEL_ELECTRIC_CHARGING_PLUGGED, energy, Energy::getCharging,
354 Charging::isPlugged);
355 updateState(CHANNEL_ELECTRIC_CHARGING_RATE, energy, Energy::getCharging, Charging::getChargingRate,
356 SIUnits.KILOMETRE_PER_HOUR);
358 updateState(CHANNEL_ELECTRIC_CHARGING_REMAININGTIME, energy, Energy::getCharging,
359 Charging::getRemainingTime, x -> new BigDecimal(x.getSeconds()), Units.SECOND);
360 updateState(CHANNEL_ELECTRIC_CHARGING_NEXTDELAYEDTIME, energy, Energy::getCharging,
361 Charging::getNextDelayedTime, x -> new BigDecimal(x.getSeconds()), Units.SECOND);
368 void buildDoorChannels(final DoorsState doorsState) {
369 ThingHandlerCallback callback = getCallback();
370 if (callback == null) {
374 ThingBuilder thingBuilder = editThing();
375 List<Channel> channels = getThing().getChannelsOfGroup(CHANNEL_GROUP_DOORS);
376 thingBuilder.withoutChannels(channels);
378 ChannelUID channelUID = new ChannelUID(getThing().getUID(), CHANNEL_DOORS_LOCK);
379 ChannelTypeUID channelTypeUID = new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_DOORLOCK);
380 thingBuilder.withChannel(callback.createChannelBuilder(channelUID, channelTypeUID).build());
382 List<Opening> openings = doorsState.getOpening();
383 if (openings != null) {
384 for (Opening opening : openings) {
385 String id = opening.getIdentifier();
387 channelUID = new ChannelUID(getThing().getUID(), CHANNEL_GROUP_DOORS, id.toLowerCase());
388 channelTypeUID = new ChannelTypeUID(BINDING_ID, CHANNEL_TYPE_DOOROPEN);
389 thingBuilder.withChannel(callback.createChannelBuilder(channelUID, channelTypeUID).build());
394 updateThing(thingBuilder.build());
397 // Various update helper functions
399 protected <T extends Quantity<T>> void updateState(String channelID, @Nullable BigDecimal number, Unit<T> unit) {
400 if (number != null) {
401 updateState(channelID, new QuantityType<>(number, unit));
403 updateState(channelID, UnDefType.UNDEF);
407 protected <T1, T2 extends Quantity<T2>> void updateState(String channelID, final @Nullable T1 object,
408 Function<? super T1, @Nullable BigDecimal> mapper, Unit<T2> unit) {
409 updateState(channelID, object != null ? mapper.apply(object) : null, unit);
412 protected <T1, T2, T3 extends Quantity<T3>> void updateState(String channelID, final @Nullable T1 object1,
413 Function<? super T1, @Nullable T2> mapper1, Function<? super T2, @Nullable BigDecimal> mapper2,
415 final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
416 updateState(channelID, object2 != null ? mapper2.apply(object2) : null, unit);
419 protected <T1, T2, T3, T4 extends Quantity<T4>> void updateState(String channelID, final @Nullable T1 object1,
420 Function<? super T1, @Nullable T2> mapper1, Function<? super T2, @Nullable T3> mapper2,
421 Function<? super T3, @Nullable BigDecimal> mapper3, Unit<T4> unit) {
422 final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
423 final @Nullable T3 object3 = object2 != null ? mapper2.apply(object2) : null;
424 updateState(channelID, object3 != null ? mapper3.apply(object3) : null, unit);
427 protected void updateState(String channelID, @Nullable ZonedDateTime date) {
429 updateState(channelID, new DateTimeType(date).toZone(timeZoneProvider.getTimeZone()));
431 updateState(channelID, UnDefType.UNDEF);
435 protected <T1> void updateStateDate(String channelID, @Nullable T1 object,
436 Function<? super T1, @Nullable ZonedDateTime> mapper) {
437 updateState(channelID, object != null ? mapper.apply(object) : null);
440 protected <T1, T2> void updateStateDate(String channelID, @Nullable T1 object1,
441 Function<? super T1, @Nullable T2> mapper1, Function<? super T2, @Nullable ZonedDateTime> mapper2) {
442 final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
443 updateState(channelID, object2 != null ? mapper2.apply(object2) : null);
446 protected void updateState(String channelID, @Nullable String text) {
448 updateState(channelID, new StringType(text));
450 updateState(channelID, UnDefType.UNDEF);
454 protected <T1> void updateState(String channelID, @Nullable T1 object,
455 Function<? super T1, @Nullable String> mapper) {
456 updateState(channelID, object != null ? mapper.apply(object) : null);
459 protected <T1, T2> void updateState(String channelID, @Nullable T1 object1,
460 Function<? super T1, @Nullable T2> mapper1, Function<? super T2, @Nullable String> mapper2) {
461 final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
462 updateState(channelID, object2 != null ? mapper2.apply(object2) : null);
465 protected void updateState(String channelID, @Nullable Boolean value) {
467 updateState(channelID, value ? OpenClosedType.OPEN : OpenClosedType.CLOSED);
469 updateState(channelID, UnDefType.UNDEF);
473 protected <T1> void updateStateBoolean(String channelID, @Nullable T1 object,
474 Function<? super T1, @Nullable Boolean> mapper) {
475 updateState(channelID, object != null ? mapper.apply(object) : null);
478 protected <T1, T2> void updateStateBoolean(String channelID, final @Nullable T1 object1,
479 Function<? super T1, @Nullable T2> mapper1, Function<? super T2, @Nullable Boolean> mapper2) {
480 final @Nullable T2 object2 = object1 != null ? mapper1.apply(object1) : null;
481 updateState(channelID, object2 != null ? mapper2.apply(object2) : null);