[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

@@ -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>