[goecharger] Add API V2 support (#12400)

* add API V2 support
* handle InterrupedException explicitly

Signed-off-by: Reinhard Plaim <reinhardplaim@gmail.com>
This commit is contained in:
Reinhard Plaim
2022-03-08 15:37:39 +01:00
committed by GitHub
parent 378a037df9
commit ba03fef082
12 changed files with 904 additions and 217 deletions

View File

@@ -31,6 +31,7 @@ public class GoEChargerBindingConstants {
// List of all Channel ids
public static final String MAX_CURRENT = "maxCurrent";
public static final String MAX_CURRENT_TEMPORARY = "maxCurrentTemporary";
public static final String ACCESS_CONFIGURATION = "accessConfiguration";
public static final String PWM_SIGNAL = "pwmSignal";
public static final String ERROR = "error";
@@ -43,10 +44,12 @@ public class GoEChargerBindingConstants {
public static final String POWER_L1 = "powerL1";
public static final String POWER_L2 = "powerL2";
public static final String POWER_L3 = "powerL3";
public static final String POWER_ALL = "powerAll";
public static final String ALLOW_CHARGING = "allowCharging";
public static final String CABLE_ENCODING = "cableCurrent";
public static final String PHASES = "phases";
public static final String TEMPERATURE = "temperature";
public static final String TEMPERATURE_TYPE2_PORT = "temperatureType2Port";
public static final String TEMPERATURE_CIRCUIT_BOARD = "temperature";
public static final String SESSION_CHARGE_CONSUMPTION = "sessionChargedEnergy";
public static final String SESSION_CHARGE_CONSUMPTION_LIMIT = "sessionChargeEnergyLimit";
public static final String TOTAL_CONSUMPTION = "totalChargedEnergy";
@@ -54,4 +57,10 @@ public class GoEChargerBindingConstants {
public static final String API_URL = "http://%IP%/status";
public static final String MQTT_URL = "http://%IP%/mqtt?payload=%KEY%=%VALUE%";
// API v2 only
public static final String FORCE_STATE = "forceState";
public static final String API_URL_V2 = "http://%IP%/api/status";
public static final String SET_URL_V2 = "http://%IP%/api/set?%KEY%=%VALUE%";
}

View File

@@ -19,10 +19,12 @@ import org.eclipse.jdt.annotation.Nullable;
* The {@link GoEChargerConfiguration} class contains fields mapping thing configuration parameters.
*
* @author Samuel Brucksch - Initial contribution
* @author Reinhard Plaim - Add apiVersion
*/
@NonNullByDefault
public class GoEChargerConfiguration {
public @Nullable String ip;
public Integer refreshInterval = 5;
public Integer apiVersion = 1;
}

View File

@@ -21,6 +21,7 @@ import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.goecharger.internal.handler.GoEChargerHandler;
import org.openhab.binding.goecharger.internal.handler.GoEChargerV2Handler;
import org.openhab.core.io.net.http.HttpClientFactory;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingTypeUID;
@@ -40,7 +41,6 @@ import org.osgi.service.component.annotations.Reference;
@NonNullByDefault
@Component(configurationPid = "binding.goecharger", service = ThingHandlerFactory.class)
public class GoEChargerHandlerFactory extends BaseThingHandlerFactory {
private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Collections.singleton(THING_TYPE_GOE);
private final HttpClient httpClient;
@@ -57,9 +57,15 @@ public class GoEChargerHandlerFactory extends BaseThingHandlerFactory {
@Override
protected @Nullable ThingHandler createHandler(Thing thing) {
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
var apiVersion = thing.getConfiguration().as(GoEChargerConfiguration.class).apiVersion;
if (THING_TYPE_GOE.equals(thingTypeUID)) {
return new GoEChargerHandler(thing, httpClient);
if (apiVersion == 1) {
return new GoEChargerHandler(thing, httpClient);
}
if (apiVersion == 2) {
return new GoEChargerV2Handler(thing, httpClient);
}
}
return null;

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2022 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.goecharger.internal.api;
import com.google.gson.annotations.SerializedName;
/**
* The {@link GoEStatusResponseBaseDTO} class represents a json response from the
* charger.
*
* @author Reinhard Plaim - Initial contribution
*/
public class GoEStatusResponseBaseDTO {
@SerializedName("car")
public Integer pwmSignal;
@SerializedName("amp")
public Integer maxCurrent;
@SerializedName("nrg")
public Integer[] energy;
@SerializedName("err")
public Integer errorCode;
@SerializedName("cbl")
public Integer cableEncoding;
@SerializedName("eto")
public Long totalChargeConsumption;
@SerializedName("fwv")
public String firmware;
}

View File

@@ -19,47 +19,30 @@ import com.google.gson.annotations.SerializedName;
* charger.
*
* @author Samuel Brucksch - Initial contribution
* @author Reinhard Plaim - move some properties to base DTO
*/
public class GoEStatusResponseDTO {
public class GoEStatusResponseDTO extends GoEStatusResponseBaseDTO {
@SerializedName("version")
public String version;
@SerializedName("car")
public Integer pwmSignal;
@SerializedName("ast")
public Integer accessConfiguration;
@SerializedName("amp")
public Integer maxCurrent;
@SerializedName("nrg")
public Integer[] energy;
@SerializedName("err")
public Integer errorCode;
@SerializedName("alw")
public Integer allowCharging;
@SerializedName("cbl")
public Integer cableEncoding;
@SerializedName("pha")
public Integer phases;
@SerializedName("ast")
public Integer accessConfiguration;
@SerializedName("alw")
public Integer allowCharging;
@SerializedName("tmp")
public Integer temperature;
@SerializedName("dws")
public Long sessionChargeConsumption;
@SerializedName("dwo")
public Integer sessionChargeConsumptionLimit;
@SerializedName("eto")
public Long totalChargeConsumption;
@SerializedName("dws")
public Long sessionChargeConsumption;
@SerializedName("fwv")
public String firmware;
@SerializedName("amx")
public Integer maxCurrentTemporary;
}

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2010-2022 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.goecharger.internal.api;
import com.google.gson.annotations.SerializedName;
/**
* The {@link GoEStatusResponseV2DTO} class represents a json response from the
* charger.
*
* @author Reinhard Plaim - Initial contribution
*/
public class GoEStatusResponseV2DTO extends GoEStatusResponseBaseDTO {
@SerializedName("mod")
public String version;
@SerializedName("psm")
public Integer phases;
@SerializedName("alw")
public Boolean allowCharging;
@SerializedName("tma")
public Double[] temperatures;
@SerializedName("wh")
public Long sessionChargeConsumption;
@SerializedName("dwo")
public Double sessionChargeConsumptionLimit;
@SerializedName("frc")
public Integer forceState;
}

View File

@@ -0,0 +1,172 @@
/**
* Copyright (c) 2010-2022 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.goecharger.internal.handler;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.openhab.binding.goecharger.internal.GoEChargerConfiguration;
import org.openhab.binding.goecharger.internal.api.GoEStatusResponseBaseDTO;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
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.binding.BaseThingHandler;
import org.openhab.core.types.Command;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
/**
* The {@link GoEChargerBaseHandler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Samuel Brucksch - Initial contribution
* @author Reinhard Plaim - Adapt to use API version 2
*/
@NonNullByDefault
public abstract class GoEChargerBaseHandler extends BaseThingHandler {
private final Logger logger = LoggerFactory.getLogger(GoEChargerBaseHandler.class);
protected @Nullable GoEChargerConfiguration config;
protected List<String> allChannels = new ArrayList<>();
protected final Gson gson = new Gson();
private @Nullable ScheduledFuture<?> refreshJob;
protected final HttpClient httpClient;
public GoEChargerBaseHandler(Thing thing, HttpClient httpClient) {
super(thing);
this.httpClient = httpClient;
}
protected State getValue(String channelId, GoEStatusResponseBaseDTO goeResponseBase) {
switch (channelId) {
case MAX_CURRENT:
if (goeResponseBase.maxCurrent == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>(goeResponseBase.maxCurrent, Units.AMPERE);
case CABLE_ENCODING:
if (goeResponseBase.cableEncoding == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>(goeResponseBase.cableEncoding, Units.AMPERE);
case FIRMWARE:
if (goeResponseBase.firmware == null) {
return UnDefType.UNDEF;
}
return new StringType(goeResponseBase.firmware);
case VOLTAGE_L1:
if (goeResponseBase.energy == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>(goeResponseBase.energy[0], Units.VOLT);
case VOLTAGE_L2:
if (goeResponseBase.energy == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>(goeResponseBase.energy[1], Units.VOLT);
case VOLTAGE_L3:
if (goeResponseBase.energy == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>(goeResponseBase.energy[2], Units.VOLT);
}
return UnDefType.UNDEF;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
}
@Override
public void initialize() {
config = getConfigAs(GoEChargerConfiguration.class);
allChannels = getThing().getChannels().stream().map(channel -> channel.getUID().getId())
.collect(Collectors.toList());
logger.debug("Number of channels found: {}", allChannels.size());
updateStatus(ThingStatus.UNKNOWN);
startAutomaticRefresh();
logger.debug("Finished initializing!");
}
@Nullable
protected GoEStatusResponseBaseDTO getGoEData()
throws InterruptedException, TimeoutException, ExecutionException, JsonSyntaxException {
return null;
}
protected void updateChannelsAndStatus(@Nullable GoEStatusResponseBaseDTO goeResponse, @Nullable String message) {
}
private void refresh() {
synchronized (this) {
// Request new GoE data
try {
GoEStatusResponseBaseDTO goeResponse = getGoEData();
updateChannelsAndStatus(goeResponse, null);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
updateChannelsAndStatus(null, ie.getMessage());
} catch (TimeoutException | ExecutionException e) {
updateChannelsAndStatus(null, e.getMessage());
}
}
}
private void startAutomaticRefresh() {
synchronized (this) {
if (refreshJob == null || refreshJob.isCancelled()) {
GoEChargerConfiguration config = getConfigAs(GoEChargerConfiguration.class);
int delay = config.refreshInterval.intValue();
logger.debug("Running refresh job with delay {} s", delay);
refreshJob = scheduler.scheduleWithFixedDelay(this::refresh, 0, delay, TimeUnit.SECONDS);
}
}
}
@Override
public void dispose() {
logger.debug("Disposing the Go-eCharger handler.");
final ScheduledFuture<?> refreshJob = this.refreshJob;
if (refreshJob != null && !refreshJob.isCancelled()) {
refreshJob.cancel(true);
this.refreshJob = null;
}
}
}

View File

@@ -12,48 +12,24 @@
*/
package org.openhab.binding.goecharger.internal.handler;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.ACCESS_CONFIGURATION;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.ALLOW_CHARGING;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.CABLE_ENCODING;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.CURRENT_L1;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.CURRENT_L2;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.CURRENT_L3;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.ERROR;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.FIRMWARE;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.MAX_CURRENT;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.PHASES;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.POWER_L1;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.POWER_L2;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.POWER_L3;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.PWM_SIGNAL;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.SESSION_CHARGE_CONSUMPTION;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.SESSION_CHARGE_CONSUMPTION_LIMIT;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.TEMPERATURE;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.TOTAL_CONSUMPTION;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.VOLTAGE_L1;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.VOLTAGE_L2;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.VOLTAGE_L3;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
import javax.measure.quantity.ElectricCurrent;
import javax.measure.quantity.Energy;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.goecharger.internal.GoEChargerBindingConstants;
import org.openhab.binding.goecharger.internal.GoEChargerConfiguration;
import org.openhab.binding.goecharger.internal.api.GoEStatusResponseBaseDTO;
import org.openhab.binding.goecharger.internal.api.GoEStatusResponseDTO;
import org.openhab.binding.goecharger.internal.api.GoEStatusResponseV2DTO;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
@@ -64,7 +40,6 @@ 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;
import org.openhab.core.types.State;
@@ -72,7 +47,6 @@ import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
/**
@@ -80,34 +54,31 @@ import com.google.gson.JsonSyntaxException;
* sent to one of the channels.
*
* @author Samuel Brucksch - Initial contribution
* @author Reinhard Plaim - Adapt to use API version 2
*/
@NonNullByDefault
public class GoEChargerHandler extends BaseThingHandler {
public class GoEChargerHandler extends GoEChargerBaseHandler {
private final Logger logger = LoggerFactory.getLogger(GoEChargerHandler.class);
private @Nullable GoEChargerConfiguration config;
private List<String> allChannels = new ArrayList<>();
private final Gson gson = new Gson();
private @Nullable ScheduledFuture<?> refreshJob;
private final HttpClient httpClient;
public GoEChargerHandler(Thing thing, HttpClient httpClient) {
super(thing);
this.httpClient = httpClient;
super(thing, httpClient);
}
private State getValue(String channelId, GoEStatusResponseDTO goeResponse) {
@Override
protected State getValue(String channelId, GoEStatusResponseBaseDTO goeResponseBase) {
var state = super.getValue(channelId, goeResponseBase);
if (state != UnDefType.UNDEF) {
return state;
}
var goeResponse = (GoEStatusResponseDTO) goeResponseBase;
switch (channelId) {
case MAX_CURRENT:
if (goeResponse.maxCurrent == null) {
case MAX_CURRENT_TEMPORARY:
if (goeResponse.maxCurrentTemporary == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>(goeResponse.maxCurrent, Units.AMPERE);
return new QuantityType<>(goeResponse.maxCurrentTemporary, Units.AMPERE);
case PWM_SIGNAL:
if (goeResponse.pwmSignal == null) {
return UnDefType.UNDEF;
@@ -178,11 +149,6 @@ public class GoEChargerHandler extends BaseThingHandler {
return UnDefType.UNDEF;
}
return goeResponse.allowCharging == 1 ? OnOffType.ON : OnOffType.OFF;
case CABLE_ENCODING:
if (goeResponse.cableEncoding == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>(goeResponse.cableEncoding, Units.AMPERE);
case PHASES:
if (goeResponse.energy == null) {
return UnDefType.UNDEF;
@@ -198,7 +164,7 @@ public class GoEChargerHandler extends BaseThingHandler {
count++;
}
return new DecimalType(count);
case TEMPERATURE:
case TEMPERATURE_CIRCUIT_BOARD:
if (goeResponse.temperature == null) {
return UnDefType.UNDEF;
}
@@ -220,26 +186,6 @@ public class GoEChargerHandler extends BaseThingHandler {
return UnDefType.UNDEF;
}
return new QuantityType<>((Double) (goeResponse.totalChargeConsumption / 10d), Units.KILOWATT_HOUR);
case FIRMWARE:
if (goeResponse.firmware == null) {
return UnDefType.UNDEF;
}
return new StringType(goeResponse.firmware);
case VOLTAGE_L1:
if (goeResponse.energy == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>(goeResponse.energy[0], Units.VOLT);
case VOLTAGE_L2:
if (goeResponse.energy == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>(goeResponse.energy[1], Units.VOLT);
case VOLTAGE_L3:
if (goeResponse.energy == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>(goeResponse.energy[2], Units.VOLT);
case CURRENT_L1:
if (goeResponse.energy == null) {
return UnDefType.UNDEF;
@@ -272,6 +218,11 @@ public class GoEChargerHandler extends BaseThingHandler {
return UnDefType.UNDEF;
}
return new QuantityType<>(goeResponse.energy[9] * 100, Units.WATT);
case POWER_ALL:
if (goeResponseBase.energy == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>(goeResponseBase.energy[11] * 10, Units.WATT);
}
return UnDefType.UNDEF;
}
@@ -286,6 +237,7 @@ public class GoEChargerHandler extends BaseThingHandler {
String key = null;
String value = null;
switch (channelUID.getId()) {
case MAX_CURRENT:
key = "amp";
@@ -295,13 +247,22 @@ public class GoEChargerHandler extends BaseThingHandler {
value = String.valueOf(((QuantityType<ElectricCurrent>) command).toUnit(Units.AMPERE).intValue());
}
break;
case MAX_CURRENT_TEMPORARY:
key = "amx";
if (command instanceof DecimalType) {
value = String.valueOf(((DecimalType) command).intValue());
} else if (command instanceof QuantityType<?>) {
value = String.valueOf(((QuantityType<ElectricCurrent>) command).toUnit(Units.AMPERE).intValue());
}
break;
case SESSION_CHARGE_CONSUMPTION_LIMIT:
key = "dwo";
var multiplier = 10;
if (command instanceof DecimalType) {
value = String.valueOf(((DecimalType) command).intValue() * 10);
value = String.valueOf(((DecimalType) command).intValue() * multiplier);
} else if (command instanceof QuantityType<?>) {
value = String
.valueOf(((QuantityType<Energy>) command).toUnit(Units.KILOWATT_HOUR).intValue() * 10);
value = String.valueOf(
((QuantityType<Energy>) command).toUnit(Units.KILOWATT_HOUR).intValue() * multiplier);
}
break;
case ALLOW_CHARGING:
@@ -330,8 +291,8 @@ public class GoEChargerHandler extends BaseThingHandler {
}
}
break;
default:
}
if (key != null && value != null) {
sendData(key, value);
} else {
@@ -341,96 +302,82 @@ public class GoEChargerHandler extends BaseThingHandler {
@Override
public void initialize() {
config = getConfigAs(GoEChargerConfiguration.class);
allChannels = getThing().getChannels().stream().map(channel -> channel.getUID().getId())
.collect(Collectors.toList());
updateStatus(ThingStatus.UNKNOWN);
startAutomaticRefresh();
logger.debug("Finished initializing!");
super.initialize();
}
private String getUrl(String type) {
return type.replace("%IP%", StringUtils.trimToEmpty(config.ip));
private String getReadUrl() {
return GoEChargerBindingConstants.API_URL.replace("%IP%", config.ip.toString());
}
private String getWriteUrl(String key, String value) {
return GoEChargerBindingConstants.MQTT_URL.replace("%IP%", config.ip.toString()).replace("%KEY%", key)
.replace("%VALUE%", value);
}
private void sendData(String key, String value) {
String urlStr = getUrl(GoEChargerBindingConstants.MQTT_URL).replace("%KEY%", key).replace("%VALUE%", value);
logger.debug("POST URL = {}", urlStr);
String urlStr = getWriteUrl(key, value);
logger.trace("GET URL = {}", urlStr);
try {
ContentResponse contentResponse = httpClient.newRequest(urlStr).method(HttpMethod.POST)
HttpMethod httpMethod = HttpMethod.GET;
ContentResponse contentResponse = httpClient.newRequest(urlStr).method(httpMethod)
.timeout(5, TimeUnit.SECONDS).send();
String response = contentResponse.getContentAsString();
logger.debug("POST Response: {}", response);
GoEStatusResponseDTO result = gson.fromJson(response, GoEStatusResponseDTO.class);
updateChannelsAndStatus(result, null);
} catch (InterruptedException | TimeoutException | ExecutionException | JsonSyntaxException e) {
updateChannelsAndStatus(null, e.getMessage());
logger.trace("{} Response: {}", httpMethod.toString(), response);
var statusCode = contentResponse.getStatus();
if (!(statusCode == 200 || statusCode == 204)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/unsuccessful.communication-error");
logger.debug("Could not send data, Response {}, StatusCode: {}", response, statusCode);
}
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ie.toString());
logger.debug("Could not send data: {}, {}", urlStr, ie.toString());
} catch (TimeoutException | ExecutionException | JsonSyntaxException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.toString());
logger.debug("Could not send data: {}, {}", urlStr, e.toString());
}
}
/**
* Request new data from Go-E charger
* Request new data from Go-eCharger
*
* @return the Go-E charger object mapping the JSON response or null in case of
* @return the Go-eCharger object mapping the JSON response or null in case of
* error
* @throws ExecutionException
* @throws TimeoutException
* @throws InterruptedException
*/
@Nullable
private GoEStatusResponseDTO getGoEData()
@Override
protected GoEStatusResponseBaseDTO getGoEData()
throws InterruptedException, TimeoutException, ExecutionException, JsonSyntaxException {
String urlStr = getUrl(GoEChargerBindingConstants.API_URL);
logger.debug("GET URL = {}", urlStr);
String urlStr = getReadUrl();
logger.trace("GET URL = {}", urlStr);
ContentResponse contentResponse = httpClient.newRequest(urlStr).method(HttpMethod.GET)
.timeout(5, TimeUnit.SECONDS).send();
String response = contentResponse.getContentAsString();
logger.debug("GET Response: {}", response);
return gson.fromJson(response, GoEStatusResponseDTO.class);
logger.trace("GET Response: {}", response);
if (config.apiVersion == 1) {
return gson.fromJson(response, GoEStatusResponseDTO.class);
}
return gson.fromJson(response, GoEStatusResponseV2DTO.class);
}
private void updateChannelsAndStatus(@Nullable GoEStatusResponseDTO goeResponse, @Nullable String message) {
@Override
protected void updateChannelsAndStatus(@Nullable GoEStatusResponseBaseDTO goeResponse, @Nullable String message) {
if (goeResponse == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message);
allChannels.forEach(channel -> updateState(channel, UnDefType.UNDEF));
} else {
updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
allChannels.forEach(channel -> updateState(channel, getValue(channel, goeResponse)));
}
}
private void refresh() {
// Request new GoE data
try {
GoEStatusResponseDTO goeResponse = getGoEData();
updateChannelsAndStatus(goeResponse, null);
} catch (InterruptedException | TimeoutException | ExecutionException e) {
updateChannelsAndStatus(null, e.getMessage());
}
}
private void startAutomaticRefresh() {
if (refreshJob == null || refreshJob.isCancelled()) {
GoEChargerConfiguration config = getConfigAs(GoEChargerConfiguration.class);
int delay = config.refreshInterval.intValue();
logger.debug("Running refresh job with delay {} s", delay);
refreshJob = scheduler.scheduleWithFixedDelay(this::refresh, 0, delay, TimeUnit.SECONDS);
}
}
@Override
public void dispose() {
logger.debug("Disposing the Go-E Charger handler.");
final ScheduledFuture<?> refreshJob = this.refreshJob;
if (refreshJob != null && !refreshJob.isCancelled()) {
refreshJob.cancel(true);
this.refreshJob = null;
allChannels.forEach(channel -> updateState(channel, getValue(channel, (GoEStatusResponseDTO) goeResponse)));
}
}
}

View File

@@ -0,0 +1,360 @@
/**
* Copyright (c) 2010-2022 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.goecharger.internal.handler;
import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import javax.measure.quantity.ElectricCurrent;
import javax.measure.quantity.Energy;
import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.http.HttpMethod;
import org.openhab.binding.goecharger.internal.GoEChargerBindingConstants;
import org.openhab.binding.goecharger.internal.api.GoEStatusResponseBaseDTO;
import org.openhab.binding.goecharger.internal.api.GoEStatusResponseDTO;
import org.openhab.binding.goecharger.internal.api.GoEStatusResponseV2DTO;
import org.openhab.core.library.types.DecimalType;
import org.openhab.core.library.types.OnOffType;
import org.openhab.core.library.types.QuantityType;
import org.openhab.core.library.types.StringType;
import org.openhab.core.library.unit.SIUnits;
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.types.Command;
import org.openhab.core.types.RefreshType;
import org.openhab.core.types.State;
import org.openhab.core.types.UnDefType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonSyntaxException;
import com.google.gson.annotations.SerializedName;
/**
* The {@link GoEChargerV2Handler} is responsible for handling commands, which are
* sent to one of the channels.
*
* @author Samuel Brucksch - Initial contribution
* @author Reinhard Plaim - Adapt to use API version 2
*/
@NonNullByDefault
public class GoEChargerV2Handler extends GoEChargerBaseHandler {
private final Logger logger = LoggerFactory.getLogger(GoEChargerV2Handler.class);
private String filter = "";
public GoEChargerV2Handler(Thing thing, HttpClient httpClient) {
super(thing, httpClient);
}
@Override
protected State getValue(String channelId, GoEStatusResponseBaseDTO goeResponseBase) {
var state = super.getValue(channelId, goeResponseBase);
if (state != UnDefType.UNDEF) {
return state;
}
var goeResponse = (GoEStatusResponseV2DTO) goeResponseBase;
switch (channelId) {
case PHASES:
if (goeResponse.phases == null) {
return UnDefType.UNDEF;
}
var phases = "1";
if (goeResponse.phases == 2) {
phases = "3";
}
return new DecimalType(phases);
case PWM_SIGNAL:
if (goeResponse.pwmSignal == null) {
return UnDefType.UNDEF;
}
String pwmSignal = null;
switch (goeResponse.pwmSignal) {
case 0:
pwmSignal = "UNKNOWN/ERROR";
case 1:
pwmSignal = "IDLE";
break;
case 2:
pwmSignal = "CHARGING";
break;
case 3:
pwmSignal = "WAITING_FOR_CAR";
break;
case 4:
pwmSignal = "COMPLETE";
break;
case 5:
pwmSignal = "ERROR";
default:
}
return new StringType(pwmSignal);
case ERROR:
if (goeResponse.errorCode == null) {
return UnDefType.UNDEF;
}
String error = null;
switch (goeResponse.errorCode) {
case 0:
error = "UNKNOWN/ERROR";
case 1:
error = "IDLE";
break;
case 2:
error = "CHARGING";
break;
case 3:
error = "WAITING_FOR_CAR";
break;
case 4:
error = "COMPLETE";
break;
case 5:
error = "ERROR";
default:
}
return new StringType(error);
case ALLOW_CHARGING:
return goeResponse.allowCharging == true ? OnOffType.ON : OnOffType.OFF;
case TEMPERATURE_TYPE2_PORT:
if (goeResponse.temperatures == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>(goeResponse.temperatures[0], SIUnits.CELSIUS);
case TEMPERATURE_CIRCUIT_BOARD:
if (goeResponse.temperatures == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>(goeResponse.temperatures[1], SIUnits.CELSIUS);
case SESSION_CHARGE_CONSUMPTION:
if (goeResponse.sessionChargeConsumption == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>((Double) (goeResponse.sessionChargeConsumption / 1000d), Units.KILOWATT_HOUR);
case SESSION_CHARGE_CONSUMPTION_LIMIT:
if (goeResponse.sessionChargeConsumptionLimit == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>((Double) (goeResponse.sessionChargeConsumptionLimit / 1000d),
Units.KILOWATT_HOUR);
case TOTAL_CONSUMPTION:
if (goeResponse.totalChargeConsumption == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>((Double) (goeResponse.totalChargeConsumption / 1000d), Units.KILOWATT_HOUR);
case CURRENT_L1:
if (goeResponse.energy == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>((Double) (goeResponse.energy[4] / 1000d), Units.AMPERE);
case CURRENT_L2:
if (goeResponse.energy == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>((Double) (goeResponse.energy[5] / 1000d), Units.AMPERE);
case CURRENT_L3:
if (goeResponse.energy == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>((Double) (goeResponse.energy[6] / 1000d), Units.AMPERE);
case POWER_L1:
if (goeResponse.energy == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>(goeResponse.energy[7] * 1000, Units.WATT);
case POWER_L2:
if (goeResponse.energy == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>(goeResponse.energy[8] * 1000, Units.WATT);
case POWER_L3:
if (goeResponse.energy == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>(goeResponse.energy[9] * 1000, Units.WATT);
case POWER_ALL:
if (goeResponse.energy == null) {
return UnDefType.UNDEF;
}
return new QuantityType<>(goeResponse.energy[11] * 1000, Units.WATT);
case FORCE_STATE:
if (goeResponse.forceState == null) {
return UnDefType.UNDEF;
}
return new DecimalType(goeResponse.forceState.toString());
}
return UnDefType.UNDEF;
}
@Override
public void handleCommand(ChannelUID channelUID, Command command) {
if (command instanceof RefreshType) {
// we can not update single channels and refresh is triggered automatically
// anyways
return;
}
String key = null;
String value = null;
switch (channelUID.getId()) {
case MAX_CURRENT:
key = "amp";
if (command instanceof DecimalType) {
value = String.valueOf(((DecimalType) command).intValue());
} else if (command instanceof QuantityType<?>) {
value = String.valueOf(((QuantityType<ElectricCurrent>) command).toUnit(Units.AMPERE).intValue());
}
break;
case SESSION_CHARGE_CONSUMPTION_LIMIT:
key = "dwo";
var multiplier = 1000;
if (command instanceof DecimalType) {
value = String.valueOf(((DecimalType) command).intValue() * multiplier);
} else if (command instanceof QuantityType<?>) {
value = String.valueOf(
((QuantityType<Energy>) command).toUnit(Units.KILOWATT_HOUR).intValue() * multiplier);
}
break;
case PHASES:
key = "psm";
if (command instanceof DecimalType) {
var phases = 1;
var help = (DecimalType) command;
if (help.intValue() == 3) {
// set value 2 for 3 phases
phases = 2;
}
value = String.valueOf(phases);
}
break;
case FORCE_STATE:
key = "frc";
if (command instanceof DecimalType) {
value = String.valueOf(((DecimalType) command).intValue());
}
}
if (key != null && value != null) {
sendData(key, value);
} else {
logger.warn("Could not update channel {} with key {} and value {}", channelUID.getId(), key, value);
}
}
@Override
public void initialize() {
// only read needed parameters
filter = "?filter=";
var declaredFields = GoEStatusResponseV2DTO.class.getDeclaredFields();
var declaredFieldsBase = GoEStatusResponseV2DTO.class.getSuperclass().getDeclaredFields();
for (var field : declaredFields) {
filter += field.getAnnotation(SerializedName.class).value() + ",";
}
for (var field : declaredFieldsBase) {
filter += field.getAnnotation(SerializedName.class).value() + ",";
}
filter = filter.substring(0, filter.length() - 1);
super.initialize();
}
private String getReadUrl() {
return GoEChargerBindingConstants.API_URL_V2.replace("%IP%", config.ip.toString()) + filter;
}
private String getWriteUrl(String key, String value) {
return GoEChargerBindingConstants.SET_URL_V2.replace("%IP%", config.ip.toString()).replace("%KEY%", key)
.replace("%VALUE%", value);
}
private void sendData(String key, String value) {
String urlStr = getWriteUrl(key, value);
logger.trace("POST URL = {}", urlStr);
try {
HttpMethod httpMethod = HttpMethod.GET;
ContentResponse contentResponse = httpClient.newRequest(urlStr).method(httpMethod)
.timeout(5, TimeUnit.SECONDS).send();
String response = contentResponse.getContentAsString();
logger.trace("{} Response: {}", httpMethod.toString(), response);
var statusCode = contentResponse.getStatus();
if (!(statusCode == 200 || statusCode == 204)) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
"@text/unsuccessful.communication-error");
logger.debug("Could not send data, Response {}, StatusCode: {}", response, statusCode);
}
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ie.toString());
logger.debug("Could not send data: {}, {}", urlStr, ie.toString());
} catch (TimeoutException | ExecutionException | JsonSyntaxException e) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.toString());
logger.debug("Could not send data: {}, {}", urlStr, e.toString());
}
}
/**
* Request new data from Go-eCharger
*
* @return the Go-eCharger object mapping the JSON response or null in case of
* error
* @throws ExecutionException
* @throws TimeoutException
* @throws InterruptedException
*/
@Nullable
@Override
protected GoEStatusResponseBaseDTO getGoEData()
throws InterruptedException, TimeoutException, ExecutionException, JsonSyntaxException {
String urlStr = getReadUrl();
logger.trace("GET URL = {}", urlStr);
ContentResponse contentResponse = httpClient.newRequest(urlStr).method(HttpMethod.GET)
.timeout(5, TimeUnit.SECONDS).send();
String response = contentResponse.getContentAsString();
logger.trace("GET Response: {}", response);
if (config.apiVersion == 1) {
return gson.fromJson(response, GoEStatusResponseDTO.class);
}
return gson.fromJson(response, GoEStatusResponseV2DTO.class);
}
protected void updateChannelsAndStatus(@Nullable GoEStatusResponseBaseDTO goeResponse, @Nullable String message) {
if (goeResponse == null) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message);
allChannels.forEach(channel -> updateState(channel, UnDefType.UNDEF));
} else {
updateStatus(ThingStatus.ONLINE);
allChannels
.forEach(channel -> updateState(channel, getValue(channel, (GoEStatusResponseV2DTO) goeResponse)));
}
}
}

View File

@@ -12,11 +12,21 @@ thing-type.goecharger.goe.description = Go-eCharger thing that represents the wa
thing-type.config.goecharger.goe.ip.label = IP Address
thing-type.config.goecharger.goe.ip.description = The IP address of the Go-eCharger
thing-type.config.goecharger.goe.apiVersion.label = API version
thing-type.config.goecharger.goe.apiVersion.description = The API version of the Go-eCharger
thing-type.config.goecharger.goe.refreshInterval.label = Refresh Interval
thing-type.config.goecharger.goe.refreshInterval.description = Refresh interval for acquiring data from Go-eCharger in seconds
# channel types
channel-type.goecharger.current.label = Maximum Current
channel-type.goecharger.current.description = Maximum current per phase allowed to use for charging
channel-type.goecharger.maxCurrTmp.label = Maximum Current Temporary
channel-type.goecharger.maxCurrTmp.description = Maximum current temporary (not written to EEPROM)
channel-type.goecharger.phs.label = Phases
channel-type.goecharger.phs.description = Amount of phases currently used for charging
channel-type.goecharger.fs.label = Force State
channel-type.goecharger.fs.description = Force state (Neutral=0, Off=1, On=2)
channel-type.goecharger.alw.label = Allow Charging
channel-type.goecharger.alw.description = If true charging is allowed
channel-type.goecharger.ast.label = Access Configuration
@@ -33,20 +43,16 @@ channel-type.goecharger.cl2.label = Current L2
channel-type.goecharger.cl2.description = Current on L2
channel-type.goecharger.cl3.label = Current L3
channel-type.goecharger.cl3.description = Current on L3
channel-type.goecharger.current.label = Maximum Current
channel-type.goecharger.current.description = Maximum current per phase allowed to use for charging
channel-type.goecharger.err.label = Error Code
channel-type.goecharger.err.description = Error code of Go-eCharger
channel-type.goecharger.err.state.option.NONE = None
channel-type.goecharger.err.state.option.RCCB = RCCB
channel-type.goecharger.err.state.option.NO_GROUND = No ground
channel-type.goecharger.err.state.option.INTERNAL = Internal
channel-type.goecharger.eto.label = Total Charged Energy
channel-type.goecharger.eto.description = Amount of energy that has been charged since installation
channel-type.goecharger.fmw.label = Firmware
channel-type.goecharger.fmw.description = Firmware Version
channel-type.goecharger.pha.label = Phases
channel-type.goecharger.pha.description = Amount of phases currently used for charging
channel-type.goecharger.scl.label = Current Session Charge Energy Limit
channel-type.goecharger.scl.description = Wallbox stops charging after defined value, deactivate with value 0
channel-type.goecharger.scs.label = Current Session Charged Energy
channel-type.goecharger.scs.description = Amount of energy that has been charged in this session
channel-type.goecharger.pl1.label = Power L1
channel-type.goecharger.pl1.description = Power on L1
channel-type.goecharger.pl2.label = Power L2
@@ -55,19 +61,16 @@ channel-type.goecharger.pl3.label = Power L3
channel-type.goecharger.pl3.description = Power on L3
channel-type.goecharger.pwm.label = PWM signal status
channel-type.goecharger.pwm.description = Pulse-width modulation signal status
channel-type.goecharger.pwm.state.option.READY_NO_CAR = Ready (no car)
channel-type.goecharger.pwm.state.option.CHARGING = Charging
channel-type.goecharger.pwm.state.option.WAITING_FOR_CAR = Waiting for car
channel-type.goecharger.pwm.state.option.CHARGING_DONE_CAR_CONNECTED = Charging done (car connected)
channel-type.goecharger.scl.label = Current Session Charge Energy Limit
channel-type.goecharger.scl.description = Wallbox stops charging after defined value, deactivate with value 0
channel-type.goecharger.scs.label = Current Session Charged Energy
channel-type.goecharger.scs.description = Amount of energy that has been charged in this session
channel-type.goecharger.tmp.label = Temperature
channel-type.goecharger.tmp.description = Temperature of the Go-eCharger
channel-type.goecharger.tmpT2p.label = Temperature type 2 port
channel-type.goecharger.tmpT2p.description = Temperature on the type 2 port of the Go-eCharger
channel-type.goecharger.tmp.label = Temperature circuit board
channel-type.goecharger.tmp.description = Temperature on the circuit board of the Go-eCharger
channel-type.goecharger.vl1.label = Voltage L1
channel-type.goecharger.vl1.description = Voltage on L1
channel-type.goecharger.vl2.label = Voltage L2
channel-type.goecharger.vl2.description = Voltage on L2
channel-type.goecharger.vl3.label = Voltage L3
channel-type.goecharger.vl3.description = Voltage on L3
# Others
unsuccessful.communication-error=Request response was unsuccessful

View File

@@ -10,6 +10,7 @@
<channels>
<channel id="maxCurrent" typeId="current"/>
<channel id="maxCurrentTemp" typeId="maxCurrTmp"/>
<channel id="pwmSignal" typeId="pwm"/>
<channel id="error" typeId="err"/>
<channel id="voltageL1" typeId="vl1"/>
@@ -21,15 +22,17 @@
<channel id="powerL1" typeId="pl1"/>
<channel id="powerL2" typeId="pl2"/>
<channel id="powerL3" typeId="pl3"/>
<channel id="phases" typeId="pha"/>
<channel id="sessionChargeEnergyLimit" typeId="scl"/>
<channel id="sessionChargedEnergy" typeId="scs"/>
<channel id="totalChargedEnergy" typeId="eto"/>
<channel id="allowCharging" typeId="alw"/>
<channel id="cableCurrent" typeId="cbl"/>
<channel id="temperature" typeId="tmp"/>
<channel id="firmware" typeId="fmw"/>
<channel id="accessConfiguration" typeId="ast"/>
<channel id="phases" typeId="pha"/>
<channel id="forceState" typeId="fs"/>
<channel id="sessionChargedEnergy" typeId="scs"/>
<channel id="sessionChargeEnergyLimit" typeId="scl"/>
<channel id="totalChargedEnergy" typeId="eto"/>
<channel id="temperatureType2Port" typeId="tmpT2p"/>
<channel id="temperature" typeId="tmp"/>
</channels>
<config-description>
@@ -38,6 +41,10 @@
<description>The IP address of the Go-eCharger</description>
<context>network-address</context>
</parameter>
<parameter name="apiVersion" type="integer" required="false" min="1" max="2">
<label>API version</label>
<description>The API version of the Go-eCharger</description>
</parameter>
<parameter name="refreshInterval" type="integer" required="false" unit="s">
<label>Refresh Interval</label>
<description>Refresh interval for acquiring data from Go-eCharger in seconds</description>
@@ -65,6 +72,12 @@
<description>Maximum current per phase allowed to use for charging</description>
<state pattern="%d %unit%" readOnly="false"/>
</channel-type>
<channel-type id="maxCurrTmp">
<item-type>Number:ElectricCurrent</item-type>
<label>Maximum Current Temporary</label>
<description>Maximum current temporary (not written to EEPROM)</description>
<state pattern="%d %unit%" readOnly="false"/>
</channel-type>
<channel-type id="pwm">
<item-type>String</item-type>
<label>PWM signal status</label>
@@ -149,6 +162,12 @@
<item-type>Number</item-type>
<label>Phases</label>
<description>Amount of phases currently used for charging</description>
<state readOnly="false"/>
</channel-type>
<channel-type id="fs">
<item-type>Number</item-type>
<label>Force state</label>
<description>Force state (Neutral=0, Off=1, On=2)</description>
<state readOnly="true"/>
</channel-type>
<channel-type id="scl">
@@ -181,11 +200,19 @@
<description>Specifies the max amps that can be charged with that cable</description>
<state pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="tmpT2p">
<item-type>Number:Temperature</item-type>
<label>Temperature type 2 port</label>
<description>Temperature of the Go-eCharger on the type 2 port</description>
<state pattern="%d %unit%" readOnly="true"/>
<category>Temperature</category>
</channel-type>
<channel-type id="tmp">
<item-type>Number:Temperature</item-type>
<label>Temperature</label>
<description>Temperature of the Go-eCharger</description>
<label>Temperature circuit board</label>
<description>Temperature of the Go-eCharger on circuit board</description>
<state pattern="%d %unit%" readOnly="true"/>
<category>Temperature</category>
</channel-type>
<channel-type id="fmw">
<item-type>String</item-type>