2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.goecharger.internal.handler;
15 import static org.openhab.binding.goecharger.internal.GoEChargerBindingConstants.*;
17 import java.util.concurrent.ExecutionException;
18 import java.util.concurrent.TimeUnit;
19 import java.util.concurrent.TimeoutException;
21 import javax.measure.quantity.ElectricCurrent;
22 import javax.measure.quantity.Energy;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.eclipse.jetty.client.HttpClient;
27 import org.eclipse.jetty.client.api.ContentResponse;
28 import org.eclipse.jetty.http.HttpMethod;
29 import org.openhab.binding.goecharger.internal.GoEChargerBindingConstants;
30 import org.openhab.binding.goecharger.internal.api.GoEStatusResponseBaseDTO;
31 import org.openhab.binding.goecharger.internal.api.GoEStatusResponseDTO;
32 import org.openhab.binding.goecharger.internal.api.GoEStatusResponseV2DTO;
33 import org.openhab.core.library.types.DecimalType;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.QuantityType;
36 import org.openhab.core.library.types.StringType;
37 import org.openhab.core.library.unit.SIUnits;
38 import org.openhab.core.library.unit.Units;
39 import org.openhab.core.thing.ChannelUID;
40 import org.openhab.core.thing.Thing;
41 import org.openhab.core.thing.ThingStatus;
42 import org.openhab.core.thing.ThingStatusDetail;
43 import org.openhab.core.types.Command;
44 import org.openhab.core.types.RefreshType;
45 import org.openhab.core.types.State;
46 import org.openhab.core.types.UnDefType;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
50 import com.google.gson.JsonSyntaxException;
53 * The {@link GoEChargerHandler} is responsible for handling commands, which are
54 * sent to one of the channels.
56 * @author Samuel Brucksch - Initial contribution
57 * @author Reinhard Plaim - Adapt to use API version 2
60 public class GoEChargerHandler extends GoEChargerBaseHandler {
62 private final Logger logger = LoggerFactory.getLogger(GoEChargerHandler.class);
64 public GoEChargerHandler(Thing thing, HttpClient httpClient) {
65 super(thing, httpClient);
69 protected State getValue(String channelId, GoEStatusResponseBaseDTO goeResponseBase) {
70 var state = super.getValue(channelId, goeResponseBase);
71 if (state != UnDefType.UNDEF) {
75 var goeResponse = (GoEStatusResponseDTO) goeResponseBase;
77 case MAX_CURRENT_TEMPORARY:
78 if (goeResponse.maxCurrentTemporary == null) {
79 return UnDefType.UNDEF;
81 return new QuantityType<>(goeResponse.maxCurrentTemporary, Units.AMPERE);
83 if (goeResponse.pwmSignal == null) {
84 return UnDefType.UNDEF;
86 String pwmSignal = null;
87 switch (goeResponse.pwmSignal) {
89 pwmSignal = "READY_NO_CAR";
92 pwmSignal = "CHARGING";
95 pwmSignal = "WAITING_FOR_CAR";
98 pwmSignal = "CHARGING_DONE_CAR_CONNECTED";
102 return new StringType(pwmSignal);
104 if (goeResponse.errorCode == null) {
105 return UnDefType.UNDEF;
108 switch (goeResponse.errorCode) {
125 return new StringType(error);
126 case ACCESS_CONFIGURATION:
127 if (goeResponse.accessConfiguration == null) {
128 return UnDefType.UNDEF;
130 String accessConfiguration = null;
131 switch (goeResponse.accessConfiguration) {
133 accessConfiguration = "OPEN";
136 accessConfiguration = "RFID";
139 accessConfiguration = "AWATTAR";
142 accessConfiguration = "TIMER";
146 return new StringType(accessConfiguration);
148 if (goeResponse.allowCharging == null) {
149 return UnDefType.UNDEF;
151 return OnOffType.from(goeResponse.allowCharging == 1);
153 if (goeResponse.energy == null) {
154 return UnDefType.UNDEF;
157 if (goeResponse.energy.length >= 5 && goeResponse.energy[4] > 0) { // current P1
160 if (goeResponse.energy.length >= 6 && goeResponse.energy[5] > 0) { // current P2
163 if (goeResponse.energy.length >= 7 && goeResponse.energy[6] > 0) { // current P3
166 return new DecimalType(count);
167 case TEMPERATURE_CIRCUIT_BOARD:
168 if (goeResponse.temperature == null) {
169 return UnDefType.UNDEF;
171 return new QuantityType<>(goeResponse.temperature, SIUnits.CELSIUS);
172 case SESSION_CHARGE_CONSUMPTION:
173 if (goeResponse.sessionChargeConsumption == null) {
174 return UnDefType.UNDEF;
176 return new QuantityType<>(goeResponse.sessionChargeConsumption / 360000d, Units.KILOWATT_HOUR);
177 case SESSION_CHARGE_CONSUMPTION_LIMIT:
178 if (goeResponse.sessionChargeConsumptionLimit == null) {
179 return UnDefType.UNDEF;
181 return new QuantityType<>(goeResponse.sessionChargeConsumptionLimit / 10d, Units.KILOWATT_HOUR);
182 case TOTAL_CONSUMPTION:
183 if (goeResponse.totalChargeConsumption == null) {
184 return UnDefType.UNDEF;
186 return new QuantityType<>(goeResponse.totalChargeConsumption / 10d, Units.KILOWATT_HOUR);
188 if (goeResponse.energy == null || goeResponse.energy.length < 5) {
189 return UnDefType.UNDEF;
191 // values come in as A*10, 41 means 4.1A
192 return new QuantityType<>(goeResponse.energy[4] / 10d, Units.AMPERE);
194 if (goeResponse.energy == null || goeResponse.energy.length < 6) {
195 return UnDefType.UNDEF;
197 return new QuantityType<>(goeResponse.energy[5] / 10d, Units.AMPERE);
199 if (goeResponse.energy == null || goeResponse.energy.length < 7) {
200 return UnDefType.UNDEF;
202 return new QuantityType<>(goeResponse.energy[6] / 10d, Units.AMPERE);
204 if (goeResponse.energy == null || goeResponse.energy.length < 8) {
205 return UnDefType.UNDEF;
207 // values come in as kW*10, 41 means 4.1kW
208 return new QuantityType<>(goeResponse.energy[7] * 100, Units.WATT);
210 if (goeResponse.energy == null || goeResponse.energy.length < 9) {
211 return UnDefType.UNDEF;
213 return new QuantityType<>(goeResponse.energy[8] * 100, Units.WATT);
215 if (goeResponse.energy == null || goeResponse.energy.length < 10) {
216 return UnDefType.UNDEF;
218 return new QuantityType<>(goeResponse.energy[9] * 100, Units.WATT);
220 if (goeResponse.energy == null || goeResponse.energy.length < 1) {
221 return UnDefType.UNDEF;
223 return new QuantityType<>(goeResponse.energy[0], Units.VOLT);
225 if (goeResponse.energy == null || goeResponse.energy.length < 2) {
226 return UnDefType.UNDEF;
228 return new QuantityType<>(goeResponse.energy[1], Units.VOLT);
230 if (goeResponse.energy == null || goeResponse.energy.length < 3) {
231 return UnDefType.UNDEF;
233 return new QuantityType<>(goeResponse.energy[2], Units.VOLT);
235 if (goeResponse.energy == null || goeResponse.energy.length < 12) {
236 return UnDefType.UNDEF;
238 return new QuantityType<>(goeResponse.energy[11] * 10, Units.WATT);
240 return UnDefType.UNDEF;
244 public void handleCommand(ChannelUID channelUID, Command command) {
245 if (command instanceof RefreshType) {
246 // we can not update single channels and refresh is triggered automatically
254 switch (channelUID.getId()) {
257 if (command instanceof DecimalType decimalCommand) {
258 value = String.valueOf(decimalCommand.intValue());
259 } else if (command instanceof QuantityType<?>) {
260 value = String.valueOf(((QuantityType<ElectricCurrent>) command).toUnit(Units.AMPERE).intValue());
263 case MAX_CURRENT_TEMPORARY:
265 if (command instanceof DecimalType decimalCommand) {
266 value = String.valueOf(decimalCommand.intValue());
267 } else if (command instanceof QuantityType<?>) {
268 value = String.valueOf(((QuantityType<ElectricCurrent>) command).toUnit(Units.AMPERE).intValue());
271 case SESSION_CHARGE_CONSUMPTION_LIMIT:
274 if (command instanceof DecimalType decimalCommand) {
275 value = String.valueOf(decimalCommand.intValue() * multiplier);
276 } else if (command instanceof QuantityType<?>) {
277 value = String.valueOf(
278 ((QuantityType<Energy>) command).toUnit(Units.KILOWATT_HOUR).intValue() * multiplier);
283 if (command instanceof OnOffType) {
284 value = command == OnOffType.ON ? "1" : "0";
287 case ACCESS_CONFIGURATION:
289 if (command instanceof StringType) {
290 switch (command.toString().toUpperCase()) {
309 if (key != null && value != null) {
310 sendData(key, value);
312 logger.warn("Could not update channel {} with key {} and value {}", channelUID.getId(), key, value);
317 public void initialize() {
321 private String getReadUrl() {
322 return GoEChargerBindingConstants.API_URL.replace("%IP%", config.ip.toString());
325 private String getWriteUrl(String key, String value) {
326 return GoEChargerBindingConstants.MQTT_URL.replace("%IP%", config.ip.toString()).replace("%KEY%", key)
327 .replace("%VALUE%", value);
330 private void sendData(String key, String value) {
331 String urlStr = getWriteUrl(key, value);
332 logger.trace("GET URL = {}", urlStr);
335 HttpMethod httpMethod = HttpMethod.GET;
336 ContentResponse contentResponse = httpClient.newRequest(urlStr).method(httpMethod)
337 .timeout(5, TimeUnit.SECONDS).send();
338 String response = contentResponse.getContentAsString();
340 logger.trace("{} Response: {}", httpMethod.toString(), response);
342 var statusCode = contentResponse.getStatus();
343 if (!(statusCode == 200 || statusCode == 204)) {
344 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
345 "@text/unsuccessful.communication-error");
346 logger.debug("Could not send data, Response {}, StatusCode: {}", response, statusCode);
348 } catch (InterruptedException ie) {
349 Thread.currentThread().interrupt();
350 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ie.toString());
351 logger.debug("Could not send data: {}, {}", urlStr, ie.toString());
352 } catch (TimeoutException | ExecutionException | JsonSyntaxException e) {
353 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.toString());
354 logger.debug("Could not send data: {}, {}", urlStr, e.toString());
359 * Request new data from Go-eCharger
361 * @return the Go-eCharger object mapping the JSON response or null in case of
363 * @throws ExecutionException
364 * @throws TimeoutException
365 * @throws InterruptedException
369 protected GoEStatusResponseBaseDTO getGoEData()
370 throws InterruptedException, TimeoutException, ExecutionException, JsonSyntaxException {
371 String urlStr = getReadUrl();
372 logger.trace("GET URL = {}", urlStr);
374 ContentResponse contentResponse = httpClient.newRequest(urlStr).method(HttpMethod.GET)
375 .timeout(5, TimeUnit.SECONDS).send();
377 String response = contentResponse.getContentAsString();
378 logger.trace("GET Response: {}", response);
380 if (config.apiVersion == 1) {
381 return gson.fromJson(response, GoEStatusResponseDTO.class);
383 return gson.fromJson(response, GoEStatusResponseV2DTO.class);
387 protected void updateChannelsAndStatus(@Nullable GoEStatusResponseBaseDTO goeResponse, @Nullable String message) {
388 if (goeResponse == null) {
389 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message);
390 allChannels.forEach(channel -> updateState(channel, UnDefType.UNDEF));
392 updateStatus(ThingStatus.ONLINE, ThingStatusDetail.NONE);
393 allChannels.forEach(channel -> updateState(channel, getValue(channel, goeResponse)));