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.
| 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 |
+
| 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:
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");
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";
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";
public static enum ErrorType {
UNKNOWN,
SERVICE_UNAVAILABLE,
+ SERVICE_UNABLE_TO_START,
IOEXCEPTION,
+ INTERRUPTED,
JSON_SYNTAX;
}
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);
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());
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.
@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
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);
- }
- }
}
+++ /dev/null
-/**
- * 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();
-}
*/
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;
*/
@ThingActionsScope(name = "volvooncall")
@NonNullByDefault
-public class VolvoOnCallActions implements ThingActions, IVolvoOnCallActions {
+public class VolvoOnCallActions implements ThingActions {
private final Logger logger = LoggerFactory.getLogger(VolvoOnCallActions.class);
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;
}
}
- 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");
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");
- }
}
--- /dev/null
+/**
+ * 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);
+ }
+ }
+ }
+ }
+}
--- /dev/null
+/**
+ * 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);
+ }
+}
--- /dev/null
+/**
+ * Copyright (c) 2010-2020 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.volvooncall.internal.config;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+
+/**
+ * The {@link ApiBridgeConfiguration} is responsible for holding
+ * configuration informations needed to access VOC API
+ *
+ * @author Gaël L'hopital - Initial contribution
+ */
+@NonNullByDefault
+public class ApiBridgeConfiguration {
+ public String username = "";
+ public String password = "";
+
+ public String getAuthorization() {
+ byte[] authorization = Base64.getEncoder().encode((String.format("%s:%s", username, password)).getBytes());
+ return "Basic " + new String(authorization, StandardCharsets.UTF_8);
+ }
+}
*/
@NonNullByDefault
public class VehicleConfiguration {
+ public static String VIN = "vin";
+
public String vin = "";
- public Integer refresh = 5;
+ public int refresh = 10;
}
+++ /dev/null
-/**
- * Copyright (c) 2010-2020 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.volvooncall.internal.config;
-
-import java.nio.charset.StandardCharsets;
-import java.util.Base64;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * The {@link VolvoOnCallBridgeConfiguration} is responsible for holding
- * configuration informations needed to access VOC API
- *
- * @author Gaël L'hopital - Initial contribution
- */
-@NonNullByDefault
-public class VolvoOnCallBridgeConfiguration {
- public String username = "";
- public String password = "";
-
- public String getAuthorization() {
- byte[] authorization = Base64.getEncoder().encode((String.format("%s:%s", username, password)).getBytes());
- return "Basic " + new String(authorization, StandardCharsets.UTF_8);
- }
-}
+++ /dev/null
-/**
- * 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();
- }
-}
--- /dev/null
+/**
+ * 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();
+ }
+}
*/
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;
@NonNullByDefault
public class CustomerAccounts extends VocAnswer {
@SerializedName("accountVehicleRelations")
- public @NonNullByDefault({}) String[] accountVehicleRelationsURL;
+ public List<String> accountVehicleRelationsURL = new ArrayList<>();
public @Nullable String username;
/*
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
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
*/
package org.openhab.binding.volvooncall.internal.dto;
+import java.time.ZonedDateTime;
+
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.SerializedName;
@SerializedName("service")
public @NonNullByDefault({}) String serviceURL;
public @NonNullByDefault({}) ServiceType serviceType;
-
+ public @NonNullByDefault({}) ZonedDateTime startTime;
/*
* Currently unused in the binding, maybe interesting in the future
*
* }
*
* private ZonedDateTime statusTimestamp;
- * private ZonedDateTime startTime;
+ *
* private FailureReason failureReason;
*
* private Integer customerServiceId;
*/
package org.openhab.binding.volvooncall.internal.dto;
+import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
*/
@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;
*/
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
*/
@NonNullByDefault
public class Trips extends VocAnswer {
- public @Nullable List<Trip> trips;
+ public List<Trip> trips = new ArrayList<>();
}
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
*/
@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;
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;
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;
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;
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.
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());
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);
}
}
refreshJob.cancel(true);
this.refreshJob = null;
}
+ pendingActions.stream().filter(f -> !f.isCancelled()).forEach(f -> f.cancel(true));
}
@Override
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;
-
- if (tripList != null) {
- List<Trip> newTrips = tripList.stream().filter(trip -> trip.id >= lastTripId).collect(Collectors.toList());
- Collections.reverse(newTrips);
-
- logger.debug("Trips discovered : {}", newTrips.size());
-
- if (!newTrips.isEmpty()) {
- Integer newTripId = newTrips.get(newTrips.size() - 1).id;
- if (newTripId > lastTripId) {
- updateProperty(LAST_TRIP_ID, newTripId.toString());
- lastTripId = newTripId;
- }
-
- newTrips.stream().map(t -> t.tripDetails.get(0)).forEach(catchUpTrip -> {
- logger.debug("Trip found {}", catchUpTrip.getStartTime());
- getThing().getChannels().stream().map(Channel::getUID).filter(
- channelUID -> isLinked(channelUID) && LAST_TRIP_GROUP.equals(channelUID.getGroupId()))
- .forEach(channelUID -> {
- State state = getTripValue(channelUID.getIdWithoutGroup(), catchUpTrip);
- updateState(channelUID, state);
- });
- });
+ 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);
+
+ logger.debug("Trips discovered : {}", newTrips.size());
+
+ 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);
+ });
+ });
}
}
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);
}
}
}
case TRIP_END_POSITION:
return tripDetails.getEndPosition();
}
-
return UnDefType.NULL;
}
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;
}
? 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
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
: 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;
}
- public void actionHonkBlink(Boolean honk, Boolean blink) {
- VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
- if (bridgeHandler != null) {
- StringBuilder url = new StringBuilder(SERVICE_URL + "vehicles/" + vehicle.vehicleId + "/honk_blink/");
-
- if (honk && blink && activeOptions.containsKey(HONK_BLINK)) {
- url.append("both");
- } else if (honk && activeOptions.containsKey(HONK_AND_OR_BLINK)) {
- url.append("horn");
- } else if (blink && activeOptions.containsKey(HONK_AND_OR_BLINK)) {
- url.append("lights");
- } else {
- logger.warn("The vehicle is not capable of this action");
- return;
- }
-
- try {
- bridgeHandler.postURL(url.toString(), vehiclePosition.getPositionAsJSon());
- } catch (VolvoOnCallException e) {
- logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
- }
+ private 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 void actionOpenClose(String action, OnOffType controlState) {
- VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
- if (bridgeHandler != null) {
- if (activeOptions.containsKey(action)) {
- if (!vehicleStatus.getCarLocked().isPresent() || vehicleStatus.getCarLocked().get() != controlState) {
- try {
- StringBuilder address = new StringBuilder(SERVICE_URL);
- address.append("vehicles/");
- address.append(configuration.vin);
- address.append("/");
- address.append(action);
- bridgeHandler.postURL(address.toString(), "{}");
- } catch (VolvoOnCallException e) {
- logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
- }
- } else {
- logger.info("The car {} is already {}ed", configuration.vin, action);
- }
- } else {
- logger.warn("The car {} does not support remote {}ing", configuration.vin, action);
- }
+ private 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 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);
- }
+ 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 actionHeater(Boolean start) {
- actionHeater(REMOTE_HEATER, start);
- }
-
- public void actionPreclimatization(Boolean start) {
- actionHeater(PRECLIMATIZATION, start);
- }
+ public void actionHonkBlink(Boolean honk, Boolean 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;
+ }
- public void actionOpen() {
- actionOpenClose(UNLOCK, OnOffType.OFF);
+ post(url.toString(), vehiclePosition.getPositionAsJSon());
}
- public void actionClose() {
- actionOpenClose(LOCK, OnOffType.ON);
+ private void post(String url, @Nullable String param) {
+ VocHttpApi api = bridgeHandler.getApi();
+ if (api != null) {
+ try {
+ 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);
}
- public void actionStart(Integer runtime) {
- VolvoOnCallBridgeHandler bridgeHandler = getBridgeHandler();
- if (bridgeHandler != null) {
- if (activeOptions.containsKey(ENGINE_START)) {
- String url = SERVICE_URL + "vehicles/" + vehicle.vehicleId + "/engine/start";
- String json = "{\"runtime\":" + runtime.toString() + "}";
-
- try {
- bridgeHandler.postURL(url, json);
- } catch (VolvoOnCallException e) {
- logger.warn("Exception occurred during execution: {}", e.getMessage(), e);
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
- }
+ 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 engine starting", vehicle.vehicleId);
+ logger.info("The car {} is already {}ed", configuration.vin, action);
}
+ } else {
+ logger.warn("The car {} does not support remote {}ing", configuration.vin, action);
}
}
- /*
- * Called by Bridge when it has to notify this of a potential state
- * update
- *
- */
- void updateIfMatches(String vin) {
- if (vin.equalsIgnoreCase(configuration.vin)) {
- queryApiAndUpdateChannels();
+ 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);
}
}
- private @Nullable VolvoOnCallBridgeHandler getBridgeHandler() {
- Bridge bridge = getBridge();
- if (bridge != null) {
- BridgeHandler handler = bridge.getHandler();
- if (handler != null) {
- return (VolvoOnCallBridgeHandler) handler;
- }
+ public void actionStart(Integer runtime) {
+ if (activeOptions.containsKey(ENGINE_START)) {
+ String address = String.format("vehicles/%s/engine/start", vehicle.vehicleId);
+ String json = "{\"runtime\":" + runtime.toString() + "}";
+
+ post(address, json);
+ } else {
+ logger.warn("The car {} does not support remote engine starting", vehicle.vehicleId);
}
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
- return null;
}
@Override
*/
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
*/
@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());
+ public void dispose() {
+ if (api != null) {
+ try {
+ api.dispose();
+ api = null;
+ } catch (Exception e) {
+ logger.warn("Unable to stop VocHttpApi : {}", e.getMessage());
}
- 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 @Nullable VocHttpApi getApi() {
+ return api;
}
- void postURL(String URL, @Nullable String body) throws VolvoOnCallException {
- InputStream inputStream = body != null ? new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)) : null;
- try {
- String jsonString = HttpUtil.executeUrl("POST", URL, httpHeader, inputStream, null, REQUEST_TIMEOUT);
- logger.debug("Post URL: {} Attributes {}", URL, httpHeader);
- PostResponse postResponse = gson.fromJson(jsonString, PostResponse.class);
- String error = postResponse.getErrorLabel();
- if (error == null) {
- pendingActions
- .add(scheduler.schedule(new ActionResultControler(postResponse), 1000, TimeUnit.MILLISECONDS));
- } else {
- throw new VolvoOnCallException(error, postResponse.getErrorDescription());
- }
- pendingActions.removeIf(ScheduledFuture::isDone);
- } catch (JsonSyntaxException | IOException e) {
- throw new VolvoOnCallException(e);
- }
+ @Override
+ public Collection<Class<? extends ThingHandlerService>> getServices() {
+ return Collections.singleton(VolvoVehicleDiscoveryService.class);
}
@Override
- public void dispose() {
- super.dispose();
- pendingActions.stream().filter(f -> !f.isCancelled()).forEach(f -> f.cancel(true));
+ public void handleCommand(ChannelUID channelUID, Command command) {
+ // Do nothing
}
}
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>
--- /dev/null
+# 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.
<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>
<channel id="washerFluidLevel" typeId="washerFluidLevel"/>
<channel id="serviceWarningStatus" typeId="serviceWarningStatus"/>
<channel id="bulbFailure" typeId="bulbFailure"/>
+ <channel id="carEvent" typeId="carEvent"/>
</channels>
</channel-group-type>
<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>
<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>
<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>
<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>
<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"/>
<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">
<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">
<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>