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.keba.internal.handler;
15 import static org.openhab.binding.keba.internal.KebaBindingConstants.*;
17 import java.io.IOException;
18 import java.math.BigDecimal;
19 import java.net.InetSocketAddress;
20 import java.net.Socket;
21 import java.net.SocketAddress;
22 import java.nio.ByteBuffer;
24 import java.util.Map.Entry;
25 import java.util.Objects;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
29 import javax.measure.quantity.Dimensionless;
30 import javax.measure.quantity.ElectricCurrent;
31 import javax.measure.quantity.ElectricPotential;
32 import javax.measure.quantity.Energy;
33 import javax.measure.quantity.Power;
34 import javax.measure.quantity.Time;
36 import org.openhab.binding.keba.internal.KebaBindingConstants.KebaSeries;
37 import org.openhab.binding.keba.internal.KebaBindingConstants.KebaType;
38 import org.openhab.core.cache.ExpiringCacheMap;
39 import org.openhab.core.config.core.Configuration;
40 import org.openhab.core.library.types.DecimalType;
41 import org.openhab.core.library.types.IncreaseDecreaseType;
42 import org.openhab.core.library.types.OnOffType;
43 import org.openhab.core.library.types.QuantityType;
44 import org.openhab.core.library.types.StringType;
45 import org.openhab.core.library.unit.Units;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.thing.binding.BaseThingHandler;
51 import org.openhab.core.types.Command;
52 import org.openhab.core.types.RefreshType;
53 import org.openhab.core.types.State;
54 import org.openhab.core.util.StringUtils;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
58 import com.google.gson.JsonElement;
59 import com.google.gson.JsonObject;
60 import com.google.gson.JsonParseException;
61 import com.google.gson.JsonParser;
64 * The {@link KeContactHandler} is responsible for handling commands, which
65 * are sent to one of the channels.
67 * @author Karel Goderis - Initial contribution
69 public class KeContactHandler extends BaseThingHandler {
71 public static final String IP_ADDRESS = "ipAddress";
72 public static final String POLLING_REFRESH_INTERVAL = "refreshInterval";
73 public static final int POLLING_REFRESH_INTERVAL_DEFAULT = 15;
74 public static final int REPORT_INTERVAL = 3000;
75 public static final int BUFFER_SIZE = 1024;
76 public static final int REMOTE_PORT_NUMBER = 7090;
77 private static final String CACHE_REPORT_1 = "REPORT_1";
78 private static final String CACHE_REPORT_2 = "REPORT_2";
79 private static final String CACHE_REPORT_3 = "REPORT_3";
80 private static final String CACHE_REPORT_100 = "REPORT_100";
81 public static final int SOCKET_TIME_OUT_MS = 3000;
82 public static final int SOCKET_CHECK_PORT_NUMBER = 80;
84 private final Logger logger = LoggerFactory.getLogger(KeContactHandler.class);
86 private final KeContactTransceiver transceiver;
88 private ScheduledFuture<?> pollingJob;
89 private ExpiringCacheMap<String, ByteBuffer> cache;
91 private int maxPresetCurrent = 0;
92 private int maxSystemCurrent = 63000;
93 private KebaType type;
94 private KebaSeries series;
95 private int lastState = -1; // trigger a report100 at startup
96 private boolean isReport100needed = true;
98 public KeContactHandler(Thing thing, KeContactTransceiver transceiver) {
100 this.transceiver = transceiver;
104 public void initialize() {
106 if (isKebaReachable()) {
107 transceiver.registerHandler(this);
109 int refreshInterval = getRefreshInterval();
110 cache = new ExpiringCacheMap<>(Math.max(refreshInterval - 5, 0) * 1000);
112 cache.put(CACHE_REPORT_1, () -> transceiver.send("report 1", getHandler()));
113 cache.put(CACHE_REPORT_2, () -> transceiver.send("report 2", getHandler()));
114 cache.put(CACHE_REPORT_3, () -> transceiver.send("report 3", getHandler()));
115 cache.put(CACHE_REPORT_100, () -> transceiver.send("report 100", getHandler()));
117 if (pollingJob == null || pollingJob.isCancelled()) {
118 pollingJob = scheduler.scheduleWithFixedDelay(this::pollingRunnable, 0, refreshInterval,
122 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
123 "IP address or port number not set");
125 } catch (IOException e) {
126 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
127 "Exception during initialization of binding: " + e.toString());
131 private boolean isKebaReachable() throws IOException {
132 boolean isReachable = false;
133 SocketAddress sockAddr = new InetSocketAddress(getIPAddress(), SOCKET_CHECK_PORT_NUMBER);
134 Socket socket = new Socket();
136 socket.connect(sockAddr, SOCKET_TIME_OUT_MS);
141 logger.debug("isKebaReachable() returns {}", isReachable);
146 public void dispose() {
147 if (pollingJob != null && !pollingJob.isCancelled()) {
148 pollingJob.cancel(true);
152 transceiver.unRegisterHandler(this);
155 public String getIPAddress() {
156 return getConfig().get(IP_ADDRESS) != null ? (String) getConfig().get(IP_ADDRESS) : "";
159 public int getRefreshInterval() {
160 return getConfig().get(POLLING_REFRESH_INTERVAL) != null
161 ? ((BigDecimal) getConfig().get(POLLING_REFRESH_INTERVAL)).intValue()
162 : POLLING_REFRESH_INTERVAL_DEFAULT;
165 private KeContactHandler getHandler() {
170 public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, String description) {
171 super.updateStatus(status, statusDetail, description);
175 protected Configuration getConfig() {
176 return super.getConfig();
179 private void pollingRunnable() {
181 logger.debug("Running pollingRunnable to connect Keba wallbox");
182 long stamp = System.currentTimeMillis();
183 if (!isKebaReachable()) {
184 logger.debug("isKebaReachable() timed out after '{}' milliseconds", System.currentTimeMillis() - stamp);
185 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
186 "A timeout occurred while polling the charging station");
188 ByteBuffer response = cache.get(CACHE_REPORT_1);
189 if (response == null) {
190 logger.debug("Missing response from Keba station for 'report 1'");
195 Thread.sleep(REPORT_INTERVAL);
197 response = cache.get(CACHE_REPORT_2);
198 if (response == null) {
199 logger.debug("Missing response from Keba station for 'report 2'");
204 Thread.sleep(REPORT_INTERVAL);
206 response = cache.get(CACHE_REPORT_3);
207 if (response == null) {
208 logger.debug("Missing response from Keba station for 'report 3'");
213 if (isReport100needed) {
214 Thread.sleep(REPORT_INTERVAL);
216 response = cache.get(CACHE_REPORT_100);
217 if (response == null) {
218 logger.debug("Missing response from Keba station for 'report 100'");
222 isReport100needed = false;
225 } catch (IOException e) {
226 logger.debug("An error occurred while polling the KEBA KeContact '{}': {}", getThing().getUID(),
228 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
229 "An error occurred while polling the charging station");
230 } catch (InterruptedException e) {
231 logger.debug("Polling job has been interrupted for handler of thing '{}'.", getThing().getUID());
235 protected void onData(ByteBuffer byteBuffer) {
236 if (getThing().getStatus() != ThingStatus.ONLINE) {
237 updateStatus(ThingStatus.ONLINE);
240 String response = new String(byteBuffer.array(), 0, byteBuffer.limit());
241 response = Objects.requireNonNull(StringUtils.chomp(response));
243 if (response.contains("TCH-OK")) {
244 // ignore confirmation messages which are not JSON
249 JsonObject readObject = JsonParser.parseString(response).getAsJsonObject();
251 for (Entry<String, JsonElement> entry : readObject.entrySet()) {
252 switch (entry.getKey()) {
254 Map<String, String> properties = editProperties();
255 String product = entry.getValue().getAsString().trim();
256 properties.put(CHANNEL_MODEL, product);
257 updateProperties(properties);
258 if (product.contains("P20")) {
260 } else if (product.contains("P30")) {
263 series = KebaSeries.getSeries(product.substring(13, 14).charAt(0));
267 Map<String, String> properties = editProperties();
268 properties.put(CHANNEL_SERIAL, entry.getValue().getAsString());
269 updateProperties(properties);
273 Map<String, String> properties = editProperties();
274 properties.put(CHANNEL_FIRMWARE, entry.getValue().getAsString());
275 updateProperties(properties);
279 int state = entry.getValue().getAsInt();
282 updateState(CHANNEL_WALLBOX, OnOffType.OFF);
283 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
284 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
288 updateState(CHANNEL_WALLBOX, OnOffType.ON);
289 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
290 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
294 updateState(CHANNEL_WALLBOX, OnOffType.ON);
295 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
296 updateState(CHANNEL_PLUG_LOCKED, OnOffType.ON);
300 updateState(CHANNEL_WALLBOX, OnOffType.ON);
301 updateState(CHANNEL_VEHICLE, OnOffType.ON);
302 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
306 updateState(CHANNEL_WALLBOX, OnOffType.ON);
307 updateState(CHANNEL_VEHICLE, OnOffType.ON);
308 updateState(CHANNEL_PLUG_LOCKED, OnOffType.ON);
315 int state = entry.getValue().getAsInt();
316 State newState = new DecimalType(state);
317 updateState(CHANNEL_STATE, newState);
318 if (lastState != state) {
319 // the state is different from the last one, so we will trigger a report100
320 isReport100needed = true;
326 int state = entry.getValue().getAsInt();
329 updateState(CHANNEL_ENABLED, OnOffType.ON);
333 updateState(CHANNEL_ENABLED, OnOffType.OFF);
340 int state = entry.getValue().getAsInt();
341 maxSystemCurrent = state;
342 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
343 updateState(CHANNEL_MAX_SYSTEM_CURRENT, newState);
344 if (maxSystemCurrent != 0) {
345 if (maxSystemCurrent < maxPresetCurrent) {
346 transceiver.send("curr " + maxSystemCurrent, this);
347 updateState(CHANNEL_MAX_PRESET_CURRENT,
348 new QuantityType<ElectricCurrent>(maxSystemCurrent / 1000.0, Units.AMPERE));
349 updateState(CHANNEL_MAX_PRESET_CURRENT_RANGE, new QuantityType<Dimensionless>(
350 (maxSystemCurrent - 6000) * 100 / (maxSystemCurrent - 6000), Units.PERCENT));
353 logger.debug("maxSystemCurrent is 0. Ignoring.");
358 int state = entry.getValue().getAsInt();
359 maxPresetCurrent = state;
360 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
361 updateState(CHANNEL_MAX_PRESET_CURRENT, newState);
362 if (maxSystemCurrent != 0) {
363 updateState(CHANNEL_MAX_PRESET_CURRENT_RANGE, new QuantityType<Dimensionless>(
364 Math.min(100, (state - 6000) * 100 / (maxSystemCurrent - 6000)), Units.PERCENT));
369 int state = entry.getValue().getAsInt();
370 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
371 updateState(CHANNEL_FAILSAFE_CURRENT, newState);
375 int state = entry.getValue().getAsInt();
376 maxPresetCurrent = state;
377 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
378 updateState(CHANNEL_PILOT_CURRENT, newState);
382 int state = entry.getValue().getAsInt();
383 State newState = new QuantityType<Dimensionless>(state / 10.0, Units.PERCENT);
384 updateState(CHANNEL_PILOT_PWM, newState);
388 int state = entry.getValue().getAsInt();
391 updateState(CHANNEL_OUTPUT, OnOffType.ON);
395 updateState(CHANNEL_OUTPUT, OnOffType.OFF);
402 int state = entry.getValue().getAsInt();
405 updateState(CHANNEL_INPUT, OnOffType.ON);
409 updateState(CHANNEL_INPUT, OnOffType.OFF);
416 long state = entry.getValue().getAsLong();
417 State newState = new QuantityType<Time>(state, Units.SECOND);
418 updateState(CHANNEL_UPTIME, newState);
422 int state = entry.getValue().getAsInt();
423 State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
424 updateState(CHANNEL_U1, newState);
428 int state = entry.getValue().getAsInt();
429 State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
430 updateState(CHANNEL_U2, newState);
434 int state = entry.getValue().getAsInt();
435 State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
436 updateState(CHANNEL_U3, newState);
440 int state = entry.getValue().getAsInt();
441 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
442 updateState(CHANNEL_I1, newState);
446 int state = entry.getValue().getAsInt();
447 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
448 updateState(CHANNEL_I2, newState);
452 int state = entry.getValue().getAsInt();
453 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
454 updateState(CHANNEL_I3, newState);
458 long state = entry.getValue().getAsLong();
459 State newState = new QuantityType<Power>(state / 1000.0, Units.WATT);
460 updateState(CHANNEL_POWER, newState);
464 int state = entry.getValue().getAsInt();
465 State newState = new QuantityType<Dimensionless>(state / 10.0, Units.PERCENT);
466 updateState(CHANNEL_POWER_FACTOR, newState);
470 long state = entry.getValue().getAsLong();
471 State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
472 updateState(CHANNEL_SESSION_CONSUMPTION, newState);
476 long state = entry.getValue().getAsLong();
477 State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
478 updateState(CHANNEL_TOTAL_CONSUMPTION, newState);
482 int state = entry.getValue().getAsInt();
483 State newState = new DecimalType(state);
484 updateState(CHANNEL_AUTHON, newState);
488 int state = entry.getValue().getAsInt();
489 State newState = new DecimalType(state);
490 updateState(CHANNEL_AUTHREQ, newState);
494 String state = entry.getValue().getAsString().trim();
495 State newState = new StringType(state);
496 updateState(CHANNEL_SESSION_RFID_TAG, newState);
500 String state = entry.getValue().getAsString().trim();
501 State newState = new StringType(state);
502 updateState(CHANNEL_SESSION_RFID_CLASS, newState);
506 int state = entry.getValue().getAsInt();
507 State newState = new DecimalType(state);
508 updateState(CHANNEL_SESSION_SESSION_ID, newState);
512 int state = entry.getValue().getAsInt();
513 State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
514 updateState(CHANNEL_SETENERGY, newState);
519 } catch (JsonParseException e) {
520 logger.debug("Invalid JSON data will be ignored: '{}'", response);
525 public void handleCommand(ChannelUID channelUID, Command command) {
526 if ((command instanceof RefreshType)) {
527 // let's assume we do frequent enough polling and ignore the REFRESH request here
528 // in order to prevent too many channel state updates
530 switch (channelUID.getId()) {
531 case CHANNEL_MAX_PRESET_CURRENT: {
532 if (command instanceof QuantityType<?> quantityCommand) {
533 QuantityType<?> value = Objects.requireNonNull(quantityCommand.toUnit("mA"));
535 transceiver.send("curr " + Math.min(Math.max(6000, value.intValue()), maxSystemCurrent), this);
539 case CHANNEL_MAX_PRESET_CURRENT_RANGE: {
540 if (command instanceof OnOffType || command instanceof IncreaseDecreaseType
541 || command instanceof QuantityType<?>) {
542 long newValue = 6000;
543 if (command == IncreaseDecreaseType.INCREASE) {
544 newValue = Math.min(Math.max(6000, maxPresetCurrent + 1), maxSystemCurrent);
545 } else if (command == IncreaseDecreaseType.DECREASE) {
546 newValue = Math.min(Math.max(6000, maxPresetCurrent - 1), maxSystemCurrent);
547 } else if (command == OnOffType.ON) {
548 newValue = maxSystemCurrent;
549 } else if (command == OnOffType.OFF) {
551 } else if (command instanceof QuantityType<?> quantityCommand) {
552 QuantityType<?> value = Objects.requireNonNull(quantityCommand.toUnit("%"));
553 newValue = Math.round(6000 + (maxSystemCurrent - 6000) * value.doubleValue() / 100.0);
557 transceiver.send("curr " + newValue, this);
561 case CHANNEL_ENABLED: {
562 if (command instanceof OnOffType) {
563 if (command == OnOffType.ON) {
564 transceiver.send("ena 1", this);
565 } else if (command == OnOffType.OFF) {
566 transceiver.send("ena 0", this);
573 case CHANNEL_OUTPUT: {
574 if (command instanceof OnOffType) {
575 if (command == OnOffType.ON) {
576 transceiver.send("output 1", this);
577 } else if (command == OnOffType.OFF) {
578 transceiver.send("output 0", this);
585 case CHANNEL_DISPLAY: {
586 if (command instanceof StringType) {
587 if (type == KebaType.P30 && (series == KebaSeries.C || series == KebaSeries.X)) {
588 String cmd = command.toString();
589 int maxLength = (cmd.length() < 23) ? cmd.length() : 23;
590 transceiver.send("display 0 0 0 0 " + cmd.substring(0, maxLength), this);
592 logger.warn("'Display' is not supported on a KEBA KeContact {}:{}", type, series);
597 case CHANNEL_SETENERGY: {
598 if (command instanceof QuantityType<?> quantityCommand) {
599 QuantityType<?> value = Objects.requireNonNull(quantityCommand.toUnit(Units.WATT_HOUR));
601 "setenergy " + Math.min(Math.max(0, Math.round(value.doubleValue() * 10.0)), 999999999),
606 case CHANNEL_AUTHENTICATE: {
607 if (command instanceof StringType) {
608 String cmd = command.toString();
609 // cmd must contain ID + CLASS (works only if the RFID TAG is in the whitelist of the Keba
611 transceiver.send("start " + cmd, this);