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.List;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.eclipse.jetty.client.HttpClient;
27 import org.openhab.binding.groupepsa.internal.GroupePSABindingConstants.VendorConstants;
28 import org.openhab.binding.groupepsa.internal.discovery.GroupePSADiscoveryService;
29 import org.openhab.binding.groupepsa.internal.rest.api.GroupePSAConnectApi;
30 import org.openhab.binding.groupepsa.internal.rest.api.dto.Vehicle;
31 import org.openhab.binding.groupepsa.internal.rest.api.dto.VehicleStatus;
32 import org.openhab.binding.groupepsa.internal.rest.exceptions.GroupePSACommunicationException;
33 import org.openhab.core.auth.client.oauth2.AccessTokenResponse;
34 import org.openhab.core.auth.client.oauth2.OAuthClientService;
35 import org.openhab.core.auth.client.oauth2.OAuthException;
36 import org.openhab.core.auth.client.oauth2.OAuthFactory;
37 import org.openhab.core.auth.client.oauth2.OAuthResponseException;
38 import org.openhab.core.thing.Bridge;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.ThingStatus;
41 import org.openhab.core.thing.ThingStatusDetail;
42 import org.openhab.core.thing.ThingTypeUID;
43 import org.openhab.core.thing.binding.BaseBridgeHandler;
44 import org.openhab.core.thing.binding.ThingHandlerService;
45 import org.openhab.core.types.Command;
48 * The {@link GroupePSABridgeHandler} is responsible for handling commands,
49 * which are sent to one of the channels.
51 * @author Arjan Mels - Initial contribution
54 public class GroupePSABridgeHandler extends BaseBridgeHandler {
55 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = Set.of(THING_TYPE_BRIDGE);
56 private static final long DEFAULT_POLLING_INTERVAL_M = TimeUnit.HOURS.toMinutes(1);
58 private final OAuthFactory oAuthFactory;
60 private @Nullable OAuthClientService oAuthService;
61 private @Nullable ScheduledFuture<?> groupepsaBridgePollingJob;
62 private final HttpClient httpClient;
64 private String vendor = "";
65 private @Nullable VendorConstants vendorConstants;
66 private String userName = "";
67 private String password = "";
68 private String clientId = "";
69 private String clientSecret = "";
71 private @Nullable GroupePSAConnectApi groupePSAApi;
73 public GroupePSABridgeHandler(Bridge bridge, OAuthFactory oAuthFactory, HttpClient httpClient) {
75 this.oAuthFactory = oAuthFactory;
76 this.httpClient = httpClient;
79 private void pollGroupePSAs() {
81 List<Vehicle> vehicles = getVehicles();
82 if (vehicles != null) {
83 updateStatus(ThingStatus.ONLINE);
85 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
86 "@text/comm-error-query-vehicles-failed");
88 } catch (GroupePSACommunicationException e) {
89 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
94 public void dispose() {
95 stopGroupePSABridgePolling();
96 if (oAuthService != null) {
97 oAuthFactory.ungetOAuthService(thing.getUID().getAsString());
103 public void initialize() {
104 GroupePSABridgeConfiguration bridgeConfiguration = getConfigAs(GroupePSABridgeConfiguration.class);
106 vendor = bridgeConfiguration.getVendor();
107 userName = bridgeConfiguration.getUserName();
108 password = bridgeConfiguration.getPassword();
109 clientId = bridgeConfiguration.getClientId();
110 clientSecret = bridgeConfiguration.getClientSecret();
112 final Integer pollingIntervalM = bridgeConfiguration.getPollingInterval();
114 if (vendor.isEmpty()) {
115 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-vendor");
116 } else if (userName.isEmpty()) {
117 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-username");
118 } else if (password.isEmpty()) {
119 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-password");
120 } else if (clientId.isEmpty()) {
121 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/conf-error-no-clientid");
122 } else if (clientSecret.isEmpty()) {
123 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
124 "@text/conf-error-no-clientsecret");
125 } else if (pollingIntervalM < 1) {
126 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
127 "@text/conf-error-invalid-polling-interval");
129 VendorConstants localVendorConstants = VendorConstants.valueOf(vendor);
130 vendorConstants = localVendorConstants;
132 oAuthService = oAuthFactory.createOAuthClientService(thing.getUID().getAsString(), localVendorConstants.url,
133 null, clientId, clientSecret, localVendorConstants.scope, true);
135 groupePSAApi = new GroupePSAConnectApi(httpClient, this, clientId, localVendorConstants.realm);
137 startGroupePSABridgePolling(pollingIntervalM);
139 updateStatus(ThingStatus.UNKNOWN);
144 public void handleRemoval() {
145 oAuthFactory.deleteServiceAndAccessToken(thing.getUID().getAsString());
146 super.handleRemoval();
149 private void startGroupePSABridgePolling(@Nullable Integer pollingIntervalM) {
150 if (groupepsaBridgePollingJob == null) {
151 final long pollingIntervalToUse = pollingIntervalM == null ? DEFAULT_POLLING_INTERVAL_M : pollingIntervalM;
152 groupepsaBridgePollingJob = scheduler.scheduleWithFixedDelay(() -> pollGroupePSAs(), 1,
153 TimeUnit.MINUTES.toSeconds(pollingIntervalToUse), TimeUnit.SECONDS);
157 private void stopGroupePSABridgePolling() {
158 final ScheduledFuture<?> job = groupepsaBridgePollingJob;
161 groupepsaBridgePollingJob = null;
166 public void handleCommand(ChannelUID channelUID, Command command) {
169 static Throwable getRootCause(Throwable e) {
172 nextE = e.getCause();
176 } while (nextE != null);
180 public String authenticate() throws GroupePSACommunicationException {
181 OAuthClientService localOAuthService = oAuthService;
182 VendorConstants localVendorConstants = vendorConstants;
183 if (localOAuthService == null) {
184 throw new GroupePSACommunicationException("OAuth service is unexpectedly null");
186 if (localVendorConstants == null) {
187 throw new GroupePSACommunicationException("Vendor constants are unexpectedly null");
190 AccessTokenResponse result = localOAuthService.getAccessTokenResponse();
191 if (result == null) {
192 result = localOAuthService.getAccessTokenByResourceOwnerPasswordCredentials(userName, password,
193 localVendorConstants.scope);
195 return result.getAccessToken();
196 } catch (OAuthException | IOException | OAuthResponseException e) {
197 throw new GroupePSACommunicationException("Unable to authenticate: " + getRootCause(e).getMessage(), e);
201 public GroupePSAConnectApi getAPI() throws GroupePSACommunicationException {
202 GroupePSAConnectApi localGroupePSAApi = groupePSAApi;
203 if (localGroupePSAApi == null) {
204 throw new GroupePSACommunicationException("groupePSAApi is unexpectedly null");
206 return localGroupePSAApi;
211 * @return A list of vehicles
212 * @throws GroupePSACommunicationException In case the query cannot be executed
215 public @Nullable List<Vehicle> getVehicles() throws GroupePSACommunicationException {
216 return getAPI().getVehicles();
220 * @param id The id of the mower to query
221 * @return A detailed status of the mower with the specified id
222 * @throws GroupePSACommunicationException In case the query cannot be executed
225 public @Nullable VehicleStatus getVehicleStatus(String vin) throws GroupePSACommunicationException {
226 return getAPI().getVehicleStatus(vin);
230 public Collection<Class<? extends ThingHandlerService>> getServices() {
231 return Set.of(GroupePSADiscoveryService.class);