2 * Copyright (c) 2010-2023 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.groupepsa.internal.bridge;
15 import static org.openhab.binding.groupepsa.internal.GroupePSABindingConstants.THING_TYPE_BRIDGE;
17 import java.io.IOException;
18 import java.util.Collection;
19 import java.util.Collections;
20 import java.util.List;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.eclipse.jetty.client.HttpClient;
28 import org.openhab.binding.groupepsa.internal.GroupePSABindingConstants.VendorConstants;
29 import org.openhab.binding.groupepsa.internal.discovery.GroupePSADiscoveryService;
30 import org.openhab.binding.groupepsa.internal.rest.api.GroupePSAConnectApi;
31 import org.openhab.binding.groupepsa.internal.rest.api.dto.Vehicle;
32 import org.openhab.binding.groupepsa.internal.rest.api.dto.VehicleStatus;
33 import org.openhab.binding.groupepsa.internal.rest.exceptions.GroupePSACommunicationException;
34 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
35 import org.openhab.core.auth.client.oauth2.OAuthClientService;
36 import org.openhab.core.auth.client.oauth2.OAuthException;
37 import org.openhab.core.auth.client.oauth2.OAuthFactory;
38 import org.openhab.core.auth.client.oauth2.OAuthResponseException;
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.ThingTypeUID;
44 import org.openhab.core.thing.binding.BaseBridgeHandler;
45 import org.openhab.core.thing.binding.ThingHandlerService;
46 import org.openhab.core.types.Command;
49 * The {@link GroupePSABridgeHandler} is responsible for handling commands,
50 * which are sent to one of the channels.
52 * @author Arjan Mels - Initial contribution
55 public class GroupePSABridgeHandler extends BaseBridgeHandler {
56 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Collections.singleton(THING_TYPE_BRIDGE);
57 private static final long DEFAULT_POLLING_INTERVAL_M = TimeUnit.HOURS.toMinutes(1);
59 private final OAuthFactory oAuthFactory;
61 private @Nullable OAuthClientService oAuthService;
62 private @Nullable ScheduledFuture<?> groupepsaBridgePollingJob;
63 private final HttpClient httpClient;
65 private String vendor = "";
66 private @Nullable VendorConstants vendorConstants;
67 private String userName = "";
68 private String password = "";
69 private String clientId = "";
70 private String clientSecret = "";
72 private @Nullable GroupePSAConnectApi groupePSAApi;
74 public GroupePSABridgeHandler(Bridge bridge, OAuthFactory oAuthFactory, HttpClient httpClient) {
76 this.oAuthFactory = oAuthFactory;
77 this.httpClient = httpClient;
80 private void pollGroupePSAs() {
82 List<Vehicle> vehicles = getVehicles();
83 if (vehicles != null) {
84 updateStatus(ThingStatus.ONLINE);
86 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
87 "@text/comm-error-query-vehicles-failed");
89 } catch (GroupePSACommunicationException e) {
90 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
95 public void dispose() {
96 stopGroupePSABridgePolling();
97 if (oAuthService != null) {
98 oAuthFactory.ungetOAuthService(thing.getUID().getAsString());
104 public void initialize() {
105 GroupePSABridgeConfiguration bridgeConfiguration = getConfigAs(GroupePSABridgeConfiguration.class);
107 vendor = bridgeConfiguration.getVendor();
108 userName = bridgeConfiguration.getUserName();
109 password = bridgeConfiguration.getPassword();
110 clientId = bridgeConfiguration.getClientId();
111 clientSecret = bridgeConfiguration.getClientSecret();
113 final Integer pollingIntervalM = bridgeConfiguration.getPollingInterval();
115 if (vendor.isEmpty()) {
116 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-vendor");
117 } else if (userName.isEmpty()) {
118 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-username");
119 } else if (password.isEmpty()) {
120 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-password");
121 } else if (clientId.isEmpty()) {
122 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-clientid");
123 } else if (clientSecret.isEmpty()) {
124 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
125 "@text/conf-error-no-clientsecret");
126 } else if (pollingIntervalM < 1) {
127 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
128 "@text/conf-error-invalid-polling-interval");
130 VendorConstants localVendorConstants = VendorConstants.valueOf(vendor);
131 vendorConstants = localVendorConstants;
133 oAuthService = oAuthFactory.createOAuthClientService(thing.getUID().getAsString(), localVendorConstants.url,
134 null, clientId, clientSecret, localVendorConstants.scope, true);
136 groupePSAApi = new GroupePSAConnectApi(httpClient, this, clientId, localVendorConstants.realm);
138 startGroupePSABridgePolling(pollingIntervalM);
140 updateStatus(ThingStatus.UNKNOWN);
145 public void handleRemoval() {
146 oAuthFactory.deleteServiceAndAccessToken(thing.getUID().getAsString());
147 super.handleRemoval();
150 private void startGroupePSABridgePolling(@Nullable Integer pollingIntervalM) {
151 if (groupepsaBridgePollingJob == null) {
152 final long pollingIntervalToUse = pollingIntervalM == null ? DEFAULT_POLLING_INTERVAL_M : pollingIntervalM;
153 groupepsaBridgePollingJob = scheduler.scheduleWithFixedDelay(() -> pollGroupePSAs(), 1,
154 TimeUnit.MINUTES.toSeconds(pollingIntervalToUse), TimeUnit.SECONDS);
158 private void stopGroupePSABridgePolling() {
159 final ScheduledFuture<?> job = groupepsaBridgePollingJob;
162 groupepsaBridgePollingJob = null;
167 public void handleCommand(ChannelUID channelUID, Command command) {
170 static Throwable getRootCause(Throwable e) {
173 nextE = e.getCause();
177 } while (nextE != null);
181 public String authenticate() throws GroupePSACommunicationException {
182 OAuthClientService localOAuthService = oAuthService;
183 VendorConstants localVendorConstants = vendorConstants;
184 if (localOAuthService == null) {
185 throw new GroupePSACommunicationException("OAuth service is unexpectedly null");
187 if (localVendorConstants == null) {
188 throw new GroupePSACommunicationException("Vendor constants are unexpectedly null");
191 AccessTokenResponse result = localOAuthService.getAccessTokenResponse();
192 if (result == null) {
193 result = localOAuthService.getAccessTokenByResourceOwnerPasswordCredentials(userName, password,
194 localVendorConstants.scope);
196 return result.getAccessToken();
197 } catch (OAuthException | IOException | OAuthResponseException e) {
198 throw new GroupePSACommunicationException("Unable to authenticate: " + getRootCause(e).getMessage(), e);
202 public GroupePSAConnectApi getAPI() throws GroupePSACommunicationException {
203 GroupePSAConnectApi localGroupePSAApi = groupePSAApi;
204 if (localGroupePSAApi == null) {
205 throw new GroupePSACommunicationException("groupePSAApi is unexpectedly null");
207 return localGroupePSAApi;
212 * @return A list of vehicles
213 * @throws GroupePSACommunicationException In case the query cannot be executed
216 public @Nullable List<Vehicle> getVehicles() throws GroupePSACommunicationException {
217 return getAPI().getVehicles();
221 * @param id The id of the mower to query
222 * @return A detailed status of the mower with the specified id
223 * @throws GroupePSACommunicationException In case the query cannot be executed
226 public @Nullable VehicleStatus getVehicleStatus(String vin) throws GroupePSACommunicationException {
227 return getAPI().getVehicleStatus(vin);
231 public Collection<Class<? extends ThingHandlerService>> getServices() {
232 return Collections.singleton(GroupePSADiscoveryService.class);