mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-10 15:11:59 +01:00
[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:
parent
68fd415975
commit
9e75a4985d
@ -646,6 +646,11 @@
|
|||||||
<artifactId>org.openhab.binding.gree</artifactId>
|
<artifactId>org.openhab.binding.gree</artifactId>
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
|
<artifactId>org.openhab.binding.gridbox</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.openhab.addons.bundles</groupId>
|
<groupId>org.openhab.addons.bundles</groupId>
|
||||||
<artifactId>org.openhab.binding.groheondus</artifactId>
|
<artifactId>org.openhab.binding.groheondus</artifactId>
|
||||||
|
13
bundles/org.openhab.binding.gridbox/NOTICE
Normal file
13
bundles/org.openhab.binding.gridbox/NOTICE
Normal 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
|
89
bundles/org.openhab.binding.gridbox/README.md
Normal file
89
bundles/org.openhab.binding.gridbox/README.md
Normal 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"}
|
||||||
|
```
|
17
bundles/org.openhab.binding.gridbox/pom.xml
Normal file
17
bundles/org.openhab.binding.gridbox/pom.xml
Normal 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>
|
@ -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>
|
@ -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";
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
+ "]";
|
||||||
|
}
|
||||||
|
}
|
@ -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 + "]";
|
||||||
|
}
|
||||||
|
}
|
@ -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 + "]";
|
||||||
|
}
|
||||||
|
}
|
@ -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 + "]";
|
||||||
|
}
|
||||||
|
}
|
@ -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 + "]";
|
||||||
|
}
|
||||||
|
}
|
@ -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 + "]";
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
@ -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
|
@ -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>
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -163,6 +163,7 @@
|
|||||||
<module>org.openhab.binding.globalcache</module>
|
<module>org.openhab.binding.globalcache</module>
|
||||||
<module>org.openhab.binding.gpstracker</module>
|
<module>org.openhab.binding.gpstracker</module>
|
||||||
<module>org.openhab.binding.gree</module>
|
<module>org.openhab.binding.gree</module>
|
||||||
|
<module>org.openhab.binding.gridbox</module>
|
||||||
<module>org.openhab.binding.groheondus</module>
|
<module>org.openhab.binding.groheondus</module>
|
||||||
<module>org.openhab.binding.groupepsa</module>
|
<module>org.openhab.binding.groupepsa</module>
|
||||||
<module>org.openhab.binding.growatt</module>
|
<module>org.openhab.binding.growatt</module>
|
||||||
|
Loading…
Reference in New Issue
Block a user