2 * Copyright (c) 2010-2020 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.volvooncall.internal.handler;
15 import static org.openhab.binding.volvooncall.internal.VolvoOnCallBindingConstants.*;
17 import java.io.ByteArrayInputStream;
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.nio.charset.StandardCharsets;
21 import java.time.ZonedDateTime;
22 import java.util.List;
23 import java.util.Properties;
24 import java.util.Stack;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.binding.volvooncall.internal.VolvoOnCallException;
31 import org.openhab.binding.volvooncall.internal.VolvoOnCallException.ErrorType;
32 import org.openhab.binding.volvooncall.internal.config.VolvoOnCallBridgeConfiguration;
33 import org.openhab.binding.volvooncall.internal.dto.CustomerAccounts;
34 import org.openhab.binding.volvooncall.internal.dto.PostResponse;
35 import org.openhab.binding.volvooncall.internal.dto.VocAnswer;
36 import org.openhab.core.io.net.http.HttpUtil;
37 import org.openhab.core.library.types.OnOffType;
38 import org.openhab.core.library.types.OpenClosedType;
39 import org.openhab.core.thing.Bridge;
40 import org.openhab.core.thing.ChannelUID;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.thing.binding.BaseBridgeHandler;
44 import org.openhab.core.types.Command;
45 import org.slf4j.Logger;
46 import org.slf4j.LoggerFactory;
48 import com.google.gson.Gson;
49 import com.google.gson.GsonBuilder;
50 import com.google.gson.JsonDeserializer;
51 import com.google.gson.JsonSyntaxException;
54 * The {@link VolvoOnCallBridgeHandler} is responsible for handling commands, which are
55 * sent to one of the channels.
57 * @author Gaƫl L'hopital - Initial contribution
60 public class VolvoOnCallBridgeHandler extends BaseBridgeHandler {
61 private static final int REQUEST_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(20);
62 private final Logger logger = LoggerFactory.getLogger(VolvoOnCallBridgeHandler.class);
63 private final Properties httpHeader = new Properties();
64 private final List<ScheduledFuture<?>> pendingActions = new Stack<>();
65 private final Gson gson;
67 private @NonNullByDefault({}) CustomerAccounts customerAccount;
69 public VolvoOnCallBridgeHandler(Bridge bridge) {
72 httpHeader.put("cache-control", "no-cache");
73 httpHeader.put("content-type", JSON_CONTENT_TYPE);
74 httpHeader.put("x-device-id", "Device");
75 httpHeader.put("x-originator-type", "App");
76 httpHeader.put("x-os-type", "Android");
77 httpHeader.put("x-os-version", "22");
78 httpHeader.put("Accept", "*/*");
80 gson = new GsonBuilder()
81 .registerTypeAdapter(ZonedDateTime.class,
82 (JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> ZonedDateTime
83 .parse(json.getAsJsonPrimitive().getAsString().replaceAll("\\+0000", "Z")))
84 .registerTypeAdapter(OpenClosedType.class,
85 (JsonDeserializer<OpenClosedType>) (json, type,
86 jsonDeserializationContext) -> json.getAsBoolean() ? OpenClosedType.OPEN
87 : OpenClosedType.CLOSED)
88 .registerTypeAdapter(OnOffType.class,
89 (JsonDeserializer<OnOffType>) (json, type,
90 jsonDeserializationContext) -> json.getAsBoolean() ? OnOffType.ON : OnOffType.OFF)
95 public void initialize() {
96 logger.debug("Initializing VolvoOnCall API bridge handler.");
97 VolvoOnCallBridgeConfiguration configuration = getConfigAs(VolvoOnCallBridgeConfiguration.class);
99 httpHeader.setProperty("Authorization", configuration.getAuthorization());
101 customerAccount = getURL(SERVICE_URL + "customeraccounts/", CustomerAccounts.class);
102 if (customerAccount.username != null) {
103 updateStatus(ThingStatus.ONLINE);
105 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
106 "Incorrect username or password");
108 } catch (JsonSyntaxException | VolvoOnCallException e) {
109 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
114 public void handleCommand(ChannelUID channelUID, Command command) {
115 logger.debug("VolvoOnCall Bridge is read-only and does not handle commands");
118 public String[] getVehiclesRelationsURL() {
119 if (customerAccount != null) {
120 return customerAccount.accountVehicleRelationsURL;
122 return new String[0];
125 public <T extends VocAnswer> T getURL(Class<T> objectClass, String vin) throws VolvoOnCallException {
126 String url = SERVICE_URL + "vehicles/" + vin + "/" + objectClass.getSimpleName().toLowerCase();
127 return getURL(url, objectClass);
130 public <T extends VocAnswer> T getURL(String url, Class<T> objectClass) throws VolvoOnCallException {
132 String jsonResponse = HttpUtil.executeUrl("GET", url, httpHeader, null, JSON_CONTENT_TYPE, REQUEST_TIMEOUT);
133 logger.debug("Request for : {}", url);
134 logger.debug("Received : {}", jsonResponse);
135 T response = gson.fromJson(jsonResponse, objectClass);
136 String error = response.getErrorLabel();
138 throw new VolvoOnCallException(error, response.getErrorDescription());
141 } catch (JsonSyntaxException | IOException e) {
142 throw new VolvoOnCallException(e);
146 public class ActionResultControler implements Runnable {
147 PostResponse postResponse;
149 ActionResultControler(PostResponse postResponse) {
150 this.postResponse = postResponse;
155 switch (postResponse.status) {
158 logger.info("Action status : {} for vehicle : {}.", postResponse.status.toString(),
159 postResponse.vehicleId);
160 getThing().getThings().stream().filter(VehicleHandler.class::isInstance)
161 .map(VehicleHandler.class::cast)
162 .forEach(handler -> handler.updateIfMatches(postResponse.vehicleId));
166 postResponse = getURL(postResponse.serviceURL, PostResponse.class);
167 scheduler.schedule(new ActionResultControler(postResponse), 1000, TimeUnit.MILLISECONDS);
168 } catch (VolvoOnCallException e) {
169 if (e.getType() == ErrorType.SERVICE_UNAVAILABLE) {
170 scheduler.schedule(new ActionResultControler(postResponse), 1000, TimeUnit.MILLISECONDS);
177 void postURL(String URL, @Nullable String body) throws VolvoOnCallException {
178 InputStream inputStream = body != null ? new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)) : null;
180 String jsonString = HttpUtil.executeUrl("POST", URL, httpHeader, inputStream, null, REQUEST_TIMEOUT);
181 logger.debug("Post URL: {} Attributes {}", URL, httpHeader);
182 PostResponse postResponse = gson.fromJson(jsonString, PostResponse.class);
183 String error = postResponse.getErrorLabel();
186 .add(scheduler.schedule(new ActionResultControler(postResponse), 1000, TimeUnit.MILLISECONDS));
188 throw new VolvoOnCallException(error, postResponse.getErrorDescription());
190 pendingActions.removeIf(ScheduledFuture::isDone);
191 } catch (JsonSyntaxException | IOException e) {
192 throw new VolvoOnCallException(e);
197 public void dispose() {
199 pendingActions.stream().filter(f -> !f.isCancelled()).forEach(f -> f.cancel(true));