[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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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.
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 |
|-----------|------------------------------------------------------------------------- |--------- |
| username | Username from the VolvoOnCall app (email address) | yes |
| password | Password from the VolvoOnCall app | yes |
| Parameter | Description | Required |
|-----------------|------------------------------------------------------|--------- |
| username | Username from the VolvoOnCall app (email address) | 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.
@ -35,7 +35,8 @@ The 'VolvoOnCall API' bridge uses the owner's email address and password in orde
| Parameter | Name | Description | Required |
|-----------------|------------------|---------------------------------------------------------|----------|
| 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 | |
## 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
demo.things:

View File

@ -30,16 +30,6 @@ public class VolvoOnCallBindingConstants {
public static final String BINDING_ID = "volvooncall";
// Vehicle properties
public static final String VIN = "vin";
// The URL to use to connect to VocAPI with.
// TODO : for North America and China syntax changes to vocapi-cn.xxx
public static final String SERVICE_URL = "https://vocapi.wirelesscar.net/customerapi/rest/v3.0/";
// The JSON content type used when talking to VocAPI.
public static final String JSON_CONTENT_TYPE = "application/json";
// List of Thing Type UIDs
public static final ThingTypeUID APIBRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "vocapi");
public static final ThingTypeUID VEHICLE_THING_TYPE = new ThingTypeUID(BINDING_ID, "vehicle");
@ -49,6 +39,10 @@ public class VolvoOnCallBindingConstants {
public static final String GROUP_WINDOWS = "windows";
public static final String GROUP_TYRES = "tyrePressure";
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
public static final String TAILGATE = "tailgate";
@ -90,6 +84,11 @@ public class VolvoOnCallBindingConstants {
public static final String CHARGING_END = "chargingEnd";
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
public static final String LAST_TRIP_GROUP = "lasttrip";
public static final String TRIP_CONSUMPTION = "tripConsumption";

View File

@ -34,7 +34,9 @@ public class VolvoOnCallException extends Exception {
public static enum ErrorType {
UNKNOWN,
SERVICE_UNAVAILABLE,
SERVICE_UNABLE_TO_START,
IOEXCEPTION,
INTERRUPTED,
JSON_SYNTAX;
}
@ -44,6 +46,8 @@ public class VolvoOnCallException extends Exception {
super(label);
if ("FoundationServicesUnavailable".equalsIgnoreCase(label)) {
cause = ErrorType.SERVICE_UNAVAILABLE;
} else if ("ServiceUnableToStart".equalsIgnoreCase(label)) {
cause = ErrorType.SERVICE_UNABLE_TO_START;
} else {
cause = ErrorType.UNKNOWN;
logger.warn("Unhandled VoC error : {} : {}", label, description);
@ -56,6 +60,8 @@ public class VolvoOnCallException extends Exception {
cause = ErrorType.IOEXCEPTION;
} else if (e instanceof JsonSyntaxException) {
cause = ErrorType.JSON_SYNTAX;
} else if (e instanceof InterruptedException) {
cause = ErrorType.INTERRUPTED;
} else {
cause = ErrorType.UNKNOWN;
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 java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;
import java.time.ZonedDateTime;
import org.eclipse.jdt.annotation.NonNullByDefault;
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.VehicleStateDescriptionProvider;
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.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.ThingUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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
* handlers.
@ -48,13 +51,31 @@ import org.slf4j.LoggerFactory;
@NonNullByDefault
@Component(configurationPid = "binding.volvooncall", service = ThingHandlerFactory.class)
public class VolvoOnCallHandlerFactory extends BaseThingHandlerFactory {
private final Logger logger = LoggerFactory.getLogger(VolvoOnCallHandlerFactory.class);
private final Map<ThingUID, ServiceRegistration<?>> discoveryServiceRegs = new HashMap<>();
private final VehicleStateDescriptionProvider stateDescriptionProvider;
private final Gson gson;
private final HttpClient httpClient;
@Activate
public VolvoOnCallHandlerFactory(@Reference VehicleStateDescriptionProvider provider) {
public VolvoOnCallHandlerFactory(@Reference VehicleStateDescriptionProvider provider,
@Reference HttpClientFactory httpClientFactory) {
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
@ -66,36 +87,11 @@ public class VolvoOnCallHandlerFactory extends BaseThingHandlerFactory {
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (APIBRIDGE_THING_TYPE.equals(thingTypeUID)) {
VolvoOnCallBridgeHandler bridgeHandler = new VolvoOnCallBridgeHandler((Bridge) thing);
registerDeviceDiscoveryService(bridgeHandler);
return bridgeHandler;
return new VolvoOnCallBridgeHandler((Bridge) thing, gson, httpClient);
} else if (VEHICLE_THING_TYPE.equals(thingTypeUID)) {
return new VehicleHandler(thing, stateDescriptionProvider);
}
logger.warn("ThingHandler not found for {}", thing.getThingTypeUID());
return null;
}
@Override
protected void removeHandler(ThingHandler thingHandler) {
if (thingHandler instanceof VolvoOnCallBridgeHandler) {
ThingUID thingUID = thingHandler.getThing().getUID();
unregisterDeviceDiscoveryService(thingUID);
}
super.removeHandler(thingHandler);
}
private void registerDeviceDiscoveryService(VolvoOnCallBridgeHandler bridgeHandler) {
VolvoOnCallDiscoveryService discoveryService = new VolvoOnCallDiscoveryService(bridgeHandler);
discoveryServiceRegs.put(bridgeHandler.getThing().getUID(),
bundleContext.registerService(DiscoveryService.class.getName(), discoveryService, new Hashtable<>()));
}
private void unregisterDeviceDiscoveryService(ThingUID thingUID) {
if (discoveryServiceRegs.containsKey(thingUID)) {
ServiceRegistration<?> serviceReg = discoveryServiceRegs.get(thingUID);
serviceReg.unregister();
discoveryServiceRegs.remove(thingUID);
}
}
}

View File

@ -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;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.volvooncall.internal.handler.VehicleHandler;
import org.openhab.core.automation.annotation.ActionInput;
import org.openhab.core.automation.annotation.RuleAction;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.thing.binding.ThingActions;
import org.openhab.core.thing.binding.ThingActionsScope;
import org.openhab.core.thing.binding.ThingHandler;
@ -34,7 +34,7 @@ import org.slf4j.LoggerFactory;
*/
@ThingActionsScope(name = "volvooncall")
@NonNullByDefault
public class VolvoOnCallActions implements ThingActions, IVolvoOnCallActions {
public class VolvoOnCallActions implements ThingActions {
private final Logger logger = LoggerFactory.getLogger(VolvoOnCallActions.class);
@ -56,40 +56,29 @@ public class VolvoOnCallActions implements ThingActions, IVolvoOnCallActions {
return this.handler;
}
@Override
@RuleAction(label = "Volvo On Call : Close", description = "Closes the car")
@RuleAction(label = "close the car", description = "Closes the car")
public void closeCarCommand() {
logger.debug("closeCarCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionClose();
handler.actionOpenClose(LOCK, OnOffType.ON);
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void closeCarCommand(@Nullable ThingActions actions) {
invokeMethodOf(actions).closeCarCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Open", description = "Opens the car")
@RuleAction(label = "open the car", description = "Opens the car")
public void openCarCommand() {
logger.debug("openCarCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionOpen();
handler.actionOpenClose(UNLOCK, OnOffType.OFF);
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void openCarCommand(@Nullable ThingActions actions) {
invokeMethodOf(actions).openCarCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Start Engine", description = "Starts the engine")
@RuleAction(label = "start the engine", description = "Starts the engine")
public void engineStartCommand(@ActionInput(name = "runtime", label = "Runtime") @Nullable Integer runtime) {
logger.debug("engineStartCommand called");
VehicleHandler handler = this.handler;
@ -100,76 +89,51 @@ public class VolvoOnCallActions implements ThingActions, IVolvoOnCallActions {
}
}
public static void engineStartCommand(@Nullable ThingActions actions, @Nullable Integer runtime) {
invokeMethodOf(actions).engineStartCommand(runtime);
}
@Override
@RuleAction(label = "Volvo On Call : Heater Start", description = "Starts car heater")
@RuleAction(label = "start the heater", description = "Starts car heater")
public void heaterStartCommand() {
logger.debug("heaterStartCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionHeater(true);
handler.actionHeater(REMOTE_HEATER, true);
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void heaterStartCommand(@Nullable ThingActions actions) {
invokeMethodOf(actions).heaterStartCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Preclimatization Start", description = "Starts car heater")
@RuleAction(label = "start preclimatization", description = "Starts the car heater")
public void preclimatizationStartCommand() {
logger.debug("preclimatizationStartCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionPreclimatization(true);
handler.actionHeater(PRECLIMATIZATION, true);
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void preclimatizationStartCommand(@Nullable ThingActions actions) {
invokeMethodOf(actions).preclimatizationStartCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Heater Stop", description = "Stops car heater")
@RuleAction(label = "stop the heater", description = "Stops car heater")
public void heaterStopCommand() {
logger.debug("heaterStopCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionHeater(false);
handler.actionHeater(REMOTE_HEATER, false);
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void heaterStopCommand(@Nullable ThingActions actions) {
invokeMethodOf(actions).heaterStopCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Preclimatization Stop", description = "Stops car heater")
@RuleAction(label = "stop preclimatization", description = "Stops the car heater")
public void preclimatizationStopCommand() {
logger.debug("preclimatizationStopCommand called");
VehicleHandler handler = this.handler;
if (handler != null) {
handler.actionPreclimatization(false);
handler.actionHeater(PRECLIMATIZATION, false);
} else {
logger.warn("VolvoOnCall Action service ThingHandler is null!");
}
}
public static void preclimatizationStopCommand(@Nullable ThingActions actions) {
invokeMethodOf(actions).preclimatizationStopCommand();
}
@Override
@RuleAction(label = "Volvo On Call : Honk-blink", description = "Activates the horn and or lights of the car")
@RuleAction(label = "honk-blink", description = "Activates the horn and or lights of the car")
public void honkBlinkCommand(@ActionInput(name = "honk", label = "Honk") Boolean honk,
@ActionInput(name = "blink", label = "Blink") Boolean blink) {
logger.debug("honkBlinkCommand called");
@ -180,27 +144,4 @@ public class VolvoOnCallActions implements ThingActions, IVolvoOnCallActions {
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;
/**
* The {@link VolvoOnCallBridgeConfiguration} is responsible for holding
* The {@link ApiBridgeConfiguration} is responsible for holding
* configuration informations needed to access VOC API
*
* @author Gaël L'hopital - Initial contribution
*/
@NonNullByDefault
public class VolvoOnCallBridgeConfiguration {
public class ApiBridgeConfiguration {
public String username = "";
public String password = "";

View File

@ -22,6 +22,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
*/
@NonNullByDefault
public class VehicleConfiguration {
public static String VIN = "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;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
@ -26,7 +29,7 @@ import com.google.gson.annotations.SerializedName;
@NonNullByDefault
public class CustomerAccounts extends VocAnswer {
@SerializedName("accountVehicleRelations")
public @NonNullByDefault({}) String[] accountVehicleRelationsURL;
public List<String> accountVehicleRelationsURL = new ArrayList<>();
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 org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.library.types.StringType;
/**
* The {@link HvBattery} is responsible for storing
@ -26,7 +27,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
public class HvBattery {
public int hvBatteryLevel = UNDEFINED;
public int distanceToHVBatteryEmpty = UNDEFINED;
public @NonNullByDefault({}) String hvBatteryChargeStatusDerived;
public @NonNullByDefault({}) StringType hvBatteryChargeStatusDerived;
public int timeToHVBatteryFullyCharged = UNDEFINED;
/*
* Currently unused in the binding, maybe interesting in the future

View File

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

View File

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

View File

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

View File

@ -12,43 +12,28 @@
*/
package org.openhab.binding.volvooncall.internal.handler;
import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.ZonedDateTime;
import java.util.List;
import java.util.Properties;
import java.util.Stack;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.Collection;
import java.util.Collections;
import org.eclipse.jdt.annotation.NonNullByDefault;
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.ErrorType;
import org.openhab.binding.volvooncall.internal.config.VolvoOnCallBridgeConfiguration;
import org.openhab.binding.volvooncall.internal.api.VocHttpApi;
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.PostResponse;
import org.openhab.binding.volvooncall.internal.dto.VocAnswer;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.OpenClosedType;
import org.openhab.core.thing.Bridge;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseBridgeHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.types.Command;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonSyntaxException;
/**
* The {@link VolvoOnCallBridgeHandler} is responsible for handling commands, which are
@ -58,144 +43,60 @@ import com.google.gson.JsonSyntaxException;
*/
@NonNullByDefault
public class VolvoOnCallBridgeHandler extends BaseBridgeHandler {
private static final int REQUEST_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(20);
private final Logger logger = LoggerFactory.getLogger(VolvoOnCallBridgeHandler.class);
private final Properties httpHeader = new Properties();
private final List<ScheduledFuture<?>> pendingActions = new Stack<>();
private final Gson gson;
private 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);
httpHeader.put("cache-control", "no-cache");
httpHeader.put("content-type", JSON_CONTENT_TYPE);
httpHeader.put("x-device-id", "Device");
httpHeader.put("x-originator-type", "App");
httpHeader.put("x-os-type", "Android");
httpHeader.put("x-os-version", "22");
httpHeader.put("Accept", "*/*");
gson = new GsonBuilder()
.registerTypeAdapter(ZonedDateTime.class,
(JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> ZonedDateTime
.parse(json.getAsJsonPrimitive().getAsString().replaceAll("\\+0000", "Z")))
.registerTypeAdapter(OpenClosedType.class,
(JsonDeserializer<OpenClosedType>) (json, type,
jsonDeserializationContext) -> json.getAsBoolean() ? OpenClosedType.OPEN
: OpenClosedType.CLOSED)
.registerTypeAdapter(OnOffType.class,
(JsonDeserializer<OnOffType>) (json, type,
jsonDeserializationContext) -> json.getAsBoolean() ? OnOffType.ON : OnOffType.OFF)
.create();
this.gson = gson;
this.httpClient = httpClient;
}
@Override
public void initialize() {
logger.debug("Initializing VolvoOnCall API bridge handler.");
VolvoOnCallBridgeConfiguration configuration = getConfigAs(VolvoOnCallBridgeConfiguration.class);
ApiBridgeConfiguration configuration = getConfigAs(ApiBridgeConfiguration.class);
httpHeader.setProperty("Authorization", configuration.getAuthorization());
try {
customerAccount = getURL(SERVICE_URL + "customeraccounts/", CustomerAccounts.class);
if (customerAccount.username != null) {
updateStatus(ThingStatus.ONLINE);
api = new VocHttpApi(configuration, gson, httpClient);
CustomerAccounts account = api.getURL("customeraccounts/", CustomerAccounts.class);
if (account.username != null) {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE, account.username);
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"Incorrect username or password");
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Incorrect login credentials");
}
} catch (JsonSyntaxException | VolvoOnCallException e) {
} catch (VolvoOnCallException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
}
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
logger.debug("VolvoOnCall Bridge is read-only and does not handle commands");
}
public String[] getVehiclesRelationsURL() {
if (customerAccount != null) {
return customerAccount.accountVehicleRelationsURL;
}
return new String[0];
}
public <T extends VocAnswer> T getURL(Class<T> objectClass, String vin) throws VolvoOnCallException {
String url = SERVICE_URL + "vehicles/" + vin + "/" + objectClass.getSimpleName().toLowerCase();
return getURL(url, objectClass);
}
public <T extends VocAnswer> T getURL(String url, Class<T> objectClass) throws VolvoOnCallException {
try {
String jsonResponse = HttpUtil.executeUrl("GET", url, httpHeader, null, JSON_CONTENT_TYPE, REQUEST_TIMEOUT);
logger.debug("Request for : {}", url);
logger.debug("Received : {}", jsonResponse);
T response = gson.fromJson(jsonResponse, objectClass);
String error = response.getErrorLabel();
if (error != null) {
throw new VolvoOnCallException(error, response.getErrorDescription());
}
return response;
} catch (JsonSyntaxException | IOException e) {
throw new VolvoOnCallException(e);
}
}
public class ActionResultControler implements Runnable {
PostResponse postResponse;
ActionResultControler(PostResponse postResponse) {
this.postResponse = postResponse;
}
@Override
public void run() {
switch (postResponse.status) {
case SUCCESSFULL:
case FAILED:
logger.info("Action status : {} for vehicle : {}.", postResponse.status.toString(),
postResponse.vehicleId);
getThing().getThings().stream().filter(VehicleHandler.class::isInstance)
.map(VehicleHandler.class::cast)
.forEach(handler -> handler.updateIfMatches(postResponse.vehicleId));
break;
default:
try {
postResponse = getURL(postResponse.serviceURL, PostResponse.class);
scheduler.schedule(new ActionResultControler(postResponse), 1000, TimeUnit.MILLISECONDS);
} catch (VolvoOnCallException e) {
if (e.getType() == ErrorType.SERVICE_UNAVAILABLE) {
scheduler.schedule(new ActionResultControler(postResponse), 1000, TimeUnit.MILLISECONDS);
}
}
public void dispose() {
if (api != null) {
try {
api.dispose();
api = null;
} catch (Exception e) {
logger.warn("Unable to stop VocHttpApi : {}", e.getMessage());
}
}
}
void postURL(String URL, @Nullable String body) throws VolvoOnCallException {
InputStream inputStream = body != null ? new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)) : null;
try {
String jsonString = HttpUtil.executeUrl("POST", URL, httpHeader, inputStream, null, REQUEST_TIMEOUT);
logger.debug("Post URL: {} Attributes {}", URL, httpHeader);
PostResponse postResponse = gson.fromJson(jsonString, PostResponse.class);
String error = postResponse.getErrorLabel();
if (error == null) {
pendingActions
.add(scheduler.schedule(new ActionResultControler(postResponse), 1000, TimeUnit.MILLISECONDS));
} else {
throw new VolvoOnCallException(error, postResponse.getErrorDescription());
}
pendingActions.removeIf(ScheduledFuture::isDone);
} catch (JsonSyntaxException | IOException e) {
throw new VolvoOnCallException(e);
}
public @Nullable VocHttpApi getApi() {
return api;
}
@Override
public void dispose() {
super.dispose();
pendingActions.stream().filter(f -> !f.isCancelled()).forEach(f -> f.cancel(true));
public Collection<Class<? extends ThingHandlerService>> getServices() {
return Collections.singleton(VolvoVehicleDiscoveryService.class);
}
@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">
<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>
</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>
</parameter>
<parameter name="refresh" type="integer" min="5" required="false">
<parameter name="refresh" type="integer" min="5" required="true">
<label>Refresh Interval</label>
<description>Specifies the refresh interval in minutes.</description>
<default>5</default>
<default>10</default>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
@ -51,6 +52,7 @@
<channel id="washerFluidLevel" typeId="washerFluidLevel"/>
<channel id="serviceWarningStatus" typeId="serviceWarningStatus"/>
<channel id="bulbFailure" typeId="bulbFailure"/>
<channel id="carEvent" typeId="carEvent"/>
</channels>
</channel-group-type>
@ -58,34 +60,34 @@
<label>Last Trip</label>
<channels>
<channel id="tripConsumption" typeId="fuelQuantity">
<label>Consumption</label>
<label>Trip Consumption</label>
<description>Indicates the quantity of fuel consumed by the trip</description>
</channel>
<channel id="tripDistance" typeId="odometer">
<label>Distance</label>
<label>Trip Distance</label>
<description>Distance traveled</description>
</channel>
<channel id="tripStartTime" typeId="timestamp">
<label>Start Time</label>
<label>Trip Start Time</label>
<description>Trip start time</description>
</channel>
<channel id="tripEndTime" typeId="timestamp">
<label>End Time</label>
<label>Trip End Time</label>
<description>Trip end time</description>
</channel>
<channel id="tripDuration" typeId="tripDuration"/>
<channel id="tripStartOdometer" typeId="odometer">
<label>Start Odometer</label>
<label>Trip Start Odometer</label>
</channel>
<channel id="tripStopOdometer" typeId="odometer">
<label>Stop Odometer</label>
<label>Trip Stop Odometer</label>
</channel>
<channel id="startPosition" typeId="location">
<label>From</label>
<label>Trip From</label>
<description>Starting location of the car</description>
</channel>
<channel id="endPosition" typeId="location">
<label>To</label>
<label>Trip To</label>
<description>Stopping location of the car</description>
</channel>
</channels>
@ -95,16 +97,16 @@
<label>Doors Opening Status</label>
<channels>
<channel id="frontLeft" typeId="door">
<label>Front Left</label>
<label>Front Left Door</label>
</channel>
<channel id="frontRight" typeId="door">
<label>Front Right</label>
<label>Front Right Door</label>
</channel>
<channel id="rearLeft" typeId="door">
<label>Rear Left</label>
<label>Rear Left Door</label>
</channel>
<channel id="rearRight" typeId="door">
<label>Rear Right</label>
<label>Rear Right Door</label>
</channel>
<channel id="hood" typeId="door">
<label>Hood</label>
@ -120,16 +122,16 @@
<label>Windows Opening Status</label>
<channels>
<channel id="frontLeftWnd" typeId="window">
<label>Front Left</label>
<label>Front Left Window</label>
</channel>
<channel id="frontRightWnd" typeId="window">
<label>Front Right</label>
<label>Front Right Window</label>
</channel>
<channel id="rearLeftWnd" typeId="window">
<label>Rear Left</label>
<label>Rear Left Window</label>
</channel>
<channel id="rearRightWnd" typeId="window">
<label>Rear Right</label>
<label>Rear Right Window</label>
</channel>
</channels>
</channel-group-type>
@ -138,16 +140,16 @@
<label>Tyre pressure status</label>
<channels>
<channel id="frontLeftTyre" typeId="tyrePressure">
<label>Front Left</label>
<label>Front Left Tyre</label>
</channel>
<channel id="frontRightTyre" typeId="tyrePressure">
<label>Front Right</label>
<label>Front Right Tyre</label>
</channel>
<channel id="rearLeftTyre" typeId="tyrePressure">
<label>Rear Left</label>
<label>Rear Left Tyre</label>
</channel>
<channel id="rearRightTyre" typeId="tyrePressure">
<label>Rear Right</label>
<label>Rear Right Tyre</label>
</channel>
</channels>
</channel-group-type>
@ -186,7 +188,7 @@
<label>Location Info</label>
<channels>
<channel id="location" typeId="location">
<label>Location</label>
<label>Current Location</label>
<description>The position of the vehicle</description>
</channel>
<channel id="calculatedLocation" typeId="calculatedLocation"/>
@ -245,7 +247,7 @@
<item-type>Number:Speed</item-type>
<label>Average speed</label>
<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 id="fuelQuantity">
@ -264,8 +266,8 @@
<channel-type id="fuelConsumption" advanced="true">
<item-type>Number</item-type>
<label>Average Consumption</label>
<description>Indicates the average fuel consumption in L/100km</description>
<state pattern="%.1f L/100km" readOnly="true"></state>
<description>Indicates the average fuel consumption in l/100km</description>
<state pattern="%.1f l/100km" readOnly="true"></state>
</channel-type>
<channel-type id="location">
@ -388,4 +390,16 @@
<state readOnly="true"/>
</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>