Skip to content

Commit 8f77e16

Browse files
authored
[fronius] Fix invalid credentials lead to unexpected exception (openhab#18130)
With the changes from this PR, the status code is now properly read and for 401, a meaningful warnings is logged instead. Signed-off-by: Florian Hotze <[email protected]>
1 parent 5fc40cc commit 8f77e16

File tree

4 files changed

+121
-25
lines changed

4 files changed

+121
-25
lines changed

bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusBatteryControl.java

+17-7
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@ public FroniusBatteryControl(HttpClient httpClient, URI baseUri, String username
7777
*
7878
* @return the time of use settings
7979
* @throws FroniusCommunicationException if an error occurs during communication with the inverter
80+
* @throws FroniusUnauthorizedException when the login failed due to invalid credentials
8081
*/
81-
private TimeOfUseRecords getTimeOfUse() throws FroniusCommunicationException {
82+
private TimeOfUseRecords getTimeOfUse() throws FroniusCommunicationException, FroniusUnauthorizedException {
8283
// Login and get the auth header for the next request
8384
String authHeader = FroniusConfigAuthUtil.login(httpClient, baseUri, username, password, HttpMethod.GET,
8485
timeOfUseUri.getPath(), API_TIMEOUT);
@@ -107,8 +108,10 @@ private TimeOfUseRecords getTimeOfUse() throws FroniusCommunicationException {
107108
*
108109
* @param records the time of use settings
109110
* @throws FroniusCommunicationException if an error occurs during communication with the inverter
111+
* @throws FroniusUnauthorizedException when the login failed due to invalid credentials
110112
*/
111-
private void setTimeOfUse(TimeOfUseRecords records) throws FroniusCommunicationException {
113+
private void setTimeOfUse(TimeOfUseRecords records)
114+
throws FroniusCommunicationException, FroniusUnauthorizedException {
112115
// Login and get the auth header for the next request
113116
String authHeader = FroniusConfigAuthUtil.login(httpClient, baseUri, username, password, HttpMethod.POST,
114117
timeOfUseUri.getPath(), API_TIMEOUT);
@@ -127,17 +130,19 @@ private void setTimeOfUse(TimeOfUseRecords records) throws FroniusCommunicationE
127130
* inverter.
128131
*
129132
* @throws FroniusCommunicationException when an error occurs during communication with the inverter
133+
* @throws FroniusUnauthorizedException when the login failed due to invalid credentials
130134
*/
131-
public void reset() throws FroniusCommunicationException {
135+
public void reset() throws FroniusCommunicationException, FroniusUnauthorizedException {
132136
setTimeOfUse(new TimeOfUseRecords(new TimeOfUseRecord[0]));
133137
}
134138

135139
/**
136140
* Holds the battery charge right now, i.e. prevents the battery from discharging.
137141
*
138142
* @throws FroniusCommunicationException when an error occurs during communication with the inverter
143+
* @throws FroniusUnauthorizedException when the login failed due to invalid credentials
139144
*/
140-
public void holdBatteryCharge() throws FroniusCommunicationException {
145+
public void holdBatteryCharge() throws FroniusCommunicationException, FroniusUnauthorizedException {
141146
reset();
142147
addHoldBatteryChargeSchedule(BEGIN_OF_DAY, END_OF_DAY);
143148
}
@@ -149,8 +154,10 @@ public void holdBatteryCharge() throws FroniusCommunicationException {
149154
* @param from start time of the hold charge period
150155
* @param until end time of the hold charge period
151156
* @throws FroniusCommunicationException when an error occurs during communication with the inverter
157+
* @throws FroniusUnauthorizedException when the login failed due to invalid credentials
152158
*/
153-
public void addHoldBatteryChargeSchedule(LocalTime from, LocalTime until) throws FroniusCommunicationException {
159+
public void addHoldBatteryChargeSchedule(LocalTime from, LocalTime until)
160+
throws FroniusCommunicationException, FroniusUnauthorizedException {
154161
TimeOfUseRecord[] currentTimeOfUse = getTimeOfUse().records();
155162
TimeOfUseRecord[] timeOfUse = new TimeOfUseRecord[currentTimeOfUse.length + 1];
156163
System.arraycopy(currentTimeOfUse, 0, timeOfUse, 0, currentTimeOfUse.length);
@@ -166,8 +173,10 @@ public void addHoldBatteryChargeSchedule(LocalTime from, LocalTime until) throws
166173
*
167174
* @param power the power to charge the battery with
168175
* @throws FroniusCommunicationException when an error occurs during communication with the inverter
176+
* @throws FroniusUnauthorizedException when the login failed due to invalid credentials
169177
*/
170-
public void forceBatteryCharging(QuantityType<Power> power) throws FroniusCommunicationException {
178+
public void forceBatteryCharging(QuantityType<Power> power)
179+
throws FroniusCommunicationException, FroniusUnauthorizedException {
171180
reset();
172181
addForcedBatteryChargingSchedule(BEGIN_OF_DAY, END_OF_DAY, power);
173182
}
@@ -179,9 +188,10 @@ public void forceBatteryCharging(QuantityType<Power> power) throws FroniusCommun
179188
* @param until end time of the forced charge period
180189
* @param power the power to charge the battery with
181190
* @throws FroniusCommunicationException when an error occurs during communication with the inverter
191+
* @throws FroniusUnauthorizedException when the login failed due to invalid credentials
182192
*/
183193
public void addForcedBatteryChargingSchedule(LocalTime from, LocalTime until, QuantityType<Power> power)
184-
throws FroniusCommunicationException {
194+
throws FroniusCommunicationException, FroniusUnauthorizedException {
185195
TimeOfUseRecords currentTimeOfUse = getTimeOfUse();
186196
TimeOfUseRecord[] timeOfUse = new TimeOfUseRecord[currentTimeOfUse.records().length + 1];
187197
System.arraycopy(currentTimeOfUse.records(), 0, timeOfUse, 0, currentTimeOfUse.records().length);

bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/api/FroniusConfigAuthUtil.java

+67-18
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,11 @@
1919
import java.util.HashMap;
2020
import java.util.Map;
2121
import java.util.concurrent.CountDownLatch;
22-
import java.util.concurrent.ExecutionException;
2322
import java.util.concurrent.TimeUnit;
24-
import java.util.concurrent.TimeoutException;
2523

2624
import org.eclipse.jdt.annotation.NonNullByDefault;
2725
import org.eclipse.jdt.annotation.Nullable;
2826
import org.eclipse.jetty.client.HttpClient;
29-
import org.eclipse.jetty.client.api.ContentResponse;
3027
import org.eclipse.jetty.client.api.Request;
3128
import org.eclipse.jetty.client.api.Response;
3229
import org.eclipse.jetty.http.HttpHeader;
@@ -69,18 +66,18 @@ private static Map<String, String> getAuthParams(HttpClient httpClient, URI logi
6966
throws IOException {
7067
LOGGER.debug("Sending login request to get authentication challenge");
7168
CountDownLatch latch = new CountDownLatch(1);
72-
Request initialRequest = httpClient.newRequest(loginUri).timeout(timeout, TimeUnit.MILLISECONDS);
73-
XWwwAuthenticateHeaderListener XWwwAuthenticateHeaderListener = new XWwwAuthenticateHeaderListener(latch);
74-
initialRequest.onResponseHeaders(XWwwAuthenticateHeaderListener);
75-
initialRequest.send(result -> latch.countDown());
69+
Request request = httpClient.newRequest(loginUri).timeout(timeout, TimeUnit.MILLISECONDS);
70+
XWwwAuthenticateHeaderListener xWwwAuthenticateHeaderListener = new XWwwAuthenticateHeaderListener(latch);
71+
request.onResponseHeaders(xWwwAuthenticateHeaderListener);
72+
request.send(result -> latch.countDown());
7673
// Wait for the request to complete
7774
try {
7875
latch.await();
7976
} catch (InterruptedException ie) {
8077
throw new RuntimeException(ie);
8178
}
8279

83-
String authHeader = XWwwAuthenticateHeaderListener.getAuthHeader();
80+
String authHeader = xWwwAuthenticateHeaderListener.getAuthHeader();
8481
if (authHeader == null) {
8582
throw new IOException("No authentication header found in login response");
8683
}
@@ -161,21 +158,40 @@ private static String md5Hex(String data) {
161158
* @param authHeader the authentication header to use for the login request
162159
* @throws InterruptedException when the request is interrupted
163160
* @throws FroniusCommunicationException when the login request failed
161+
* @throws FroniusUnauthorizedException when the login failed due to invalid credentials
164162
*/
165163
private static void performLoginRequest(HttpClient httpClient, URI loginUri, String authHeader, int timeout)
166-
throws InterruptedException, FroniusCommunicationException {
167-
Request loginRequest = httpClient.newRequest(loginUri).header(HttpHeader.AUTHORIZATION, authHeader)
168-
.timeout(timeout, TimeUnit.MILLISECONDS);
169-
ContentResponse loginResponse;
164+
throws InterruptedException, FroniusCommunicationException, FroniusUnauthorizedException {
165+
CountDownLatch latch = new CountDownLatch(1);
166+
Request request = httpClient.newRequest(loginUri).header(HttpHeader.AUTHORIZATION, authHeader).timeout(timeout,
167+
TimeUnit.MILLISECONDS);
168+
StatusListener statusListener = new StatusListener(latch);
169+
request.onResponseBegin(statusListener);
170+
Integer status;
170171
try {
171-
loginResponse = loginRequest.send();
172-
if (loginResponse.getStatus() != 200) {
173-
throw new FroniusCommunicationException(
174-
"Failed to send login request, status code: " + loginResponse.getStatus());
172+
request.send(result -> latch.countDown());
173+
// Wait for the request to complete
174+
try {
175+
latch.await();
176+
} catch (InterruptedException ie) {
177+
throw new RuntimeException(ie);
178+
}
179+
180+
status = statusListener.getStatus();
181+
if (status == null) {
182+
throw new FroniusCommunicationException("Failed to send login request: No status code received.");
175183
}
176-
} catch (TimeoutException | ExecutionException e) {
184+
} catch (IOException e) {
177185
throw new FroniusCommunicationException("Failed to send login request", e);
178186
}
187+
188+
if (status == 401) {
189+
throw new FroniusUnauthorizedException(
190+
"Failed to send login request, status code: 401 Unauthorized. Please check your credentials.");
191+
}
192+
if (status != 200) {
193+
throw new FroniusCommunicationException("Failed to send login request, status code: " + status);
194+
}
179195
}
180196

181197
/**
@@ -191,9 +207,11 @@ private static void performLoginRequest(HttpClient httpClient, URI loginUri, Str
191207
* @param timeout the timeout in milliseconds for the login requests
192208
* @return the authentication header for the next request
193209
* @throws FroniusCommunicationException when the login failed or interrupted
210+
* @throws FroniusUnauthorizedException when the login failed due to invalid credentials
194211
*/
195212
public static synchronized String login(HttpClient httpClient, URI baseUri, String username, String password,
196-
HttpMethod method, String relativeUrl, int timeout) throws FroniusCommunicationException {
213+
HttpMethod method, String relativeUrl, int timeout)
214+
throws FroniusCommunicationException, FroniusUnauthorizedException {
197215
// Perform request to get authentication parameters
198216
LOGGER.debug("Getting authentication parameters");
199217
URI loginUri = baseUri.resolve(URI.create(LOGIN_ENDPOINT + "?user=" + username));
@@ -246,6 +264,8 @@ public static synchronized String login(HttpClient httpClient, URI baseUri, Stri
246264
Thread.sleep(500 * attemptCount);
247265
attemptCount++;
248266
lastException = e;
267+
} catch (FroniusUnauthorizedException e) {
268+
throw e;
249269
}
250270

251271
if (attemptCount >= 3) {
@@ -269,6 +289,9 @@ public static synchronized String login(HttpClient httpClient, URI baseUri, Stri
269289

270290
/**
271291
* Listener to extract the X-Www-Authenticate header from the response of a {@link Request}.
292+
* Required to mitigate {@link org.eclipse.jetty.client.HttpResponseException}: HTTP protocol violation:
293+
* Authentication challenge without WWW-Authenticate header being thrown due to Fronius non-standard authentication
294+
* header.
272295
*/
273296
private static class XWwwAuthenticateHeaderListener extends Response.Listener.Adapter {
274297
private final CountDownLatch latch;
@@ -288,4 +311,30 @@ public void onHeaders(Response response) {
288311
return authHeader;
289312
}
290313
}
314+
315+
/**
316+
* Listener to extract the HTTP status code from the response of a {@link Request} on response begin.
317+
* Required to mitigate {@link org.eclipse.jetty.client.HttpResponseException}: HTTP protocol violation:
318+
* Authentication challenge without WWW-Authenticate header being thrown due to Fronius non-standard authentication
319+
* header.
320+
*/
321+
private static class StatusListener extends Response.Listener.Adapter {
322+
private final CountDownLatch latch;
323+
private @Nullable Integer status;
324+
325+
public StatusListener(CountDownLatch latch) {
326+
this.latch = latch;
327+
}
328+
329+
@Override
330+
public void onBegin(Response response) {
331+
this.status = response.getStatus();
332+
latch.countDown();
333+
super.onBegin(response);
334+
}
335+
336+
public @Nullable Integer getStatus() {
337+
return status;
338+
}
339+
}
291340
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright (c) 2010-2025 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.fronius.internal.api;
14+
15+
/**
16+
* Exception for 401 response from the Fronius controller.
17+
*
18+
* @author Florian Hotze - Initial contribution
19+
*/
20+
public class FroniusUnauthorizedException extends Exception {
21+
public FroniusUnauthorizedException(String message) {
22+
super(message);
23+
}
24+
}

bundles/org.openhab.binding.fronius/src/main/java/org/openhab/binding/fronius/internal/handler/FroniusSymoInverterHandler.java

+13
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.openhab.binding.fronius.internal.action.FroniusSymoInverterActions;
3030
import org.openhab.binding.fronius.internal.api.FroniusBatteryControl;
3131
import org.openhab.binding.fronius.internal.api.FroniusCommunicationException;
32+
import org.openhab.binding.fronius.internal.api.FroniusUnauthorizedException;
3233
import org.openhab.binding.fronius.internal.api.dto.ValueUnit;
3334
import org.openhab.binding.fronius.internal.api.dto.inverter.InverterDeviceStatus;
3435
import org.openhab.binding.fronius.internal.api.dto.inverter.InverterRealtimeBody;
@@ -115,6 +116,8 @@ public boolean resetBatteryControl() {
115116
return true;
116117
} catch (FroniusCommunicationException e) {
117118
logger.warn("Failed to reset battery control", e);
119+
} catch (FroniusUnauthorizedException e) {
120+
logger.warn("Failed to reset battery control: Invalid username or password");
118121
}
119122
}
120123
return false;
@@ -128,6 +131,8 @@ public boolean holdBatteryCharge() {
128131
return true;
129132
} catch (FroniusCommunicationException e) {
130133
logger.warn("Failed to set battery control to hold battery charge", e);
134+
} catch (FroniusUnauthorizedException e) {
135+
logger.warn("Failed to set battery control to hold battery charge: Invalid username or password");
131136
}
132137
}
133138
return false;
@@ -141,6 +146,9 @@ public boolean addHoldBatteryChargeSchedule(LocalTime from, LocalTime until) {
141146
return true;
142147
} catch (FroniusCommunicationException e) {
143148
logger.warn("Failed to add hold battery charge schedule to battery control", e);
149+
} catch (FroniusUnauthorizedException e) {
150+
logger.warn(
151+
"Failed to add hold battery charge schedule to battery control: Invalid username or password");
144152
}
145153
}
146154
return false;
@@ -154,6 +162,8 @@ public boolean forceBatteryCharging(QuantityType<Power> power) {
154162
return true;
155163
} catch (FroniusCommunicationException e) {
156164
logger.warn("Failed to set battery control to force battery charge", e);
165+
} catch (FroniusUnauthorizedException e) {
166+
logger.warn("Failed to set battery control to force battery charge: Invalid username or password");
157167
}
158168
}
159169
return false;
@@ -167,6 +177,9 @@ public boolean addForcedBatteryChargingSchedule(LocalTime from, LocalTime until,
167177
return true;
168178
} catch (FroniusCommunicationException e) {
169179
logger.warn("Failed to add forced battery charge schedule to battery control", e);
180+
} catch (FroniusUnauthorizedException e) {
181+
logger.warn(
182+
"Failed to add forced battery charge schedule to battery control: Invalid username or password");
170183
}
171184
}
172185
return false;

0 commit comments

Comments
 (0)