[gridbox] Initial contribution (#16664)

* [gridbox] Add binding for Viessmann GridBox - Initial contribution

Signed-off-by: Benedikt Kuntz <benkuntz@web.de>
This commit is contained in:
benediktkuntz 2024-05-19 12:38:35 +02:00 committed by GitHub
parent 68fd415975
commit 9e75a4985d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 2096 additions and 0 deletions

View File

@ -646,6 +646,11 @@
<artifactId>org.openhab.binding.gree</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.gridbox</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.groheondus</artifactId>

View File

@ -0,0 +1,13 @@
This content is produced and maintained by the openHAB project.
* Project home: https://www.openhab.org
== Declared Project Licenses
This program and the accompanying materials are made available under the terms
of the Eclipse Public License 2.0 which is available at
https://www.eclipse.org/legal/epl-2.0/.
== Source Code
https://github.com/openhab/openhab-addons

View File

@ -0,0 +1,89 @@
# GridBox Binding
The [Viessmann GridBox](https://www.viessmann.de/de/produkte/energiemanagement/gridbox.html) is a energy management device which gathers information about produced and consumed electrical power from compatible energy meters, photovoltaic inverters, batteries, heat pumps, EV charging stations etc. and steers the connected components to increase the self consumption rate and efficiency of the system.
The Viessmann GridBox is a variety of the [gridX Gateway](https://de.gridx.ai/edge-services) and uses the gridX Xenon cloud service to upload the fetched data and deliver the data to the GridBox app and web service.
The measured data (energy production, consumptions, etc.) cannot be accessed locally. However, thanks to the pioneer work in the [unl0ck/viessmann-gridbox-connector](https://github.com/unl0ck/viessmann-gridbox-connector) repository, we can retrieve the data from the gridX cloud service using Rest-API calls.
The API is documented [here](https://developer.gridx.ai/reference/).
This binding polls the "live data" API endpoint to gather the available data from the GridBox.
It creates a GridBox thing with the channels representing the data points of the live data API call.
For connection to the cloud service, account E-Mail and password used to connect to the [GridBox web service](https://mygridbox.viessmann.com/login) are required.
Authentication is handled by a OAuth call generating a ID Token which is required as a bearer token for subsequent calls to the gridX API.
At the moment, only one API-"system" per account is supported by this binding.
A "system" is the representation of a GridBox together with its connected appliances (PV inverter, heat pump etc.).
The binding will use the first system ID retrieved by a call to the https://api.gridx.de/systems API.
Also, only the live data API endpoint is supported by the binding as it is the most interesting for openHAB use cases.
There is another API endpoint for fetching aggregated measurement data which could be added in the future.
Only the Viessmann GridBox variant is supported, other variants would need adaptions to the OAuth mechanism.
This binding is not endorsed or supported by Viessmann or gridX.
Arbitrary breaking changes to the API can happen at any time, resulting in this binding failing to retrieve the data.
## Supported Things
The following thing can be created with the binding:
- `gridbox`: A thing representing the GridBox, tied to an account of the Viessmann GridBox.
## Discovery
No support for auto discovery at the moment.
## Thing Configuration
The following configuration parameters are available on the GridBox thing:
### `gridbox` Thing Configuration
| Name | Type | Description | Default | Required | Advanced |
|-----------------|---------|---------------------------------------------------|---------|----------|----------|
| email | text | E-Mail address used to log in to the GridBox API | N/A | yes | no |
| password | text | Password to access the GridBox API | N/A | yes | no |
| refreshInterval | integer | Interval the device is polled in sec. | 60 | no | yes |
## Channels
The following channels are supplied by the GridBox thing (descriptions taken from the API documentation):
| Channel | Type | Read/Write | Description |
|-------------------------------|-----------|-------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| battery-capacity | Number | R | Maximum energy the battery can provide in Wh. |
| battery-nominal-capacity | Number | R | |
| battery-power | Number | R | Power is the measured power used to charge/discharge the battery. Unit W, Meaning, Positive values indicate discharging. Negative values indicate charging. |
| battery-remaining-charge | Number | R | Remaining Charge is the amount of energy left. |
| battery-state-of-charge | Number | R | State of Charge indicates how full a battery is. Unit Percentage points 0.0-1.0. |
| battery-level | Number | R | Battery level, ratio of remaining charge to capacity. |
| consumption | Number | R | Adjusted power/energy of the system. |
| direct-consumption | Number | R | Power/energy consumed through production directly. |
| direct-consumption-ev | Number | R | Power/energy consumed by the EV through production directly. |
| direct-consumption-heat-pump | Number | R | Power/energy consumed by the heat pump through production directly. |
| direct-consumption-heater | Number | R | Power/energy consumed by the heater through production directly. |
| direct-consumption-household | Number | R | Power/energy consumed by the household through production directly. |
| direct-consumption-rate | Number | R | Ratio of direct consumption vs production (0.0-1.0). |
| ev-charging-station-power | Number | R | Measured power used to charge/discharge via EV station, positive values indicate charging, negatives discharging. |
| heat-pump-power | Number | R | Aggregated measured power/energy for heat pumps. |
| photovoltaic-production | Number | R | Photovoltaic is the measured power/energy in front of the photovoltaic systems. |
| production | Number | R | Sum of all energy producing appliances (e.g. PV). |
| self-consumption | Number | R | Power/Energy consumed through production and charged into battery. |
| self-consumption-rate | Number | R | Ratio of self consumption vs production (0.0-1.0). |
| self-sufficiency-rate | Number | R | Ratio of produced energy vs total consumed energy (0.0-1.0). |
| self-supply | Number | R | Power/energy consumed through storage and production. |
| total-consumption | Number | R | Adjusted power/energy of the system including heatpumps and EV charging stations. |
## Full Example
### Thing Configuration
```java
Thing gridbox:gridbox:901b4766e2 "GridBox" [email="abc@example.com",password="mypassword",refreshInterval=120]
```
### Item Configuration
```java
Number GridBox_PhotovoltaicProduction "PV Production [%.0f W]" {channel="gridbox:gridbox:901b4766e2:photovoltaicProduction"}
```

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>4.2.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.gridbox</artifactId>
<name>openHAB Add-ons :: Bundles :: GridBox Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.gridbox-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-gridbox" description="GridBox Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.gridbox/${project.version}</bundle>
</feature>
</features>

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.gridbox.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.core.thing.ThingTypeUID;
/**
* The {@link GridBoxBindingConstants} class defines common constants, which are
* used across the whole binding.
*
* @author Benedikt Kuntz - Initial contribution
*/
@NonNullByDefault
public class GridBoxBindingConstants {
private static final String BINDING_ID = "gridbox";
// List of all Thing Type UIDs
public static final ThingTypeUID THING_TYPE_GRIDBOX = new ThingTypeUID(BINDING_ID, "gridbox");
// List of all Channel ids
public static final String BATTERY_CAPACITY = "battery-capacity";
public static final String BATTERY_NOMINAL_CAPACITY = "battery-nominal-capacity";
public static final String BATTERY_POWER = "battery-power";
public static final String BATTERY_REMAINING_CHARGE = "battery-remaining-charge";
public static final String BATTERY_STATE_OF_CHARGE = "battery-state-of-charge";
public static final String BATTERY_LEVEL = "battery-level";
public static final String CONSUMPTION = "consumption";
public static final String DIRECT_CONSUMPTION = "direct-consumption";
public static final String DIRECT_CONSUMPTION_EV = "direct-consumption-ev";
public static final String DIRECT_CONSUMPTION_HEAT_PUMP = "direct-consumption-heat-pump";
public static final String DIRECT_CONSUMPTION_HEATER = "direct-consumption-heater";
public static final String DIRECT_CONSUMPTION_HOUSEHOLD = "direct-consumption-household";
public static final String DIRECT_CONSUMPTION_RATE = "direct-consumption-rate";
public static final String EV_CHARGING_STATION_POWER = "ev-charging-station-power";
public static final String HEAT_PUMP_POWER = "heat-pump-power";
public static final String PHOTOVOLTAIC_PRODUCTION = "photovoltaic-production";
public static final String PRODUCTION = "production";
public static final String SELF_CONSUMPTION = "self-consumption";
public static final String SELF_CONSUMPTION_RATE = "self-consumption-rate";
public static final String SELF_SUFFICIENCY_RATE = "self-sufficiency-rate";
public static final String SELF_SUPPLY = "self-supply";
public static final String TOTAL_CONSUMPTION = "total-consumption";
}

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.gridbox.internal;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
/**
* The {@link GridBoxConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Benedikt Kuntz - Initial contribution
*/
@NonNullByDefault
public class GridBoxConfiguration {
@Nullable
public String email;
@Nullable
public String password;
public int refreshInterval = 5;
@Nullable
public String systemId;
@Nullable
public String idToken;
}

View File

@ -0,0 +1,227 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.gridbox.internal;
import java.io.IOException;
import java.net.http.HttpClient;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.binding.gridbox.internal.api.GridBoxApi;
import org.openhab.binding.gridbox.internal.api.GridBoxApi.GridBoxApiAuthenticationException;
import org.openhab.binding.gridbox.internal.api.GridBoxApi.GridBoxApiException;
import org.openhab.binding.gridbox.internal.api.GridBoxApi.GridBoxApiSystemNotFoundException;
import org.openhab.binding.gridbox.internal.model.BatterySummary;
import org.openhab.binding.gridbox.internal.model.EvChargingStationSummary;
import org.openhab.binding.gridbox.internal.model.LiveData;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.RefreshType;
/**
* The {@link GridBoxHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Benedikt Kuntz - Initial contribution
*/
@NonNullByDefault
public class GridBoxHandler extends BaseThingHandler {
private static final int CONNECTION_RETRY_PERIOD = 10;
private static final int MAX_NUMBER_OF_RECONNECT_ATTEMPTS = 10;
private static final GridBoxApi API = new GridBoxApi(HttpClient.newHttpClient());
private GridBoxConfiguration config = new GridBoxConfiguration();
private int reConnectAttempts;
private @Nullable ScheduledFuture<?> updateScheduledFuture;
public GridBoxHandler(Thing thing) {
super(thing);
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
scheduler.execute(this::update);
}
}
@Override
public void handleRemoval() {
stopUpdater();
super.handleRemoval();
}
public void handleLiveDataResponse(LiveData newLiveData) {
BatterySummary battery = newLiveData.getBattery();
if (battery != null) {
updateState(GridBoxBindingConstants.BATTERY_CAPACITY,
new QuantityType<>(battery.getCapacity(), Units.WATT_HOUR));
updateState(GridBoxBindingConstants.BATTERY_NOMINAL_CAPACITY,
new QuantityType<>(battery.getNominalCapacity(), Units.WATT_HOUR));
updateState(GridBoxBindingConstants.BATTERY_POWER, new QuantityType<>(battery.getPower(), Units.WATT));
updateState(GridBoxBindingConstants.BATTERY_REMAINING_CHARGE,
new QuantityType<>(battery.getRemainingCharge(), Units.WATT_HOUR));
updateState(GridBoxBindingConstants.BATTERY_STATE_OF_CHARGE, new DecimalType(battery.getStateOfCharge()));
double batteryLevel = ((double) battery.getRemainingCharge()) / battery.getCapacity() * 100;
updateState(GridBoxBindingConstants.BATTERY_LEVEL, new QuantityType<>(batteryLevel, Units.PERCENT));
}
updateState(GridBoxBindingConstants.CONSUMPTION, new QuantityType<>(newLiveData.getConsumption(), Units.WATT));
updateState(GridBoxBindingConstants.DIRECT_CONSUMPTION,
new QuantityType<>(newLiveData.getDirectConsumption(), Units.WATT));
updateState(GridBoxBindingConstants.DIRECT_CONSUMPTION_EV,
new QuantityType<>(newLiveData.getDirectConsumptionEV(), Units.WATT));
updateState(GridBoxBindingConstants.DIRECT_CONSUMPTION_HEAT_PUMP,
new QuantityType<>(newLiveData.getDirectConsumptionHeatPump(), Units.WATT));
updateState(GridBoxBindingConstants.DIRECT_CONSUMPTION_HEATER,
new QuantityType<>(newLiveData.getDirectConsumptionHeater(), Units.WATT));
updateState(GridBoxBindingConstants.DIRECT_CONSUMPTION_HOUSEHOLD,
new QuantityType<>(newLiveData.getDirectConsumptionHousehold(), Units.WATT));
updateState(GridBoxBindingConstants.DIRECT_CONSUMPTION_RATE,
new QuantityType<>(newLiveData.getDirectConsumptionRate() * 100, Units.PERCENT));
EvChargingStationSummary evChargingStation = newLiveData.getEvChargingStation();
if (evChargingStation != null) {
updateState(GridBoxBindingConstants.EV_CHARGING_STATION_POWER,
new QuantityType<>(evChargingStation.getPower(), Units.WATT));
}
updateState(GridBoxBindingConstants.HEAT_PUMP_POWER, new QuantityType<>(newLiveData.getHeatPump(), Units.WATT));
updateState(GridBoxBindingConstants.PHOTOVOLTAIC_PRODUCTION,
new QuantityType<>(newLiveData.getPhotovoltaic(), Units.WATT));
updateState(GridBoxBindingConstants.PRODUCTION, new QuantityType<>(newLiveData.getProduction(), Units.WATT));
updateState(GridBoxBindingConstants.SELF_CONSUMPTION,
new QuantityType<>(newLiveData.getSelfConsumption(), Units.WATT));
updateState(GridBoxBindingConstants.SELF_CONSUMPTION_RATE,
new QuantityType<>(newLiveData.getSelfConsumptionRate() * 100, Units.PERCENT));
updateState(GridBoxBindingConstants.SELF_SUFFICIENCY_RATE,
new QuantityType<>(newLiveData.getSelfSufficiencyRate() * 100, Units.PERCENT));
updateState(GridBoxBindingConstants.SELF_SUPPLY, new QuantityType<>(newLiveData.getSelfSupply(), Units.WATT));
updateState(GridBoxBindingConstants.TOTAL_CONSUMPTION,
new QuantityType<>(newLiveData.getTotalConsumption(), Units.WATT));
}
@Override
public void initialize() {
config = getConfigAs(GridBoxConfiguration.class);
String email = config.email;
if (email == null || email.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.configuration-error.noemail");
return;
}
String password = config.password;
if (password == null || password.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.configuration-error.nopassword");
return;
}
updateStatus(ThingStatus.UNKNOWN);
scheduler.execute(this::initializeApi);
}
private void initializeApi() {
try {
config.idToken = API.getIdToken(config);
String systemId = config.systemId;
if (systemId == null || systemId.isBlank()) {
config.systemId = API.getSystemId(config);
}
updateStatus(ThingStatus.ONLINE);
updateScheduledFuture = scheduler.scheduleWithFixedDelay(this::update, 0, config.refreshInterval,
TimeUnit.SECONDS);
} catch (GridBoxApiAuthenticationException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
"@text/offline.configuration-error.credentialsinvalid");
} catch (IOException | InterruptedException e) {
updateStatusAndTryToReconnect();
} catch (GridBoxApiException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error.initializeinvalid");
}
}
private void update() {
try {
API.retrieveLiveData(config, this::handleLiveDataResponse);
updateStatus(ThingStatus.ONLINE);
reConnectAttempts = 0;
} catch (GridBoxApiAuthenticationException e) {
// maybe the authentication is no longer valid, so try to re-authenticate
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_CONFIGURATION_PENDING,
"@text/offline.configuration-error.authenticationlost");
stopUpdater();
config.idToken = null;
initializeApi();
} catch (GridBoxApiSystemNotFoundException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_CONFIGURATION_PENDING,
"@text/offline.configuration-error.systemidunknown");
stopUpdater();
config.systemId = null;
initializeApi();
} catch (IOException | InterruptedException | GridBoxApiException e) {
stopUpdater();
updateStatusAndTryToReconnect();
}
}
private void stopUpdater() {
ScheduledFuture<?> updateScheduledFuture = this.updateScheduledFuture;
if (updateScheduledFuture != null) {
updateScheduledFuture.cancel(true);
this.updateScheduledFuture = null;
}
}
private void updateStatusAndTryToReconnect() {
if (reConnectAttempts > MAX_NUMBER_OF_RECONNECT_ATTEMPTS) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error.connectionfinallylost");
} else {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/offline.communication-error.connectionlost");
scheduler.schedule(this::initialize, getDelayUntilNextConnectionAttempt(), TimeUnit.SECONDS);
}
}
private long getDelayUntilNextConnectionAttempt() {
// progressively increase the time until the re-connect attempt by .5 times the refreshInterval
long delay = CONNECTION_RETRY_PERIOD + config.refreshInterval * reConnectAttempts / 2;
reConnectAttempts++;
return delay;
}
@Override
public void dispose() {
super.dispose();
stopUpdater();
}
}

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.gridbox.internal;
import static org.openhab.binding.gridbox.internal.GridBoxBindingConstants.*;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerFactory;
import org.osgi.service.component.annotations.Component;
/**
* The {@link GridBoxHandlerFactory} is responsible for creating things and thing
* handlers.
*
* @author Benedikt Kuntz - Initial contribution
*/
@NonNullByDefault
@Component(configurationPid = "binding.gridbox", service = ThingHandlerFactory.class)
public class GridBoxHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_GRIDBOX);
@Override
public boolean supportsThingType(ThingTypeUID thingTypeUID) {
return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID);
}
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
if (THING_TYPE_GRIDBOX.equals(thingTypeUID)) {
return new GridBoxHandler(thing);
}
return null;
}
}

View File

@ -0,0 +1,289 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.gridbox.internal.api;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Optional;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.openhab.binding.gridbox.internal.GridBoxConfiguration;
import org.openhab.binding.gridbox.internal.model.LiveData;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.JsonParser;
/**
* The {@link GridBoxApi} is responsible for executing the HTTP calls to the GridBox API. Calls are executed
* synchronously, so the functions should be called from a parallel thread.
*
* @author Benedikt Kuntz - Initial contribution
*/
@NonNullByDefault
public class GridBoxApi {
public class GridBoxApiSystemNotFoundException extends Exception {
private static final long serialVersionUID = 2485670225601258718L;
public GridBoxApiSystemNotFoundException(String message) {
super(message);
}
}
public class GridBoxApiException extends Exception {
private static final long serialVersionUID = -5295192044532589122L;
public GridBoxApiException(String message) {
super(message);
}
}
public class GridBoxApiAuthenticationException extends Exception {
private static final long serialVersionUID = 7626147923372849513L;
public GridBoxApiAuthenticationException(String message) {
super(message);
}
}
private static final Duration TIMEOUT_DURATION = Duration.ofSeconds(5);
private static final Gson GSON = new Gson();
private final Logger logger = LoggerFactory.getLogger(GridBoxApi.class);
private final HttpClient httpClient;
public GridBoxApi(HttpClient httpClient) {
this.httpClient = httpClient;
}
/**
* Fetch the ID Token required for the GridBox API calls by querying an OAuth request.
*
* @param config The {@link GridBoxConfiguration} to use, must contain valid user email/password
* @return The fetched ID Token. Will always be non-null, if no exception occurs.
* @throws IOException see {@link HttpClient#send(HttpRequest, java.net.http.HttpResponse.BodyHandler)}
* @throws InterruptedException see {@link HttpClient#send(HttpRequest, java.net.http.HttpResponse.BodyHandler)}
* @throws GridBoxApiAuthenticationException Thrown in case of an invalid response of the authorization query,
* either if a status code not equal to 200 is returned or the ID Token cannot be parsed from the
* response body
*/
public String getIdToken(GridBoxConfiguration config)
throws IOException, InterruptedException, GridBoxApiAuthenticationException {
HttpRequest.BodyPublisher userPublisher = HttpRequest.BodyPublishers.ofString("""
{
"grant_type": "http://auth0.com/oauth/grant-type/password-realm",
"username": "%s",
"password": "%s",
"audience": "my.gridx",
"client_id": "oZpr934Ikn8OZOHTJEcrgXkjio0I0Q7b",
"scope": "email openid",
"realm": "viessmann-authentication-db"
}
""".formatted(config.email, config.password));
// @formatter:off
HttpRequest request = HttpRequest.newBuilder(URI.create("https://gridx.eu.auth0.com/oauth/token"))
.POST(userPublisher)
.timeout(TIMEOUT_DURATION)
.setHeader("Content-Type", "application/json")
.build();
// @formatter:on
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
String body = response.body();
int status = response.statusCode();
if (status != 200) {
logger.debug("Invalid response of authentication request, returned status: {}", status);
logger.trace("Response body: {}", body);
throw new GridBoxApiAuthenticationException("Authentication request returned an invalid response");
}
logger.atTrace().log(() -> "Authentication request returned body: {}".formatted(body));
Optional<String> idTokenValue = parseIdTokenValue(body);
if (idTokenValue.isEmpty()) {
logger.debug("Invalid response of authentication request, returned status: {}", status);
logger.trace("Response body: {}", body);
throw new GridBoxApiAuthenticationException("Authentication request returned an invalid response");
}
return idTokenValue.get();
}
/**
* Parse the ID Token from a JSON response body
*
* @param body A JSON response containing an object with an "id_token" attribute
* @return The parsed "id_token" value, only non-empty if parsing was successful
*/
public static Optional<String> parseIdTokenValue(String body) {
String idTokenValue = null;
JsonElement parentElement = JsonParser.parseString(body);
if (parentElement.isJsonObject()) {
JsonElement idTokenElement = parentElement.getAsJsonObject().get("id_token");
if (idTokenElement != null && idTokenElement.isJsonPrimitive()) {
idTokenValue = idTokenElement.getAsString();
}
}
return Optional.ofNullable(idTokenValue);
}
/**
* Fetch the System ID required for the GridBox API calls by executing a gateway API query.
*
* @param config The {@link GridBoxConfiguration} to use, containing a valid ID token
* @return The fetched System ID. Will always be non-null, if no exception occurs.
* @throws IOException see {@link HttpClient#send(HttpRequest, java.net.http.HttpResponse.BodyHandler)}
* @throws InterruptedException see {@link HttpClient#send(HttpRequest, java.net.http.HttpResponse.BodyHandler)}
* @throws GridBoxApiException Thrown in case of an invalid response of the gateway query, either
* if a status code not equal to 200 or 403 is returned or the System ID cannot be parsed from the
* response body
* @throws GridBoxApiAuthenticationException Thrown in case of an invalid response of the authorization query, if a
* status code equal to 403 is returned
*/
public String getSystemId(GridBoxConfiguration config)
throws IOException, InterruptedException, GridBoxApiAuthenticationException, GridBoxApiException {
// @formatter:off
HttpRequest gatewayRequest = HttpRequest.newBuilder(URI.create("https://api.gridx.de/gateways"))
.GET()
.timeout(TIMEOUT_DURATION)
.setHeader("Content-Type", "application/json")
.setHeader("Authorization", "Bearer %s".formatted(config.idToken))
.build();
// @formatter:on
HttpResponse<String> response = httpClient.send(gatewayRequest, HttpResponse.BodyHandlers.ofString());
String body = response.body();
int status = response.statusCode();
if (status == 403) {
logger.debug("Gateway request returned access forbidden, returned status: {}", status);
logger.trace("Response body: {}", body);
throw new GridBoxApiAuthenticationException("Gateway request forbidden");
} else if (status != 200) {
logger.debug("Invalid response of gateway request, returned status {}", status);
logger.trace("Response body: {}", body);
throw new GridBoxApiException("Gateway request returned an invalid response");
}
logger.atTrace().log(() -> "Gateway request returned body: {}".formatted(body));
Optional<String> systemIdValue = parseSystemIdValue(body);
if (systemIdValue.isEmpty()) {
logger.debug("Invalid response of gateway request, returned status {}", status);
logger.trace("Response body: {}", body);
throw new GridBoxApiException("Gateway request returned an invalid response");
}
return systemIdValue.get();
}
/**
* Parse the System ID from a JSON response body
*
* @param body A JSON response containing the System ID in an "id" attribute
* @return The parsed "id" value, only non-empty if parsing was successful
*/
public static Optional<String> parseSystemIdValue(String body) {
JsonElement jsonObject = JsonParser.parseString(body);
String systemIdValue = null;
if (jsonObject.isJsonArray()) {
JsonElement index0Element = jsonObject.getAsJsonArray().get(0);
if (index0Element != null && index0Element.isJsonObject()) {
JsonElement systemElement = index0Element.getAsJsonObject().get("system");
if (systemElement != null && systemElement.isJsonObject()) {
JsonElement idElement = systemElement.getAsJsonObject().get("id");
if (idElement != null && idElement.isJsonPrimitive()) {
systemIdValue = idElement.getAsString();
}
}
}
}
return Optional.ofNullable(systemIdValue);
}
/**
* Fetch the Live Data from the GridBox API by executing an API query.
*
* @param config The {@link GridBoxConfiguration} to use, containing a valid ID token
* @param responseHandler A function handling the retrieved live data, called if a valid response was received
* with a {@link LiveData} object containing the content of the response.
* @throws IOException see {@link HttpClient#send(HttpRequest, java.net.http.HttpResponse.BodyHandler)}
* @throws InterruptedException see {@link HttpClient#send(HttpRequest, java.net.http.HttpResponse.BodyHandler)}
* @throws GridBoxApiException Thrown in case of an invalid response of the live data query, either
* if a status code not equal to 200 or 403 is returned or the {@link LiveData} instance cannot be
* parsed from the response body
* @throws GridBoxApiSystemNotFoundException Thrown in case the query returned status code 404, representing a
* non-existing System ID
* @throws GridBoxApiAuthenticationException Thrown in case of an invalid response of the authorization query if a
* status code equal to 403 is returned
*/
public void retrieveLiveData(GridBoxConfiguration config, Consumer<LiveData> responseHandler)
throws IOException, InterruptedException, GridBoxApiAuthenticationException,
GridBoxApiSystemNotFoundException, GridBoxApiException {
// @formatter:off
HttpRequest liveDataRequest = HttpRequest
.newBuilder(URI.create("https://api.gridx.de/systems/%s/live".formatted(config.systemId)))
.GET()
.timeout(TIMEOUT_DURATION).setHeader("Content-Type", "application/json")
.setHeader("Authorization", "Bearer %s".formatted(config.idToken))
.build();
// @formatter:on
HttpResponse<String> response = httpClient.send(liveDataRequest, HttpResponse.BodyHandlers.ofString());
int status = response.statusCode();
String body = response.body();
if (status == 403) {
logger.debug("Live data request returned access forbidden, status {}", status);
logger.trace("Response body: {}", body);
throw new GridBoxApiAuthenticationException("Live Data request forbidden");
} else if (status == 404) {
logger.debug("Invalid response of live data request, returned status {}", status);
logger.trace("Response body: {}", body);
throw new GridBoxApiSystemNotFoundException("Live Data request returned an invalid response");
} else if (status != 200) {
logger.debug("Invalid response of live data request, returned status {}", status);
logger.trace("Response body: {}", body);
throw new GridBoxApiException("Live Data request returned an invalid response");
}
JsonElement jsonObject;
try {
jsonObject = JsonParser.parseString(body);
} catch (JsonParseException e) {
logger.debug("Invalid response of live data request, could not parse JSON body");
logger.trace("JSON body: {}", body);
throw new GridBoxApiException("Live Data request returned an invalid response");
}
logger.atTrace().log(() -> "Live data request returned body: {}".formatted(jsonObject));
LiveData liveData = GSON.fromJson(jsonObject, LiveData.class);
if (liveData != null && !liveData.allValuesZero()) {
responseHandler.accept(liveData);
}
}
}

View File

@ -0,0 +1,110 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.gridbox.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* {@link Battery} is a gson-mapped class that will be used to contain information of one battery in a GridBox API
* response
*
* @author Benedikt Kuntz - Initial contribution
*/
@NonNullByDefault
public class Battery {
@SerializedName("applianceID")
@Expose
@Nullable
private String applianceID;
@SerializedName("capacity")
@Expose
private long capacity;
@SerializedName("nominalCapacity")
@Expose
private long nominalCapacity;
@SerializedName("power")
@Expose
private long power;
@SerializedName("remainingCharge")
@Expose
private long remainingCharge;
@SerializedName("stateOfCharge")
@Expose
private long stateOfCharge;
@Nullable
public String getApplianceID() {
return applianceID;
}
public void setApplianceID(String applianceID) {
this.applianceID = applianceID;
}
public long getCapacity() {
return capacity;
}
public void setCapacity(long capacity) {
this.capacity = capacity;
}
public long getNominalCapacity() {
return nominalCapacity;
}
public void setNominalCapacity(long nominalCapacity) {
this.nominalCapacity = nominalCapacity;
}
public long getPower() {
return power;
}
public void setPower(long power) {
this.power = power;
}
public long getRemainingCharge() {
return remainingCharge;
}
public void setRemainingCharge(long remainingCharge) {
this.remainingCharge = remainingCharge;
}
public long getStateOfCharge() {
return stateOfCharge;
}
public void setStateOfCharge(long stateOfCharge) {
this.stateOfCharge = stateOfCharge;
}
@Override
public String toString() {
return "Battery [applianceID=" + applianceID + ", capacity=" + capacity + ", nominalCapacity=" + nominalCapacity
+ ", power=" + power + ", remainingCharge=" + remainingCharge + ", stateOfCharge=" + stateOfCharge
+ "]";
}
}

View File

@ -0,0 +1,94 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.gridbox.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* {@link BatterySummary} is a gson-mapped class that will be used to contain summarized information of all batteries in
* a GridBox API response
*
* @author Benedikt Kuntz - Initial contribution
*/
@NonNullByDefault
public class BatterySummary {
@SerializedName("capacity")
@Expose
private long capacity;
@SerializedName("nominalCapacity")
@Expose
private long nominalCapacity;
@SerializedName("power")
@Expose
private long power;
@SerializedName("remainingCharge")
@Expose
private long remainingCharge;
@SerializedName("stateOfCharge")
@Expose
private long stateOfCharge;
public long getCapacity() {
return capacity;
}
public void setCapacity(long capacity) {
this.capacity = capacity;
}
public long getNominalCapacity() {
return nominalCapacity;
}
public void setNominalCapacity(long nominalCapacity) {
this.nominalCapacity = nominalCapacity;
}
public long getPower() {
return power;
}
public void setPower(long power) {
this.power = power;
}
public long getRemainingCharge() {
return remainingCharge;
}
public void setRemainingCharge(long remainingCharge) {
this.remainingCharge = remainingCharge;
}
public long getStateOfCharge() {
return stateOfCharge;
}
public void setStateOfCharge(long stateOfCharge) {
this.stateOfCharge = stateOfCharge;
}
@Override
public String toString() {
return "BatterySummary [capacity=" + capacity + ", nominalCapacity=" + nominalCapacity + ", power=" + power
+ ", remainingCharge=" + remainingCharge + ", stateOfCharge=" + stateOfCharge + "]";
}
}

View File

@ -0,0 +1,124 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.gridbox.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* {@link EvChargingStation} is a gson-mapped class that will be used to contain information of one EV charging
* station/wallbox in a GridBox API response
*
* @author Benedikt Kuntz - Initial contribution
*/
@NonNullByDefault
public class EvChargingStation {
@SerializedName("applianceID")
@Expose
@Nullable
private String applianceID;
@SerializedName("currentL1")
@Expose
private long currentL1;
@SerializedName("currentL2")
@Expose
private long currentL2;
@SerializedName("currentL3")
@Expose
private long currentL3;
@SerializedName("plugState")
@Expose
@Nullable
private String plugState;
@SerializedName("power")
@Expose
private long power;
@SerializedName("readingTotal")
@Expose
private long readingTotal;
@Nullable
public String getApplianceID() {
return applianceID;
}
public void setApplianceID(String applianceID) {
this.applianceID = applianceID;
}
public long getCurrentL1() {
return currentL1;
}
public void setCurrentL1(long currentL1) {
this.currentL1 = currentL1;
}
public long getCurrentL2() {
return currentL2;
}
public void setCurrentL2(long currentL2) {
this.currentL2 = currentL2;
}
public long getCurrentL3() {
return currentL3;
}
public void setCurrentL3(long currentL3) {
this.currentL3 = currentL3;
}
@Nullable
public String getPlugState() {
return plugState;
}
public void setPlugState(String plugState) {
this.plugState = plugState;
}
public long getPower() {
return power;
}
public void setPower(long power) {
this.power = power;
}
public long getReadingTotal() {
return readingTotal;
}
public void setReadingTotal(long readingTotal) {
this.readingTotal = readingTotal;
}
@Override
public String toString() {
return "EvChargingStation [applianceID=" + applianceID + ", currentL1=" + currentL1 + ", currentL2=" + currentL2
+ ", currentL3=" + currentL3 + ", plugState=" + plugState + ", power=" + power + ", readingTotal="
+ readingTotal + "]";
}
}

View File

@ -0,0 +1,45 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.gridbox.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* {@link EvChargingStationSummary} is a gson-mapped class that will be used to contain summarized information of all EV
* charging stations/wallboxes in a GridBox API response
*
* @author Benedikt Kuntz - Initial contribution
*/
@NonNullByDefault
public class EvChargingStationSummary {
@SerializedName("power")
@Expose
private long power;
public long getPower() {
return power;
}
public void setPower(long power) {
this.power = power;
}
@Override
public String toString() {
return "EvChargingStationSummary [power=" + power + "]";
}
}

View File

@ -0,0 +1,60 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.gridbox.internal.model;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* {@link HeatPump} is a gson-mapped class that will be used to contain information of a heat pump in a GridBox API
* response
*
* @author Benedikt Kuntz - Initial contribution
*/
@NonNullByDefault
public class HeatPump {
@SerializedName("applianceID")
@Expose
@Nullable
private String applianceID;
@SerializedName("power")
@Expose
private long power;
@Nullable
public String getApplianceID() {
return applianceID;
}
public void setApplianceID(String applianceID) {
this.applianceID = applianceID;
}
public long getPower() {
return power;
}
public void setPower(long power) {
this.power = power;
}
@Override
public String toString() {
return "HeatPump [applianceID=" + applianceID + ", power=" + power + "]";
}
}

View File

@ -0,0 +1,400 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.gridbox.internal.model;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* {@link LiveData} is a gson-mapped class representing the response of a call to the live data endpoint of the GridBox
* API
*
* @author Benedikt Kuntz - Initial contribution
*/
@NonNullByDefault
public class LiveData {
private static final double ZERO_THRESHOLD = 1e-15;
@SerializedName("batteries")
@Expose
private List<Battery> batteries = new ArrayList<>();
@SerializedName("battery")
@Expose
@Nullable
private BatterySummary battery;
@SerializedName("consumption")
@Expose
private long consumption;
@SerializedName("directConsumption")
@Expose
private long directConsumption;
@SerializedName("directConsumptionEV")
@Expose
private long directConsumptionEV;
@SerializedName("directConsumptionHeatPump")
@Expose
private long directConsumptionHeatPump;
@SerializedName("directConsumptionHeater")
@Expose
private long directConsumptionHeater;
@SerializedName("directConsumptionHousehold")
@Expose
private long directConsumptionHousehold;
@SerializedName("directConsumptionRate")
@Expose
private double directConsumptionRate;
@SerializedName("evChargingStation")
@Expose
@Nullable
private EvChargingStationSummary evChargingStation;
@SerializedName("evChargingStations")
@Expose
private List<EvChargingStation> evChargingStations = new ArrayList<>();
@SerializedName("grid")
@Expose
private long grid;
@SerializedName("gridMeterReadingNegative")
@Expose
private long gridMeterReadingNegative;
@SerializedName("gridMeterReadingPositive")
@Expose
private long gridMeterReadingPositive;
@SerializedName("heatPump")
@Expose
private long heatPump;
@SerializedName("heatPumps")
@Expose
private List<HeatPump> heatPumps = new ArrayList<>();
@SerializedName("l1CurtailmentPower")
@Expose
private long l1CurtailmentPower;
@SerializedName("l2CurtailmentPower")
@Expose
private long l2CurtailmentPower;
@SerializedName("l3CurtailmentPower")
@Expose
private long l3CurtailmentPower;
@SerializedName("measuredAt")
@Expose
@Nullable
private String measuredAt;
@SerializedName("photovoltaic")
@Expose
private long photovoltaic;
@SerializedName("production")
@Expose
private long production;
@SerializedName("selfConsumption")
@Expose
private long selfConsumption;
@SerializedName("selfConsumptionRate")
@Expose
private double selfConsumptionRate;
@SerializedName("selfSufficiencyRate")
@Expose
private long selfSufficiencyRate;
@SerializedName("selfSupply")
@Expose
private long selfSupply;
@SerializedName("totalConsumption")
@Expose
private long totalConsumption;
public List<Battery> getBatteries() {
return batteries;
}
public void setBatteries(List<Battery> batteries) {
this.batteries = batteries;
}
@Nullable
public BatterySummary getBattery() {
return battery;
}
public void setBattery(BatterySummary battery) {
this.battery = battery;
}
public long getConsumption() {
return consumption;
}
public void setConsumption(long consumption) {
this.consumption = consumption;
}
public long getDirectConsumption() {
return directConsumption;
}
public void setDirectConsumption(long directConsumption) {
this.directConsumption = directConsumption;
}
public long getDirectConsumptionEV() {
return directConsumptionEV;
}
public void setDirectConsumptionEV(long directConsumptionEV) {
this.directConsumptionEV = directConsumptionEV;
}
public long getDirectConsumptionHeatPump() {
return directConsumptionHeatPump;
}
public void setDirectConsumptionHeatPump(long directConsumptionHeatPump) {
this.directConsumptionHeatPump = directConsumptionHeatPump;
}
public long getDirectConsumptionHeater() {
return directConsumptionHeater;
}
public void setDirectConsumptionHeater(long directConsumptionHeater) {
this.directConsumptionHeater = directConsumptionHeater;
}
public long getDirectConsumptionHousehold() {
return directConsumptionHousehold;
}
public void setDirectConsumptionHousehold(long directConsumptionHousehold) {
this.directConsumptionHousehold = directConsumptionHousehold;
}
public double getDirectConsumptionRate() {
return directConsumptionRate;
}
public void setDirectConsumptionRate(double directConsumptionRate) {
this.directConsumptionRate = directConsumptionRate;
}
@Nullable
public EvChargingStationSummary getEvChargingStation() {
return evChargingStation;
}
public void setEvChargingStation(EvChargingStationSummary evChargingStation) {
this.evChargingStation = evChargingStation;
}
public List<EvChargingStation> getEvChargingStations() {
return evChargingStations;
}
public void setEvChargingStations(List<EvChargingStation> evChargingStations) {
this.evChargingStations = evChargingStations;
}
public long getGrid() {
return grid;
}
public void setGrid(long grid) {
this.grid = grid;
}
public long getGridMeterReadingNegative() {
return gridMeterReadingNegative;
}
public void setGridMeterReadingNegative(long gridMeterReadingNegative) {
this.gridMeterReadingNegative = gridMeterReadingNegative;
}
public long getGridMeterReadingPositive() {
return gridMeterReadingPositive;
}
public void setGridMeterReadingPositive(long gridMeterReadingPositive) {
this.gridMeterReadingPositive = gridMeterReadingPositive;
}
public long getHeatPump() {
return heatPump;
}
public void setHeatPump(long heatPump) {
this.heatPump = heatPump;
}
public List<HeatPump> getHeatPumps() {
return heatPumps;
}
public void setHeatPumps(List<HeatPump> heatPumps) {
this.heatPumps = heatPumps;
}
public long getL1CurtailmentPower() {
return l1CurtailmentPower;
}
public void setL1CurtailmentPower(long l1CurtailmentPower) {
this.l1CurtailmentPower = l1CurtailmentPower;
}
public long getL2CurtailmentPower() {
return l2CurtailmentPower;
}
public void setL2CurtailmentPower(long l2CurtailmentPower) {
this.l2CurtailmentPower = l2CurtailmentPower;
}
public long getL3CurtailmentPower() {
return l3CurtailmentPower;
}
public void setL3CurtailmentPower(long l3CurtailmentPower) {
this.l3CurtailmentPower = l3CurtailmentPower;
}
@Nullable
public String getMeasuredAt() {
return measuredAt;
}
public void setMeasuredAt(String measuredAt) {
this.measuredAt = measuredAt;
}
public long getPhotovoltaic() {
return photovoltaic;
}
public void setPhotovoltaic(long photovoltaic) {
this.photovoltaic = photovoltaic;
}
public long getProduction() {
return production;
}
public void setProduction(long production) {
this.production = production;
}
public long getSelfConsumption() {
return selfConsumption;
}
public void setSelfConsumption(long selfConsumption) {
this.selfConsumption = selfConsumption;
}
public double getSelfConsumptionRate() {
return selfConsumptionRate;
}
public void setSelfConsumptionRate(double selfConsumptionRate) {
this.selfConsumptionRate = selfConsumptionRate;
}
public long getSelfSufficiencyRate() {
return selfSufficiencyRate;
}
public void setSelfSufficiencyRate(long selfSufficiencyRate) {
this.selfSufficiencyRate = selfSufficiencyRate;
}
public long getSelfSupply() {
return selfSupply;
}
public void setSelfSupply(long selfSupply) {
this.selfSupply = selfSupply;
}
public long getTotalConsumption() {
return totalConsumption;
}
public void setTotalConsumption(long totalConsumption) {
this.totalConsumption = totalConsumption;
}
public boolean allValuesZero() {
return isZero(consumption) && isZero(directConsumption) && isZero(directConsumptionEV)
&& isZero(directConsumptionHeatPump) && isZero(directConsumptionHeater)
&& isZero(directConsumptionHousehold) && isZero(directConsumptionRate) && isZero(grid)
&& isZero(gridMeterReadingNegative) && isZero(gridMeterReadingPositive) && isZero(heatPump)
&& isZero(l1CurtailmentPower) && isZero(l2CurtailmentPower) && isZero(l3CurtailmentPower)
&& isZero(photovoltaic) && isZero(production) && isZero(selfConsumption) && isZero(selfConsumptionRate)
&& isZero(selfSufficiencyRate) && isZero(selfSupply) && isZero(totalConsumption);
}
private boolean isZero(double value) {
return Math.abs(value) < ZERO_THRESHOLD;
}
private boolean isZero(long value) {
return value == 0;
}
@Override
public String toString() {
return "LiveData [batteries=" + batteries + ", battery=" + battery + ", consumption=" + consumption
+ ", directConsumption=" + directConsumption + ", directConsumptionEV=" + directConsumptionEV
+ ", directConsumptionHeatPump=" + directConsumptionHeatPump + ", directConsumptionHeater="
+ directConsumptionHeater + ", directConsumptionHousehold=" + directConsumptionHousehold
+ ", directConsumptionRate=" + directConsumptionRate + ", evChargingStation=" + evChargingStation
+ ", evChargingStations=" + evChargingStations + ", grid=" + grid + ", gridMeterReadingNegative="
+ gridMeterReadingNegative + ", gridMeterReadingPositive=" + gridMeterReadingPositive + ", heatPump="
+ heatPump + ", heatPumps=" + heatPumps + ", l1CurtailmentPower=" + l1CurtailmentPower
+ ", l2CurtailmentPower=" + l2CurtailmentPower + ", l3CurtailmentPower=" + l3CurtailmentPower
+ ", measuredAt=" + measuredAt + ", photovoltaic=" + photovoltaic + ", production=" + production
+ ", selfConsumption=" + selfConsumption + ", selfConsumptionRate=" + selfConsumptionRate
+ ", selfSufficiencyRate=" + selfSufficiencyRate + ", selfSupply=" + selfSupply + ", totalConsumption="
+ totalConsumption + "]";
}
}

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<addon:addon id="gridbox" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:addon="https://openhab.org/schemas/addon/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/addon/v1.0.0 https://openhab.org/schemas/addon-1.0.0.xsd">
<type>binding</type>
<name>GridBox Binding</name>
<description>Get Information from the Viessmann GridBox</description>
<connection>cloud</connection>
</addon:addon>

View File

@ -0,0 +1,34 @@
# add-on
addon.gridbox.name = GridBox Binding
addon.gridbox.description = Get Information from the Viessmann GridBox
# thing types
thing-type.gridbox.gridbox.label = GridBox
thing-type.gridbox.gridbox.description = GridBox Thing
# thing types config
thing-type.config.gridbox.gridbox.email.label = E-Mail
thing-type.config.gridbox.gridbox.email.description = E-Mail address used to log in to the GridBox API (https://mygridbox.viessmann.com/)
thing-type.config.gridbox.gridbox.password.label = Password
thing-type.config.gridbox.gridbox.password.description = Password to access the GridBox API
thing-type.config.gridbox.gridbox.refreshInterval.label = Refresh Interval
thing-type.config.gridbox.gridbox.refreshInterval.description = Interval the device is polled in sec.
# channel types
channel-type.gridbox.rate.label = Rate
channel-type.gridbox.state-of-charge.label = State of Charge
# thing status descriptions
offline.configuration-error.authenticationlost = Authentication lost, trying to re-authenticate
offline.configuration-error.credentialsinvalid = Username or password invalid?
offline.configuration-error.noemail = User email not provided
offline.configuration-error.nopassword = Password not provided
offline.configuration-error.systemidunknown = System ID not known, try to re-acquire it
offline.communication-error.connectionfinallylost = Connection to GridBox lost, no connection could be established after 10 attempts
offline.communication-error.connectionlost = Connection to GridBox lost, reconnecting in some seconds
offline.communication-error.initializeinvalid = Unable to initialize GridBox thing, invalid response from API

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="gridbox"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<thing-type id="gridbox">
<label>GridBox</label>
<description>GridBox Thing</description>
<channels>
<channel id="battery-capacity" typeId="system.electric-energy"/>
<channel id="battery-nominal-capacity" typeId="system.electric-energy"/>
<channel id="battery-power" typeId="system.electric-power"/>
<channel id="battery-remaining-charge" typeId="system.electric-energy"/>
<channel id="battery-state-of-charge" typeId="state-of-charge"/>
<channel id="battery-level" typeId="system.battery-level"/>
<channel id="consumption" typeId="system.electric-power"/>
<channel id="direct-consumption" typeId="system.electric-power"/>
<channel id="direct-consumption-ev" typeId="system.electric-power"/>
<channel id="direct-consumption-heat-pump" typeId="system.electric-power"/>
<channel id="direct-consumption-heater" typeId="system.electric-power"/>
<channel id="direct-consumption-household" typeId="system.electric-power"/>
<channel id="direct-consumption-rate" typeId="rate"/>
<channel id="ev-charging-station-power" typeId="system.electric-power"/>
<channel id="heat-pump-power" typeId="system.electric-power"/>
<channel id="photovoltaic-production" typeId="system.electric-power"/>
<channel id="production" typeId="system.electric-power"/>
<channel id="self-consumption" typeId="system.electric-power"/>
<channel id="self-consumption-rate" typeId="rate"/>
<channel id="self-sufficiency-rate" typeId="rate"/>
<channel id="self-supply" typeId="system.electric-power"/>
<channel id="total-consumption" typeId="system.electric-power"/>
</channels>
<config-description>
<parameter name="email" type="text" required="true">
<context>email</context>
<label>E-Mail</label>
<description>E-Mail address used to log in to the GridBox API (https://mygridbox.viessmann.com/)</description>
</parameter>
<parameter name="password" type="text" required="true">
<context>password</context>
<label>Password</label>
<description>Password to access the GridBox API</description>
</parameter>
<parameter name="refreshInterval" type="integer" unit="s" min="1">
<label>Refresh Interval</label>
<description>Interval the device is polled in sec.</description>
<default>60</default>
<advanced>true</advanced>
</parameter>
</config-description>
</thing-type>
<channel-type id="rate">
<item-type>Number</item-type>
<label>Rate</label>
<state min="0" max="100" step="0.01" pattern="%.2f %" readOnly="true"/>
</channel-type>
<channel-type id="state-of-charge">
<item-type>Number</item-type>
<label>State of Charge</label>
<state min="0" max="100" step="1" readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,350 @@
/**
* Copyright (c) 2010-2024 Contributors to the openHAB project
*
* See the NOTICE file(s) distributed with this work for additional
* information.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.openhab.binding.gridbox;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.net.http.HttpClient;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandler;
import java.net.http.HttpTimeoutException;
import java.util.Optional;
import java.util.function.Consumer;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import org.openhab.binding.gridbox.internal.GridBoxConfiguration;
import org.openhab.binding.gridbox.internal.api.GridBoxApi;
import org.openhab.binding.gridbox.internal.api.GridBoxApi.GridBoxApiAuthenticationException;
import org.openhab.binding.gridbox.internal.api.GridBoxApi.GridBoxApiException;
import org.openhab.binding.gridbox.internal.api.GridBoxApi.GridBoxApiSystemNotFoundException;
import org.openhab.binding.gridbox.internal.model.LiveData;
/**
* The {@link GridBoxApiTest} tests the parsing functions of the {@link GridBoxApi} class.
*
* @author Benedikt Kuntz - Initial contribution
*/
@ExtendWith(MockitoExtension.class)
@NonNullByDefault
public class GridBoxApiTest {
private static final String AUTH_RESPONSE = """
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik16UkRSakU1UVRrd1JEQXhOVU15UlRnMVFrRTNNemRCUmpZNE5rRTFOamRCTjBZd1FrWkdOQSJ9.eyJpc3MiOiJodHRwczovL2dyaWR4LmV1LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHw2M2ViYmM5ZDUyNDZkOTEwZWYyYzBkYjEiLCJhdWQiOlsibXkuZ3JpZHgiLCJodHRwczovL2dyaWR4LmV1LmF1dGgwLmNvbS91c2VyaW5mbyJdLCJpYXQiOjE3MTM0NjUzMjMsImV4cCI6MTcxMzU1MTcyMywic2NvcGUiOiJlbWFpbCBvcGVuaWQiLCJndHkiOiJwYXNzd29yZCIsImF6cCI6Im9acHI5MzRJa244T1pPSFRKRWNyZ1hramlvMEkwUTdiIn0.Hapn-J94bjKenQUVqY3lnPYQb2QEUIS-pSWZe0tEKXRyOLFmC5u5AjMxlEoNd4eC1cqmu3xySyDoCJQaaSmWSF3xNfZsdQmokiOWPfptikNwecH9JdDhtJFobME8b_tfid7tpMk4TVKVNEm6Ns86w9QyrtMkXP3GlrayHlXCL_90lSfsOA2D0V-uSV1VL_1wz0p_-9_Scl7DUyQJP9qg-H6GVF6eyA0iieaYfbJrkfPSQjK5U6-srU37GY3Ync54hYog5WfFCXH5haU1Wv_DgmcvDjHvoP1X2UpYJxrKKHSYTI-xQI3iyYSk9lFxjGcfHuRdaQmXiWGvxlsGg2SUCw",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik16UkRSakU1UVRrd1JEQXhOVU15UlRnMVFrRTNNemRCUmpZNE5rRTFOamRCTjBZd1FrWkdOQSJ9.eyJlbWFpbCI6ImJlbmt1bnR6QGdteC5uZXQiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOi8vZ3JpZHguZXUuYXV0aDAuY29tLyIsImF1ZCI6Im9acHI5MzRJa244T1pPSFRKRWNyZ1hramlvMEkwUTdiIiwiaWF0IjoxNzEzNDY1MzIzLCJleHAiOjE3MTM1MDEzMjMsInN1YiI6ImF1dGgwfDYzZWJiYzlkNTI0NmQ5MTBlZjJjMGRiMSJ9.Sox2ftNHzQ-P7FRRFAhdLWz5RrSdfCVp1EnAgCQGMyTFq9DKNyAyTbAgUoSnLSwoofXPDit_QBKrIGblSHLOHOH0-KUmCQHL-4PajbFDVUlUe-57J7s5_OeoHUcN0Hlj_9ypbiJ0oVknoojMIgmXjJOBHNiwel8VxWAq83sn9B2Ve2E_JxqENbydkuhy15CqNg3zL8FOORxkAduJ5LwanCohutHlpXj6lH_DVYqIUUjqlB32keAPvBV5kGagOPnrohwQxnFuZS6I5MoT4Foid15hDMMf9Z8wupu8OUzIopQZ1deWr1lC9twdui6BrLP4KFxvneyfTHoi5j1NYqap5Q",
"scope": "email openid",
"expires_in": 86400,
"token_type": "Bearer"
}
""";
private static final String GATEWAY_RESPONSE = """
[
{
"additionalIdentifiers": [
{
"identifier": "c8e78ebba2abcdefabcdef819a98eefade9e505",
"service": "EEBUS",
"type": "SKI"
}
],
"applianceComposition": [
"EVSTATION",
"GRID",
"HEAT_PUMP",
"HYBRID"
],
"connectionStatus": {
"contactedAt": "2024-04-18T18:37:27Z",
"status": "AVAILABLE"
},
"createdAt": "2023-02-14T16:54:45Z",
"debugModeUntil": "2023-02-17T16:54:45Z",
"id": "973abcdef-3f18-44b2-aed2-8a81068e1e75",
"internalDeviceID": "29a203a45b4cbabcdefg5f6adac7170cdf2e7908e1ccb812538bff07412353",
"manufacturer": "gridX",
"model": "4.50P-X",
"registeredAt": "2023-02-14T16:54:45Z",
"scanners": [
"SMA_INVERTER_IGMP_HOST_DISCOVERY",
"SMA_INVERTER_ARP_HOST_DISCOVERY",
"SMA_METER",
"BCONTROL_METER",
"SOLAREDGE_INVERTER_METER_MODBUS_TCP",
"SOLAREDGE_INVERTER_METER_MODBUS_RTU",
"SOLARLOG_MONITOR",
"KEBA_CHARGING_STATION",
"KOSTAL_INVERTER_PLENTICORE",
"EEBUS_GENERIC",
"ALFEN_NG9XX_MODBUS_CHARGING_STATION",
"MY_PV_AC_THOR_HEATER",
"BENDER_CHARGING_STATION",
"HEIDELBERG_ENERGY_CONTROL_MODBUS_RTU_CHARGING_STATION",
"RUTENBECK_TCR_IP4_IO_DEVICE"
],
"serialnumber": "G505F88A-450-000-001-182-P-X",
"startcode": "4C2FE51B9EF",
"system": {
"createdAt": "2023-02-14T16:53:52Z",
"id": "09abb340-7739-494b-afcc-fbffecbe7ccc",
"metadata": {
"energy": {
"curtailment": -0.5,
"ems": {
"agreedDynamicPVControlTerms": true,
"agreedEMSTerms": true,
"agreedForecastBasedEMSTerms": false,
"agreedPriorityConfigurationTerms": true,
"enabledDynamicPVControl": true,
"enabledEMS": true,
"enabledForecastBasedEMS": true,
"enabledPriorityConfiguration": true
},
"heatingSystem": "Sonstige",
"installer": "MyInstaller",
"norminalPower": 12000
},
"energySupplier": {
"baseFee": 10,
"expectedConsumption": 9000,
"feedInTariff": 8,
"type": "OTHER",
"unitPrice": 35
},
"wizard": {
"step": "DONE"
}
},
"name": "Test User",
"operatingSince": "2023-02-14T16:55:51Z",
"solution": "HOME",
"updatedAt": "2024-04-07T11:39:06Z"
},
"type": "PHYSICAL",
"updatedAt": "2023-02-14T16:54:45Z",
"vendorID": "ae7c5770-df86-4b4c-8888-293sdfsd0531"
}
]
""";
private static final String LIVE_DATA_RESPONSE = """
{
"batteries": [
{
"applianceID": "xxxxxxx-xxxxx-xxxxx-xxxxx-xxxxxx",
"capacity": 10000,
"nominalCapacity": 10000,
"power": 266,
"remainingCharge": 7800,
"stateOfCharge": 0.78
}
],
"battery": {
"capacity": 10000,
"nominalCapacity": 10000,
"power": 266,
"remainingCharge": 7800,
"stateOfCharge": 0.78
},
"consumption": 581,
"directConsumption": 311,
"directConsumptionEV": 0,
"directConsumptionHeatPump": 0,
"directConsumptionHeater": 0,
"directConsumptionHousehold": 311,
"directConsumptionRate": 1,
"evChargingStation": {
"power": 0
},
"evChargingStations": [
{
"applianceID": "xxxx-xxxxxx-xxxxxx-xxxxx-xxxxx",
"currentL1": 0,
"currentL2": 0,
"currentL3": 0,
"plugState": "PLUGGED_ON_STATION",
"power": 0,
"readingTotal": 4394
}
],
"grid": 4,
"gridMeterReadingNegative": 28015560000,
"gridMeterReadingPositive": 48789000000,
"heatPump": 0,
"heatPumps": [
{
"applianceID": "xxxx-xxxxxx-xxxxxx-xxxxx-xxxxx",
"power": 0
}
],
"measuredAt": "2024-05-04T17:26:01Z",
"photovoltaic": 311,
"production": 311,
"selfConsumption": 311,
"selfConsumptionRate": 1,
"selfSufficiencyRate": 0.9931153184165232,
"selfSupply": 577,
"totalConsumption": 581
}
""";
private static final String EMPTY_LIVE_DATA_RESPONSE = """
{
}
""";
@Mock
@NonNullByDefault({})
private HttpResponse<String> response;
@Spy
@NonNullByDefault({})
private HttpClient httpClient;
@Test
public void testParseIdToken() {
@SuppressWarnings("null")
Optional<String> idTokenValue = GridBoxApi.parseIdTokenValue(AUTH_RESPONSE);
assertTrue(idTokenValue.isPresent());
assertEquals(
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik16UkRSakU1UVRrd1JEQXhOVU15UlRnMVFrRTNNemRCUmpZNE5rRTFOamRCTjBZd1FrWkdOQSJ9.eyJlbWFpbCI6ImJlbmt1bnR6QGdteC5uZXQiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOi8vZ3JpZHguZXUuYXV0aDAuY29tLyIsImF1ZCI6Im9acHI5MzRJa244T1pPSFRKRWNyZ1hramlvMEkwUTdiIiwiaWF0IjoxNzEzNDY1MzIzLCJleHAiOjE3MTM1MDEzMjMsInN1YiI6ImF1dGgwfDYzZWJiYzlkNTI0NmQ5MTBlZjJjMGRiMSJ9.Sox2ftNHzQ-P7FRRFAhdLWz5RrSdfCVp1EnAgCQGMyTFq9DKNyAyTbAgUoSnLSwoofXPDit_QBKrIGblSHLOHOH0-KUmCQHL-4PajbFDVUlUe-57J7s5_OeoHUcN0Hlj_9ypbiJ0oVknoojMIgmXjJOBHNiwel8VxWAq83sn9B2Ve2E_JxqENbydkuhy15CqNg3zL8FOORxkAduJ5LwanCohutHlpXj6lH_DVYqIUUjqlB32keAPvBV5kGagOPnrohwQxnFuZS6I5MoT4Foid15hDMMf9Z8wupu8OUzIopQZ1deWr1lC9twdui6BrLP4KFxvneyfTHoi5j1NYqap5Q",
idTokenValue.get());
}
@Test
public void testParseInvalidIdToken() {
@SuppressWarnings("null")
Optional<String> idTokenValue = GridBoxApi.parseIdTokenValue(AUTH_RESPONSE.replace('_', 'x'));
assertFalse(idTokenValue.isPresent());
Optional<String> idTokenValue2 = GridBoxApi.parseIdTokenValue("_");
assertFalse(idTokenValue2.isPresent());
}
@Test
public void testParseSystemId() {
@SuppressWarnings("null")
Optional<String> systemIdValue = GridBoxApi.parseSystemIdValue(GATEWAY_RESPONSE);
assertTrue(systemIdValue.isPresent());
assertEquals("09abb340-7739-494b-afcc-fbffecbe7ccc", systemIdValue.get());
}
@Test
public void testParseInvalidSystemId() {
@SuppressWarnings("null")
Optional<String> systemIdValue = GridBoxApi.parseSystemIdValue(GATEWAY_RESPONSE.replace("system", "systemxxx"));
assertFalse(systemIdValue.isPresent());
Optional<String> systemIdValue2 = GridBoxApi.parseSystemIdValue("_");
assertFalse(systemIdValue2.isPresent());
}
@Test
public void testGetIdToken() throws IOException, InterruptedException, GridBoxApiAuthenticationException {
@SuppressWarnings("unchecked")
Class<BodyHandler<String>> clazz = (Class<BodyHandler<String>>) HttpResponse.BodyHandlers.ofString().getClass();
when(httpClient.send(any(), any(clazz))).thenReturn(response);
GridBoxApi api = new GridBoxApi(httpClient);
GridBoxConfiguration config = new GridBoxConfiguration();
prepareResponse(200, AUTH_RESPONSE);
String idToken = api.getIdToken(config);
assertEquals(
"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik16UkRSakU1UVRrd1JEQXhOVU15UlRnMVFrRTNNemRCUmpZNE5rRTFOamRCTjBZd1FrWkdOQSJ9.eyJlbWFpbCI6ImJlbmt1bnR6QGdteC5uZXQiLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOi8vZ3JpZHguZXUuYXV0aDAuY29tLyIsImF1ZCI6Im9acHI5MzRJa244T1pPSFRKRWNyZ1hramlvMEkwUTdiIiwiaWF0IjoxNzEzNDY1MzIzLCJleHAiOjE3MTM1MDEzMjMsInN1YiI6ImF1dGgwfDYzZWJiYzlkNTI0NmQ5MTBlZjJjMGRiMSJ9.Sox2ftNHzQ-P7FRRFAhdLWz5RrSdfCVp1EnAgCQGMyTFq9DKNyAyTbAgUoSnLSwoofXPDit_QBKrIGblSHLOHOH0-KUmCQHL-4PajbFDVUlUe-57J7s5_OeoHUcN0Hlj_9ypbiJ0oVknoojMIgmXjJOBHNiwel8VxWAq83sn9B2Ve2E_JxqENbydkuhy15CqNg3zL8FOORxkAduJ5LwanCohutHlpXj6lH_DVYqIUUjqlB32keAPvBV5kGagOPnrohwQxnFuZS6I5MoT4Foid15hDMMf9Z8wupu8OUzIopQZ1deWr1lC9twdui6BrLP4KFxvneyfTHoi5j1NYqap5Q",
idToken);
prepareResponse(200, " ");
assertThrows(GridBoxApiAuthenticationException.class, () -> api.getIdToken(config));
prepareResponse(404, null);
assertThrows(GridBoxApiAuthenticationException.class, () -> api.getIdToken(config));
when(httpClient.send(any(), any(clazz))).thenThrow(HttpTimeoutException.class);
assertThrows(HttpTimeoutException.class, () -> api.getIdToken(config));
}
@Test
public void testGetSystemId()
throws IOException, InterruptedException, GridBoxApiAuthenticationException, GridBoxApiException {
@SuppressWarnings("unchecked")
Class<BodyHandler<String>> clazz = (Class<BodyHandler<String>>) HttpResponse.BodyHandlers.ofString().getClass();
when(httpClient.send(any(), any(clazz))).thenReturn(response);
GridBoxApi api = new GridBoxApi(httpClient);
GridBoxConfiguration config = new GridBoxConfiguration();
prepareResponse(200, GATEWAY_RESPONSE);
String systemId = api.getSystemId(config);
assertEquals("09abb340-7739-494b-afcc-fbffecbe7ccc", systemId);
prepareResponse(200, " ");
assertThrows(GridBoxApiException.class, () -> api.getSystemId(config));
prepareResponse(403, null);
assertThrows(GridBoxApiAuthenticationException.class, () -> api.getSystemId(config));
when(httpClient.send(any(), any(clazz))).thenThrow(HttpTimeoutException.class);
assertThrows(HttpTimeoutException.class, () -> api.getSystemId(config));
}
@Test
public void testRetrieveLiveData() throws IOException, InterruptedException, GridBoxApiAuthenticationException,
GridBoxApiException, GridBoxApiSystemNotFoundException {
@SuppressWarnings("unchecked")
Class<BodyHandler<String>> clazz = (Class<BodyHandler<String>>) HttpResponse.BodyHandlers.ofString().getClass();
when(httpClient.send(any(), any(clazz))).thenReturn(response);
GridBoxApi api = new GridBoxApi(httpClient);
GridBoxConfiguration config = new GridBoxConfiguration();
prepareResponse(200, LIVE_DATA_RESPONSE);
api.retrieveLiveData(config, d -> {
assertNotNull(d);
assertEquals(311, d.getSelfConsumption());
});
// check that empty responses (all values zero) are ignored
prepareResponse(200, EMPTY_LIVE_DATA_RESPONSE);
api.retrieveLiveData(config, d -> {
// make sure that the responseHandler is not called
fail();
});
Consumer<LiveData> doNothing = d -> {
};
prepareResponse(200, LIVE_DATA_RESPONSE.replace('{', '<'));
assertThrows(GridBoxApiException.class, () -> api.retrieveLiveData(config, doNothing));
prepareResponse(404, null);
assertThrows(GridBoxApiSystemNotFoundException.class, () -> api.retrieveLiveData(config, doNothing));
prepareResponse(403, null);
assertThrows(GridBoxApiAuthenticationException.class, () -> api.retrieveLiveData(config, doNothing));
when(httpClient.send(any(), any(clazz))).thenThrow(HttpTimeoutException.class);
assertThrows(HttpTimeoutException.class, () -> api.retrieveLiveData(config, doNothing));
}
private void prepareResponse(int statusCode, @Nullable String body) {
when(response.statusCode()).thenReturn(statusCode);
when(response.body()).thenReturn(body);
}
}

View File

@ -163,6 +163,7 @@
<module>org.openhab.binding.globalcache</module>
<module>org.openhab.binding.gpstracker</module>
<module>org.openhab.binding.gree</module>
<module>org.openhab.binding.gridbox</module>
<module>org.openhab.binding.groheondus</module>
<module>org.openhab.binding.groupepsa</module>
<module>org.openhab.binding.growatt</module>