mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-29 12:34:21 +02:00
[fronius] Fix config API authentication for firmware >= 1.38.6 (#19344)
* [fronius] Fix config authentication for firmware >= 1.38.6 Signed-off-by: Florian Hotze <dev@florianhotze.com>
This commit is contained in:
+23
-9
@@ -36,6 +36,7 @@ import org.openhab.binding.fronius.internal.api.dto.inverter.batterycontrol.Time
|
||||
import org.openhab.binding.fronius.internal.api.dto.inverter.batterycontrol.WeekdaysRecord;
|
||||
import org.openhab.core.library.types.QuantityType;
|
||||
import org.openhab.core.library.unit.Units;
|
||||
import org.openhab.core.thing.firmware.types.SemverVersion;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -64,6 +65,7 @@ public class FroniusBatteryControl {
|
||||
private final Logger logger = LoggerFactory.getLogger(FroniusBatteryControl.class);
|
||||
private final Gson gson = new Gson();
|
||||
private final HttpClient httpClient;
|
||||
private final SemverVersion firmwareVersion;
|
||||
private final URI baseUri;
|
||||
private final String username;
|
||||
private final String password;
|
||||
@@ -74,19 +76,31 @@ public class FroniusBatteryControl {
|
||||
* Creates a new instance of {@link FroniusBatteryControl}.
|
||||
*
|
||||
* @param httpClient the HTTP client to use
|
||||
* @param baseUri the base URI of the Fronius hybrid inverter, MUST NOT end with a slash
|
||||
* @param firmwareVersion the firmware version of the inverter
|
||||
* @param scheme http or https
|
||||
* @param hostname the hostname or IP address of the inverter
|
||||
* @param username the username for the inverter Web UI
|
||||
* @param password the password for the inverter Web UI
|
||||
*/
|
||||
public FroniusBatteryControl(HttpClient httpClient, URI baseUri, String username, String password) {
|
||||
public FroniusBatteryControl(HttpClient httpClient, SemverVersion firmwareVersion, String scheme, String hostname,
|
||||
String username, String password) {
|
||||
this.httpClient = httpClient;
|
||||
this.baseUri = baseUri;
|
||||
this.firmwareVersion = firmwareVersion;
|
||||
this.baseUri = getBaseUri(firmwareVersion, scheme, hostname);
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.timeOfUseUri = URI.create(baseUri + TIME_OF_USE_ENDPOINT);
|
||||
this.batteriesUri = URI.create(baseUri + BATTERIES_ENDPOINT);
|
||||
}
|
||||
|
||||
private static URI getBaseUri(SemverVersion firmwareVersion, String scheme, String hostname) {
|
||||
String apiPrefix = "";
|
||||
if (firmwareVersion.isGreaterThanOrEqualTo(new SemverVersion(1, 36, 0))) {
|
||||
apiPrefix = "/api";
|
||||
}
|
||||
return URI.create(String.format("%s://%s%s", scheme, hostname, apiPrefix));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the time of use settings of the Fronius hybrid inverter.
|
||||
*
|
||||
@@ -96,8 +110,8 @@ public class FroniusBatteryControl {
|
||||
*/
|
||||
private TimeOfUseRecords getTimeOfUse() throws FroniusCommunicationException, FroniusUnauthorizedException {
|
||||
// Login and get the auth header for the next request
|
||||
String authHeader = FroniusConfigAuthUtil.login(httpClient, baseUri, username, password, HttpMethod.GET,
|
||||
timeOfUseUri.getPath(), API_TIMEOUT);
|
||||
String authHeader = FroniusConfigAuthUtil.login(httpClient, firmwareVersion, baseUri, username, password,
|
||||
HttpMethod.GET, timeOfUseUri.getPath(), API_TIMEOUT);
|
||||
Properties headers = new Properties();
|
||||
headers.put(HttpHeader.AUTHORIZATION.asString(), authHeader);
|
||||
// Get the time of use settings
|
||||
@@ -128,8 +142,8 @@ public class FroniusBatteryControl {
|
||||
private void setTimeOfUse(TimeOfUseRecords records)
|
||||
throws FroniusCommunicationException, FroniusUnauthorizedException {
|
||||
// Login and get the auth header for the next request
|
||||
String authHeader = FroniusConfigAuthUtil.login(httpClient, baseUri, username, password, HttpMethod.POST,
|
||||
timeOfUseUri.getPath(), API_TIMEOUT);
|
||||
String authHeader = FroniusConfigAuthUtil.login(httpClient, firmwareVersion, baseUri, username, password,
|
||||
HttpMethod.POST, timeOfUseUri.getPath(), API_TIMEOUT);
|
||||
Properties headers = new Properties();
|
||||
headers.put(HttpHeader.AUTHORIZATION.asString(), authHeader);
|
||||
|
||||
@@ -304,8 +318,8 @@ public class FroniusBatteryControl {
|
||||
}
|
||||
|
||||
// Login and get the auth header for the next request
|
||||
String authHeader = FroniusConfigAuthUtil.login(httpClient, baseUri, username, password, HttpMethod.POST,
|
||||
batteriesUri.getPath(), API_TIMEOUT);
|
||||
String authHeader = FroniusConfigAuthUtil.login(httpClient, firmwareVersion, baseUri, username, password,
|
||||
HttpMethod.POST, batteriesUri.getPath(), API_TIMEOUT);
|
||||
Properties headers = new Properties();
|
||||
headers.put(HttpHeader.AUTHORIZATION.asString(), authHeader);
|
||||
|
||||
|
||||
+57
-44
@@ -28,6 +28,7 @@ import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.api.Response;
|
||||
import org.eclipse.jetty.http.HttpHeader;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.openhab.core.thing.firmware.types.SemverVersion;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -44,13 +45,13 @@ import org.slf4j.LoggerFactory;
|
||||
@NonNullByDefault
|
||||
public class FroniusConfigAuthUtil {
|
||||
private static final String AUTHENTICATE_HEADER = "X-Www-Authenticate";
|
||||
private static final String DIGEST_AUTH_HEADER_FORMAT = "Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\", qop=%s, nc=%08x, cnonce=\"%s\"";
|
||||
private static final String DIGEST_AUTH_HEADER_FORMAT = "Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", algorithm=\"%s\", uri=\"%s\", response=\"%s\", qop=%s, nc=%08x, cnonce=\"%s\"";
|
||||
private static final String LOGIN_ENDPOINT = "/commands/Login";
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(FroniusConfigAuthUtil.class);
|
||||
|
||||
/**
|
||||
* Sends a HTTP GET request to the given login URI and extracts the authentication parameters from the
|
||||
* Sends an HTTP GET request to the given login URI and extracts the authentication parameters from the
|
||||
* authentication header.
|
||||
* This method uses a {@link Response.Listener.Adapter} to intercept the response headers and extract the
|
||||
* authentication header, as normal digest authentication using
|
||||
@@ -65,7 +66,7 @@ public class FroniusConfigAuthUtil {
|
||||
*/
|
||||
private static Map<String, String> getAuthParams(HttpClient httpClient, URI loginUri, int timeout)
|
||||
throws FroniusCommunicationException {
|
||||
LOGGER.debug("Sending login request to get authentication challenge");
|
||||
LOGGER.debug("Sending login request to get authentication challenge ...");
|
||||
CountDownLatch latch = new CountDownLatch(1);
|
||||
Request request = httpClient.newRequest(loginUri).timeout(timeout, TimeUnit.MILLISECONDS);
|
||||
XWwwAuthenticateHeaderListener xWwwAuthenticateHeaderListener = new XWwwAuthenticateHeaderListener(latch);
|
||||
@@ -82,7 +83,7 @@ public class FroniusConfigAuthUtil {
|
||||
if (authHeader == null) {
|
||||
throw new FroniusCommunicationException("No authentication header found in login response");
|
||||
}
|
||||
LOGGER.debug("Parsing authentication challenge");
|
||||
LOGGER.debug("Got login response. Parsing authentication challenge ...");
|
||||
|
||||
// Extract parameters from the header
|
||||
Map<String, String> params = new HashMap<>();
|
||||
@@ -96,57 +97,55 @@ public class FroniusConfigAuthUtil {
|
||||
params.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.debug("Got authentication challenge.");
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Digest Authentication header for the given parameters.
|
||||
*
|
||||
* @param nonce
|
||||
* @param realm
|
||||
* @param qop
|
||||
* @param hashAlgorithm the hash algorithm to use, e.g. <code>MD5</code> or <code>SHA-256</code>
|
||||
* @param uri
|
||||
* @param method
|
||||
* @param username
|
||||
* @param password
|
||||
* @param nonce
|
||||
* @param realm
|
||||
* @param qop
|
||||
* @param nc
|
||||
* @param cnonce
|
||||
* @return the digest authentication header
|
||||
* @throws FroniusCommunicationException if an authentication parameter is missing or the digest header could not be
|
||||
* created
|
||||
* @throws IllegalArgumentException when authentication parameters are missing
|
||||
* @throws NoSuchAlgorithmException when no hash algorithm with the given name is available
|
||||
*/
|
||||
private static String createDigestHeader(@Nullable String nonce, @Nullable String realm, @Nullable String qop,
|
||||
String uri, HttpMethod method, String username, String password, int nc, String cnonce)
|
||||
throws FroniusCommunicationException {
|
||||
if (nonce == null || realm == null || qop == null) {
|
||||
throw new FroniusCommunicationException("Failed to create digest header",
|
||||
new IllegalArgumentException("Missing authentication parameters"));
|
||||
}
|
||||
LOGGER.debug("Creating digest authentication header");
|
||||
String ha1;
|
||||
String ha2;
|
||||
String response;
|
||||
try {
|
||||
ha1 = md5Hex(username + ":" + realm + ":" + password);
|
||||
ha2 = md5Hex(method.asString() + ":" + uri);
|
||||
response = md5Hex(
|
||||
ha1 + ":" + nonce + ":" + String.format("%08x", nc) + ":" + cnonce + ":" + qop + ":" + ha2);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new FroniusCommunicationException("Failed to create digest header", e);
|
||||
private static String createDigestHeader(String hashAlgorithm, String uri, HttpMethod method, String username,
|
||||
String password, @Nullable String nonce, @Nullable String realm, @Nullable String qop, int nc,
|
||||
@Nullable String cnonce) throws IllegalArgumentException, NoSuchAlgorithmException {
|
||||
if (nonce == null || realm == null || qop == null || cnonce == null) {
|
||||
throw new IllegalArgumentException("Missing authentication parameters");
|
||||
}
|
||||
String ha1 = hashAsHex(hashAlgorithm, username + ":" + realm + ":" + password);
|
||||
String ha2 = hashAsHex(hashAlgorithm, method.asString() + ":" + uri);
|
||||
String response = hashAsHex(hashAlgorithm,
|
||||
ha1 + ":" + nonce + ":" + String.format("%08x", nc) + ":" + cnonce + ":" + qop + ":" + ha2);
|
||||
|
||||
return String.format(DIGEST_AUTH_HEADER_FORMAT, username, realm, nonce, uri, response, qop, nc, cnonce);
|
||||
// Fronius expects "SHA256" as algorithm name instead of the standard "SHA-256"
|
||||
String algorithm = hashAlgorithm.equals("SHA-256") ? "SHA256" : "MD5";
|
||||
return String.format(DIGEST_AUTH_HEADER_FORMAT, username, realm, nonce, algorithm, uri, response, qop, nc,
|
||||
cnonce);
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the MD5 has of the given data and returns it as a hex string.
|
||||
* Computes the hash for the given algorithm of the given data and returns it as a hex string.
|
||||
*
|
||||
* @param algorithm the hash algorithm to use
|
||||
* @param data the data to hash
|
||||
* @return the hashed data as a hex string
|
||||
* @throws NoSuchAlgorithmException if the MD5 algorithm is not available
|
||||
* @throws NoSuchAlgorithmException if the specified algorithm is not available
|
||||
*/
|
||||
private static String md5Hex(String data) throws NoSuchAlgorithmException {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
private static String hashAsHex(String algorithm, String data) throws NoSuchAlgorithmException {
|
||||
MessageDigest md = MessageDigest.getInstance(algorithm);
|
||||
byte[] array = md.digest(data.getBytes());
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : array) {
|
||||
@@ -172,6 +171,7 @@ public class FroniusConfigAuthUtil {
|
||||
StatusListener statusListener = new StatusListener(latch);
|
||||
request.onResponseBegin(statusListener);
|
||||
Integer status;
|
||||
LOGGER.debug("Logging in ...");
|
||||
try {
|
||||
request.send(result -> latch.countDown());
|
||||
// Wait for the request to complete
|
||||
@@ -192,6 +192,7 @@ public class FroniusConfigAuthUtil {
|
||||
if (status != 200) {
|
||||
throw new FroniusCommunicationException("Failed to send login request, status code: " + status);
|
||||
}
|
||||
LOGGER.debug("Login successful.");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,13 +210,16 @@ public class FroniusConfigAuthUtil {
|
||||
* @throws FroniusCommunicationException when the login failed or interrupted
|
||||
* @throws FroniusUnauthorizedException when the login failed due to invalid credentials
|
||||
*/
|
||||
public static synchronized String login(HttpClient httpClient, URI baseUri, String username, String password,
|
||||
HttpMethod method, String relativeUrl, int timeout)
|
||||
public static synchronized String login(HttpClient httpClient, SemverVersion firmwareVersion, URI baseUri,
|
||||
String username, String password, HttpMethod method, String relativeUrl, int timeout)
|
||||
throws FroniusCommunicationException, FroniusUnauthorizedException {
|
||||
final String hashAlgorithm = firmwareVersion.isGreaterThanOrEqualTo(SemverVersion.fromString("1.38.6"))
|
||||
? "SHA-256"
|
||||
: "MD5";
|
||||
final URI loginUri = URI.create(baseUri + LOGIN_ENDPOINT + "?user=" + username);
|
||||
final String relativeLoginUrl = loginUri.getPath();
|
||||
|
||||
// Perform request to get authentication parameters
|
||||
LOGGER.debug("Getting authentication parameters");
|
||||
URI loginUri = URI.create(baseUri + LOGIN_ENDPOINT + "?user=" + username);
|
||||
String relativeLoginUrl = loginUri.getPath();
|
||||
Map<String, String> authDetails;
|
||||
|
||||
int attemptCount = 1;
|
||||
@@ -245,15 +249,19 @@ public class FroniusConfigAuthUtil {
|
||||
int nc = 1;
|
||||
String cnonce;
|
||||
try {
|
||||
cnonce = md5Hex(String.valueOf(System.currentTimeMillis()));
|
||||
cnonce = hashAsHex(hashAlgorithm, String.valueOf(System.currentTimeMillis()));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new FroniusCommunicationException("Failed to create cnonce", e);
|
||||
}
|
||||
String authHeader = createDigestHeader(authDetails.get("nonce"), authDetails.get("realm"),
|
||||
authDetails.get("qop"), relativeLoginUrl, HttpMethod.GET, username, password, nc, cnonce);
|
||||
String authHeader;
|
||||
try {
|
||||
authHeader = createDigestHeader(hashAlgorithm, relativeLoginUrl, HttpMethod.GET, username, password,
|
||||
authDetails.get("nonce"), authDetails.get("realm"), authDetails.get("qop"), nc, cnonce);
|
||||
} catch (NoSuchAlgorithmException | IllegalArgumentException e) {
|
||||
throw new FroniusCommunicationException("Failed to create digest authentication header for login", e);
|
||||
}
|
||||
|
||||
// Perform login request with Digest Authentication
|
||||
LOGGER.debug("Sending login request");
|
||||
attemptCount = 1;
|
||||
try {
|
||||
while (true) {
|
||||
@@ -281,10 +289,15 @@ public class FroniusConfigAuthUtil {
|
||||
}
|
||||
|
||||
// Create new auth header for next request
|
||||
LOGGER.debug("Login successful, creating auth header for next request");
|
||||
LOGGER.debug("Creating auth header for next request ...");
|
||||
nc++;
|
||||
authHeader = createDigestHeader(authDetails.get("nonce"), authDetails.get("realm"), authDetails.get("qop"),
|
||||
relativeUrl, method, username, password, nc, cnonce);
|
||||
try {
|
||||
authHeader = createDigestHeader(hashAlgorithm, relativeUrl, method, username, password,
|
||||
authDetails.get("nonce"), authDetails.get("realm"), authDetails.get("qop"), nc, cnonce);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new FroniusCommunicationException("Failed to create digest authentication header for request", e);
|
||||
}
|
||||
LOGGER.debug("Created auth header for next request.");
|
||||
|
||||
return authHeader;
|
||||
}
|
||||
|
||||
+17
-17
@@ -14,7 +14,6 @@ package org.openhab.binding.fronius.internal.handler;
|
||||
|
||||
import static org.openhab.binding.fronius.internal.FroniusBindingConstants.API_TIMEOUT;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -45,6 +44,7 @@ import org.openhab.core.library.unit.Units;
|
||||
import org.openhab.core.thing.Bridge;
|
||||
import org.openhab.core.thing.Thing;
|
||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||
import org.openhab.core.thing.firmware.types.SemverVersion;
|
||||
import org.openhab.core.types.State;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -96,29 +96,27 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler {
|
||||
updateChannels();
|
||||
}
|
||||
|
||||
private void initializeBatteryControl(String hostname, @Nullable String username, @Nullable String password) {
|
||||
private void initializeBatteryControl(String scheme, String hostname, @Nullable String username,
|
||||
@Nullable String password) {
|
||||
if (username == null || password == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String apiPrefix = "";
|
||||
|
||||
InverterInfo localInverterInfo = inverterInfo;
|
||||
if (localInverterInfo != null) {
|
||||
String firmwareVersion = localInverterInfo.firmware();
|
||||
int lastDotIndex = firmwareVersion.lastIndexOf('.');
|
||||
float version = Float.parseFloat(firmwareVersion.substring(0, lastDotIndex));
|
||||
if (version >= 1.36) {
|
||||
apiPrefix = "/api";
|
||||
} else {
|
||||
logger.warn("Fronius Symo Inverter firmware version {} is not supported for battery control.",
|
||||
firmwareVersion);
|
||||
return;
|
||||
if (firmwareVersion != null) {
|
||||
int hyphenIndex = firmwareVersion.indexOf('-');
|
||||
String versionString = (hyphenIndex > 0) ? firmwareVersion.substring(0, hyphenIndex) : firmwareVersion;
|
||||
SemverVersion version = SemverVersion.fromString(versionString);
|
||||
if (version.isGreaterThanOrEqualTo(SemverVersion.fromString("1.36.0"))) {
|
||||
batteryControl = new FroniusBatteryControl(httpClient, version, scheme, hostname, username,
|
||||
password);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
batteryControl = new FroniusBatteryControl(httpClient, URI.create("http://" + hostname + apiPrefix), username,
|
||||
password);
|
||||
logger.warn("Your Fronius Symo Inverter firmware version is not supported by battery control.");
|
||||
}
|
||||
|
||||
private void updateProperties() {
|
||||
@@ -144,7 +142,8 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler {
|
||||
FroniusBridgeConfiguration bridgeConfig = bridge.getConfiguration().as(FroniusBridgeConfiguration.class);
|
||||
inverterInfo = getInverterInfo(bridgeConfig.scheme, bridgeConfig.hostname, config.deviceId);
|
||||
updateProperties();
|
||||
initializeBatteryControl(bridgeConfig.hostname, bridgeConfig.username, bridgeConfig.password);
|
||||
initializeBatteryControl(bridgeConfig.scheme, bridgeConfig.hostname, bridgeConfig.username,
|
||||
bridgeConfig.password);
|
||||
}
|
||||
super.initialize();
|
||||
}
|
||||
@@ -163,7 +162,8 @@ public class FroniusSymoInverterHandler extends FroniusBaseThingHandler {
|
||||
FroniusBridgeConfiguration bridgeConfig = bridge.getConfiguration().as(FroniusBridgeConfiguration.class);
|
||||
inverterInfo = getInverterInfo(bridgeConfig.scheme, bridgeConfig.hostname, config.deviceId);
|
||||
updateProperties();
|
||||
initializeBatteryControl(bridgeConfig.hostname, bridgeConfig.username, bridgeConfig.password);
|
||||
initializeBatteryControl(bridgeConfig.scheme, bridgeConfig.hostname, bridgeConfig.username,
|
||||
bridgeConfig.password);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user