added migrated 2.x add-ons

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2020-09-21 01:58:32 +02:00
parent bbf1a7fd29
commit 6df6783b60
11662 changed files with 1302875 additions and 11 deletions

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.astro-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-volvooncall" description="Volvo On Call Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.volvooncall/${project.version}</bundle>
</feature>
</features>

View File

@@ -0,0 +1,128 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link VolvoOnCallBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class VolvoOnCallBindingConstants {
public static final String BINDING_ID = "volvooncall";
// Vehicle properties
public static final String VIN = "vin";
// The URL to use to connect to VocAPI with.
// TODO : for North America and China syntax changes to vocapi-cn.xxx
public static final String SERVICE_URL = "https://vocapi.wirelesscar.net/customerapi/rest/v3.0/";
// The JSON content type used when talking to VocAPI.
public static final String JSON_CONTENT_TYPE = "application/json";
// List of Thing Type UIDs
public static final ThingTypeUID APIBRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "vocapi");
public static final ThingTypeUID VEHICLE_THING_TYPE = new ThingTypeUID(BINDING_ID, "vehicle");
// List of Channel groups
public static final String GROUP_DOORS = "doors";
public static final String GROUP_WINDOWS = "windows";
public static final String GROUP_TYRES = "tyrePressure";
public static final String GROUP_BATTERY = "battery";
// List of Channel id's
public static final String TAILGATE = "tailgate";
public static final String REAR_RIGHT = "rearRight";
public static final String REAR_LEFT = "rearLeft";
public static final String FRONT_RIGHT = "frontRight";
public static final String FRONT_LEFT = "frontLeft";
public static final String HOOD = "hood";
public static final String REAR_RIGHT_WND = "rearRightWnd";
public static final String REAR_LEFT_WND = "rearLeftWnd";
public static final String FRONT_RIGHT_WND = "frontRightWnd";
public static final String FRONT_LEFT_WND = "frontLeftWnd";
public static final String REAR_RIGHT_TYRE = "rearRightTyre";
public static final String REAR_LEFT_TYRE = "rearLeftTyre";
public static final String FRONT_RIGHT_TYRE = "frontRightTyre";
public static final String FRONT_LEFT_TYRE = "frontLeftTyre";
public static final String ODOMETER = "odometer";
public static final String TRIPMETER1 = "tripmeter1";
public static final String TRIPMETER2 = "tripmeter2";
public static final String DISTANCE_TO_EMPTY = "distanceToEmpty";
public static final String FUEL_AMOUNT = "fuelAmount";
public static final String FUEL_LEVEL = "fuelLevel";
public static final String FUEL_CONSUMPTION = "fuelConsumption";
public static final String FUEL_ALERT = "fuelAlert";
public static final String CALCULATED_LOCATION = "calculatedLocation";
public static final String ACTUAL_LOCATION = "location";
public static final String LOCATION_TIMESTAMP = "locationTimestamp";
public static final String HEADING = "heading";
public static final String CAR_LOCKED = "carLocked";
public static final String ENGINE_RUNNING = "engineRunning";
public static final String BRAKE_FLUID_LEVEL = "brakeFluidLevel";
public static final String WASHER_FLUID_LEVEL = "washerFluidLevel";
public static final String AVERAGE_SPEED = "averageSpeed";
public static final String SERVICE_WARNING = "serviceWarningStatus";
public static final String BATTERY_LEVEL = "batteryLevel";
public static final String BATTERY_DISTANCE_TO_EMPTY = "batteryDistanceToEmpty";
public static final String CHARGE_STATUS = "chargeStatus";
public static final String TIME_TO_BATTERY_FULLY_CHARGED = "timeToHVBatteryFullyCharged";
public static final String CHARGING_END = "chargingEnd";
public static final String BULB_FAILURE = "bulbFailure";
// Last Trip Channel Id's
public static final String LAST_TRIP_GROUP = "lasttrip";
public static final String TRIP_CONSUMPTION = "tripConsumption";
public static final String TRIP_DISTANCE = "tripDistance";
public static final String TRIP_DURATION = "tripDuration";
public static final String TRIP_START_TIME = "tripStartTime";
public static final String TRIP_END_TIME = "tripEndTime";
public static final String TRIP_START_ODOMETER = "tripStartOdometer";
public static final String TRIP_STOP_ODOMETER = "tripStopOdometer";
public static final String TRIP_START_POSITION = "startPosition";
public static final String TRIP_END_POSITION = "endPosition";
// Optional Channels depends upon car version
public static final String CAR_LOCATOR = "carLocator";
public static final String JOURNAL_LOG = "journalLog";
// Car properties
public static final String ENGINE_START = "engineStart";
public static final String UNLOCK = "unlock";
public static final String UNLOCK_TIME = "unlockTimeFrame";
public static final String LOCK = "lock";
public static final String HONK = "honk";
public static final String BLINK = "blink";
public static final String HONK_BLINK = "honkAndBlink";
public static final String HONK_AND_OR_BLINK = "honkAndOrBlink";
public static final String REMOTE_HEATER = "remoteHeater";
public static final String PRECLIMATIZATION = "preclimatization";
public static final String LAST_TRIP_ID = "lastTripId";
// List of all adressable things in OH = SUPPORTED_DEVICE_THING_TYPES_UIDS + the virtual bridge
public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Stream
.of(APIBRIDGE_THING_TYPE, VEHICLE_THING_TYPE).collect(Collectors.toSet());
// Default value for undefined integers
public static final int UNDEFINED = -1;
}

View File

@@ -0,0 +1,68 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal;
import java.io.IOException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonSyntaxException;
/**
* Exception for errors when using the VolvoOnCall API
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class VolvoOnCallException extends Exception {
private final Logger logger = LoggerFactory.getLogger(VolvoOnCallException.class);
private static final long serialVersionUID = -6215621577081394328L;
public static enum ErrorType {
UNKNOWN,
SERVICE_UNAVAILABLE,
IOEXCEPTION,
JSON_SYNTAX;
}
private final ErrorType cause;
public VolvoOnCallException(String label, @Nullable String description) {
super(label);
if ("FoundationServicesUnavailable".equalsIgnoreCase(label)) {
cause = ErrorType.SERVICE_UNAVAILABLE;
} else {
cause = ErrorType.UNKNOWN;
logger.warn("Unhandled VoC error : {} : {}", label, description);
}
}
public VolvoOnCallException(Exception e) {
super(e);
if (e instanceof IOException) {
cause = ErrorType.IOEXCEPTION;
} else if (e instanceof JsonSyntaxException) {
cause = ErrorType.JSON_SYNTAX;
} else {
cause = ErrorType.UNKNOWN;
logger.warn("Unhandled VoC error : {}", e.getMessage());
}
}
public ErrorType getType() {
return cause;
}
}

View File

@@ -0,0 +1,101 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal;
import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.volvooncall.internal.discovery.VolvoOnCallDiscoveryService;
import org.openhab.binding.volvooncall.internal.handler.VehicleHandler;
import org.openhab.binding.volvooncall.internal.handler.VehicleStateDescriptionProvider;
import org.openhab.binding.volvooncall.internal.handler.VolvoOnCallBridgeHandler;
import org.openhab.core.config.discovery.DiscoveryService;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link VolvoOnCallHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.volvooncall", service = ThingHandlerFactory.class)
public class VolvoOnCallHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(VolvoOnCallHandlerFactory.class);
private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
private final VehicleStateDescriptionProvider stateDescriptionProvider;
@Activate
public VolvoOnCallHandlerFactory(@Reference VehicleStateDescriptionProvider provider) {
this.stateDescriptionProvider = provider;
}
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (APIBRIDGE_THING_TYPE.equals(thingTypeUID)) {
VolvoOnCallBridgeHandler bridgeHandler = new VolvoOnCallBridgeHandler((Bridge) thing);
registerDeviceDiscoveryService(bridgeHandler);
return bridgeHandler;
} else if (VEHICLE_THING_TYPE.equals(thingTypeUID)) {
return new VehicleHandler(thing, stateDescriptionProvider);
}
logger.warn("ThingHandler not found for {}", thing.getThingTypeUID());
return null;
}
@Override
protected void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof VolvoOnCallBridgeHandler) {
ThingUID thingUID = thingHandler.getThing().getUID();
unregisterDeviceDiscoveryService(thingUID);
}
super.removeHandler(thingHandler);
}
private void registerDeviceDiscoveryService(VolvoOnCallBridgeHandler bridgeHandler) {
VolvoOnCallDiscoveryService discoveryService = new VolvoOnCallDiscoveryService(bridgeHandler);
discoveryServiceRegs.put(bridgeHandler.getThing().getUID(),
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
}
private void unregisterDeviceDiscoveryService(ThingUID thingUID) {
if (discoveryServiceRegs.containsKey(thingUID)) {
ServiceRegistration<?> serviceReg = discoveryServiceRegs.get(thingUID);
serviceReg.unregister();
discoveryServiceRegs.remove(thingUID);
}
}
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.action;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link IVolvoOnCallActions} defines the interface for all thing actions supported by the binding.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public interface IVolvoOnCallActions {
public void honkBlinkCommand(Boolean honk, Boolean blink);
public void preclimatizationStopCommand();
public void heaterStopCommand();
public void heaterStartCommand();
public void preclimatizationStartCommand();
public void engineStartCommand(@Nullable Integer runtime);
public void openCarCommand();
public void closeCarCommand();
}

View File

@@ -0,0 +1,206 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.action;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.volvooncall.internal.handler.VehicleHandler;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@VehicleAction } class is responsible to call corresponding
* action on Vehicle Handler
*
* @author Gaël L'hopital - Initial contribution
*/
@ThingActionsScope(name = "volvooncall")
@NonNullByDefault
public class VolvoOnCallActions implements ThingActions, IVolvoOnCallActions {
private final Logger logger = LoggerFactory.getLogger(VolvoOnCallActions.class);
private @Nullable VehicleHandler handler;
public VolvoOnCallActions() {
logger.info("Volvo On Call actions service instanciated");
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof VehicleHandler) {
this.handler = (VehicleHandler) handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return this.handler;
}
@Override
@RuleAction(label = "Volvo On Call : Close", description = "Closes the car")
public void closeCarCommand() {
logger.debug("closeCarCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionClose();
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void closeCarCommand(@Nullable ThingActions actions) {
invokeMethodOf(actions).closeCarCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Open", description = "Opens the car")
public void openCarCommand() {
logger.debug("openCarCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionOpen();
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void openCarCommand(@Nullable ThingActions actions) {
invokeMethodOf(actions).openCarCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Start Engine", description = "Starts the engine")
public void engineStartCommand(@ActionInput(name = "runtime", label = "Runtime") @Nullable Integer runtime) {
logger.debug("engineStartCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionStart(runtime != null ? runtime : 5);
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void engineStartCommand(@Nullable ThingActions actions, @Nullable Integer runtime) {
invokeMethodOf(actions).engineStartCommand(runtime);
}
@Override
@RuleAction(label = "Volvo On Call : Heater Start", description = "Starts car heater")
public void heaterStartCommand() {
logger.debug("heaterStartCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionHeater(true);
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void heaterStartCommand(@Nullable ThingActions actions) {
invokeMethodOf(actions).heaterStartCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Preclimatization Start", description = "Starts car heater")
public void preclimatizationStartCommand() {
logger.debug("preclimatizationStartCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionPreclimatization(true);
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void preclimatizationStartCommand(@Nullable ThingActions actions) {
invokeMethodOf(actions).preclimatizationStartCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Heater Stop", description = "Stops car heater")
public void heaterStopCommand() {
logger.debug("heaterStopCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionHeater(false);
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void heaterStopCommand(@Nullable ThingActions actions) {
invokeMethodOf(actions).heaterStopCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Preclimatization Stop", description = "Stops car heater")
public void preclimatizationStopCommand() {
logger.debug("preclimatizationStopCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionPreclimatization(false);
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void preclimatizationStopCommand(@Nullable ThingActions actions) {
invokeMethodOf(actions).preclimatizationStopCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Honk-blink", description = "Activates the horn and or lights of the car")
public void honkBlinkCommand(@ActionInput(name = "honk", label = "Honk") Boolean honk,
@ActionInput(name = "blink", label = "Blink") Boolean blink) {
logger.debug("honkBlinkCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionHonkBlink(honk, blink);
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void honkBlinkCommand(@Nullable ThingActions actions, Boolean honk, Boolean blink) {
invokeMethodOf(actions).honkBlinkCommand(honk, blink);
}
private static IVolvoOnCallActions invokeMethodOf(@Nullable ThingActions actions) {
if (actions == null) {
throw new IllegalArgumentException("actions cannot be null");
}
if (actions.getClass().getName().equals(VolvoOnCallActions.class.getName())) {
if (actions instanceof IVolvoOnCallActions) {
return (IVolvoOnCallActions) actions;
} else {
return (IVolvoOnCallActions) Proxy.newProxyInstance(IVolvoOnCallActions.class.getClassLoader(),
new Class[] { IVolvoOnCallActions.class }, (Object proxy, Method method, Object[] args) -> {
Method m = actions.getClass().getDeclaredMethod(method.getName(),
method.getParameterTypes());
return m.invoke(actions, args);
});
}
}
throw new IllegalArgumentException("Actions is not an instance of VolvoOnCallActions");
}
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.config;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link VehicleConfiguration} is the class used to match the
* Vehicle thing configuration.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class VehicleConfiguration {
public String vin = "";
public Integer refresh = 5;
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.config;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link VolvoOnCallBridgeConfiguration} is responsible for holding
* configuration informations needed to access VOC API
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class VolvoOnCallBridgeConfiguration {
public String username = "";
public String password = "";
public String getAuthorization() {
byte[] authorization = Base64.getEncoder().encode((String.format("%s:%s", username, password)).getBytes());
return "Basic " + new String(authorization, StandardCharsets.UTF_8);
}
}

View File

@@ -0,0 +1,73 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.discovery;
import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*;
import java.util.Arrays;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.volvooncall.internal.VolvoOnCallException;
import org.openhab.binding.volvooncall.internal.dto.AccountVehicleRelation;
import org.openhab.binding.volvooncall.internal.dto.Attributes;
import org.openhab.binding.volvooncall.internal.dto.Vehicles;
import org.openhab.binding.volvooncall.internal.handler.VolvoOnCallBridgeHandler;
import org.openhab.core.config.discovery.AbstractDiscoveryService;
import org.openhab.core.config.discovery.DiscoveryResultBuilder;
import org.openhab.core.thing.ThingUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link VolvoOnCallDiscoveryService} searches for available
* cars discoverable through VocAPI
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class VolvoOnCallDiscoveryService extends AbstractDiscoveryService {
private static final int SEARCH_TIME = 2;
private final Logger logger = LoggerFactory.getLogger(VolvoOnCallDiscoveryService.class);
private final VolvoOnCallBridgeHandler bridgeHandler;
public VolvoOnCallDiscoveryService(VolvoOnCallBridgeHandler bridgeHandler) {
super(SUPPORTED_THING_TYPES_UIDS, SEARCH_TIME);
this.bridgeHandler = bridgeHandler;
}
@Override
public void startScan() {
String[] relations = bridgeHandler.getVehiclesRelationsURL();
Arrays.stream(relations).forEach(relationURL -> {
try {
AccountVehicleRelation accountVehicle = bridgeHandler.getURL(relationURL, AccountVehicleRelation.class);
logger.debug("Found vehicle : {}", accountVehicle.vehicleId);
Vehicles vehicle = bridgeHandler.getURL(accountVehicle.vehicleURL, Vehicles.class);
Attributes attributes = bridgeHandler.getURL(Attributes.class, vehicle.vehicleId);
thingDiscovered(DiscoveryResultBuilder
.create(new ThingUID(VEHICLE_THING_TYPE, bridgeHandler.getThing().getUID(),
accountVehicle.vehicleId))
.withLabel(attributes.vehicleType + " " + attributes.registrationNumber)
.withBridge(bridgeHandler.getThing().getUID()).withProperty(VIN, attributes.vin)
.withRepresentationProperty(accountVehicle.vehicleId).build());
} catch (VolvoOnCallException e) {
logger.warn("Error while discovering vehicle: {}", e.getMessage());
}
});
stopScan();
}
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* The {@link AccountVehicleRelation} is responsible for storing
* informations returned by vehicle position rest
* answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class AccountVehicleRelation extends VocAnswer {
@SerializedName("vehicle")
public @NonNullByDefault({}) String vehicleURL;
public @NonNullByDefault({}) String vehicleId;
/*
* Currently unused in the binding, maybe interesting in the future
* private String account;
* private String username;
* private String status;
* private Integer customerVehicleRelationId;
* private String accountId;
* private String accountVehicleRelation;
*/
}

View File

@@ -0,0 +1,78 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import java.util.List;
/**
* The {@link Attributes} is responsible for storing
* informations returned by vehicule attributes rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
public class Attributes extends VocAnswer {
public String vehicleType;
public String registrationNumber;
public Boolean carLocatorSupported;
public Boolean honkAndBlinkSupported;
public List<String> honkAndBlinkVersionsSupported;
public Boolean remoteHeaterSupported;
public Boolean unlockSupported;
public Boolean lockSupported;
public Boolean journalLogSupported;
public Integer unlockTimeFrame;
public Boolean journalLogEnabled;
public Boolean preclimatizationSupported;
public String vin;
public Boolean engineStartSupported;
/*
* Currently unused in the binding, maybe interesting in the future
* public class Country {
* public @NonNullByDefault({}) String iso2;
* }
* private String engineCode;
* private String exteriorCode;
* private String interiorCode;
* private String tyreDimensionCode;
* private Object tyreInflationPressureLightCode;
* private Object tyreInflationPressureHeavyCode;
* private String gearboxCode;
* private String fuelType;
* private Integer fuelTankVolume;
* private Integer grossWeight;
* private Integer modelYear;
* private String vehicleTypeCode;
* private Integer numberOfDoors;
* private Country country;
* private Integer carLocatorDistance;
* private Integer honkAndBlinkDistance;
* private String bCallAssistanceNumber;
* private Boolean assistanceCallSupported;
* private Integer verificationTimeFrame;
* private Integer timeFullyAccessible;
* private Integer timePartiallyAccessible;
* private String subscriptionType;
* private String subscriptionStartDate;
* private String subscriptionEndDate;
* private String serverVersion;
* private Boolean highVoltageBatterySupported;
* private Object maxActiveDelayChargingLocations;
* private Integer climatizationCalendarMaxTimers;
* private String vehiclePlatform;
* private Boolean statusParkedIndoorSupported;
* private Boolean overrideDelayChargingSupported;
* private @Nullable List<String> sendPOIToVehicleVersionsSupported = null;
* private @Nullable List<String> climatizationCalendarVersionsSupported = null;
*/
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* The {@link CustomerAccounts} is responsible for storing
* informations returned by customerAccount rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class CustomerAccounts extends VocAnswer {
@SerializedName("accountVehicleRelations")
public @NonNullByDefault({}) String[] accountVehicleRelationsURL;
public @Nullable String username;
/*
* Currently unused in the binding, maybe interesting in the future
* private String firstName;
* private String lastName;
* private String accountId;
* private String account;
*/
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.OpenClosedType;
/**
* The {@link DoorsStatus} is responsible for storing
* informations returned by vehicle status rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class DoorsStatus {
public @NonNullByDefault({}) OpenClosedType tailgateOpen;
public @NonNullByDefault({}) OpenClosedType rearRightDoorOpen;
public @NonNullByDefault({}) OpenClosedType rearLeftDoorOpen;
public @NonNullByDefault({}) OpenClosedType frontRightDoorOpen;
public @NonNullByDefault({}) OpenClosedType frontLeftDoorOpen;
public @NonNullByDefault({}) OpenClosedType hoodOpen;
/*
* Currently unused in the binding, maybe interesting in the future
* private ZonedDateTime timestamp;
*/
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link ERSStatus} is responsible for storing
* ERS Status informations returned by vehicule status rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class ERSStatus {
public @NonNullByDefault({}) String status;
public @NonNullByDefault({}) ZonedDateTime timestamp;
public @NonNullByDefault({}) String engineStartWarning;
public @NonNullByDefault({}) ZonedDateTime engineStartWarningTimestamp;
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* The {@link Heater} is responsible for storing
* heater information returned by vehicle status rest answer
*
* @author Arie van der Lee - Initial contribution
*/
@NonNullByDefault
public class Heater {
private String status = "";
/*
* Currently unused in the binding, maybe interesting in the future
* private ZonedDateTime timestamp;
*/
public State getStatus() {
if ("off".equalsIgnoreCase(status)) {
return OnOffType.OFF;
} else if ("on".equalsIgnoreCase(status) || "onOther".equalsIgnoreCase(status)) {
return OnOffType.ON;
}
return UnDefType.UNDEF;
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.UNDEFINED;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link HvBattery} is responsible for storing
* PHEV Battery information returned by vehicle status rest answer
*
* @author Arie van der Lee - Initial contribution
*/
@NonNullByDefault
public class HvBattery {
public int hvBatteryLevel = UNDEFINED;
public int distanceToHVBatteryEmpty = UNDEFINED;
public @NonNullByDefault({}) String hvBatteryChargeStatusDerived;
public int timeToHVBatteryFullyCharged = UNDEFINED;
/*
* Currently unused in the binding, maybe interesting in the future
* private ZonedDateTime timestamp;
*/
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link Position} is responsible for storing
* informations returned by vehicle position rest
* answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class Position extends VocAnswer {
public @NonNullByDefault({}) PositionData position;
public @NonNullByDefault({}) PositionData calculatedPosition;
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import java.time.ZonedDateTime;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link PositionData} is responsible for storing
* informations returned by vehicle position rest
* answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class PositionData {
public @Nullable Double longitude;
public @Nullable Double latitude;
private @Nullable ZonedDateTime timestamp;
public @Nullable String speed;
private @Nullable String heading;
public Boolean isHeading() {
return "true".equalsIgnoreCase(heading);
}
public Optional<ZonedDateTime> getTimestamp() {
ZonedDateTime timestamp = this.timestamp;
if (timestamp != null) {
return Optional.of(timestamp);
}
return Optional.empty();
}
/*
* Currently unused in the binding, maybe interesting in the future
* private String streetAddress;
* private String postalCode;
* private String city;
* private String iSO2CountryCode;
* private String region;
*/
}

View File

@@ -0,0 +1,67 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* The {@link PostResponse} is responsible for storing
* elements given back after a post to VOC api
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class PostResponse extends VocAnswer {
public static enum Status {
@SerializedName("Started")
STARTED,
@SerializedName("MessageDelivered")
DELIVERED,
@SerializedName("Failed")
FAILED,
@SerializedName("Successful")
SUCCESSFULL
}
public static enum ServiceType {
RHBLF, // Remote Honk and Blink Lights ?
RDU, // Remote door unlock
ERS, // Remote engine start
TN // Theft notification
}
public @NonNullByDefault({}) Status status;
public @NonNullByDefault({}) String vehicleId;
@SerializedName("service")
public @NonNullByDefault({}) String serviceURL;
public @NonNullByDefault({}) ServiceType serviceType;
/*
* Currently unused in the binding, maybe interesting in the future
*
* public static enum FailureReason {
*
* @SerializedName("TimeframePassed")
* TIME_FRAME_PASSED
* }
*
* private ZonedDateTime statusTimestamp;
* private ZonedDateTime startTime;
* private FailureReason failureReason;
*
* private Integer customerServiceId;
*/
}

View File

@@ -0,0 +1,140 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.UNDEFINED;
import java.util.List;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.OnOffType;
/**
* The {@link Status} is responsible for storing
* Door Status informations returned by vehicule status rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class Status extends VocAnswer {
public double averageFuelConsumption = UNDEFINED;
public int averageSpeed = UNDEFINED;
public int fuelAmount = UNDEFINED;
public int fuelAmountLevel = UNDEFINED;
public int distanceToEmpty = UNDEFINED;
public int odometer = UNDEFINED;
public int tripMeter1 = UNDEFINED;
public int tripMeter2 = UNDEFINED;
private @Nullable OnOffType carLocked;
private @Nullable OnOffType engineRunning;
public String brakeFluid = "";
public String washerFluidLevel = "";
private @Nullable WindowsStatus windows;
private @Nullable DoorsStatus doors;
private @Nullable TyrePressure tyrePressure;
private @Nullable HvBattery hvBattery;
private @Nullable Heater heater;
public String serviceWarningStatus = "";
private @NonNullByDefault({}) List<Object> bulbFailures;
public Optional<WindowsStatus> getWindows() {
WindowsStatus windows = this.windows;
if (windows != null) {
return Optional.of(windows);
}
return Optional.empty();
}
public Optional<DoorsStatus> getDoors() {
DoorsStatus doors = this.doors;
if (doors != null) {
return Optional.of(doors);
}
return Optional.empty();
}
public Optional<TyrePressure> getTyrePressure() {
TyrePressure tyrePressure = this.tyrePressure;
if (tyrePressure != null) {
return Optional.of(tyrePressure);
}
return Optional.empty();
}
public Optional<HvBattery> getHvBattery() {
HvBattery hvBattery = this.hvBattery;
if (hvBattery != null) {
return Optional.of(hvBattery);
}
return Optional.empty();
}
public Optional<Heater> getHeater() {
Heater heater = this.heater;
if (heater != null) {
return Optional.of(heater);
}
return Optional.empty();
}
public Optional<OnOffType> getCarLocked() {
OnOffType carLocked = this.carLocked;
if (carLocked != null) {
return Optional.of(carLocked);
}
return Optional.empty();
}
public Optional<OnOffType> getEngineRunning() {
OnOffType engineRunning = this.engineRunning;
if (engineRunning != null) {
return Optional.of(engineRunning);
}
return Optional.empty();
}
public boolean aFailedBulb() {
return bulbFailures.size() > 0;
}
/*
* Currently not used in the binding, maybe interesting for the future
*
* @SerializedName("ERS")
* private ERSStatus ers;
* private ZonedDateTime averageFuelConsumptionTimestamp;
* private ZonedDateTime averageSpeedTimestamp;
* private ZonedDateTime brakeFluidTimestamp;
* private ZonedDateTime bulbFailuresTimestamp;
* private ZonedDateTime carLockedTimestamp;
* private ZonedDateTime distanceToEmptyTimestamp;
* private ZonedDateTime engineRunningTimestamp;
* private ZonedDateTime fuelAmountLevelTimestamp;
* private ZonedDateTime fuelAmountTimestamp;
* private ZonedDateTime odometerTimestamp;
* private Boolean privacyPolicyEnabled;
* private ZonedDateTime privacyPolicyEnabledTimestamp;
* private String remoteClimatizationStatus;
* private ZonedDateTime remoteClimatizationStatusTimestamp;
* private ZonedDateTime serviceWarningStatusTimestamp;
* private Object theftAlarm;
* private String timeFullyAccessibleUntil;
* private String timePartiallyAccessibleUntil;
* private ZonedDateTime tripMeter1Timestamp;
* private ZonedDateTime tripMeter2Timestamp;
* private ZonedDateTime washerFluidLevelTimestamp;
*/
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
/**
* The {@link Trip} is responsible for storing
* trip informations returned by trip rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class Trip {
public int id;
public @NonNullByDefault({}) List<TripDetail> tripDetails;
@SerializedName("trip")
public @Nullable String tripURL;
/*
* Currently unused in the binding, maybe interesting in the future
* private String name;
* private String category;
* private String userNotes;
*/
}

View File

@@ -0,0 +1,103 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.UNDEFINED;
import java.time.Duration;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* The {@link TripDetail} is responsible for storing
* trip details returned by trip rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class TripDetail {
private @Nullable Integer fuelConsumption;
private @Nullable Integer electricalConsumption;
private @Nullable Integer electricalRegeneration;
public int distance = UNDEFINED;
public int startOdometer = UNDEFINED;
public int endOdometer = UNDEFINED;
private @Nullable ZonedDateTime endTime;
private @Nullable ZonedDateTime startTime;
private @NonNullByDefault({}) PositionData startPosition;
private @NonNullByDefault({}) PositionData endPosition;
private State ZonedDateTimeToState(@Nullable ZonedDateTime datetime) {
return datetime != null ? new DateTimeType(datetime.withZoneSameInstant(ZoneId.systemDefault()))
: UnDefType.NULL;
}
private State getPositionAsState(PositionData details) {
if (details.latitude != null && details.longitude != null) {
return new PointType(details.latitude + "," + details.longitude);
}
return UnDefType.NULL;
}
public State getStartTime() {
return ZonedDateTimeToState(startTime);
}
public State getStartPosition() {
return getPositionAsState(startPosition);
}
public State getEndTime() {
return ZonedDateTimeToState(endTime);
}
public State getEndPosition() {
return getPositionAsState(endPosition);
}
public long getDurationInMinutes() {
return Duration.between(startTime, endTime).toMinutes();
}
public Optional<Integer> getFuelConsumption() {
Integer fuelConsumption = this.fuelConsumption;
if (fuelConsumption != null) {
return Optional.of(fuelConsumption);
}
return Optional.empty();
}
public Optional<Integer> getElectricalConsumption() {
Integer electricalConsumption = this.electricalConsumption;
if (electricalConsumption != null) {
return Optional.of(electricalConsumption);
}
return Optional.empty();
}
public Optional<Integer> getElectricalRegeneration() {
Integer electricalRegeneration = this.electricalRegeneration;
if (electricalRegeneration != null) {
return Optional.of(electricalRegeneration);
}
return Optional.empty();
}
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link Trips} is responsible for storing
* trip informations returned by trip rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class Trips extends VocAnswer {
public @Nullable List<Trip> trips;
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
/**
* The {@link TyrePressure} is responsible for storing
* Tyre Pressure informations returned by vehicule status rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class TyrePressure {
public @NonNullByDefault({}) String frontLeftTyrePressure;
public @NonNullByDefault({}) String frontRightTyrePressure;
public @NonNullByDefault({}) String rearLeftTyrePressure;
public @NonNullByDefault({}) String rearRightTyrePressure;
/*
* Currently unused in the binding, maybe interesting in the future
* private ZonedDateTime timestamp;
*/
}

View File

@@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
/**
* The {@link Vehicles} is responsible for storing
* informations returned by vehicule rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class Vehicles extends VocAnswer {
public @NonNullByDefault({}) String vehicleId;
@SerializedName("attributes")
public @NonNullByDefault({}) String attributesURL;
@SerializedName("status")
public @NonNullByDefault({}) String statusURL;
/*
* Currently unused in the binding, maybe interesting in the future
*
*
* private String[] vehicleAccountRelations;
*/
}

View File

@@ -0,0 +1,36 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link VocAnswer} is the base class for all Voc API requests
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public abstract class VocAnswer {
private @Nullable String errorLabel;
private @Nullable String errorDescription;
public @Nullable String getErrorLabel() {
return errorLabel;
}
public @Nullable String getErrorDescription() {
return errorDescription;
}
}

View File

@@ -0,0 +1,34 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.OpenClosedType;
/**
* The {@link WindowsStatus} is responsible for storing
* Windows Status informations returned by vehicule status rest answer
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class WindowsStatus {
public @NonNullByDefault({}) OpenClosedType frontLeftWindowOpen;
public @NonNullByDefault({}) OpenClosedType frontRightWindowOpen;
public @NonNullByDefault({}) OpenClosedType rearLeftWindowOpen;
public @NonNullByDefault({}) OpenClosedType rearRightWindowOpen;
/*
* Currently unused in the binding, maybe interesting in the future
* private ZonedDateTime timestamp;
*/
}

View File

@@ -0,0 +1,568 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.handler;
import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*;
import static org.openhab.core.library.unit.MetricPrefix.KILO;
import static org.openhab.core.library.unit.SIUnits.*;
import static org.openhab.core.library.unit.SmartHomeUnits.*;
import java.io.IOException;
import java.time.ZonedDateTime;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.volvooncall.internal.VolvoOnCallException;
import org.openhab.binding.volvooncall.internal.action.VolvoOnCallActions;
import org.openhab.binding.volvooncall.internal.config.VehicleConfiguration;
import org.openhab.binding.volvooncall.internal.dto.Attributes;
import org.openhab.binding.volvooncall.internal.dto.DoorsStatus;
import org.openhab.binding.volvooncall.internal.dto.Heater;
import org.openhab.binding.volvooncall.internal.dto.HvBattery;
import org.openhab.binding.volvooncall.internal.dto.Position;
import org.openhab.binding.volvooncall.internal.dto.Status;
import org.openhab.binding.volvooncall.internal.dto.Trip;
import org.openhab.binding.volvooncall.internal.dto.TripDetail;
import org.openhab.binding.volvooncall.internal.dto.Trips;
import org.openhab.binding.volvooncall.internal.dto.TyrePressure;
import org.openhab.binding.volvooncall.internal.dto.Vehicles;
import org.openhab.binding.volvooncall.internal.dto.WindowsStatus;
import org.openhab.binding.volvooncall.internal.wrapper.VehiclePositionWrapper;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Channel;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonSyntaxException;
/**
* The {@link VehicleHandler} is responsible for handling commands, which are sent
* to one of the channels.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class VehicleHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
private final Map<String, String> activeOptions = new HashMap<>();
private @Nullable ScheduledFuture<?> refreshJob;
private Vehicles vehicle = new Vehicles();
private VehiclePositionWrapper vehiclePosition = new VehiclePositionWrapper(new Position());
private Status vehicleStatus = new Status();
private @NonNullByDefault({}) VehicleConfiguration configuration;
private Integer lastTripId = 0;
public VehicleHandler(Thing thing, VehicleStateDescriptionProvider stateDescriptionProvider) {
super(thing);
}
private Map<String, String> discoverAttributes(VolvoOnCallBridgeHandler bridgeHandler)
throws JsonSyntaxException, IOException, VolvoOnCallException {
Attributes attributes = bridgeHandler.getURL(vehicle.attributesURL, Attributes.class);
Map<String, String> properties = new HashMap<>();
properties.put(CAR_LOCATOR, attributes.carLocatorSupported.toString());
properties.put(HONK_AND_OR_BLINK, Boolean.toString(attributes.honkAndBlinkSupported
&& attributes.honkAndBlinkVersionsSupported.contains(HONK_AND_OR_BLINK)));
properties.put(HONK_BLINK, Boolean.toString(
attributes.honkAndBlinkSupported && attributes.honkAndBlinkVersionsSupported.contains(HONK_BLINK)));
properties.put(REMOTE_HEATER, attributes.remoteHeaterSupported.toString());
properties.put(UNLOCK, attributes.unlockSupported.toString());
properties.put(LOCK, attributes.lockSupported.toString());
properties.put(JOURNAL_LOG, Boolean.toString(attributes.journalLogSupported && attributes.journalLogEnabled));
properties.put(PRECLIMATIZATION, attributes.preclimatizationSupported.toString());
properties.put(ENGINE_START, attributes.engineStartSupported.toString());
properties.put(UNLOCK_TIME, attributes.unlockTimeFrame.toString());
return properties;
}
@Override
public void initialize() {
logger.trace("Initializing the Volvo On Call handler for {}", getThing().getUID());
VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler != null) {
configuration = getConfigAs(VehicleConfiguration.class);
try {
vehicle = bridgeHandler.getURL(SERVICE_URL + "vehicles/" + configuration.vin, Vehicles.class);
if (thing.getProperties().isEmpty()) {
Map<String, String> properties = discoverAttributes(bridgeHandler);
updateProperties(properties);
}
activeOptions.putAll(thing.getProperties().entrySet().stream().filter(p -> "true".equals(p.getValue()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
if (thing.getProperties().containsKey(LAST_TRIP_ID)) {
lastTripId = Integer.parseInt(thing.getProperties().get(LAST_TRIP_ID));
}
updateStatus(ThingStatus.ONLINE);
startAutomaticRefresh(configuration.refresh);
} catch (JsonSyntaxException | IOException | VolvoOnCallException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, e.getMessage());
}
}
}
/**
* Start the job refreshing the vehicle data
*
* @param refresh : refresh frequency in minutes
*/
private void startAutomaticRefresh(int refresh) {
ScheduledFuture<?> refreshJob = this.refreshJob;
if (refreshJob == null || refreshJob.isCancelled()) {
refreshJob = scheduler.scheduleWithFixedDelay(this::queryApiAndUpdateChannels, 10, refresh,
TimeUnit.MINUTES);
}
}
private void queryApiAndUpdateChannels() {
VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler != null) {
try {
vehicleStatus = bridgeHandler.getURL(vehicle.statusURL, Status.class);
vehiclePosition = new VehiclePositionWrapper(bridgeHandler.getURL(Position.class, configuration.vin));
// Update all channels from the updated data
getThing().getChannels().stream().map(Channel::getUID)
.filter(channelUID -> isLinked(channelUID) && !LAST_TRIP_GROUP.equals(channelUID.getGroupId()))
.forEach(channelUID -> {
State state = getValue(channelUID.getGroupId(), channelUID.getIdWithoutGroup(),
vehicleStatus, vehiclePosition);
updateState(channelUID, state);
});
updateTrips(bridgeHandler);
} catch (VolvoOnCallException e) {
logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
freeRefreshJob();
startAutomaticRefresh(configuration.refresh);
}
}
}
private void freeRefreshJob() {
ScheduledFuture<?> refreshJob = this.refreshJob;
if (refreshJob != null) {
refreshJob.cancel(true);
this.refreshJob = null;
}
}
@Override
public void dispose() {
freeRefreshJob();
super.dispose();
}
private void updateTrips(VolvoOnCallBridgeHandler bridgeHandler) throws VolvoOnCallException {
// This seems to rewind 100 days by default, did not find any way to filter it
Trips carTrips = bridgeHandler.getURL(Trips.class, configuration.vin);
List<Trip> tripList = carTrips.trips;
if (tripList != null) {
List<Trip> newTrips = tripList.stream().filter(trip -> trip.id >= lastTripId).collect(Collectors.toList());
Collections.reverse(newTrips);
logger.debug("Trips discovered : {}", newTrips.size());
if (!newTrips.isEmpty()) {
Integer newTripId = newTrips.get(newTrips.size() - 1).id;
if (newTripId > lastTripId) {
updateProperty(LAST_TRIP_ID, newTripId.toString());
lastTripId = newTripId;
}
newTrips.stream().map(t -> t.tripDetails.get(0)).forEach(catchUpTrip -> {
logger.debug("Trip found {}", catchUpTrip.getStartTime());
getThing().getChannels().stream().map(Channel::getUID).filter(
channelUID -> isLinked(channelUID) && LAST_TRIP_GROUP.equals(channelUID.getGroupId()))
.forEach(channelUID -> {
State state = getTripValue(channelUID.getIdWithoutGroup(), catchUpTrip);
updateState(channelUID, state);
});
});
}
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
String channelID = channelUID.getIdWithoutGroup();
if (command instanceof RefreshType) {
queryApiAndUpdateChannels();
} else if (command instanceof OnOffType) {
OnOffType onOffCommand = (OnOffType) command;
if (ENGINE_START.equals(channelID) && onOffCommand == OnOffType.ON) {
actionStart(5);
} else if (REMOTE_HEATER.equals(channelID)) {
actionHeater(onOffCommand == OnOffType.ON);
} else if (PRECLIMATIZATION.equals(channelID)) {
actionPreclimatization(onOffCommand == OnOffType.ON);
} else if (CAR_LOCKED.equals(channelID)) {
if (onOffCommand == OnOffType.ON) {
actionClose();
} else {
actionOpen();
}
}
}
}
private State getTripValue(String channelId, TripDetail tripDetails) {
switch (channelId) {
case TRIP_CONSUMPTION:
return tripDetails.getFuelConsumption()
.map(value -> (State) new QuantityType<>(value.floatValue() / 100, LITRE))
.orElse(UnDefType.UNDEF);
case TRIP_DISTANCE:
return new QuantityType<>((double) tripDetails.distance / 1000, KILO(METRE));
case TRIP_START_TIME:
return tripDetails.getStartTime();
case TRIP_END_TIME:
return tripDetails.getEndTime();
case TRIP_DURATION:
return new QuantityType<>(tripDetails.getDurationInMinutes(), MINUTE);
case TRIP_START_ODOMETER:
return new QuantityType<>((double) tripDetails.startOdometer / 1000, KILO(METRE));
case TRIP_STOP_ODOMETER:
return new QuantityType<>((double) tripDetails.endOdometer / 1000, KILO(METRE));
case TRIP_START_POSITION:
return tripDetails.getStartPosition();
case TRIP_END_POSITION:
return tripDetails.getEndPosition();
}
return UnDefType.NULL;
}
private State getDoorsValue(String channelId, DoorsStatus doors) {
switch (channelId) {
case TAILGATE:
return doors.tailgateOpen;
case REAR_RIGHT:
return doors.rearRightDoorOpen;
case REAR_LEFT:
return doors.rearLeftDoorOpen;
case FRONT_RIGHT:
return doors.frontRightDoorOpen;
case FRONT_LEFT:
return doors.frontLeftDoorOpen;
case HOOD:
return doors.hoodOpen;
}
return UnDefType.NULL;
}
private State getWindowsValue(String channelId, WindowsStatus windows) {
switch (channelId) {
case REAR_RIGHT_WND:
return windows.rearRightWindowOpen;
case REAR_LEFT_WND:
return windows.rearLeftWindowOpen;
case FRONT_RIGHT_WND:
return windows.frontRightWindowOpen;
case FRONT_LEFT_WND:
return windows.frontLeftWindowOpen;
}
return UnDefType.NULL;
}
private State getTyresValue(String channelId, TyrePressure tyrePressure) {
switch (channelId) {
case REAR_RIGHT_TYRE:
return new StringType(tyrePressure.rearRightTyrePressure);
case REAR_LEFT_TYRE:
return new StringType(tyrePressure.rearLeftTyrePressure);
case FRONT_RIGHT_TYRE:
return new StringType(tyrePressure.frontRightTyrePressure);
case FRONT_LEFT_TYRE:
return new StringType(tyrePressure.frontLeftTyrePressure);
}
return UnDefType.NULL;
}
private State getHeaterValue(String channelId, Heater heater) {
switch (channelId) {
case REMOTE_HEATER:
case PRECLIMATIZATION:
return heater.getStatus();
}
return UnDefType.NULL;
}
private State getBatteryValue(String channelId, HvBattery hvBattery) {
switch (channelId) {
case BATTERY_LEVEL:
return hvBattery.hvBatteryLevel != UNDEFINED ? new QuantityType<>(hvBattery.hvBatteryLevel, PERCENT)
: UnDefType.UNDEF;
case BATTERY_DISTANCE_TO_EMPTY:
return hvBattery.distanceToHVBatteryEmpty != UNDEFINED
? new QuantityType<>(hvBattery.distanceToHVBatteryEmpty, KILO(METRE))
: UnDefType.UNDEF;
case CHARGE_STATUS:
return hvBattery.hvBatteryChargeStatusDerived != null
? new StringType(hvBattery.hvBatteryChargeStatusDerived)
: UnDefType.UNDEF;
case TIME_TO_BATTERY_FULLY_CHARGED:
return hvBattery.timeToHVBatteryFullyCharged != UNDEFINED
? new QuantityType<>(hvBattery.timeToHVBatteryFullyCharged, MINUTE)
: UnDefType.UNDEF;
case CHARGING_END:
return hvBattery.timeToHVBatteryFullyCharged != UNDEFINED && hvBattery.timeToHVBatteryFullyCharged > 0
? new DateTimeType(ZonedDateTime.now().plusMinutes(hvBattery.timeToHVBatteryFullyCharged))
: UnDefType.UNDEF;
}
return UnDefType.NULL;
}
private State getValue(@Nullable String groupId, String channelId, Status status, VehiclePositionWrapper position) {
switch (channelId) {
case ODOMETER:
return status.odometer != UNDEFINED ? new QuantityType<>((double) status.odometer / 1000, KILO(METRE))
: UnDefType.UNDEF;
case TRIPMETER1:
return status.tripMeter1 != UNDEFINED
? new QuantityType<>((double) status.tripMeter1 / 1000, KILO(METRE))
: UnDefType.UNDEF;
case TRIPMETER2:
return status.tripMeter2 != UNDEFINED
? new QuantityType<>((double) status.tripMeter2 / 1000, KILO(METRE))
: UnDefType.UNDEF;
case DISTANCE_TO_EMPTY:
return status.distanceToEmpty != UNDEFINED ? new QuantityType<>(status.distanceToEmpty, KILO(METRE))
: UnDefType.UNDEF;
case FUEL_AMOUNT:
return status.fuelAmount != UNDEFINED ? new QuantityType<>(status.fuelAmount, LITRE) : UnDefType.UNDEF;
case FUEL_LEVEL:
return status.fuelAmountLevel != UNDEFINED ? new QuantityType<>(status.fuelAmountLevel, PERCENT)
: UnDefType.UNDEF;
case FUEL_CONSUMPTION:
return status.averageFuelConsumption != UNDEFINED ? new DecimalType(status.averageFuelConsumption / 10)
: UnDefType.UNDEF;
case ACTUAL_LOCATION:
return position.getPosition();
case CALCULATED_LOCATION:
return position.isCalculated();
case HEADING:
return position.isHeading();
case LOCATION_TIMESTAMP:
return position.getTimestamp();
case CAR_LOCKED:
// Warning : carLocked is in the Doors group but is part of general status informations.
// Did not change it to avoid breaking change for users
return status.getCarLocked().map(State.class::cast).orElse(UnDefType.UNDEF);
case ENGINE_RUNNING:
return status.getEngineRunning().map(State.class::cast).orElse(UnDefType.UNDEF);
case BRAKE_FLUID_LEVEL:
return new StringType(status.brakeFluid);
case WASHER_FLUID_LEVEL:
return new StringType(status.washerFluidLevel);
case AVERAGE_SPEED:
return status.averageSpeed != UNDEFINED ? new QuantityType<>(status.averageSpeed, KILOMETRE_PER_HOUR)
: UnDefType.UNDEF;
case SERVICE_WARNING:
return new StringType(status.serviceWarningStatus);
case FUEL_ALERT:
return status.distanceToEmpty < 100 ? OnOffType.ON : OnOffType.OFF;
case BULB_FAILURE:
return status.aFailedBulb() ? OnOffType.ON : OnOffType.OFF;
case REMOTE_HEATER:
case PRECLIMATIZATION:
return status.getHeater().map(heater -> getHeaterValue(channelId, heater)).orElse(UnDefType.NULL);
}
if (groupId != null) {
switch (groupId) {
case GROUP_DOORS:
return status.getDoors().map(doors -> getDoorsValue(channelId, doors)).orElse(UnDefType.NULL);
case GROUP_WINDOWS:
return status.getWindows().map(windows -> getWindowsValue(channelId, windows))
.orElse(UnDefType.NULL);
case GROUP_TYRES:
return status.getTyrePressure().map(tyres -> getTyresValue(channelId, tyres))
.orElse(UnDefType.NULL);
case GROUP_BATTERY:
return status.getHvBattery().map(batteries -> getBatteryValue(channelId, batteries))
.orElse(UnDefType.NULL);
}
}
return UnDefType.NULL;
}
public void actionHonkBlink(Boolean honk, Boolean blink) {
VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler != null) {
StringBuilder url = new StringBuilder(SERVICE_URL + "vehicles/" + vehicle.vehicleId + "/honk_blink/");
if (honk && blink && activeOptions.containsKey(HONK_BLINK)) {
url.append("both");
} else if (honk && activeOptions.containsKey(HONK_AND_OR_BLINK)) {
url.append("horn");
} else if (blink && activeOptions.containsKey(HONK_AND_OR_BLINK)) {
url.append("lights");
} else {
logger.warn("The vehicle is not capable of this action");
return;
}
try {
bridgeHandler.postURL(url.toString(), vehiclePosition.getPositionAsJSon());
} catch (VolvoOnCallException e) {
logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
}
private void actionOpenClose(String action, OnOffType controlState) {
VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler != null) {
if (activeOptions.containsKey(action)) {
if (!vehicleStatus.getCarLocked().isPresent() || vehicleStatus.getCarLocked().get() != controlState) {
try {
StringBuilder address = new StringBuilder(SERVICE_URL);
address.append("vehicles/");
address.append(configuration.vin);
address.append("/");
address.append(action);
bridgeHandler.postURL(address.toString(), "{}");
} catch (VolvoOnCallException e) {
logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
} else {
logger.info("The car {} is already {}ed", configuration.vin, action);
}
} else {
logger.warn("The car {} does not support remote {}ing", configuration.vin, action);
}
}
}
private void actionHeater(String action, Boolean start) {
VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler != null) {
if (activeOptions.containsKey(action)) {
try {
if (action.contains(REMOTE_HEATER)) {
String command = start ? "start" : "stop";
String address = SERVICE_URL + "vehicles/" + configuration.vin + "/heater/" + command;
bridgeHandler.postURL(address, start ? "{}" : null);
} else if (action.contains(PRECLIMATIZATION)) {
String command = start ? "start" : "stop";
String address = SERVICE_URL + "vehicles/" + configuration.vin + "/preclimatization/" + command;
bridgeHandler.postURL(address, start ? "{}" : null);
}
} catch (VolvoOnCallException e) {
logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
} else {
logger.warn("The car {} does not support {}", configuration.vin, action);
}
}
}
public void actionHeater(Boolean start) {
actionHeater(REMOTE_HEATER, start);
}
public void actionPreclimatization(Boolean start) {
actionHeater(PRECLIMATIZATION, start);
}
public void actionOpen() {
actionOpenClose(UNLOCK, OnOffType.OFF);
}
public void actionClose() {
actionOpenClose(LOCK, OnOffType.ON);
}
public void actionStart(Integer runtime) {
VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
if (bridgeHandler != null) {
if (activeOptions.containsKey(ENGINE_START)) {
String url = SERVICE_URL + "vehicles/" + vehicle.vehicleId + "/engine/start";
String json = "{\"runtime\":" + runtime.toString() + "}";
try {
bridgeHandler.postURL(url, json);
} catch (VolvoOnCallException e) {
logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
} else {
logger.warn("The car {} does not support remote engine starting", vehicle.vehicleId);
}
}
}
/*
* Called by Bridge when it has to notify this of a potential state
* update
*
*/
void updateIfMatches(String vin) {
if (vin.equalsIgnoreCase(configuration.vin)) {
queryApiAndUpdateChannels();
}
}
private @Nullable VolvoOnCallBridgeHandler getBridgeHandler() {
Bridge bridge = getBridge();
if (bridge != null) {
BridgeHandler handler = bridge.getHandler();
if (handler != null) {
return (VolvoOnCallBridgeHandler) handler;
}
}
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
return null;
}
@Override
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singletonList(VolvoOnCallActions.class);
}
}

View File

@@ -0,0 +1,42 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.handler;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.binding.BaseDynamicStateDescriptionProvider;
import org.openhab.core.thing.i18n.ChannelTypeI18nLocalizationService;
import org.openhab.core.thing.type.DynamicStateDescriptionProvider;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
/**
* Dynamic provider of state options for VehicleHandler.
*
* @author Gregory Moyer - Initial contribution
* @author Gaël L'hopital - Copied as-is in VolvoOnCall binding
*/
@Component(service = { DynamicStateDescriptionProvider.class, VehicleStateDescriptionProvider.class })
@NonNullByDefault
public class VehicleStateDescriptionProvider extends BaseDynamicStateDescriptionProvider {
@Reference
protected void setChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = channelTypeI18nLocalizationService;
}
protected void unsetChannelTypeI18nLocalizationService(
final ChannelTypeI18nLocalizationService channelTypeI18nLocalizationService) {
this.channelTypeI18nLocalizationService = null;
}
}

View File

@@ -0,0 +1,201 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.handler;
import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Properties;
import java.util.Stack;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.volvooncall.internal.VolvoOnCallException;
import org.openhab.binding.volvooncall.internal.VolvoOnCallException.ErrorType;
import org.openhab.binding.volvooncall.internal.config.VolvoOnCallBridgeConfiguration;
import org.openhab.binding.volvooncall.internal.dto.CustomerAccounts;
import org.openhab.binding.volvooncall.internal.dto.PostResponse;
import org.openhab.binding.volvooncall.internal.dto.VocAnswer;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonSyntaxException;
/**
* The {@link VolvoOnCallBridgeHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class VolvoOnCallBridgeHandler extends BaseBridgeHandler {
private static final int REQUEST_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(20);
private final Logger logger = LoggerFactory.getLogger(VolvoOnCallBridgeHandler.class);
private final Properties httpHeader = new Properties();
private final List<ScheduledFuture<?>> pendingActions = new Stack<>();
private final Gson gson;
private @NonNullByDefault({}) CustomerAccounts customerAccount;
public VolvoOnCallBridgeHandler(Bridge bridge) {
super(bridge);
httpHeader.put("cache-control", "no-cache");
httpHeader.put("content-type", JSON_CONTENT_TYPE);
httpHeader.put("x-device-id", "Device");
httpHeader.put("x-originator-type", "App");
httpHeader.put("x-os-type", "Android");
httpHeader.put("x-os-version", "22");
httpHeader.put("Accept", "*/*");
gson = new GsonBuilder()
.registerTypeAdapter(ZonedDateTime.class,
(JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> ZonedDateTime
.parse(json.getAsJsonPrimitive().getAsString().replaceAll("\\+0000", "Z")))
.registerTypeAdapter(OpenClosedType.class,
(JsonDeserializer<OpenClosedType>) (json, type,
jsonDeserializationContext) -> json.getAsBoolean() ? OpenClosedType.OPEN
: OpenClosedType.CLOSED)
.registerTypeAdapter(OnOffType.class,
(JsonDeserializer<OnOffType>) (json, type,
jsonDeserializationContext) -> json.getAsBoolean() ? OnOffType.ON : OnOffType.OFF)
.create();
}
@Override
public void initialize() {
logger.debug("Initializing VolvoOnCall API bridge handler.");
VolvoOnCallBridgeConfiguration configuration = getConfigAs(VolvoOnCallBridgeConfiguration.class);
httpHeader.setProperty("Authorization", configuration.getAuthorization());
try {
customerAccount = getURL(SERVICE_URL + "customeraccounts/", CustomerAccounts.class);
if (customerAccount.username != null) {
updateStatus(ThingStatus.ONLINE);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Incorrect username or password");
}
} catch (JsonSyntaxException | VolvoOnCallException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("VolvoOnCall Bridge is read-only and does not handle commands");
}
public String[] getVehiclesRelationsURL() {
if (customerAccount != null) {
return customerAccount.accountVehicleRelationsURL;
}
return new String[0];
}
public <T extends VocAnswer> T getURL(Class<T> objectClass, String vin) throws VolvoOnCallException {
String url = SERVICE_URL + "vehicles/" + vin + "/" + objectClass.getSimpleName().toLowerCase();
return getURL(url, objectClass);
}
public <T extends VocAnswer> T getURL(String url, Class<T> objectClass) throws VolvoOnCallException {
try {
String jsonResponse = HttpUtil.executeUrl("GET", url, httpHeader, null, JSON_CONTENT_TYPE, REQUEST_TIMEOUT);
logger.debug("Request for : {}", url);
logger.debug("Received : {}", jsonResponse);
T response = gson.fromJson(jsonResponse, objectClass);
String error = response.getErrorLabel();
if (error != null) {
throw new VolvoOnCallException(error, response.getErrorDescription());
}
return response;
} catch (JsonSyntaxException | IOException e) {
throw new VolvoOnCallException(e);
}
}
public class ActionResultControler implements Runnable {
PostResponse postResponse;
ActionResultControler(PostResponse postResponse) {
this.postResponse = postResponse;
}
@Override
public void run() {
switch (postResponse.status) {
case SUCCESSFULL:
case FAILED:
logger.info("Action status : {} for vehicle : {}.", postResponse.status.toString(),
postResponse.vehicleId);
getThing().getThings().stream().filter(VehicleHandler.class::isInstance)
.map(VehicleHandler.class::cast)
.forEach(handler -> handler.updateIfMatches(postResponse.vehicleId));
break;
default:
try {
postResponse = getURL(postResponse.serviceURL, PostResponse.class);
scheduler.schedule(new ActionResultControler(postResponse), 1000, TimeUnit.MILLISECONDS);
} catch (VolvoOnCallException e) {
if (e.getType() == ErrorType.SERVICE_UNAVAILABLE) {
scheduler.schedule(new ActionResultControler(postResponse), 1000, TimeUnit.MILLISECONDS);
}
}
}
}
}
void postURL(String URL, @Nullable String body) throws VolvoOnCallException {
InputStream inputStream = body != null ? new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)) : null;
try {
String jsonString = HttpUtil.executeUrl("POST", URL, httpHeader, inputStream, null, REQUEST_TIMEOUT);
logger.debug("Post URL: {} Attributes {}", URL, httpHeader);
PostResponse postResponse = gson.fromJson(jsonString, PostResponse.class);
String error = postResponse.getErrorLabel();
if (error == null) {
pendingActions
.add(scheduler.schedule(new ActionResultControler(postResponse), 1000, TimeUnit.MILLISECONDS));
} else {
throw new VolvoOnCallException(error, postResponse.getErrorDescription());
}
pendingActions.removeIf(ScheduledFuture::isDone);
} catch (JsonSyntaxException | IOException e) {
throw new VolvoOnCallException(e);
}
}
@Override
public void dispose() {
super.dispose();
pendingActions.stream().filter(f -> !f.isCancelled()).forEach(f -> f.cancel(true));
}
}

View File

@@ -0,0 +1,88 @@
/**
* Copyright (c) 2010-2020 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.volvooncall.internal.wrapper;
import java.time.ZoneId;
import java.util.Optional;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.volvooncall.internal.dto.Position;
import org.openhab.binding.volvooncall.internal.dto.PositionData;
import org.openhab.core.library.types.DateTimeType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.PointType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
/**
* The {@link VehiclePositionWrapper} stores provides utility functions
* over a {@link Position} provided by the rest API
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class VehiclePositionWrapper {
private final Optional<PositionData> position;
private boolean isCalculated;
public VehiclePositionWrapper(Position vehicle) {
if (vehicle.calculatedPosition != null && vehicle.position.latitude != null) {
position = Optional.of(vehicle.position);
isCalculated = false;
} else if (vehicle.calculatedPosition != null && vehicle.calculatedPosition.latitude != null) {
position = Optional.of(vehicle.calculatedPosition);
isCalculated = true;
} else {
position = Optional.empty();
}
}
private State getPositionAsState(PositionData details) {
if (details.latitude != null && details.longitude != null) {
return new PointType(details.latitude + "," + details.longitude);
}
return UnDefType.NULL;
}
public State getPosition() {
return position.map(pos -> getPositionAsState(pos)).orElse(UnDefType.UNDEF);
}
public @Nullable String getPositionAsJSon() {
if (getPosition() != UnDefType.UNDEF) {
StringBuilder json = new StringBuilder("{\"clientLatitude\":");
json.append(position.get().latitude);
json.append(",\"clientLongitude\":");
json.append(position.get().longitude);
json.append(",\"clientAccuracy\":0}");
return json.toString();
}
return null;
}
public State isCalculated() {
return position.map(pos -> isCalculated ? (State) OnOffType.ON : OnOffType.OFF).orElse(UnDefType.UNDEF);
}
public State isHeading() {
return position.map(pos -> pos.isHeading() ? (State) OnOffType.ON : OnOffType.OFF).orElse(UnDefType.UNDEF);
}
public State getTimestamp() {
return position.flatMap(pos -> pos.getTimestamp())
.map(dt -> (State) new DateTimeType(dt.withZoneSameInstant(ZoneId.systemDefault())))
.orElse(UnDefType.NULL);
}
}

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="volvooncall" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>VolvoOnCall Binding</name>
<description>This binding enables the access to VolvoOnCall features.</description>
<author>Gaël L'hopital</author>
</binding:binding>

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<config-description:config-descriptions
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:config-description="https://openhab.org/schemas/config-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/config-description/v1.0.0
https://openhab.org/schemas/config-description-1.0.0.xsd">
<config-description uri="thing-type:volvooncall:vocapi">
<parameter name="username" type="text">
<label>Username</label>
<description>Your VOC username (email)</description>
<required>true</required>
</parameter>
<parameter name="password" type="text">
<label>Password</label>
<description>Your VOC password</description>
<required>true</required>
<context>password</context>
</parameter>
</config-description>
</config-description:config-descriptions>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="volvooncall"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- VOC API Bridge -->
<bridge-type id="vocapi">
<label>VolvoOnCall API</label>
<description>This bridge represents the gateway to Volvo On Call API.</description>
<config-description-ref uri="thing-type:volvooncall:vocapi"/>
</bridge-type>
</thing:thing-descriptions>

View File

@@ -0,0 +1,391 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="volvooncall"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<!-- Volvo Vehicle -->
<thing-type id="vehicle">
<supported-bridge-type-refs>
<bridge-type-ref id="vocapi"/>
</supported-bridge-type-refs>
<label>Volvo Vehicle</label>
<channel-groups>
<channel-group id="doors" typeId="doors"/>
<channel-group id="windows" typeId="windows"/>
<channel-group id="odometer" typeId="odometer"/>
<channel-group id="tank" typeId="tank"/>
<channel-group id="position" typeId="position"/>
<channel-group id="tyrePressure" typeId="tyrePressure"/>
<channel-group id="battery" typeId="battery"/>
<channel-group id="other" typeId="other"/>
<channel-group id="lasttrip" typeId="lasttrip"/>
</channel-groups>
<representation-property>vin</representation-property>
<config-description>
<parameter name="vin" type="text" required="true">
<label>Vehicle Identification Number</label>
<description>VIN of the vehicle associated with this Thing</description>
</parameter>
<parameter name="refresh" type="integer" min="5" required="false">
<label>Refresh Interval</label>
<description>Specifies the refresh interval in minutes.</description>
<default>5</default>
</parameter>
</config-description>
</thing-type>
<channel-group-type id="other">
<label>Other</label>
<channels>
<channel id="averageSpeed" typeId="averageSpeed"/>
<channel id="engineRunning" typeId="engineRunning"/>
<channel id="remoteHeater" typeId="remoteHeater"/>
<channel id="preclimatization" typeId="preclimatization"/>
<channel id="brakeFluidLevel" typeId="brakeFluidLevel"/>
<channel id="washerFluidLevel" typeId="washerFluidLevel"/>
<channel id="serviceWarningStatus" typeId="serviceWarningStatus"/>
<channel id="bulbFailure" typeId="bulbFailure"/>
</channels>
</channel-group-type>
<channel-group-type id="lasttrip">
<label>Last Trip</label>
<channels>
<channel id="tripConsumption" typeId="fuelQuantity">
<label>Consumption</label>
<description>Indicates the quantity of fuel consumed by the trip</description>
</channel>
<channel id="tripDistance" typeId="odometer">
<label>Distance</label>
<description>Distance traveled</description>
</channel>
<channel id="tripStartTime" typeId="timestamp">
<label>Start Time</label>
<description>Trip start time</description>
</channel>
<channel id="tripEndTime" typeId="timestamp">
<label>End Time</label>
<description>Trip end time</description>
</channel>
<channel id="tripDuration" typeId="tripDuration"/>
<channel id="tripStartOdometer" typeId="odometer">
<label>Start Odometer</label>
</channel>
<channel id="tripStopOdometer" typeId="odometer">
<label>Stop Odometer</label>
</channel>
<channel id="startPosition" typeId="location">
<label>From</label>
<description>Starting location of the car</description>
</channel>
<channel id="endPosition" typeId="location">
<label>To</label>
<description>Stopping location of the car</description>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="doors">
<label>Doors Opening Status</label>
<channels>
<channel id="frontLeft" typeId="door">
<label>Front Left</label>
</channel>
<channel id="frontRight" typeId="door">
<label>Front Right</label>
</channel>
<channel id="rearLeft" typeId="door">
<label>Rear Left</label>
</channel>
<channel id="rearRight" typeId="door">
<label>Rear Right</label>
</channel>
<channel id="hood" typeId="door">
<label>Hood</label>
</channel>
<channel id="tailgate" typeId="door">
<label>Tailgate</label>
</channel>
<channel id="carLocked" typeId="carLocked"/>
</channels>
</channel-group-type>
<channel-group-type id="windows">
<label>Windows Opening Status</label>
<channels>
<channel id="frontLeftWnd" typeId="window">
<label>Front Left</label>
</channel>
<channel id="frontRightWnd" typeId="window">
<label>Front Right</label>
</channel>
<channel id="rearLeftWnd" typeId="window">
<label>Rear Left</label>
</channel>
<channel id="rearRightWnd" typeId="window">
<label>Rear Right</label>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="tyrePressure">
<label>Tyre pressure status</label>
<channels>
<channel id="frontLeftTyre" typeId="tyrePressure">
<label>Front Left</label>
</channel>
<channel id="frontRightTyre" typeId="tyrePressure">
<label>Front Right</label>
</channel>
<channel id="rearLeftTyre" typeId="tyrePressure">
<label>Rear Left</label>
</channel>
<channel id="rearRightTyre" typeId="tyrePressure">
<label>Rear Right</label>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="odometer">
<label>Trip Meters</label>
<channels>
<channel id="odometer" typeId="odometer"/>
<channel id="tripmeter1" typeId="odometer">
<label>Tripmeter 1</label>
</channel>
<channel id="tripmeter2" typeId="odometer">
<label>Tripmeter 2</label>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="tank">
<label>Tank Info</label>
<channels>
<channel id="fuelAmount" typeId="fuelQuantity">
<label>Fuel Amount</label>
<description>Indicates the quantity of fuel available in the tank</description>
</channel>
<channel id="fuelLevel" typeId="fuelLevel"/>
<channel id="fuelConsumption" typeId="fuelConsumption"/>
<channel id="fuelAlert" typeId="fuelAlert"/>
<channel id="distanceToEmpty" typeId="odometer">
<label>Distance Left</label>
<description>Distance left with given quantity of fuel</description>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="position">
<label>Location Info</label>
<channels>
<channel id="location" typeId="location">
<label>Location</label>
<description>The position of the vehicle</description>
</channel>
<channel id="calculatedLocation" typeId="calculatedLocation"/>
<channel id="heading" typeId="heading"/>
<channel id="locationTimestamp" typeId="timestamp">
<label>Location Timestamp</label>
<description>Timestamp of location value update</description>
</channel>
</channels>
</channel-group-type>
<channel-group-type id="battery">
<label>Plugin Hybrid / Twin Engine info</label>
<channels>
<channel id="batteryLevel" typeId="batteryLevel"/>
<channel id="batteryDistanceToEmpty" typeId="odometer">
<label>Distance to battery empty</label>
</channel>
<channel id="chargeStatus" typeId="chargeStatus"/>
<channel id="timeToHVBatteryFullyCharged" typeId="timeToHVBatteryFullyCharged"/>
<channel id="chargingEnd" typeId="timestamp">
<label>Charging end</label>
</channel>
</channels>
</channel-group-type>
<channel-type id="door">
<item-type>Contact</item-type>
<label>Door</label>
<description>Indicates if the door is opened</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="tripDuration">
<item-type>Number:Time</item-type>
<label>Duration</label>
<description>Trip Duration</description>
<state pattern="%d %unit%" readOnly="true"></state>
</channel-type>
<channel-type id="window">
<item-type>Contact</item-type>
<label>Window Status</label>
<description>Indicates if the window is opened</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="odometer">
<item-type>Number:Length</item-type>
<label>Odometer</label>
<description>Odometer of the vehicle</description>
<state pattern="%.2f %unit%" readOnly="true"></state>
</channel-type>
<channel-type id="averageSpeed">
<item-type>Number:Speed</item-type>
<label>Average speed</label>
<description>Average speed of the vehicle</description>
<state pattern="%d %unit%" readOnly="true"></state>
</channel-type>
<channel-type id="fuelQuantity">
<item-type>Number:Volume</item-type>
<label>Fuel Quantity</label>
<state pattern="%.2f %unit%" readOnly="true"></state>
</channel-type>
<channel-type id="fuelLevel">
<item-type>Number:Dimensionless</item-type>
<label>Fuel Level</label>
<description>Indicates the level of fuel in the tank</description>
<state pattern="%d %unit%" readOnly="true"></state>
</channel-type>
<channel-type id="fuelConsumption" advanced="true">
<item-type>Number</item-type>
<label>Average Consumption</label>
<description>Indicates the average fuel consumption in L/100km</description>
<state pattern="%.1f L/100km" readOnly="true"></state>
</channel-type>
<channel-type id="location">
<item-type>Location</item-type>
<label>Location</label>
<category>Location</category>
<state readOnly="true"></state>
</channel-type>
<channel-type id="calculatedLocation" advanced="true">
<item-type>Switch</item-type>
<label>Calculated Location</label>
<description>Indicates if the location is actual or calculated</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="heading" advanced="true">
<item-type>Switch</item-type>
<label>Heading</label>
<state readOnly="true"></state>
</channel-type>
<channel-type id="timestamp">
<item-type>DateTime</item-type>
<label>Timestamp</label>
<description>Data timestamp</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="engineStart">
<item-type>Switch</item-type>
<label>Start Engine</label>
<description>Starts the vehicle engine</description>
</channel-type>
<channel-type id="carLocked">
<item-type>Switch</item-type>
<label>Locked</label>
<description>Car locking status</description>
</channel-type>
<channel-type id="engineRunning">
<item-type>Switch</item-type>
<label>Engine Started</label>
<description>Engine status (running or not)</description>
</channel-type>
<channel-type id="preclimatization">
<item-type>Switch</item-type>
<label>Preclimatization</label>
<description>Starts pre-climatization</description>
</channel-type>
<channel-type id="remoteHeater">
<item-type>Switch</item-type>
<label>Remote Heater</label>
<description>(De)Activates remote heater</description>
</channel-type>
<channel-type id="bulbFailure">
<item-type>Switch</item-type>
<label>Bulb Failure</label>
<description>At least on bulb is reported as dead</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="brakeFluidLevel">
<item-type>String</item-type>
<label>Brake Fluid</label>
<description>“VeryLow”,"Low", "Normal"</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="washerFluidLevel">
<item-type>String</item-type>
<label>Washer Fluid</label>
<description>“VeryLow”,"Low", "Normal"</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="tyrePressure">
<item-type>String</item-type>
<label>Tyre pressure</label>
<description>“LowSoft”, "Normal"</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="serviceWarningStatus">
<item-type>String</item-type>
<label>Service Warning</label>
<description>Is car service needed ?</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="batteryLevel">
<item-type>Number:Dimensionless</item-type>
<label>Battery Level</label>
<description>Indicates the level of power in the battery (in case of PHEV / Twin Engine)</description>
<state pattern="%d %unit%" readOnly="true"></state>
</channel-type>
<channel-type id="chargeStatus">
<item-type>String</item-type>
<label>Charging status</label>
<description>Status of charging (in case of PHEV / Twin Engine)</description>
<state readOnly="true"></state>
</channel-type>
<channel-type id="timeToHVBatteryFullyCharged">
<item-type>Number:Time</item-type>
<label>Time to battery fully charged</label>
<description>Time in seconds until the battery is fully charged (in case of PHEV / Twin Engine)</description>
<state pattern="%d %unit%" readOnly="true"></state>
</channel-type>
<channel-type id="fuelAlert">
<item-type>Switch</item-type>
<label>Fuel Alarm</label>
<description>set to 'ON' when the tank level is low</description>
<state readOnly="true"/>
</channel-type>
</thing:thing-descriptions>