]> git.basschouten.com Git - openhab-addons.git/blob
7278c60fc0171ca38e5e71e31e05b30031548ed1
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.mielecloud.internal.config.servlet;
14
15 import java.util.concurrent.ExecutionException;
16 import java.util.concurrent.TimeUnit;
17 import java.util.concurrent.TimeoutException;
18 import java.util.function.BooleanSupplier;
19
20 import javax.servlet.http.HttpServletRequest;
21
22 import org.eclipse.jdt.annotation.NonNullByDefault;
23 import org.eclipse.jdt.annotation.Nullable;
24 import org.openhab.binding.mielecloud.internal.MieleCloudBindingConstants;
25 import org.openhab.binding.mielecloud.internal.config.exception.BridgeCreationFailedException;
26 import org.openhab.binding.mielecloud.internal.config.exception.BridgeReconfigurationFailedException;
27 import org.openhab.binding.mielecloud.internal.handler.MieleBridgeHandler;
28 import org.openhab.binding.mielecloud.internal.util.LocaleValidator;
29 import org.openhab.core.config.discovery.DiscoveryResult;
30 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
31 import org.openhab.core.config.discovery.inbox.Inbox;
32 import org.openhab.core.thing.Thing;
33 import org.openhab.core.thing.ThingRegistry;
34 import org.openhab.core.thing.ThingStatus;
35 import org.openhab.core.thing.ThingUID;
36 import org.openhab.core.thing.binding.ThingHandler;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40 /**
41  * Servlet that automatically creates a bridge and then redirects the browser to the account overview page.
42  *
43  * @author Björn Lange - Initial Contribution
44  */
45 @NonNullByDefault
46 public final class CreateBridgeServlet extends AbstractRedirectionServlet {
47     private static final String MIELE_CLOUD_BRIDGE_NAME = "Cloud Connector";
48     private static final String MIELE_CLOUD_BRIDGE_LABEL = "Miele@home Account";
49
50     private static final String LOCALE_PARAMETER_NAME = "locale";
51     public static final String BRIDGE_UID_PARAMETER_NAME = "bridgeUid";
52     public static final String EMAIL_PARAMETER_NAME = "email";
53
54     private static final long serialVersionUID = -2912042079128722887L;
55
56     private static final String DEFAULT_LOCALE = "en";
57
58     private static final long DISCOVERY_COMPLETION_TIMEOUT_IN_MILLISECONDS = 5000;
59     private static final long CHECK_INTERVAL_IN_MILLISECONDS = 100;
60
61     private static final long INBOX_ENTRY_CREATION_TIMEOUT = 15;
62     private static final TimeUnit INBOX_ENTRY_CREATION_TIMEOUT_UNIT = TimeUnit.SECONDS;
63
64     private final Logger logger = LoggerFactory.getLogger(CreateBridgeServlet.class);
65
66     private final Inbox inbox;
67     private final ThingRegistry thingRegistry;
68
69     private long onlineWaitTimeoutInMilliseconds = 5000;
70
71     /**
72      * Creates a new {@link CreateBridgeServlet}.
73      *
74      * @param inbox openHAB inbox for discovery results.
75      * @param thingRegistry openHAB thing registry.
76      */
77     public CreateBridgeServlet(Inbox inbox, ThingRegistry thingRegistry) {
78         this.inbox = inbox;
79         this.thingRegistry = thingRegistry;
80     }
81
82     public void setOnlineWaitTimeoutInMilliseconds(long onlineWaitTimeoutInMilliseconds) {
83         this.onlineWaitTimeoutInMilliseconds = onlineWaitTimeoutInMilliseconds;
84     }
85
86     @Override
87     protected String getRedirectionDestination(HttpServletRequest request) {
88         String bridgeUidString = request.getParameter(BRIDGE_UID_PARAMETER_NAME);
89         if (bridgeUidString == null || bridgeUidString.isEmpty()) {
90             logger.warn("Cannot create bridge: Bridge UID is missing.");
91             return "/mielecloud/failure?" + FailureServlet.MISSING_BRIDGE_UID_PARAMETER_NAME + "=true";
92         }
93
94         String email = request.getParameter(EMAIL_PARAMETER_NAME);
95         if (email == null || email.isEmpty()) {
96             logger.warn("Cannot create bridge: E-mail address is missing.");
97             return "/mielecloud/failure?" + FailureServlet.MISSING_EMAIL_PARAMETER_NAME + "=true";
98         }
99
100         ThingUID bridgeUid = null;
101         try {
102             bridgeUid = new ThingUID(bridgeUidString);
103         } catch (IllegalArgumentException e) {
104             logger.warn("Cannot create bridge: Bridge UID '{}' is malformed.", bridgeUid);
105             return "/mielecloud/failure?" + FailureServlet.MALFORMED_BRIDGE_UID_PARAMETER_NAME + "=true";
106         }
107
108         String locale = getValidLocale(request.getParameter(LOCALE_PARAMETER_NAME));
109
110         logger.debug("Auto configuring Miele account using locale '{}' (requested locale was '{}')", locale,
111                 request.getParameter(LOCALE_PARAMETER_NAME));
112         try {
113             Thing bridge = pairOrReconfigureBridge(locale, bridgeUid, email);
114             waitForBridgeToComeOnline(bridge);
115             return "/mielecloud";
116         } catch (BridgeReconfigurationFailedException e) {
117             logger.warn("Thing reconfiguration failed: {}", e.getMessage(), e);
118             return "/mielecloud/success?" + SuccessServlet.BRIDGE_RECONFIGURATION_FAILED_PARAMETER_NAME + "=true&"
119                     + SuccessServlet.BRIDGE_UID_PARAMETER_NAME + "=" + bridgeUidString + "&"
120                     + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + email;
121         } catch (BridgeCreationFailedException e) {
122             logger.warn("Thing creation failed: {}", e.getMessage(), e);
123             return "/mielecloud/success?" + SuccessServlet.BRIDGE_CREATION_FAILED_PARAMETER_NAME + "=true&"
124                     + SuccessServlet.BRIDGE_UID_PARAMETER_NAME + "=" + bridgeUidString + "&"
125                     + SuccessServlet.EMAIL_PARAMETER_NAME + "=" + email;
126         }
127     }
128
129     private Thing pairOrReconfigureBridge(String locale, ThingUID bridgeUid, String email) {
130         var thing = thingRegistry.get(bridgeUid);
131         if (thing != null) {
132             reconfigureBridge(thing);
133             return thing;
134         } else {
135             return pairBridge(bridgeUid, locale, email);
136         }
137     }
138
139     private Thing pairBridge(ThingUID bridgeUid, String locale, String email) {
140         DiscoveryResult result = DiscoveryResultBuilder.create(bridgeUid)
141                 .withRepresentationProperty(Thing.PROPERTY_MODEL_ID).withLabel(MIELE_CLOUD_BRIDGE_LABEL)
142                 .withProperty(Thing.PROPERTY_MODEL_ID, MIELE_CLOUD_BRIDGE_NAME)
143                 .withProperty(MieleCloudBindingConstants.CONFIG_PARAM_LOCALE, locale)
144                 .withProperty(MieleCloudBindingConstants.CONFIG_PARAM_EMAIL, email).build();
145
146         try {
147             var success = inbox.add(result).get(INBOX_ENTRY_CREATION_TIMEOUT, INBOX_ENTRY_CREATION_TIMEOUT_UNIT);
148             if (!Boolean.TRUE.equals(success)) {
149                 throw new BridgeCreationFailedException("Adding bridge to inbox failed");
150             }
151         } catch (InterruptedException e) {
152             Thread.currentThread().interrupt();
153             throw new BridgeCreationFailedException("Interrupted while adding bridge to inbox", e);
154         } catch (ExecutionException e) {
155             throw new BridgeCreationFailedException("Adding bridge to inbox failed", e);
156         } catch (TimeoutException e) {
157             throw new BridgeCreationFailedException("Adding bridge to inbox failed: Timeout", e);
158         }
159
160         Thing thing = inbox.approve(bridgeUid, MIELE_CLOUD_BRIDGE_LABEL, null);
161         if (thing == null) {
162             throw new BridgeCreationFailedException("Approving thing from inbox failed");
163         }
164
165         logger.debug("Successfully created bridge {}", bridgeUid);
166         return thing;
167     }
168
169     private void reconfigureBridge(Thing thing) {
170         logger.debug("Thing already exists. Modifying configuration.");
171         ThingHandler handler = thing.getHandler();
172         if (handler == null) {
173             throw new BridgeReconfigurationFailedException("Bridge exists but has no handler.");
174         }
175         if (!(handler instanceof MieleBridgeHandler)) {
176             throw new BridgeReconfigurationFailedException("Bridge handler is of wrong type, expected '"
177                     + MieleBridgeHandler.class.getSimpleName() + "' but got '" + handler.getClass().getName() + "'.");
178         }
179
180         MieleBridgeHandler bridgeHandler = (MieleBridgeHandler) handler;
181         bridgeHandler.disposeWebservice();
182         bridgeHandler.initializeWebservice();
183     }
184
185     private String getValidLocale(@Nullable String localeParameterValue) {
186         if (localeParameterValue == null || localeParameterValue.isEmpty()
187                 || !LocaleValidator.isValidLanguage(localeParameterValue)) {
188             return DEFAULT_LOCALE;
189         } else {
190             return localeParameterValue;
191         }
192     }
193
194     private void waitForBridgeToComeOnline(Thing bridge) {
195         try {
196             waitForConditionWithTimeout(() -> bridge.getStatus() == ThingStatus.ONLINE,
197                     onlineWaitTimeoutInMilliseconds);
198             waitForConditionWithTimeout(new DiscoveryResultCountDoesNotChangeCondition(),
199                     DISCOVERY_COMPLETION_TIMEOUT_IN_MILLISECONDS);
200         } catch (InterruptedException e) {
201             Thread.currentThread().interrupt();
202         }
203     }
204
205     private void waitForConditionWithTimeout(BooleanSupplier condition, long timeoutInMilliseconds)
206             throws InterruptedException {
207         long remainingWaitTime = timeoutInMilliseconds;
208         while (!condition.getAsBoolean() && remainingWaitTime > 0) {
209             TimeUnit.MILLISECONDS.sleep(CHECK_INTERVAL_IN_MILLISECONDS);
210             remainingWaitTime -= CHECK_INTERVAL_IN_MILLISECONDS;
211         }
212     }
213
214     private class DiscoveryResultCountDoesNotChangeCondition implements BooleanSupplier {
215         private long previousDiscoveryResultCount = 0;
216
217         @Override
218         public boolean getAsBoolean() {
219             var discoveryResultCount = countOwnDiscoveryResults();
220             var discoveryResultCountUnchanged = previousDiscoveryResultCount == discoveryResultCount;
221             previousDiscoveryResultCount = discoveryResultCount;
222             return discoveryResultCountUnchanged;
223         }
224
225         private long countOwnDiscoveryResults() {
226             return inbox.stream().map(DiscoveryResult::getBindingId)
227                     .filter(MieleCloudBindingConstants.BINDING_ID::equals).count();
228         }
229     }
230 }