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.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
28 import javax.measure.quantity.Dimensionless;
29 import javax.measure.quantity.ElectricCurrent;
30 import javax.measure.quantity.ElectricPotential;
31 import javax.measure.quantity.Energy;
32 import javax.measure.quantity.Power;
33 import javax.measure.quantity.Time;
35 import org.apache.commons.lang3.StringUtils;
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.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
57 import com.google.gson.JsonElement;
58 import com.google.gson.JsonObject;
59 import com.google.gson.JsonParseException;
60 import com.google.gson.JsonParser;
63 * The {@link KeContactHandler} is responsible for handling commands, which
64 * are sent to one of the channels.
66 * @author Karel Goderis - Initial contribution
68 public class KeContactHandler extends BaseThingHandler {
70 public static final String IP_ADDRESS = "ipAddress";
71 public static final String POLLING_REFRESH_INTERVAL = "refreshInterval";
72 public static final int POLLING_REFRESH_INTERVAL_DEFAULT = 15;
73 public static final int REPORT_INTERVAL = 3000;
74 public static final int BUFFER_SIZE = 1024;
75 public static final int REMOTE_PORT_NUMBER = 7090;
76 private static final String CACHE_REPORT_1 = "REPORT_1";
77 private static final String CACHE_REPORT_2 = "REPORT_2";
78 private static final String CACHE_REPORT_3 = "REPORT_3";
79 private static final String CACHE_REPORT_100 = "REPORT_100";
80 public static final int SOCKET_TIME_OUT_MS = 3000;
81 public static final int SOCKET_CHECK_PORT_NUMBER = 80;
83 private final Logger logger = LoggerFactory.getLogger(KeContactHandler.class);
85 private final KeContactTransceiver transceiver;
87 private ScheduledFuture<?> pollingJob;
88 private ExpiringCacheMap<String, ByteBuffer> cache;
90 private int maxPresetCurrent = 0;
91 private int maxSystemCurrent = 63000;
92 private KebaType type;
93 private KebaSeries series;
94 private int lastState = -1; // trigger a report100 at startup
95 private boolean isReport100needed = true;
97 public KeContactHandler(Thing thing, KeContactTransceiver transceiver) {
99 this.transceiver = transceiver;
103 public void initialize() {
105 if (isKebaReachable()) {
106 transceiver.registerHandler(this);
108 int refreshInterval = getRefreshInterval();
109 cache = new ExpiringCacheMap<>(Math.max(refreshInterval - 5, 0) * 1000);
111 cache.put(CACHE_REPORT_1, () -> transceiver.send("report 1", getHandler()));
112 cache.put(CACHE_REPORT_2, () -> transceiver.send("report 2", getHandler()));
113 cache.put(CACHE_REPORT_3, () -> transceiver.send("report 3", getHandler()));
114 cache.put(CACHE_REPORT_100, () -> transceiver.send("report 100", getHandler()));
116 if (pollingJob == null || pollingJob.isCancelled()) {
117 pollingJob = scheduler.scheduleWithFixedDelay(this::pollingRunnable, 0, refreshInterval,
121 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
122 "IP address or port number not set");
124 } catch (IOException e) {
125 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
126 "Exception during initialization of binding: " + e.toString());
130 private boolean isKebaReachable() throws IOException {
131 boolean isReachable = false;
132 SocketAddress sockAddr = new InetSocketAddress(getIPAddress(), SOCKET_CHECK_PORT_NUMBER);
133 Socket socket = new Socket();
135 socket.connect(sockAddr, SOCKET_TIME_OUT_MS);
140 logger.debug("isKebaReachable() returns {}", isReachable);
145 public void dispose() {
146 if (pollingJob != null && !pollingJob.isCancelled()) {
147 pollingJob.cancel(true);
151 transceiver.unRegisterHandler(this);
154 public String getIPAddress() {
155 return getConfig().get(IP_ADDRESS) != null ? (String) getConfig().get(IP_ADDRESS) : "";
158 public int getRefreshInterval() {
159 return getConfig().get(POLLING_REFRESH_INTERVAL) != null
160 ? ((BigDecimal) getConfig().get(POLLING_REFRESH_INTERVAL)).intValue()
161 : POLLING_REFRESH_INTERVAL_DEFAULT;
164 private KeContactHandler getHandler() {
169 public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, String description) {
170 super.updateStatus(status, statusDetail, description);
174 protected Configuration getConfig() {
175 return super.getConfig();
178 private void pollingRunnable() {
180 logger.debug("Running pollingRunnable to connect Keba wallbox");
181 long stamp = System.currentTimeMillis();
182 if (!isKebaReachable()) {
183 logger.debug("isKebaReachable() timed out after '{}' milliseconds", System.currentTimeMillis() - stamp);
184 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
185 "A timeout occurred while polling the charging station");
187 ByteBuffer response = cache.get(CACHE_REPORT_1);
188 if (response == null) {
189 logger.debug("Missing response from Keba station for 'report 1'");
194 Thread.sleep(REPORT_INTERVAL);
196 response = cache.get(CACHE_REPORT_2);
197 if (response == null) {
198 logger.debug("Missing response from Keba station for 'report 2'");
203 Thread.sleep(REPORT_INTERVAL);
205 response = cache.get(CACHE_REPORT_3);
206 if (response == null) {
207 logger.debug("Missing response from Keba station for 'report 3'");
212 if (isReport100needed) {
213 Thread.sleep(REPORT_INTERVAL);
215 response = cache.get(CACHE_REPORT_100);
216 if (response == null) {
217 logger.debug("Missing response from Keba station for 'report 100'");
221 isReport100needed = false;
224 } catch (IOException e) {
225 logger.debug("An error occurred while polling the KEBA KeContact '{}': {}", getThing().getUID(),
227 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
228 "An error occurred while polling the charging station");
229 } catch (InterruptedException e) {
230 logger.debug("Polling job has been interrupted for handler of thing '{}'.", getThing().getUID());
234 protected void onData(ByteBuffer byteBuffer) {
235 if (getThing().getStatus() != ThingStatus.ONLINE) {
236 updateStatus(ThingStatus.ONLINE);
239 String response = new String(byteBuffer.array(), 0, byteBuffer.limit());
240 response = StringUtils.chomp(response);
242 if (response.contains("TCH-OK")) {
243 // ignore confirmation messages which are not JSON
248 JsonObject readObject = JsonParser.parseString(response).getAsJsonObject();
250 for (Entry<String, JsonElement> entry : readObject.entrySet()) {
251 switch (entry.getKey()) {
253 Map<String, String> properties = editProperties();
254 String product = entry.getValue().getAsString().trim();
255 properties.put(CHANNEL_MODEL, product);
256 updateProperties(properties);
257 if (product.contains("P20")) {
259 } else if (product.contains("P30")) {
262 series = KebaSeries.getSeries(product.substring(13, 14).charAt(0));
266 Map<String, String> properties = editProperties();
267 properties.put(CHANNEL_SERIAL, entry.getValue().getAsString());
268 updateProperties(properties);
272 Map<String, String> properties = editProperties();
273 properties.put(CHANNEL_FIRMWARE, entry.getValue().getAsString());
274 updateProperties(properties);
278 int state = entry.getValue().getAsInt();
281 updateState(CHANNEL_WALLBOX, OnOffType.OFF);
282 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
283 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
287 updateState(CHANNEL_WALLBOX, OnOffType.ON);
288 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
289 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
293 updateState(CHANNEL_WALLBOX, OnOffType.ON);
294 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
295 updateState(CHANNEL_PLUG_LOCKED, OnOffType.ON);
299 updateState(CHANNEL_WALLBOX, OnOffType.ON);
300 updateState(CHANNEL_VEHICLE, OnOffType.ON);
301 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
305 updateState(CHANNEL_WALLBOX, OnOffType.ON);
306 updateState(CHANNEL_VEHICLE, OnOffType.ON);
307 updateState(CHANNEL_PLUG_LOCKED, OnOffType.ON);
314 int state = entry.getValue().getAsInt();
315 State newState = new DecimalType(state);
316 updateState(CHANNEL_STATE, newState);
317 if (lastState != state) {
318 // the state is different from the last one, so we will trigger a report100
319 isReport100needed = true;
325 int state = entry.getValue().getAsInt();
328 updateState(CHANNEL_ENABLED, OnOffType.ON);
332 updateState(CHANNEL_ENABLED, OnOffType.OFF);
339 int state = entry.getValue().getAsInt();
340 maxSystemCurrent = state;
341 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
342 updateState(CHANNEL_MAX_SYSTEM_CURRENT, newState);
343 if (maxSystemCurrent != 0) {
344 if (maxSystemCurrent < maxPresetCurrent) {
345 transceiver.send("curr " + String.valueOf(maxSystemCurrent), this);
346 updateState(CHANNEL_MAX_PRESET_CURRENT,
347 new QuantityType<ElectricCurrent>(maxSystemCurrent / 1000.0, Units.AMPERE));
348 updateState(CHANNEL_MAX_PRESET_CURRENT_RANGE, new QuantityType<Dimensionless>(
349 (maxSystemCurrent - 6000) * 100 / (maxSystemCurrent - 6000), Units.PERCENT));
352 logger.debug("maxSystemCurrent is 0. Ignoring.");
357 int state = entry.getValue().getAsInt();
358 maxPresetCurrent = state;
359 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
360 updateState(CHANNEL_MAX_PRESET_CURRENT, newState);
361 if (maxSystemCurrent != 0) {
362 updateState(CHANNEL_MAX_PRESET_CURRENT_RANGE, new QuantityType<Dimensionless>(
363 Math.min(100, (state - 6000) * 100 / (maxSystemCurrent - 6000)), Units.PERCENT));
368 int state = entry.getValue().getAsInt();
369 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
370 updateState(CHANNEL_FAILSAFE_CURRENT, newState);
374 int state = entry.getValue().getAsInt();
375 maxPresetCurrent = state;
376 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
377 updateState(CHANNEL_PILOT_CURRENT, newState);
381 int state = entry.getValue().getAsInt();
382 State newState = new QuantityType<Dimensionless>(state / 10.0, Units.PERCENT);
383 updateState(CHANNEL_PILOT_PWM, newState);
387 int state = entry.getValue().getAsInt();
390 updateState(CHANNEL_OUTPUT, OnOffType.ON);
394 updateState(CHANNEL_OUTPUT, OnOffType.OFF);
401 int state = entry.getValue().getAsInt();
404 updateState(CHANNEL_INPUT, OnOffType.ON);
408 updateState(CHANNEL_INPUT, OnOffType.OFF);
415 long state = entry.getValue().getAsLong();
416 State newState = new QuantityType<Time>(state, Units.SECOND);
417 updateState(CHANNEL_UPTIME, newState);
421 int state = entry.getValue().getAsInt();
422 State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
423 updateState(CHANNEL_U1, newState);
427 int state = entry.getValue().getAsInt();
428 State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
429 updateState(CHANNEL_U2, newState);
433 int state = entry.getValue().getAsInt();
434 State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
435 updateState(CHANNEL_U3, newState);
439 int state = entry.getValue().getAsInt();
440 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
441 updateState(CHANNEL_I1, newState);
445 int state = entry.getValue().getAsInt();
446 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
447 updateState(CHANNEL_I2, newState);
451 int state = entry.getValue().getAsInt();
452 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
453 updateState(CHANNEL_I3, newState);
457 long state = entry.getValue().getAsLong();
458 State newState = new QuantityType<Power>(state / 1000.0, Units.WATT);
459 updateState(CHANNEL_POWER, newState);
463 int state = entry.getValue().getAsInt();
464 State newState = new QuantityType<Dimensionless>(state / 10.0, Units.PERCENT);
465 updateState(CHANNEL_POWER_FACTOR, newState);
469 long state = entry.getValue().getAsLong();
470 State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
471 updateState(CHANNEL_SESSION_CONSUMPTION, newState);
475 long state = entry.getValue().getAsLong();
476 State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
477 updateState(CHANNEL_TOTAL_CONSUMPTION, newState);
481 int state = entry.getValue().getAsInt();
482 State newState = new DecimalType(state);
483 updateState(CHANNEL_AUTHON, newState);
487 int state = entry.getValue().getAsInt();
488 State newState = new DecimalType(state);
489 updateState(CHANNEL_AUTHREQ, newState);
493 String state = entry.getValue().getAsString().trim();
494 State newState = new StringType(state);
495 updateState(CHANNEL_SESSION_RFID_TAG, newState);
499 String state = entry.getValue().getAsString().trim();
500 State newState = new StringType(state);
501 updateState(CHANNEL_SESSION_RFID_CLASS, newState);
505 int state = entry.getValue().getAsInt();
506 State newState = new DecimalType(state);
507 updateState(CHANNEL_SESSION_SESSION_ID, newState);
511 int state = entry.getValue().getAsInt();
512 State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
513 updateState(CHANNEL_SETENERGY, newState);
518 } catch (JsonParseException e) {
519 logger.debug("Invalid JSON data will be ignored: '{}'", response);
524 public void handleCommand(ChannelUID channelUID, Command command) {
525 if ((command instanceof RefreshType)) {
526 // let's assume we do frequent enough polling and ignore the REFRESH request here
527 // in order to prevent too many channel state updates
529 switch (channelUID.getId()) {
530 case CHANNEL_MAX_PRESET_CURRENT: {
531 if (command instanceof QuantityType<?>) {
532 QuantityType<?> value = ((QuantityType<?>) command).toUnit("mA");
535 "curr " + String.valueOf(Math.min(Math.max(6000, value.intValue()), maxSystemCurrent)),
540 case CHANNEL_MAX_PRESET_CURRENT_RANGE: {
541 if (command instanceof OnOffType || command instanceof IncreaseDecreaseType
542 || command instanceof QuantityType<?>) {
543 long newValue = 6000;
544 if (command == IncreaseDecreaseType.INCREASE) {
545 newValue = Math.min(Math.max(6000, maxPresetCurrent + 1), maxSystemCurrent);
546 } else if (command == IncreaseDecreaseType.DECREASE) {
547 newValue = Math.min(Math.max(6000, maxPresetCurrent - 1), maxSystemCurrent);
548 } else if (command == OnOffType.ON) {
549 newValue = maxSystemCurrent;
550 } else if (command == OnOffType.OFF) {
552 } else if (command instanceof QuantityType<?>) {
553 QuantityType<?> value = ((QuantityType<?>) command).toUnit("%");
554 newValue = Math.round(6000 + (maxSystemCurrent - 6000) * value.doubleValue() / 100.0);
558 transceiver.send("curr " + String.valueOf(newValue), this);
562 case CHANNEL_ENABLED: {
563 if (command instanceof OnOffType) {
564 if (command == OnOffType.ON) {
565 transceiver.send("ena 1", this);
566 } else if (command == OnOffType.OFF) {
567 transceiver.send("ena 0", this);
574 case CHANNEL_OUTPUT: {
575 if (command instanceof OnOffType) {
576 if (command == OnOffType.ON) {
577 transceiver.send("output 1", this);
578 } else if (command == OnOffType.OFF) {
579 transceiver.send("output 0", this);
586 case CHANNEL_DISPLAY: {
587 if (command instanceof StringType) {
588 if (type == KebaType.P30 && (series == KebaSeries.C || series == KebaSeries.X)) {
589 String cmd = command.toString();
590 int maxLength = (cmd.length() < 23) ? cmd.length() : 23;
591 transceiver.send("display 0 0 0 0 " + cmd.substring(0, maxLength), this);
593 logger.warn("'Display' is not supported on a KEBA KeContact {}:{}", type, series);
598 case CHANNEL_SETENERGY: {
599 if (command instanceof QuantityType<?>) {
600 QuantityType<?> value = ((QuantityType<?>) command).toUnit(Units.WATT_HOUR);
602 "setenergy " + String.valueOf(
603 Math.min(Math.max(0, Math.round(value.doubleValue() * 10.0)), 999999999)),
608 case CHANNEL_AUTHENTICATE: {
609 if (command instanceof StringType) {
610 String cmd = command.toString();
611 // cmd must contain ID + CLASS (works only if the RFID TAG is in the whitelist of the Keba
613 transceiver.send("start " + cmd, this);