[goecharger] Add cloud API support (#18781)

* [goecharger] add go-e charger cloud api support (#18770)

Signed-off-by: Stefan Fussenegger <stefan.fussenegger+git@gmail.com>
pull/18810/head
Stefan Fussenegger 2025-06-16 21:37:01 +02:00 committed by GitHub
parent 3e24c2c978
commit 75b5db9928
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 1513 additions and 37 deletions

View File

@ -15,15 +15,19 @@ Please note that v1 is the default, but more functions (channels) are supported
## Thing Configuration ## Thing Configuration
The thing has three configuration parameters: The thing has these configuration parameters:
| Parameter | Description | Required | | Parameter | Description | Required |
|-----------------|-----------------------------------------------|----------| |-----------------|------------------------------------------------|----------|
| ip | The IP-address of your go-e Charger | yes | | ip | The IP-address of your go-e Charger | yes* |
| apiVersion | The API version to use (1=default or 2) | no | | serial | The serial number of the Go-eCharger | yes* |
| refreshInterval | Interval to read data, default 5 (in seconds) | no | | token | The access token for the Go-eCharger Cloud API | yes* |
| apiVersion | The API version to use (1=default or 2) | no |
| refreshInterval | Interval to read data, default 5 (in seconds) | no |
The apiVersion 2 is only available for go-e Charger with new hardware revision (CM-03, GM-10 and potentially others), which can be recognized with the serial number on the back of the device. *) Configure ip for the Local API or serial and token for Cloud API. If both are configured the local API will be used.
The apiVersion 2 is only available for go-e Charger with new hardware revision (CM-03, GM-10 and potentially others), which can be recognized with the serial number on the back of the device. It's also mandatory for use with the Go-eCharger Cloud API.
## Channels ## Channels

View File

@ -65,4 +65,7 @@ public class GoEChargerBindingConstants {
public static final String API_URL_V2 = "http://%IP%/api/status"; public static final String API_URL_V2 = "http://%IP%/api/status";
public static final String SET_URL_V2 = "http://%IP%/api/set?%KEY%=%VALUE%"; public static final String SET_URL_V2 = "http://%IP%/api/set?%KEY%=%VALUE%";
public static final String API_URL_CLOUD_V2 = "https://%SERIAL%.api.v3.go-e.io/api/status?token=%TOKEN%";
public static final String SET_URL_CLOUD_V2 = "https://%SERIAL%.api.v3.go-e.io/api/set?token=%TOKEN%&%KEY%=%VALUE%";
} }

View File

@ -25,6 +25,8 @@ import org.eclipse.jdt.annotation.Nullable;
public class GoEChargerConfiguration { public class GoEChargerConfiguration {
public @Nullable String ip; public @Nullable String ip;
public @Nullable String serial;
public @Nullable String token;
public Integer refreshInterval = 5; public Integer refreshInterval = 5;
public Integer apiVersion = 1; public Integer apiVersion = 1;
} }

View File

@ -12,6 +12,8 @@
*/ */
package org.openhab.binding.goecharger.internal.api; package org.openhab.binding.goecharger.internal.api;
import static org.openhab.binding.goecharger.internal.api.GoEStatusV2ApiKeys.*;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
/** /**
@ -21,21 +23,21 @@ import com.google.gson.annotations.SerializedName;
* @author Reinhard Plaim - Initial contribution * @author Reinhard Plaim - Initial contribution
*/ */
public class GoEStatusResponseBaseDTO { public class GoEStatusResponseBaseDTO {
@SerializedName("car") @SerializedName(CAR)
public Integer pwmSignal; public Integer pwmSignal;
@SerializedName("amp") @SerializedName(AMP)
public Integer maxCurrent; public Integer maxCurrent;
@SerializedName("err") @SerializedName(ERR)
public Integer errorCode; public Integer errorCode;
@SerializedName("cbl") @SerializedName(CBL)
public Integer cableEncoding; public Integer cableEncoding;
@SerializedName("eto") @SerializedName(ETO)
public Long totalChargeConsumption; public Long totalChargeConsumption;
@SerializedName("fwv") @SerializedName(FWV)
public String firmware; public String firmware;
} }

View File

@ -12,6 +12,8 @@
*/ */
package org.openhab.binding.goecharger.internal.api; package org.openhab.binding.goecharger.internal.api;
import static org.openhab.binding.goecharger.internal.api.GoEStatusV2ApiKeys.*;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
/** /**
@ -21,33 +23,35 @@ import com.google.gson.annotations.SerializedName;
* @author Reinhard Plaim - Initial contribution * @author Reinhard Plaim - Initial contribution
*/ */
public class GoEStatusResponseV2DTO extends GoEStatusResponseBaseDTO { public class GoEStatusResponseV2DTO extends GoEStatusResponseBaseDTO {
@Deprecated
@SerializedName("mod") @SerializedName("mod")
public String version; public String version;
@SerializedName("psm") @SerializedName(PSM)
public Integer phases; public Integer phases;
@SerializedName("trx") @SerializedName(TRX)
public Integer transaction; public Integer transaction;
@SerializedName("alw") @SerializedName(ALW)
public Boolean allowCharging; public Boolean allowCharging;
@SerializedName("tma") @SerializedName(TMA)
public Double[] temperatures; public Double[] temperatures;
@SerializedName("wh") @SerializedName(WH)
public Double sessionChargeConsumption; public Double sessionChargeConsumption;
@SerializedName("dwo") @SerializedName(DWO)
public Double sessionChargeConsumptionLimit; public Double sessionChargeConsumptionLimit;
@SerializedName("frc") @SerializedName(FRC)
public Integer forceState; public Integer forceState;
@SerializedName("nrg") @SerializedName(NRG)
public Double[] energy; public Double[] energy;
@SerializedName("awp") @SerializedName(AWP)
public Double awattarMaxPrice; public Double awattarMaxPrice;
} }

View File

@ -33,6 +33,7 @@ import org.openhab.core.library.unit.Units;
import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing; import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatus;
import org.openhab.core.thing.ThingStatusDetail;
import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.thing.binding.BaseThingHandler;
import org.openhab.core.types.Command; import org.openhab.core.types.Command;
import org.openhab.core.types.State; import org.openhab.core.types.State;
@ -110,8 +111,8 @@ public abstract class GoEChargerBaseHandler extends BaseThingHandler {
} }
@Nullable @Nullable
protected GoEStatusResponseBaseDTO getGoEData() protected GoEStatusResponseBaseDTO getGoEData() throws InterruptedException, TimeoutException, ExecutionException,
throws InterruptedException, TimeoutException, ExecutionException, JsonSyntaxException { JsonSyntaxException, IllegalArgumentException {
return null; return null;
} }
@ -129,6 +130,9 @@ public abstract class GoEChargerBaseHandler extends BaseThingHandler {
updateChannelsAndStatus(null, ie.getMessage()); updateChannelsAndStatus(null, ie.getMessage());
} catch (TimeoutException | ExecutionException | JsonSyntaxException e) { } catch (TimeoutException | ExecutionException | JsonSyntaxException e) {
updateChannelsAndStatus(null, e.getMessage()); updateChannelsAndStatus(null, e.getMessage());
} catch (IllegalArgumentException e) {
logger.debug("Invalid configuration getting data: {}", e.toString());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, e.getMessage());
} }
} }
} }

View File

@ -366,8 +366,8 @@ public class GoEChargerHandler extends GoEChargerBaseHandler {
*/ */
@Nullable @Nullable
@Override @Override
protected GoEStatusResponseBaseDTO getGoEData() protected GoEStatusResponseBaseDTO getGoEData() throws InterruptedException, TimeoutException, ExecutionException,
throws InterruptedException, TimeoutException, ExecutionException, JsonSyntaxException { JsonSyntaxException, IllegalArgumentException {
String urlStr = getReadUrl(); String urlStr = getReadUrl();
logger.trace("GET URL = {}", urlStr); logger.trace("GET URL = {}", urlStr);

View File

@ -148,6 +148,9 @@ public class GoEChargerV2Handler extends GoEChargerBaseHandler {
} }
return new DecimalType(goeResponse.awattarMaxPrice); return new DecimalType(goeResponse.awattarMaxPrice);
case ALLOW_CHARGING: case ALLOW_CHARGING:
if (goeResponse.allowCharging == null) {
return UnDefType.UNDEF;
}
return OnOffType.from(goeResponse.allowCharging); return OnOffType.from(goeResponse.allowCharging);
case TEMPERATURE_TYPE2_PORT: case TEMPERATURE_TYPE2_PORT:
// It was reported that the temperature is invalid when only one value is returned // It was reported that the temperature is invalid when only one value is returned
@ -306,7 +309,7 @@ public class GoEChargerV2Handler extends GoEChargerBaseHandler {
@Override @Override
public void initialize() { public void initialize() {
// only read needed parameters // only read needed parameters
filter = "?filter="; filter = "filter=";
var declaredFields = GoEStatusResponseV2DTO.class.getDeclaredFields(); var declaredFields = GoEStatusResponseV2DTO.class.getDeclaredFields();
var declaredFieldsBase = GoEStatusResponseV2DTO.class.getSuperclass().getDeclaredFields(); var declaredFieldsBase = GoEStatusResponseV2DTO.class.getSuperclass().getDeclaredFields();
@ -321,21 +324,43 @@ public class GoEChargerV2Handler extends GoEChargerBaseHandler {
super.initialize(); super.initialize();
} }
private String getReadUrl() { private String getReadUrl() throws IllegalArgumentException {
return GoEChargerBindingConstants.API_URL_V2.replace("%IP%", config.ip.toString()) + filter; if (config.ip != null) {
return GoEChargerBindingConstants.API_URL_V2.replace("%IP%", config.ip.toString()) + "?" + filter;
} else if (config.serial != null && config.token != null) {
return GoEChargerBindingConstants.API_URL_CLOUD_V2.replace("%SERIAL%", config.serial.toString())
.replace("%TOKEN%", config.token.toString()) + "&" + filter;
} else {
throw new IllegalArgumentException("either ip or token+serial must be configured");
}
} }
private String getWriteUrl(String key, String value) { private String getWriteUrl(String key, String value) throws IllegalArgumentException {
return GoEChargerBindingConstants.SET_URL_V2.replace("%IP%", config.ip.toString()).replace("%KEY%", key) if (config.ip != null) {
.replace("%VALUE%", value); return GoEChargerBindingConstants.SET_URL_V2.replace("%IP%", config.ip.toString()).replace("%KEY%", key)
.replace("%VALUE%", value);
} else if (config.serial != null && config.token != null) {
return GoEChargerBindingConstants.SET_URL_CLOUD_V2.replace("%SERIAL%", config.serial.toString())
.replace("%TOKEN%", config.token.toString()).replace("%KEY%", key).replace("%VALUE%", value);
} else {
throw new IllegalArgumentException("either ip or token+serial must be configured");
}
} }
private void sendData(String key, String value) { private void sendData(String key, String value) {
String urlStr = getWriteUrl(key, value); String urlStr;
logger.trace("POST URL = {}", urlStr); try {
urlStr = getWriteUrl(key, value);
} catch (IllegalArgumentException e) {
logger.debug("Invalid configuration writing data: {}", e.toString());
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, e.getMessage());
return;
}
HttpMethod httpMethod = HttpMethod.GET;
logger.trace("{} URL = {}", httpMethod, urlStr);
try { try {
HttpMethod httpMethod = HttpMethod.GET;
ContentResponse contentResponse = httpClient.newRequest(urlStr).method(httpMethod) ContentResponse contentResponse = httpClient.newRequest(urlStr).method(httpMethod)
.timeout(5, TimeUnit.SECONDS).send(); .timeout(5, TimeUnit.SECONDS).send();
String response = contentResponse.getContentAsString(); String response = contentResponse.getContentAsString();
@ -343,7 +368,7 @@ public class GoEChargerV2Handler extends GoEChargerBaseHandler {
logger.trace("{} Response: {}", httpMethod.toString(), response); logger.trace("{} Response: {}", httpMethod.toString(), response);
var statusCode = contentResponse.getStatus(); var statusCode = contentResponse.getStatus();
if (!(statusCode == 200 || statusCode == 204)) { if (!(statusCode == 200 || statusCode == 202 || statusCode == 204)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/unsuccessful.communication-error"); "@text/unsuccessful.communication-error");
logger.debug("Could not send data, Response {}, StatusCode: {}", response, statusCode); logger.debug("Could not send data, Response {}, StatusCode: {}", response, statusCode);

View File

@ -43,11 +43,19 @@
</properties> </properties>
<config-description> <config-description>
<parameter name="ip" type="text" required="true"> <parameter name="ip" type="text" required="false">
<label>IP Address</label> <label>IP Address</label>
<description>The IP address of the Go-eCharger</description> <description>The IP address of the Go-eCharger</description>
<context>network-address</context> <context>network-address</context>
</parameter> </parameter>
<parameter name="serial" type="text" required="false">
<label>Serial Number</label>
<description>The serial number of the Go-eCharger</description>
</parameter>
<parameter name="token" type="text" required="false">
<label>Cloud Access Token</label>
<description>The access token for the Go-eCharger Cloud API</description>
</parameter>
<parameter name="apiVersion" type="integer" required="false" min="1" max="2"> <parameter name="apiVersion" type="integer" required="false" min="1" max="2">
<label>API version</label> <label>API version</label>
<description>The API version of the Go-eCharger</description> <description>The API version of the Go-eCharger</description>