[VolvoOnCall] OH3 update (#8595)

* [VolvoOnCall] OH3 update

Solving issue #8554  and issue #8518
Some code corrections and enhancements.
Introduced new trigger channels.

* Doing spotless apply

* Some other code improvements

* Moving back to Jetty HttpClient
Introduced Expiring cache for request to avoid flooding voc servers
Reduced the number of requests emitted
Changed  user agent identification

* Correcting spotless
* Pleasing Travis
* Code review corrections
* Adressing cpmeister code review

Signed-off-by: clinique <gael@lhopital.org>
This commit is contained in:
Gaël L'hopital
2020-10-03 20:59:14 +02:00
committed by GitHub
parent d42efe0b22
commit 5648f5e51d
23 changed files with 761 additions and 673 deletions

View File

@@ -20,10 +20,10 @@ The binding has no configuration options itself, all configuration is done at 'T
The 'VolvoOnCall API' bridge uses the owner's email address and password in order to access the VOC Remote API. The 'VolvoOnCall API' bridge uses the owner's email address and password in order to access the VOC Remote API.
This is the same email address and password as used in the VolvoOnCall smartphone app, that allows to remotely control your car(s). This is the same email address and password as used in the VolvoOnCall smartphone app, that allows to remotely control your car(s).
| Parameter | Description | Required | | Parameter | Description | Required |
|-----------|------------------------------------------------------------------------- |--------- | |-----------------|------------------------------------------------------|--------- |
| username | Username from the VolvoOnCall app (email address) | yes | | username | Username from the VolvoOnCall app (email address) | yes |
| password | Password from the VolvoOnCall app | yes | | password | Password from the VolvoOnCall app | yes |
Once the bridge created, you will be able to launch discovery of the vehicles attached to it. Once the bridge created, you will be able to launch discovery of the vehicles attached to it.
@@ -35,7 +35,8 @@ The 'VolvoOnCall API' bridge uses the owner's email address and password in orde
| Parameter | Name | Description | Required | | Parameter | Name | Description | Required |
|-----------------|------------------|---------------------------------------------------------|----------| |-----------------|------------------|---------------------------------------------------------|----------|
| vin | Vin | Vehicle Identification Number of the car | yes | | vin | Vin | Vehicle Identification Number of the car | yes |
| refreshinterval | Refresh interval | Interval in minutes to refresh the data (default=10) | no | | refreshinterval | Refresj Interval | Interval in minutes to refresh the data (default=10) | yes |
@@ -100,6 +101,15 @@ Following channels are currently available:
| lasttrip#endPosition | Location | Last trip end location | | | lasttrip#endPosition | Location | Last trip end location | |
## Events
| Channel Type ID | Options | Description |
|--------------------|-------------|----------------------------------------------------------------|
| other#carEvent | | |
| | CAR_STOPPED | Triggered when the car has finished a trip |
| | CAR_MOVED | Triggered if the car mileage has changed between two polls |
| | CAR_STARTED | Triggered when the engine of the car went on between two polls |
## Full Example ## Full Example
demo.things: demo.things:

View File

@@ -30,16 +30,6 @@ public class VolvoOnCallBindingConstants {
public static final String BINDING_ID = "volvooncall"; 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 // List of Thing Type UIDs
public static final ThingTypeUID APIBRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "vocapi"); public static final ThingTypeUID APIBRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "vocapi");
public static final ThingTypeUID VEHICLE_THING_TYPE = new ThingTypeUID(BINDING_ID, "vehicle"); public static final ThingTypeUID VEHICLE_THING_TYPE = new ThingTypeUID(BINDING_ID, "vehicle");
@@ -49,6 +39,10 @@ public class VolvoOnCallBindingConstants {
public static final String GROUP_WINDOWS = "windows"; public static final String GROUP_WINDOWS = "windows";
public static final String GROUP_TYRES = "tyrePressure"; public static final String GROUP_TYRES = "tyrePressure";
public static final String GROUP_BATTERY = "battery"; public static final String GROUP_BATTERY = "battery";
public static final String GROUP_OTHER = "other";
public static final String GROUP_POSITION = "position";
public static final String GROUP_ODOMETER = "odometer";
public static final String GROUP_TANK = "tank";
// List of Channel id's // List of Channel id's
public static final String TAILGATE = "tailgate"; public static final String TAILGATE = "tailgate";
@@ -90,6 +84,11 @@ public class VolvoOnCallBindingConstants {
public static final String CHARGING_END = "chargingEnd"; public static final String CHARGING_END = "chargingEnd";
public static final String BULB_FAILURE = "bulbFailure"; public static final String BULB_FAILURE = "bulbFailure";
// Car Events
public static final String CAR_EVENT = "carEvent";
public static final String EVENT_CAR_STOPPED = "CAR_STOPPED";
public static final String EVENT_CAR_MOVED = "CAR_MOVED";
public static final String EVENT_CAR_STARTED = "CAR_STARTED";
// Last Trip Channel Id's // Last Trip Channel Id's
public static final String LAST_TRIP_GROUP = "lasttrip"; public static final String LAST_TRIP_GROUP = "lasttrip";
public static final String TRIP_CONSUMPTION = "tripConsumption"; public static final String TRIP_CONSUMPTION = "tripConsumption";

View File

@@ -34,7 +34,9 @@ public class VolvoOnCallException extends Exception {
public static enum ErrorType { public static enum ErrorType {
UNKNOWN, UNKNOWN,
SERVICE_UNAVAILABLE, SERVICE_UNAVAILABLE,
SERVICE_UNABLE_TO_START,
IOEXCEPTION, IOEXCEPTION,
INTERRUPTED,
JSON_SYNTAX; JSON_SYNTAX;
} }
@@ -44,6 +46,8 @@ public class VolvoOnCallException extends Exception {
super(label); super(label);
if ("FoundationServicesUnavailable".equalsIgnoreCase(label)) { if ("FoundationServicesUnavailable".equalsIgnoreCase(label)) {
cause = ErrorType.SERVICE_UNAVAILABLE; cause = ErrorType.SERVICE_UNAVAILABLE;
} else if ("ServiceUnableToStart".equalsIgnoreCase(label)) {
cause = ErrorType.SERVICE_UNABLE_TO_START;
} else { } else {
cause = ErrorType.UNKNOWN; cause = ErrorType.UNKNOWN;
logger.warn("Unhandled VoC error : {} : {}", label, description); logger.warn("Unhandled VoC error : {} : {}", label, description);
@@ -56,6 +60,8 @@ public class VolvoOnCallException extends Exception {
cause = ErrorType.IOEXCEPTION; cause = ErrorType.IOEXCEPTION;
} else if (e instanceof JsonSyntaxException) { } else if (e instanceof JsonSyntaxException) {
cause = ErrorType.JSON_SYNTAX; cause = ErrorType.JSON_SYNTAX;
} else if (e instanceof InterruptedException) {
cause = ErrorType.INTERRUPTED;
} else { } else {
cause = ErrorType.UNKNOWN; cause = ErrorType.UNKNOWN;
logger.warn("Unhandled VoC error : {}", e.getMessage()); logger.warn("Unhandled VoC error : {}", e.getMessage());

View File

@@ -14,31 +14,34 @@ package org.openhab.binding.volvooncall.internal;
import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*; import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*;
import java.util.HashMap; import java.time.ZonedDateTime;
import java.util.Hashtable;
import java.util.Map;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.volvooncall.internal.discovery.VolvoOnCallDiscoveryService; import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.volvooncall.internal.handler.VehicleHandler; import org.openhab.binding.volvooncall.internal.handler.VehicleHandler;
import org.openhab.binding.volvooncall.internal.handler.VehicleStateDescriptionProvider; import org.openhab.binding.volvooncall.internal.handler.VehicleStateDescriptionProvider;
import org.openhab.binding.volvooncall.internal.handler.VolvoOnCallBridgeHandler; import org.openhab.binding.volvooncall.internal.handler.VolvoOnCallBridgeHandler;
import org.openhab.core.config.discovery.DiscoveryService; import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.thing.Bridge; import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID; 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.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory; 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.Activate;
import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializer;
/** /**
* The {@link VolvoOnCallHandlerFactory} is responsible for creating things and thing * The {@link VolvoOnCallHandlerFactory} is responsible for creating things and thing
* handlers. * handlers.
@@ -48,13 +51,31 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault @NonNullByDefault
@Component(configurationPid = "binding.volvooncall", service = ThingHandlerFactory.class) @Component(configurationPid = "binding.volvooncall", service = ThingHandlerFactory.class)
public class VolvoOnCallHandlerFactory extends BaseThingHandlerFactory { public class VolvoOnCallHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(VolvoOnCallHandlerFactory.class); private final Logger logger = LoggerFactory.getLogger(VolvoOnCallHandlerFactory.class);
private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
private final VehicleStateDescriptionProvider stateDescriptionProvider; private final VehicleStateDescriptionProvider stateDescriptionProvider;
private final Gson gson;
private final HttpClient httpClient;
@Activate @Activate
public VolvoOnCallHandlerFactory(@Reference VehicleStateDescriptionProvider provider) { public VolvoOnCallHandlerFactory(@Reference VehicleStateDescriptionProvider provider,
@Reference HttpClientFactory httpClientFactory) {
this.stateDescriptionProvider = provider; this.stateDescriptionProvider = provider;
this.httpClient = httpClientFactory.createHttpClient(BINDING_ID);
this.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)
.registerTypeAdapter(StringType.class, (JsonDeserializer<StringType>) (json, type,
jsonDeserializationContext) -> StringType.valueOf(json.getAsString()))
.create();
} }
@Override @Override
@@ -66,36 +87,11 @@ public class VolvoOnCallHandlerFactory extends BaseThingHandlerFactory {
protected @Nullable ThingHandler createHandler(Thing thing) { protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID(); ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (APIBRIDGE_THING_TYPE.equals(thingTypeUID)) { if (APIBRIDGE_THING_TYPE.equals(thingTypeUID)) {
VolvoOnCallBridgeHandler bridgeHandler = new VolvoOnCallBridgeHandler((Bridge) thing); return new VolvoOnCallBridgeHandler((Bridge) thing, gson, httpClient);
registerDeviceDiscoveryService(bridgeHandler);
return bridgeHandler;
} else if (VEHICLE_THING_TYPE.equals(thingTypeUID)) { } else if (VEHICLE_THING_TYPE.equals(thingTypeUID)) {
return new VehicleHandler(thing, stateDescriptionProvider); return new VehicleHandler(thing, stateDescriptionProvider);
} }
logger.warn("ThingHandler not found for {}", thing.getThingTypeUID()); logger.warn("ThingHandler not found for {}", thing.getThingTypeUID());
return null; 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

@@ -1,40 +0,0 @@
/**
* 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

@@ -12,14 +12,14 @@
*/ */
package org.openhab.binding.volvooncall.internal.action; package org.openhab.binding.volvooncall.internal.action;
import java.lang.reflect.Method; import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*;
import java.lang.reflect.Proxy;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.volvooncall.internal.handler.VehicleHandler; import org.openhab.binding.volvooncall.internal.handler.VehicleHandler;
import org.openhab.core.automation.annotation.ActionInput; import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction; import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.binding.ThingActions; import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope; import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler; import org.openhab.core.thing.binding.ThingHandler;
@@ -34,7 +34,7 @@ import org.slf4j.LoggerFactory;
*/ */
@ThingActionsScope(name = "volvooncall") @ThingActionsScope(name = "volvooncall")
@NonNullByDefault @NonNullByDefault
public class VolvoOnCallActions implements ThingActions, IVolvoOnCallActions { public class VolvoOnCallActions implements ThingActions {
private final Logger logger = LoggerFactory.getLogger(VolvoOnCallActions.class); private final Logger logger = LoggerFactory.getLogger(VolvoOnCallActions.class);
@@ -56,40 +56,29 @@ public class VolvoOnCallActions implements ThingActions, IVolvoOnCallActions {
return this.handler; return this.handler;
} }
@Override @RuleAction(label = "close the car", description = "Closes the car")
@RuleAction(label = "Volvo On Call : Close", description = "Closes the car")
public void closeCarCommand() { public void closeCarCommand() {
logger.debug("closeCarCommand called"); logger.debug("closeCarCommand called");
VehicleHandler handler = this.handler; VehicleHandler handler = this.handler;
if (handler != null) { if (handler != null) {
handler.actionClose(); handler.actionOpenClose(LOCK, OnOffType.ON);
} else { } else {
logger.warn("VolvoOnCall Action service ThingHandler is null!"); logger.warn("VolvoOnCall Action service ThingHandler is null!");
} }
} }
public static void closeCarCommand(@Nullable ThingActions actions) { @RuleAction(label = "open the car", description = "Opens the car")
invokeMethodOf(actions).closeCarCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Open", description = "Opens the car")
public void openCarCommand() { public void openCarCommand() {
logger.debug("openCarCommand called"); logger.debug("openCarCommand called");
VehicleHandler handler = this.handler; VehicleHandler handler = this.handler;
if (handler != null) { if (handler != null) {
handler.actionOpen(); handler.actionOpenClose(UNLOCK, OnOffType.OFF);
} else { } else {
logger.warn("VolvoOnCall Action service ThingHandler is null!"); logger.warn("VolvoOnCall Action service ThingHandler is null!");
} }
} }
public static void openCarCommand(@Nullable ThingActions actions) { @RuleAction(label = "start the engine", description = "Starts the engine")
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) { public void engineStartCommand(@ActionInput(name = "runtime", label = "Runtime") @Nullable Integer runtime) {
logger.debug("engineStartCommand called"); logger.debug("engineStartCommand called");
VehicleHandler handler = this.handler; VehicleHandler handler = this.handler;
@@ -100,76 +89,51 @@ public class VolvoOnCallActions implements ThingActions, IVolvoOnCallActions {
} }
} }
public static void engineStartCommand(@Nullable ThingActions actions, @Nullable Integer runtime) { @RuleAction(label = "start the heater", description = "Starts car heater")
invokeMethodOf(actions).engineStartCommand(runtime);
}
@Override
@RuleAction(label = "Volvo On Call : Heater Start", description = "Starts car heater")
public void heaterStartCommand() { public void heaterStartCommand() {
logger.debug("heaterStartCommand called"); logger.debug("heaterStartCommand called");
VehicleHandler handler = this.handler; VehicleHandler handler = this.handler;
if (handler != null) { if (handler != null) {
handler.actionHeater(true); handler.actionHeater(REMOTE_HEATER, true);
} else { } else {
logger.warn("VolvoOnCall Action service ThingHandler is null!"); logger.warn("VolvoOnCall Action service ThingHandler is null!");
} }
} }
public static void heaterStartCommand(@Nullable ThingActions actions) { @RuleAction(label = "start preclimatization", description = "Starts the car heater")
invokeMethodOf(actions).heaterStartCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Preclimatization Start", description = "Starts car heater")
public void preclimatizationStartCommand() { public void preclimatizationStartCommand() {
logger.debug("preclimatizationStartCommand called"); logger.debug("preclimatizationStartCommand called");
VehicleHandler handler = this.handler; VehicleHandler handler = this.handler;
if (handler != null) { if (handler != null) {
handler.actionPreclimatization(true); handler.actionHeater(PRECLIMATIZATION, true);
} else { } else {
logger.warn("VolvoOnCall Action service ThingHandler is null!"); logger.warn("VolvoOnCall Action service ThingHandler is null!");
} }
} }
public static void preclimatizationStartCommand(@Nullable ThingActions actions) { @RuleAction(label = "stop the heater", description = "Stops car heater")
invokeMethodOf(actions).preclimatizationStartCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Heater Stop", description = "Stops car heater")
public void heaterStopCommand() { public void heaterStopCommand() {
logger.debug("heaterStopCommand called"); logger.debug("heaterStopCommand called");
VehicleHandler handler = this.handler; VehicleHandler handler = this.handler;
if (handler != null) { if (handler != null) {
handler.actionHeater(false); handler.actionHeater(REMOTE_HEATER, false);
} else { } else {
logger.warn("VolvoOnCall Action service ThingHandler is null!"); logger.warn("VolvoOnCall Action service ThingHandler is null!");
} }
} }
public static void heaterStopCommand(@Nullable ThingActions actions) { @RuleAction(label = "stop preclimatization", description = "Stops the car heater")
invokeMethodOf(actions).heaterStopCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Preclimatization Stop", description = "Stops car heater")
public void preclimatizationStopCommand() { public void preclimatizationStopCommand() {
logger.debug("preclimatizationStopCommand called"); logger.debug("preclimatizationStopCommand called");
VehicleHandler handler = this.handler; VehicleHandler handler = this.handler;
if (handler != null) { if (handler != null) {
handler.actionPreclimatization(false); handler.actionHeater(PRECLIMATIZATION, false);
} else { } else {
logger.warn("VolvoOnCall Action service ThingHandler is null!"); logger.warn("VolvoOnCall Action service ThingHandler is null!");
} }
} }
public static void preclimatizationStopCommand(@Nullable ThingActions actions) { @RuleAction(label = "honk-blink", description = "Activates the horn and or lights of the car")
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, public void honkBlinkCommand(@ActionInput(name = "honk", label = "Honk") Boolean honk,
@ActionInput(name = "blink", label = "Blink") Boolean blink) { @ActionInput(name = "blink", label = "Blink") Boolean blink) {
logger.debug("honkBlinkCommand called"); logger.debug("honkBlinkCommand called");
@@ -180,27 +144,4 @@ public class VolvoOnCallActions implements ThingActions, IVolvoOnCallActions {
logger.warn("VolvoOnCall Action service ThingHandler is null!"); 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,72 @@
/**
* 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.api;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.volvooncall.internal.VolvoOnCallException;
import org.openhab.binding.volvooncall.internal.VolvoOnCallException.ErrorType;
import org.openhab.binding.volvooncall.internal.dto.PostResponse;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.types.RefreshType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link ActionResultController} is responsible for triggering information
* update after a post has been submitted to the webservice.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class ActionResultController implements Runnable {
private final Logger logger = LoggerFactory.getLogger(ActionResultController.class);
private final VocHttpApi service;
private final ScheduledExecutorService scheduler;
private final PostResponse postResponse;
private final ThingHandler vehicle;
public ActionResultController(VocHttpApi service, PostResponse postResponse, ScheduledExecutorService scheduler,
ThingHandler vehicle) {
this.postResponse = postResponse;
this.service = service;
this.scheduler = scheduler;
this.vehicle = vehicle;
}
@Override
public void run() {
switch (postResponse.status) {
case SUCCESSFULL:
case FAILED:
logger.debug("Action {} for vehicle {} resulted : {}.", postResponse.serviceType,
postResponse.vehicleId, postResponse.status);
vehicle.handleCommand(vehicle.getThing().getChannels().get(0).getUID(), RefreshType.REFRESH);
break;
default:
try {
scheduler.schedule(
new ActionResultController(service,
service.getURL(postResponse.serviceURL, PostResponse.class), scheduler, vehicle),
10000, TimeUnit.MILLISECONDS);
} catch (VolvoOnCallException e) {
if (e.getType() == ErrorType.SERVICE_UNAVAILABLE) {
scheduler.schedule(this, 10000, TimeUnit.MILLISECONDS);
}
}
}
}
}

View File

@@ -0,0 +1,144 @@
/**
* 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.api;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentProvider;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.client.util.StringContentProvider;
import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.volvooncall.internal.VolvoOnCallException;
import org.openhab.binding.volvooncall.internal.VolvoOnCallException.ErrorType;
import org.openhab.binding.volvooncall.internal.config.ApiBridgeConfiguration;
import org.openhab.binding.volvooncall.internal.dto.PostResponse;
import org.openhab.binding.volvooncall.internal.dto.VocAnswer;
import org.openhab.core.cache.ExpiringCacheMap;
import org.openhab.core.id.InstanceUUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
/**
* {@link VocHttpApi} wraps the VolvoOnCall REST API.
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class VocHttpApi {
// The URL to use to connect to VocAPI.
// For North America and China syntax changes to vocapi-cn.xxx
private static final String SERVICE_URL = "https://vocapi.wirelesscar.net/customerapi/rest/v3.0/";
private static final int TIMEOUT_MS = 10000;
private static final String JSON_CONTENT_TYPE = "application/json";
private final Logger logger = LoggerFactory.getLogger(VocHttpApi.class);
private final Gson gson;
private final ExpiringCacheMap<String, @Nullable String> cache;
private final HttpClient httpClient;
private final ApiBridgeConfiguration configuration;
public VocHttpApi(ApiBridgeConfiguration configuration, Gson gson, HttpClient httpClient)
throws VolvoOnCallException {
this.gson = gson;
this.cache = new ExpiringCacheMap<>(120 * 1000);
this.configuration = configuration;
this.httpClient = httpClient;
httpClient.setUserAgentField(new HttpField(HttpHeader.USER_AGENT, "openhab/voc_binding/" + InstanceUUID.get()));
try {
httpClient.start();
} catch (Exception e) {
throw new VolvoOnCallException(new IOException("Unable to start Jetty HttpClient", e));
}
}
public void dispose() throws Exception {
httpClient.stop();
}
private @Nullable String getResponse(HttpMethod method, String url, @Nullable String body) {
try {
Request request = httpClient.newRequest(url).header(HttpHeader.CACHE_CONTROL, "no-cache")
.header(HttpHeader.CONTENT_TYPE, JSON_CONTENT_TYPE).header(HttpHeader.ACCEPT, "*/*")
.header(HttpHeader.AUTHORIZATION, configuration.getAuthorization()).header("x-device-id", "Device")
.header("x-originator-type", "App").header("x-os-type", "Android").header("x-os-version", "22")
.timeout(TIMEOUT_MS, TimeUnit.MILLISECONDS);
if (body != null) {
ContentProvider content = new StringContentProvider(JSON_CONTENT_TYPE, body, StandardCharsets.UTF_8);
request = request.content(content);
}
ContentResponse contentResponse = request.method(method).send();
return contentResponse.getContentAsString();
} catch (InterruptedException | TimeoutException | ExecutionException e) {
return null;
}
}
private <T extends VocAnswer> T callUrl(HttpMethod method, String endpoint, Class<T> objectClass,
@Nullable String body) throws VolvoOnCallException {
try {
String url = endpoint.startsWith("http") ? endpoint : SERVICE_URL + endpoint;
String jsonResponse = method == HttpMethod.GET
? cache.putIfAbsentAndGet(endpoint, () -> getResponse(method, url, body))
: getResponse(method, url, body);
if (jsonResponse == null) {
throw new IOException();
} else {
logger.debug("Request to `{}` answered : {}", url, jsonResponse);
T responseDTO = gson.fromJson(jsonResponse, objectClass);
String error = responseDTO.getErrorLabel();
if (error != null) {
throw new VolvoOnCallException(error, responseDTO.getErrorDescription());
}
return responseDTO;
}
} catch (JsonSyntaxException | IOException e) {
throw new VolvoOnCallException(e);
}
}
public <T extends VocAnswer> T getURL(String endpoint, Class<T> objectClass) throws VolvoOnCallException {
return callUrl(HttpMethod.GET, endpoint, objectClass, null);
}
public @Nullable PostResponse postURL(String endpoint, @Nullable String body) throws VolvoOnCallException {
try {
return callUrl(HttpMethod.POST, endpoint, PostResponse.class, body);
} catch (VolvoOnCallException e) {
if (e.getType() == ErrorType.SERVICE_UNABLE_TO_START) {
logger.info("Unable to start service request sent to VoC");
return null;
} else {
throw e;
}
}
}
public <T extends VocAnswer> T getURL(Class<T> objectClass, String vin) throws VolvoOnCallException {
String url = String.format("vehicles/%s/%s", vin, objectClass.getSimpleName().toLowerCase());
return getURL(url, objectClass);
}
}

View File

@@ -18,13 +18,13 @@ import java.util.Base64;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
/** /**
* The {@link VolvoOnCallBridgeConfiguration} is responsible for holding * The {@link ApiBridgeConfiguration} is responsible for holding
* configuration informations needed to access VOC API * configuration informations needed to access VOC API
* *
* @author Gaël L'hopital - Initial contribution * @author Gaël L'hopital - Initial contribution
*/ */
@NonNullByDefault @NonNullByDefault
public class VolvoOnCallBridgeConfiguration { public class ApiBridgeConfiguration {
public String username = ""; public String username = "";
public String password = ""; public String password = "";

View File

@@ -22,6 +22,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
*/ */
@NonNullByDefault @NonNullByDefault
public class VehicleConfiguration { public class VehicleConfiguration {
public static String VIN = "vin";
public String vin = ""; public String vin = "";
public Integer refresh = 5; public int refresh = 10;
} }

View File

@@ -1,73 +0,0 @@
/**
* 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,111 @@
/**
* 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.Map;
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.api.VocHttpApi;
import org.openhab.binding.volvooncall.internal.config.VehicleConfiguration;
import org.openhab.binding.volvooncall.internal.dto.AccountVehicleRelation;
import org.openhab.binding.volvooncall.internal.dto.Attributes;
import org.openhab.binding.volvooncall.internal.dto.CustomerAccounts;
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.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link VolvoVehicleDiscoveryService} searches for available
* cars discoverable through VocAPI
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class VolvoVehicleDiscoveryService extends AbstractDiscoveryService implements ThingHandlerService {
private static final int SEARCH_TIME = 2;
private final Logger logger = LoggerFactory.getLogger(VolvoVehicleDiscoveryService.class);
private @Nullable VolvoOnCallBridgeHandler handler;
public VolvoVehicleDiscoveryService() {
super(SUPPORTED_THING_TYPES_UIDS, SEARCH_TIME);
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof VolvoOnCallBridgeHandler) {
this.handler = (VolvoOnCallBridgeHandler) handler;
}
}
@Override
public @Nullable ThingHandler getThingHandler() {
return handler;
}
@Override
public void activate(@Nullable Map<String, @Nullable Object> configProperties) {
super.activate(configProperties);
}
@Override
public void deactivate() {
super.deactivate();
}
@Override
protected void startScan() {
VolvoOnCallBridgeHandler bridgeHandler = this.handler;
if (bridgeHandler != null) {
ThingUID bridgeUID = bridgeHandler.getThing().getUID();
VocHttpApi api = bridgeHandler.getApi();
if (api != null) {
try {
CustomerAccounts account = api.getURL("customeraccounts/", CustomerAccounts.class);
account.accountVehicleRelationsURL.forEach(relationURL -> {
try {
AccountVehicleRelation accountVehicle = api.getURL(relationURL,
AccountVehicleRelation.class);
logger.debug("Found vehicle : {}", accountVehicle.vehicleId);
Vehicles vehicle = api.getURL(accountVehicle.vehicleURL, Vehicles.class);
Attributes attributes = api.getURL(Attributes.class, vehicle.vehicleId);
thingDiscovered(DiscoveryResultBuilder
.create(new ThingUID(VEHICLE_THING_TYPE, bridgeUID, accountVehicle.vehicleId))
.withLabel(attributes.vehicleType + " " + attributes.registrationNumber)
.withBridge(bridgeUID).withProperty(VehicleConfiguration.VIN, attributes.vin)
.withRepresentationProperty(VehicleConfiguration.VIN).build());
} catch (VolvoOnCallException e) {
logger.warn("Error while getting vehicle informations : {}", e.getMessage());
}
});
} catch (VolvoOnCallException e) {
logger.warn("Error while discovering vehicle: {}", e.getMessage());
}
}
;
}
stopScan();
}
}

View File

@@ -12,6 +12,9 @@
*/ */
package org.openhab.binding.volvooncall.internal.dto; package org.openhab.binding.volvooncall.internal.dto;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
@@ -26,7 +29,7 @@ import com.google.gson.annotations.SerializedName;
@NonNullByDefault @NonNullByDefault
public class CustomerAccounts extends VocAnswer { public class CustomerAccounts extends VocAnswer {
@SerializedName("accountVehicleRelations") @SerializedName("accountVehicleRelations")
public @NonNullByDefault({}) String[] accountVehicleRelationsURL; public List<String> accountVehicleRelationsURL = new ArrayList<>();
public @Nullable String username; public @Nullable String username;
/* /*

View File

@@ -15,6 +15,7 @@ package org.openhab.binding.volvooncall.internal.dto;
import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.UNDEFINED; import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.UNDEFINED;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.StringType;
/** /**
* The {@link HvBattery} is responsible for storing * The {@link HvBattery} is responsible for storing
@@ -26,7 +27,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
public class HvBattery { public class HvBattery {
public int hvBatteryLevel = UNDEFINED; public int hvBatteryLevel = UNDEFINED;
public int distanceToHVBatteryEmpty = UNDEFINED; public int distanceToHVBatteryEmpty = UNDEFINED;
public @NonNullByDefault({}) String hvBatteryChargeStatusDerived; public @NonNullByDefault({}) StringType hvBatteryChargeStatusDerived;
public int timeToHVBatteryFullyCharged = UNDEFINED; public int timeToHVBatteryFullyCharged = UNDEFINED;
/* /*
* Currently unused in the binding, maybe interesting in the future * Currently unused in the binding, maybe interesting in the future

View File

@@ -12,6 +12,8 @@
*/ */
package org.openhab.binding.volvooncall.internal.dto; package org.openhab.binding.volvooncall.internal.dto;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
@@ -48,7 +50,7 @@ public class PostResponse extends VocAnswer {
@SerializedName("service") @SerializedName("service")
public @NonNullByDefault({}) String serviceURL; public @NonNullByDefault({}) String serviceURL;
public @NonNullByDefault({}) ServiceType serviceType; public @NonNullByDefault({}) ServiceType serviceType;
public @NonNullByDefault({}) ZonedDateTime startTime;
/* /*
* Currently unused in the binding, maybe interesting in the future * Currently unused in the binding, maybe interesting in the future
* *
@@ -59,7 +61,7 @@ public class PostResponse extends VocAnswer {
* } * }
* *
* private ZonedDateTime statusTimestamp; * private ZonedDateTime statusTimestamp;
* private ZonedDateTime startTime; *
* private FailureReason failureReason; * private FailureReason failureReason;
* *
* private Integer customerServiceId; * private Integer customerServiceId;

View File

@@ -12,6 +12,7 @@
*/ */
package org.openhab.binding.volvooncall.internal.dto; package org.openhab.binding.volvooncall.internal.dto;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
@@ -27,8 +28,8 @@ import com.google.gson.annotations.SerializedName;
*/ */
@NonNullByDefault @NonNullByDefault
public class Trip { public class Trip {
public int id; public long id;
public @NonNullByDefault({}) List<TripDetail> tripDetails; public List<TripDetail> tripDetails = new ArrayList<>();
@SerializedName("trip") @SerializedName("trip")
public @Nullable String tripURL; public @Nullable String tripURL;

View File

@@ -12,10 +12,10 @@
*/ */
package org.openhab.binding.volvooncall.internal.dto; package org.openhab.binding.volvooncall.internal.dto;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/** /**
* The {@link Trips} is responsible for storing * The {@link Trips} is responsible for storing
@@ -25,5 +25,5 @@ import org.eclipse.jdt.annotation.Nullable;
*/ */
@NonNullByDefault @NonNullByDefault
public class Trips extends VocAnswer { public class Trips extends VocAnswer {
public @Nullable List<Trip> trips; public List<Trip> trips = new ArrayList<>();
} }

View File

@@ -13,6 +13,7 @@
package org.openhab.binding.volvooncall.internal.dto; package org.openhab.binding.volvooncall.internal.dto;
import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.StringType;
/** /**
* The {@link TyrePressure} is responsible for storing * The {@link TyrePressure} is responsible for storing
@@ -22,10 +23,10 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
*/ */
@NonNullByDefault @NonNullByDefault
public class TyrePressure { public class TyrePressure {
public @NonNullByDefault({}) String frontLeftTyrePressure; public @NonNullByDefault({}) StringType frontLeftTyrePressure;
public @NonNullByDefault({}) String frontRightTyrePressure; public @NonNullByDefault({}) StringType frontRightTyrePressure;
public @NonNullByDefault({}) String rearLeftTyrePressure; public @NonNullByDefault({}) StringType rearLeftTyrePressure;
public @NonNullByDefault({}) String rearRightTyrePressure; public @NonNullByDefault({}) StringType rearRightTyrePressure;
/* /*
* Currently unused in the binding, maybe interesting in the future * Currently unused in the binding, maybe interesting in the future
* private ZonedDateTime timestamp; * private ZonedDateTime timestamp;

View File

@@ -17,13 +17,13 @@ import static org.openhab.core.library.unit.MetricPrefix.KILO;
import static org.openhab.core.library.unit.SIUnits.*; import static org.openhab.core.library.unit.SIUnits.*;
import static org.openhab.core.library.unit.SmartHomeUnits.*; import static org.openhab.core.library.unit.SmartHomeUnits.*;
import java.io.IOException;
import java.time.ZonedDateTime; import java.time.ZonedDateTime;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Stack;
import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -32,12 +32,15 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.volvooncall.internal.VolvoOnCallException; import org.openhab.binding.volvooncall.internal.VolvoOnCallException;
import org.openhab.binding.volvooncall.internal.action.VolvoOnCallActions; import org.openhab.binding.volvooncall.internal.action.VolvoOnCallActions;
import org.openhab.binding.volvooncall.internal.api.ActionResultController;
import org.openhab.binding.volvooncall.internal.api.VocHttpApi;
import org.openhab.binding.volvooncall.internal.config.VehicleConfiguration; import org.openhab.binding.volvooncall.internal.config.VehicleConfiguration;
import org.openhab.binding.volvooncall.internal.dto.Attributes; import org.openhab.binding.volvooncall.internal.dto.Attributes;
import org.openhab.binding.volvooncall.internal.dto.DoorsStatus; import org.openhab.binding.volvooncall.internal.dto.DoorsStatus;
import org.openhab.binding.volvooncall.internal.dto.Heater; import org.openhab.binding.volvooncall.internal.dto.Heater;
import org.openhab.binding.volvooncall.internal.dto.HvBattery; import org.openhab.binding.volvooncall.internal.dto.HvBattery;
import org.openhab.binding.volvooncall.internal.dto.Position; import org.openhab.binding.volvooncall.internal.dto.Position;
import org.openhab.binding.volvooncall.internal.dto.PostResponse;
import org.openhab.binding.volvooncall.internal.dto.Status; import org.openhab.binding.volvooncall.internal.dto.Status;
import org.openhab.binding.volvooncall.internal.dto.Trip; import org.openhab.binding.volvooncall.internal.dto.Trip;
import org.openhab.binding.volvooncall.internal.dto.TripDetail; import org.openhab.binding.volvooncall.internal.dto.TripDetail;
@@ -57,8 +60,9 @@ import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.ThingStatusInfo;
import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.thing.binding.BridgeHandler; import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService; import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType; import org.openhab.core.types.RefreshType;
@@ -67,8 +71,6 @@ import org.openhab.core.types.UnDefType;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.gson.JsonSyntaxException;
/** /**
* The {@link VehicleHandler} is responsible for handling commands, which are sent * The {@link VehicleHandler} is responsible for handling commands, which are sent
* to one of the channels. * to one of the channels.
@@ -80,20 +82,76 @@ public class VehicleHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class); private final Logger logger = LoggerFactory.getLogger(VehicleHandler.class);
private final Map<String, String> activeOptions = new HashMap<>(); private final Map<String, String> activeOptions = new HashMap<>();
private @Nullable ScheduledFuture<?> refreshJob; private @Nullable ScheduledFuture<?> refreshJob;
private final List<ScheduledFuture<?>> pendingActions = new Stack<>();
private Vehicles vehicle = new Vehicles(); private Vehicles vehicle = new Vehicles();
private VehiclePositionWrapper vehiclePosition = new VehiclePositionWrapper(new Position()); private VehiclePositionWrapper vehiclePosition = new VehiclePositionWrapper(new Position());
private Status vehicleStatus = new Status(); private Status vehicleStatus = new Status();
private @NonNullByDefault({}) VehicleConfiguration configuration; private @NonNullByDefault({}) VehicleConfiguration configuration;
private Integer lastTripId = 0; private @NonNullByDefault({}) VolvoOnCallBridgeHandler bridgeHandler;
private long lastTripId;
public VehicleHandler(Thing thing, VehicleStateDescriptionProvider stateDescriptionProvider) { public VehicleHandler(Thing thing, VehicleStateDescriptionProvider stateDescriptionProvider) {
super(thing); super(thing);
} }
private Map<String, String> discoverAttributes(VolvoOnCallBridgeHandler bridgeHandler) @Override
throws JsonSyntaxException, IOException, VolvoOnCallException { public void initialize() {
Attributes attributes = bridgeHandler.getURL(vehicle.attributesURL, Attributes.class); logger.trace("Initializing the Volvo On Call handler for {}", getThing().getUID());
Bridge bridge = getBridge();
initializeBridge(bridge == null ? null : bridge.getHandler(), bridge == null ? null : bridge.getStatus());
}
@Override
public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
logger.debug("bridgeStatusChanged {} for thing {}", bridgeStatusInfo, getThing().getUID());
Bridge bridge = getBridge();
initializeBridge(bridge == null ? null : bridge.getHandler(), bridgeStatusInfo.getStatus());
}
private void initializeBridge(@Nullable ThingHandler thingHandler, @Nullable ThingStatus bridgeStatus) {
logger.debug("initializeBridge {} for thing {}", bridgeStatus, getThing().getUID());
if (thingHandler != null && bridgeStatus != null) {
bridgeHandler = (VolvoOnCallBridgeHandler) thingHandler;
if (bridgeStatus == ThingStatus.ONLINE) {
configuration = getConfigAs(VehicleConfiguration.class);
VocHttpApi api = bridgeHandler.getApi();
if (api != null) {
try {
vehicle = api.getURL("vehicles/" + configuration.vin, Vehicles.class);
if (thing.getProperties().isEmpty()) {
Map<String, String> properties = discoverAttributes(api);
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 = Long.parseLong(thing.getProperties().get(LAST_TRIP_ID));
}
updateStatus(ThingStatus.ONLINE);
startAutomaticRefresh(configuration.refresh, api);
} catch (VolvoOnCallException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_INITIALIZING_ERROR, e.getMessage());
}
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
}
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
}
}
private Map<String, String> discoverAttributes(VocHttpApi service) throws VolvoOnCallException {
Attributes attributes = service.getURL(vehicle.attributesURL, Attributes.class);
Map<String, String> properties = new HashMap<>(); Map<String, String> properties = new HashMap<>();
properties.put(CAR_LOCATOR, attributes.carLocatorSupported.toString()); properties.put(CAR_LOCATOR, attributes.carLocatorSupported.toString());
@@ -112,71 +170,50 @@ public class VehicleHandler extends BaseThingHandler {
return properties; 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 * Start the job refreshing the vehicle data
* *
* @param refresh : refresh frequency in minutes * @param refresh : refresh frequency in minutes
* @param service
*/ */
private void startAutomaticRefresh(int refresh) { private void startAutomaticRefresh(int refresh, VocHttpApi service) {
ScheduledFuture<?> refreshJob = this.refreshJob; ScheduledFuture<?> refreshJob = this.refreshJob;
if (refreshJob == null || refreshJob.isCancelled()) { if (refreshJob == null || refreshJob.isCancelled()) {
refreshJob = scheduler.scheduleWithFixedDelay(this::queryApiAndUpdateChannels, 10, refresh, this.refreshJob = scheduler.scheduleWithFixedDelay(() -> queryApiAndUpdateChannels(service), 1, refresh,
TimeUnit.MINUTES); TimeUnit.MINUTES);
} }
} }
private void queryApiAndUpdateChannels() { private void queryApiAndUpdateChannels(VocHttpApi service) {
VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler(); try {
if (bridgeHandler != null) { Status newVehicleStatus = service.getURL(vehicle.statusURL, Status.class);
try { vehiclePosition = new VehiclePositionWrapper(service.getURL(Position.class, configuration.vin));
vehicleStatus = bridgeHandler.getURL(vehicle.statusURL, Status.class); // Update all channels from the updated data
vehiclePosition = new VehiclePositionWrapper(bridgeHandler.getURL(Position.class, configuration.vin)); getThing().getChannels().stream().map(Channel::getUID)
// Update all channels from the updated data .filter(channelUID -> isLinked(channelUID) && !LAST_TRIP_GROUP.equals(channelUID.getGroupId()))
getThing().getChannels().stream().map(Channel::getUID) .forEach(channelUID -> {
.filter(channelUID -> isLinked(channelUID) && !LAST_TRIP_GROUP.equals(channelUID.getGroupId())) String groupID = channelUID.getGroupId();
.forEach(channelUID -> { if (groupID != null) {
State state = getValue(channelUID.getGroupId(), channelUID.getIdWithoutGroup(), State state = getValue(groupID, channelUID.getIdWithoutGroup(), newVehicleStatus,
vehicleStatus, vehiclePosition); vehiclePosition);
updateState(channelUID, state); updateState(channelUID, state);
}); }
updateTrips(bridgeHandler); });
} catch (VolvoOnCallException e) { if (newVehicleStatus.odometer != vehicleStatus.odometer) {
logger.warn("Exception occurred during execution: {}", e.getMessage(), e); triggerChannel(GROUP_OTHER + "#" + CAR_EVENT, EVENT_CAR_MOVED);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); // We will update trips only if car position has changed to save server queries
freeRefreshJob(); updateTrips(service);
startAutomaticRefresh(configuration.refresh);
} }
if (!vehicleStatus.getEngineRunning().equals(newVehicleStatus.getEngineRunning())
&& newVehicleStatus.getEngineRunning().get() == OnOffType.ON) {
triggerChannel(GROUP_OTHER + "#" + CAR_EVENT, EVENT_CAR_STARTED);
}
vehicleStatus = newVehicleStatus;
} catch (VolvoOnCallException e) {
logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
freeRefreshJob();
startAutomaticRefresh(configuration.refresh, service);
} }
} }
@@ -186,6 +223,7 @@ public class VehicleHandler extends BaseThingHandler {
refreshJob.cancel(true); refreshJob.cancel(true);
this.refreshJob = null; this.refreshJob = null;
} }
pendingActions.stream().filter(f -> !f.isCancelled()).forEach(f -> f.cancel(true));
} }
@Override @Override
@@ -194,34 +232,32 @@ public class VehicleHandler extends BaseThingHandler {
super.dispose(); super.dispose();
} }
private void updateTrips(VolvoOnCallBridgeHandler bridgeHandler) throws VolvoOnCallException { private void updateTrips(VocHttpApi service) throws VolvoOnCallException {
// This seems to rewind 100 days by default, did not find any way to filter it // This seems to rewind 100 days by default, did not find any way to filter it
Trips carTrips = bridgeHandler.getURL(Trips.class, configuration.vin); Trips carTrips = service.getURL(Trips.class, configuration.vin);
List<Trip> tripList = carTrips.trips; List<Trip> newTrips = carTrips.trips.stream().filter(trip -> trip.id >= lastTripId)
.collect(Collectors.toList());
Collections.reverse(newTrips);
if (tripList != null) { logger.debug("Trips discovered : {}", newTrips.size());
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()) {
Long newTripId = newTrips.get(newTrips.size() - 1).id;
if (!newTrips.isEmpty()) { if (newTripId > lastTripId) {
Integer newTripId = newTrips.get(newTrips.size() - 1).id; updateProperty(LAST_TRIP_ID, newTripId.toString());
if (newTripId > lastTripId) { triggerChannel(GROUP_OTHER + "#" + CAR_EVENT, EVENT_CAR_STOPPED);
updateProperty(LAST_TRIP_ID, newTripId.toString()); lastTripId = newTripId;
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);
});
});
} }
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);
});
});
} }
} }
@@ -229,21 +265,18 @@ public class VehicleHandler extends BaseThingHandler {
public void handleCommand(ChannelUID channelUID, Command command) { public void handleCommand(ChannelUID channelUID, Command command) {
String channelID = channelUID.getIdWithoutGroup(); String channelID = channelUID.getIdWithoutGroup();
if (command instanceof RefreshType) { if (command instanceof RefreshType) {
queryApiAndUpdateChannels(); VocHttpApi api = bridgeHandler.getApi();
if (api != null) {
queryApiAndUpdateChannels(api);
}
} else if (command instanceof OnOffType) { } else if (command instanceof OnOffType) {
OnOffType onOffCommand = (OnOffType) command; OnOffType onOffCommand = (OnOffType) command;
if (ENGINE_START.equals(channelID) && onOffCommand == OnOffType.ON) { if (ENGINE_START.equals(channelID) && onOffCommand == OnOffType.ON) {
actionStart(5); actionStart(5);
} else if (REMOTE_HEATER.equals(channelID)) { } else if (REMOTE_HEATER.equals(channelID) || PRECLIMATIZATION.equals(channelID)) {
actionHeater(onOffCommand == OnOffType.ON); actionHeater(channelID, onOffCommand == OnOffType.ON);
} else if (PRECLIMATIZATION.equals(channelID)) {
actionPreclimatization(onOffCommand == OnOffType.ON);
} else if (CAR_LOCKED.equals(channelID)) { } else if (CAR_LOCKED.equals(channelID)) {
if (onOffCommand == OnOffType.ON) { actionOpenClose((onOffCommand == OnOffType.ON) ? LOCK : UNLOCK, onOffCommand);
actionClose();
} else {
actionOpen();
}
} }
} }
} }
@@ -271,7 +304,6 @@ public class VehicleHandler extends BaseThingHandler {
case TRIP_END_POSITION: case TRIP_END_POSITION:
return tripDetails.getEndPosition(); return tripDetails.getEndPosition();
} }
return UnDefType.NULL; return UnDefType.NULL;
} }
@@ -310,13 +342,13 @@ public class VehicleHandler extends BaseThingHandler {
private State getTyresValue(String channelId, TyrePressure tyrePressure) { private State getTyresValue(String channelId, TyrePressure tyrePressure) {
switch (channelId) { switch (channelId) {
case REAR_RIGHT_TYRE: case REAR_RIGHT_TYRE:
return new StringType(tyrePressure.rearRightTyrePressure); return tyrePressure.rearRightTyrePressure;
case REAR_LEFT_TYRE: case REAR_LEFT_TYRE:
return new StringType(tyrePressure.rearLeftTyrePressure); return tyrePressure.rearLeftTyrePressure;
case FRONT_RIGHT_TYRE: case FRONT_RIGHT_TYRE:
return new StringType(tyrePressure.frontRightTyrePressure); return tyrePressure.frontRightTyrePressure;
case FRONT_LEFT_TYRE: case FRONT_LEFT_TYRE:
return new StringType(tyrePressure.frontLeftTyrePressure); return tyrePressure.frontLeftTyrePressure;
} }
return UnDefType.NULL; return UnDefType.NULL;
} }
@@ -340,8 +372,7 @@ public class VehicleHandler extends BaseThingHandler {
? new QuantityType<>(hvBattery.distanceToHVBatteryEmpty, KILO(METRE)) ? new QuantityType<>(hvBattery.distanceToHVBatteryEmpty, KILO(METRE))
: UnDefType.UNDEF; : UnDefType.UNDEF;
case CHARGE_STATUS: case CHARGE_STATUS:
return hvBattery.hvBatteryChargeStatusDerived != null return hvBattery.hvBatteryChargeStatusDerived != null ? hvBattery.hvBatteryChargeStatusDerived
? new StringType(hvBattery.hvBatteryChargeStatusDerived)
: UnDefType.UNDEF; : UnDefType.UNDEF;
case TIME_TO_BATTERY_FULLY_CHARGED: case TIME_TO_BATTERY_FULLY_CHARGED:
return hvBattery.timeToHVBatteryFullyCharged != UNDEFINED return hvBattery.timeToHVBatteryFullyCharged != UNDEFINED
@@ -351,43 +382,12 @@ public class VehicleHandler extends BaseThingHandler {
return hvBattery.timeToHVBatteryFullyCharged != UNDEFINED && hvBattery.timeToHVBatteryFullyCharged > 0 return hvBattery.timeToHVBatteryFullyCharged != UNDEFINED && hvBattery.timeToHVBatteryFullyCharged > 0
? new DateTimeType(ZonedDateTime.now().plusMinutes(hvBattery.timeToHVBatteryFullyCharged)) ? new DateTimeType(ZonedDateTime.now().plusMinutes(hvBattery.timeToHVBatteryFullyCharged))
: UnDefType.UNDEF; : UnDefType.UNDEF;
} }
return UnDefType.NULL; return UnDefType.NULL;
} }
private State getValue(@Nullable String groupId, String channelId, Status status, VehiclePositionWrapper position) { private State getValue(String groupId, String channelId, Status status, VehiclePositionWrapper position) {
switch (channelId) { 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: case CAR_LOCKED:
// Warning : carLocked is in the Doors group but is part of general status informations. // Warning : carLocked is in the Doors group but is part of general status informations.
// Did not change it to avoid breaking change for users // Did not change it to avoid breaking change for users
@@ -403,164 +403,151 @@ public class VehicleHandler extends BaseThingHandler {
: UnDefType.UNDEF; : UnDefType.UNDEF;
case SERVICE_WARNING: case SERVICE_WARNING:
return new StringType(status.serviceWarningStatus); return new StringType(status.serviceWarningStatus);
case FUEL_ALERT:
return status.distanceToEmpty < 100 ? OnOffType.ON : OnOffType.OFF;
case BULB_FAILURE: case BULB_FAILURE:
return status.aFailedBulb() ? OnOffType.ON : OnOffType.OFF; return status.aFailedBulb() ? OnOffType.ON : OnOffType.OFF;
case REMOTE_HEATER: case REMOTE_HEATER:
case PRECLIMATIZATION: case PRECLIMATIZATION:
return status.getHeater().map(heater -> getHeaterValue(channelId, heater)).orElse(UnDefType.NULL); return status.getHeater().map(heater -> getHeaterValue(channelId, heater)).orElse(UnDefType.NULL);
} }
if (groupId != null) { switch (groupId) {
switch (groupId) { case GROUP_TANK:
case GROUP_DOORS: return getTankValue(channelId, status);
return status.getDoors().map(doors -> getDoorsValue(channelId, doors)).orElse(UnDefType.NULL); case GROUP_ODOMETER:
case GROUP_WINDOWS: return getOdometerValue(channelId, status);
return status.getWindows().map(windows -> getWindowsValue(channelId, windows)) case GROUP_POSITION:
.orElse(UnDefType.NULL); return getPositionValue(channelId, position);
case GROUP_TYRES: case GROUP_DOORS:
return status.getTyrePressure().map(tyres -> getTyresValue(channelId, tyres)) return status.getDoors().map(doors -> getDoorsValue(channelId, doors)).orElse(UnDefType.NULL);
.orElse(UnDefType.NULL); case GROUP_WINDOWS:
case GROUP_BATTERY: return status.getWindows().map(windows -> getWindowsValue(channelId, windows)).orElse(UnDefType.NULL);
return status.getHvBattery().map(batteries -> getBatteryValue(channelId, batteries)) case GROUP_TYRES:
.orElse(UnDefType.NULL); 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; return UnDefType.NULL;
} }
private State getTankValue(String channelId, Status status) {
switch (channelId) {
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 FUEL_ALERT:
return status.distanceToEmpty < 100 ? OnOffType.ON : OnOffType.OFF;
}
return UnDefType.UNDEF;
}
private State getOdometerValue(String channelId, Status status) {
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;
}
return UnDefType.UNDEF;
}
private State getPositionValue(String channelId, VehiclePositionWrapper position) {
switch (channelId) {
case ACTUAL_LOCATION:
return position.getPosition();
case CALCULATED_LOCATION:
return position.isCalculated();
case HEADING:
return position.isHeading();
case LOCATION_TIMESTAMP:
return position.getTimestamp();
}
return UnDefType.UNDEF;
}
public void actionHonkBlink(Boolean honk, Boolean blink) { public void actionHonkBlink(Boolean honk, Boolean blink) {
VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler(); StringBuilder url = new StringBuilder("vehicles/" + vehicle.vehicleId + "/honk_blink/");
if (bridgeHandler != null) {
StringBuilder url = new StringBuilder(SERVICE_URL + "vehicles/" + vehicle.vehicleId + "/honk_blink/");
if (honk && blink && activeOptions.containsKey(HONK_BLINK)) { if (honk && blink && activeOptions.containsKey(HONK_BLINK)) {
url.append("both"); url.append("both");
} else if (honk && activeOptions.containsKey(HONK_AND_OR_BLINK)) { } else if (honk && activeOptions.containsKey(HONK_AND_OR_BLINK)) {
url.append("horn"); url.append("horn");
} else if (blink && activeOptions.containsKey(HONK_AND_OR_BLINK)) { } else if (blink && activeOptions.containsKey(HONK_AND_OR_BLINK)) {
url.append("lights"); url.append("lights");
} else { } else {
logger.warn("The vehicle is not capable of this action"); logger.warn("The vehicle is not capable of this action");
return; return;
} }
post(url.toString(), vehiclePosition.getPositionAsJSon());
}
private void post(String url, @Nullable String param) {
VocHttpApi api = bridgeHandler.getApi();
if (api != null) {
try { try {
bridgeHandler.postURL(url.toString(), vehiclePosition.getPositionAsJSon()); PostResponse postResponse = api.postURL(url.toString(), param);
if (postResponse != null) {
pendingActions
.add(scheduler.schedule(new ActionResultController(api, postResponse, scheduler, this),
1000, TimeUnit.MILLISECONDS));
}
} catch (VolvoOnCallException e) { } catch (VolvoOnCallException e) {
logger.warn("Exception occurred during execution: {}", e.getMessage(), e); logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} }
} }
;
pendingActions.removeIf(ScheduledFuture::isDone);
} }
private void actionOpenClose(String action, OnOffType controlState) { public void actionOpenClose(String action, OnOffType controlState) {
VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler(); if (activeOptions.containsKey(action)) {
if (bridgeHandler != null) { if (!vehicleStatus.getCarLocked().isPresent() || vehicleStatus.getCarLocked().get() != controlState) {
if (activeOptions.containsKey(action)) { post(String.format("vehicles/%s/%s", configuration.vin, 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 { } else {
logger.warn("The car {} does not support remote {}ing", configuration.vin, action); 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) { public void actionHeater(String action, Boolean start) {
VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler(); if (activeOptions.containsKey(action)) {
if (bridgeHandler != null) { String address = String.format("vehicles/%s/%s/%s", configuration.vin,
if (activeOptions.containsKey(action)) { action.contains(REMOTE_HEATER) ? "heater" : "preclimatization", start ? "start" : "stop");
try { post(address, start ? "{}" : null);
if (action.contains(REMOTE_HEATER)) { } else {
String command = start ? "start" : "stop"; logger.warn("The car {} does not support {}", configuration.vin, action);
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) { public void actionStart(Integer runtime) {
VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler(); if (activeOptions.containsKey(ENGINE_START)) {
if (bridgeHandler != null) { String address = String.format("vehicles/%s/engine/start", vehicle.vehicleId);
if (activeOptions.containsKey(ENGINE_START)) { String json = "{\"runtime\":" + runtime.toString() + "}";
String url = SERVICE_URL + "vehicles/" + vehicle.vehicleId + "/engine/start";
String json = "{\"runtime\":" + runtime.toString() + "}";
try { post(address, json);
bridgeHandler.postURL(url, json); } else {
} catch (VolvoOnCallException e) { logger.warn("The car {} does not support remote engine starting", vehicle.vehicleId);
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 @Override
public Collection<Class<? extends ThingHandlerService>> getServices() { public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singletonList(VolvoOnCallActions.class); return Collections.singletonList(VolvoOnCallActions.class);

View File

@@ -12,43 +12,28 @@
*/ */
package org.openhab.binding.volvooncall.internal.handler; package org.openhab.binding.volvooncall.internal.handler;
import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*; import java.util.Collection;
import java.util.Collections;
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.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable; import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.volvooncall.internal.VolvoOnCallException; import org.openhab.binding.volvooncall.internal.VolvoOnCallException;
import org.openhab.binding.volvooncall.internal.VolvoOnCallException.ErrorType; import org.openhab.binding.volvooncall.internal.api.VocHttpApi;
import org.openhab.binding.volvooncall.internal.config.VolvoOnCallBridgeConfiguration; import org.openhab.binding.volvooncall.internal.config.ApiBridgeConfiguration;
import org.openhab.binding.volvooncall.internal.discovery.VolvoVehicleDiscoveryService;
import org.openhab.binding.volvooncall.internal.dto.CustomerAccounts; 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.Bridge;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler; import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import com.google.gson.Gson; 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 * The {@link VolvoOnCallBridgeHandler} is responsible for handling commands, which are
@@ -58,144 +43,60 @@ import com.google.gson.JsonSyntaxException;
*/ */
@NonNullByDefault @NonNullByDefault
public class VolvoOnCallBridgeHandler extends BaseBridgeHandler { 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 Logger logger = LoggerFactory.getLogger(VolvoOnCallBridgeHandler.class);
private final Properties httpHeader = new Properties();
private final List<ScheduledFuture<?>> pendingActions = new Stack<>();
private final Gson gson; private final Gson gson;
private final HttpClient httpClient;
private @NonNullByDefault({}) CustomerAccounts customerAccount; private @Nullable VocHttpApi api;
public VolvoOnCallBridgeHandler(Bridge bridge) { public VolvoOnCallBridgeHandler(Bridge bridge, Gson gson, HttpClient httpClient) {
super(bridge); super(bridge);
this.gson = gson;
httpHeader.put("cache-control", "no-cache"); this.httpClient = httpClient;
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 @Override
public void initialize() { public void initialize() {
logger.debug("Initializing VolvoOnCall API bridge handler."); logger.debug("Initializing VolvoOnCall API bridge handler.");
VolvoOnCallBridgeConfiguration configuration = getConfigAs(VolvoOnCallBridgeConfiguration.class); ApiBridgeConfiguration configuration = getConfigAs(ApiBridgeConfiguration.class);
httpHeader.setProperty("Authorization", configuration.getAuthorization());
try { try {
customerAccount = getURL(SERVICE_URL + "customeraccounts/", CustomerAccounts.class); api = new VocHttpApi(configuration, gson, httpClient);
if (customerAccount.username != null) { CustomerAccounts account = api.getURL("customeraccounts/", CustomerAccounts.class);
updateStatus(ThingStatus.ONLINE); if (account.username != null) {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, account.username);
} else { } else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Incorrect login credentials");
"Incorrect username or password");
} }
} catch (JsonSyntaxException | VolvoOnCallException e) { } catch (VolvoOnCallException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
} }
} }
@Override @Override
public void handleCommand(ChannelUID channelUID, Command command) { public void dispose() {
logger.debug("VolvoOnCall Bridge is read-only and does not handle commands"); if (api != null) {
} try {
api.dispose();
public String[] getVehiclesRelationsURL() { api = null;
if (customerAccount != null) { } catch (Exception e) {
return customerAccount.accountVehicleRelationsURL; logger.warn("Unable to stop VocHttpApi : {}", e.getMessage());
}
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 { public @Nullable VocHttpApi getApi() {
InputStream inputStream = body != null ? new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)) : null; return api;
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 @Override
public void dispose() { public Collection<Class<? extends ThingHandlerService>> getServices() {
super.dispose(); return Collections.singleton(VolvoVehicleDiscoveryService.class);
pendingActions.stream().filter(f -> !f.isCancelled()).forEach(f -> f.cancel(true)); }
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
// Do nothing
} }
} }

View File

@@ -4,7 +4,7 @@
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd"> xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>VolvoOnCall Binding</name> <name>VolvoOnCall Binding</name>
<description>This binding enables the access to VolvoOnCall features.</description> <description>This binding enables the access to VolvoOnCall services.</description>
<author>Gaël L'hopital</author> <author>Gaël L'hopital</author>
</binding:binding> </binding:binding>

View File

@@ -0,0 +1,10 @@
# binding
binding.volvooncall.name = Extension VolvoOnCall
binding.volvooncall.description = Cette extension fournit l'accès aux services de Volvo On Call.
# thing types
thing-type.volvooncall.vocapi.label = API Volvo On Call
thing-type.volvooncall.vocapi.description = Fournit l'interface avec le service en ligne Volvo On Call. Pour recevoir les données, vous devez vous munir de vos informations de connection (nom d'utilisateur, mot de passe).
thing-type.volvooncall.vehicle.label = Véhicule
thing-type.volvooncall.vehicle.description = Toutes les informations disponibles sur le véhicule Volvo.

View File

@@ -32,10 +32,11 @@
<description>VIN of the vehicle associated with this Thing</description> <description>VIN of the vehicle associated with this Thing</description>
</parameter> </parameter>
<parameter name="refresh" type="integer" min="5" required="false"> <parameter name="refresh" type="integer" min="5" required="true">
<label>Refresh Interval</label> <label>Refresh Interval</label>
<description>Specifies the refresh interval in minutes.</description> <description>Specifies the refresh interval in minutes.</description>
<default>5</default> <default>10</default>
<advanced>true</advanced>
</parameter> </parameter>
</config-description> </config-description>
</thing-type> </thing-type>
@@ -51,6 +52,7 @@
<channel id="washerFluidLevel" typeId="washerFluidLevel"/> <channel id="washerFluidLevel" typeId="washerFluidLevel"/>
<channel id="serviceWarningStatus" typeId="serviceWarningStatus"/> <channel id="serviceWarningStatus" typeId="serviceWarningStatus"/>
<channel id="bulbFailure" typeId="bulbFailure"/> <channel id="bulbFailure" typeId="bulbFailure"/>
<channel id="carEvent" typeId="carEvent"/>
</channels> </channels>
</channel-group-type> </channel-group-type>
@@ -58,34 +60,34 @@
<label>Last Trip</label> <label>Last Trip</label>
<channels> <channels>
<channel id="tripConsumption" typeId="fuelQuantity"> <channel id="tripConsumption" typeId="fuelQuantity">
<label>Consumption</label> <label>Trip Consumption</label>
<description>Indicates the quantity of fuel consumed by the trip</description> <description>Indicates the quantity of fuel consumed by the trip</description>
</channel> </channel>
<channel id="tripDistance" typeId="odometer"> <channel id="tripDistance" typeId="odometer">
<label>Distance</label> <label>Trip Distance</label>
<description>Distance traveled</description> <description>Distance traveled</description>
</channel> </channel>
<channel id="tripStartTime" typeId="timestamp"> <channel id="tripStartTime" typeId="timestamp">
<label>Start Time</label> <label>Trip Start Time</label>
<description>Trip start time</description> <description>Trip start time</description>
</channel> </channel>
<channel id="tripEndTime" typeId="timestamp"> <channel id="tripEndTime" typeId="timestamp">
<label>End Time</label> <label>Trip End Time</label>
<description>Trip end time</description> <description>Trip end time</description>
</channel> </channel>
<channel id="tripDuration" typeId="tripDuration"/> <channel id="tripDuration" typeId="tripDuration"/>
<channel id="tripStartOdometer" typeId="odometer"> <channel id="tripStartOdometer" typeId="odometer">
<label>Start Odometer</label> <label>Trip Start Odometer</label>
</channel> </channel>
<channel id="tripStopOdometer" typeId="odometer"> <channel id="tripStopOdometer" typeId="odometer">
<label>Stop Odometer</label> <label>Trip Stop Odometer</label>
</channel> </channel>
<channel id="startPosition" typeId="location"> <channel id="startPosition" typeId="location">
<label>From</label> <label>Trip From</label>
<description>Starting location of the car</description> <description>Starting location of the car</description>
</channel> </channel>
<channel id="endPosition" typeId="location"> <channel id="endPosition" typeId="location">
<label>To</label> <label>Trip To</label>
<description>Stopping location of the car</description> <description>Stopping location of the car</description>
</channel> </channel>
</channels> </channels>
@@ -95,16 +97,16 @@
<label>Doors Opening Status</label> <label>Doors Opening Status</label>
<channels> <channels>
<channel id="frontLeft" typeId="door"> <channel id="frontLeft" typeId="door">
<label>Front Left</label> <label>Front Left Door</label>
</channel> </channel>
<channel id="frontRight" typeId="door"> <channel id="frontRight" typeId="door">
<label>Front Right</label> <label>Front Right Door</label>
</channel> </channel>
<channel id="rearLeft" typeId="door"> <channel id="rearLeft" typeId="door">
<label>Rear Left</label> <label>Rear Left Door</label>
</channel> </channel>
<channel id="rearRight" typeId="door"> <channel id="rearRight" typeId="door">
<label>Rear Right</label> <label>Rear Right Door</label>
</channel> </channel>
<channel id="hood" typeId="door"> <channel id="hood" typeId="door">
<label>Hood</label> <label>Hood</label>
@@ -120,16 +122,16 @@
<label>Windows Opening Status</label> <label>Windows Opening Status</label>
<channels> <channels>
<channel id="frontLeftWnd" typeId="window"> <channel id="frontLeftWnd" typeId="window">
<label>Front Left</label> <label>Front Left Window</label>
</channel> </channel>
<channel id="frontRightWnd" typeId="window"> <channel id="frontRightWnd" typeId="window">
<label>Front Right</label> <label>Front Right Window</label>
</channel> </channel>
<channel id="rearLeftWnd" typeId="window"> <channel id="rearLeftWnd" typeId="window">
<label>Rear Left</label> <label>Rear Left Window</label>
</channel> </channel>
<channel id="rearRightWnd" typeId="window"> <channel id="rearRightWnd" typeId="window">
<label>Rear Right</label> <label>Rear Right Window</label>
</channel> </channel>
</channels> </channels>
</channel-group-type> </channel-group-type>
@@ -138,16 +140,16 @@
<label>Tyre pressure status</label> <label>Tyre pressure status</label>
<channels> <channels>
<channel id="frontLeftTyre" typeId="tyrePressure"> <channel id="frontLeftTyre" typeId="tyrePressure">
<label>Front Left</label> <label>Front Left Tyre</label>
</channel> </channel>
<channel id="frontRightTyre" typeId="tyrePressure"> <channel id="frontRightTyre" typeId="tyrePressure">
<label>Front Right</label> <label>Front Right Tyre</label>
</channel> </channel>
<channel id="rearLeftTyre" typeId="tyrePressure"> <channel id="rearLeftTyre" typeId="tyrePressure">
<label>Rear Left</label> <label>Rear Left Tyre</label>
</channel> </channel>
<channel id="rearRightTyre" typeId="tyrePressure"> <channel id="rearRightTyre" typeId="tyrePressure">
<label>Rear Right</label> <label>Rear Right Tyre</label>
</channel> </channel>
</channels> </channels>
</channel-group-type> </channel-group-type>
@@ -186,7 +188,7 @@
<label>Location Info</label> <label>Location Info</label>
<channels> <channels>
<channel id="location" typeId="location"> <channel id="location" typeId="location">
<label>Location</label> <label>Current Location</label>
<description>The position of the vehicle</description> <description>The position of the vehicle</description>
</channel> </channel>
<channel id="calculatedLocation" typeId="calculatedLocation"/> <channel id="calculatedLocation" typeId="calculatedLocation"/>
@@ -245,7 +247,7 @@
<item-type>Number:Speed</item-type> <item-type>Number:Speed</item-type>
<label>Average speed</label> <label>Average speed</label>
<description>Average speed of the vehicle</description> <description>Average speed of the vehicle</description>
<state pattern="%d %unit%" readOnly="true"></state> <state pattern="%.2f %unit%" readOnly="true"></state>
</channel-type> </channel-type>
<channel-type id="fuelQuantity"> <channel-type id="fuelQuantity">
@@ -264,8 +266,8 @@
<channel-type id="fuelConsumption" advanced="true"> <channel-type id="fuelConsumption" advanced="true">
<item-type>Number</item-type> <item-type>Number</item-type>
<label>Average Consumption</label> <label>Average Consumption</label>
<description>Indicates the average fuel consumption in L/100km</description> <description>Indicates the average fuel consumption in l/100km</description>
<state pattern="%.1f L/100km" readOnly="true"></state> <state pattern="%.1f l/100km" readOnly="true"></state>
</channel-type> </channel-type>
<channel-type id="location"> <channel-type id="location">
@@ -388,4 +390,16 @@
<state readOnly="true"/> <state readOnly="true"/>
</channel-type> </channel-type>
<channel-type id="carEvent">
<kind>trigger</kind>
<label>Car Event</label>
<event>
<options>
<option value="CAR_STOPPED">Car stopped</option>
<option value="CAR_STARTED">Car started</option>
<option value="CAR_MOVED">Car has moved</option>
</options>
</event>
</channel-type>
</thing:thing-descriptions> </thing:thing-descriptions>