2 * Copyright (c) 2010-2021 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 protected final JsonParser parser = new JsonParser();
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 transceiver.unRegisterHandler(getHandler());
187 if (getThing().getStatus() == ThingStatus.ONLINE) {
188 ByteBuffer response = cache.get(CACHE_REPORT_1);
189 if (response != null) {
193 Thread.sleep(REPORT_INTERVAL);
195 response = cache.get(CACHE_REPORT_2);
196 if (response != null) {
200 Thread.sleep(REPORT_INTERVAL);
202 response = cache.get(CACHE_REPORT_3);
203 if (response != null) {
207 if (isReport100needed) {
208 Thread.sleep(REPORT_INTERVAL);
210 response = cache.get(CACHE_REPORT_100);
211 if (response != null) {
214 isReport100needed = false;
218 } catch (NumberFormatException | IOException e) {
219 logger.debug("An exception occurred while polling the KEBA KeContact '{}': {}", getThing().getUID(),
221 Thread.currentThread().interrupt();
222 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
223 "An exception occurred while while polling the charging station");
224 } catch (InterruptedException e) {
225 logger.debug("Polling job has been interrupted for handler of thing '{}'.", getThing().getUID());
229 protected void onData(ByteBuffer byteBuffer) {
230 String response = new String(byteBuffer.array(), 0, byteBuffer.limit());
231 response = StringUtils.chomp(response);
233 if (response.contains("TCH-OK")) {
234 // ignore confirmation messages which are not JSON
239 JsonObject readObject = parser.parse(response).getAsJsonObject();
241 for (Entry<String, JsonElement> entry : readObject.entrySet()) {
242 switch (entry.getKey()) {
244 Map<String, String> properties = editProperties();
245 String product = entry.getValue().getAsString().trim();
246 properties.put(CHANNEL_MODEL, product);
247 updateProperties(properties);
248 if (product.contains("P20")) {
250 } else if (product.contains("P30")) {
253 series = KebaSeries.getSeries(product.substring(13, 14).charAt(0));
257 Map<String, String> properties = editProperties();
258 properties.put(CHANNEL_SERIAL, entry.getValue().getAsString());
259 updateProperties(properties);
263 Map<String, String> properties = editProperties();
264 properties.put(CHANNEL_FIRMWARE, entry.getValue().getAsString());
265 updateProperties(properties);
269 int state = entry.getValue().getAsInt();
272 updateState(CHANNEL_WALLBOX, OnOffType.OFF);
273 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
274 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
278 updateState(CHANNEL_WALLBOX, OnOffType.ON);
279 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
280 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
284 updateState(CHANNEL_WALLBOX, OnOffType.ON);
285 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
286 updateState(CHANNEL_PLUG_LOCKED, OnOffType.ON);
290 updateState(CHANNEL_WALLBOX, OnOffType.ON);
291 updateState(CHANNEL_VEHICLE, OnOffType.ON);
292 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
296 updateState(CHANNEL_WALLBOX, OnOffType.ON);
297 updateState(CHANNEL_VEHICLE, OnOffType.ON);
298 updateState(CHANNEL_PLUG_LOCKED, OnOffType.ON);
305 int state = entry.getValue().getAsInt();
306 State newState = new DecimalType(state);
307 updateState(CHANNEL_STATE, newState);
308 if (lastState != state) {
309 // the state is different from the last one, so we will trigger a report100
310 isReport100needed = true;
316 int state = entry.getValue().getAsInt();
319 updateState(CHANNEL_ENABLED, OnOffType.ON);
323 updateState(CHANNEL_ENABLED, OnOffType.OFF);
330 int state = entry.getValue().getAsInt();
331 maxSystemCurrent = state;
332 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
333 updateState(CHANNEL_MAX_SYSTEM_CURRENT, newState);
334 if (maxSystemCurrent != 0) {
335 if (maxSystemCurrent < maxPresetCurrent) {
336 transceiver.send("curr " + String.valueOf(maxSystemCurrent), this);
337 updateState(CHANNEL_MAX_PRESET_CURRENT,
338 new QuantityType<ElectricCurrent>(maxSystemCurrent / 1000.0, Units.AMPERE));
339 updateState(CHANNEL_MAX_PRESET_CURRENT_RANGE, new QuantityType<Dimensionless>(
340 (maxSystemCurrent - 6000) * 100 / (maxSystemCurrent - 6000), Units.PERCENT));
343 logger.debug("maxSystemCurrent is 0. Ignoring.");
348 int state = entry.getValue().getAsInt();
349 maxPresetCurrent = state;
350 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
351 updateState(CHANNEL_MAX_PRESET_CURRENT, newState);
352 if (maxSystemCurrent != 0) {
353 updateState(CHANNEL_MAX_PRESET_CURRENT_RANGE, new QuantityType<Dimensionless>(
354 Math.min(100, (state - 6000) * 100 / (maxSystemCurrent - 6000)), Units.PERCENT));
359 int state = entry.getValue().getAsInt();
360 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
361 updateState(CHANNEL_FAILSAFE_CURRENT, newState);
365 int state = entry.getValue().getAsInt();
366 maxPresetCurrent = state;
367 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
368 updateState(CHANNEL_PILOT_CURRENT, newState);
372 int state = entry.getValue().getAsInt();
373 State newState = new QuantityType<Dimensionless>(state / 10.0, Units.PERCENT);
374 updateState(CHANNEL_PILOT_PWM, newState);
378 int state = entry.getValue().getAsInt();
381 updateState(CHANNEL_OUTPUT, OnOffType.ON);
385 updateState(CHANNEL_OUTPUT, OnOffType.OFF);
392 int state = entry.getValue().getAsInt();
395 updateState(CHANNEL_INPUT, OnOffType.ON);
399 updateState(CHANNEL_INPUT, OnOffType.OFF);
406 long state = entry.getValue().getAsLong();
407 State newState = new QuantityType<Time>(state, Units.SECOND);
408 updateState(CHANNEL_UPTIME, newState);
412 int state = entry.getValue().getAsInt();
413 State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
414 updateState(CHANNEL_U1, newState);
418 int state = entry.getValue().getAsInt();
419 State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
420 updateState(CHANNEL_U2, newState);
424 int state = entry.getValue().getAsInt();
425 State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
426 updateState(CHANNEL_U3, newState);
430 int state = entry.getValue().getAsInt();
431 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
432 updateState(CHANNEL_I1, newState);
436 int state = entry.getValue().getAsInt();
437 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
438 updateState(CHANNEL_I2, newState);
442 int state = entry.getValue().getAsInt();
443 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
444 updateState(CHANNEL_I3, newState);
448 long state = entry.getValue().getAsLong();
449 State newState = new QuantityType<Power>(state / 1000.0, Units.WATT);
450 updateState(CHANNEL_POWER, newState);
454 int state = entry.getValue().getAsInt();
455 State newState = new QuantityType<Dimensionless>(state / 10.0, Units.PERCENT);
456 updateState(CHANNEL_POWER_FACTOR, newState);
460 long state = entry.getValue().getAsLong();
461 State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
462 updateState(CHANNEL_SESSION_CONSUMPTION, newState);
466 long state = entry.getValue().getAsLong();
467 State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
468 updateState(CHANNEL_TOTAL_CONSUMPTION, newState);
472 int state = entry.getValue().getAsInt();
473 State newState = new DecimalType(state);
474 updateState(CHANNEL_AUTHON, newState);
478 int state = entry.getValue().getAsInt();
479 State newState = new DecimalType(state);
480 updateState(CHANNEL_AUTHREQ, newState);
484 String state = entry.getValue().getAsString().trim();
485 State newState = new StringType(state);
486 updateState(CHANNEL_SESSION_RFID_TAG, newState);
490 String state = entry.getValue().getAsString().trim();
491 State newState = new StringType(state);
492 updateState(CHANNEL_SESSION_RFID_CLASS, newState);
496 int state = entry.getValue().getAsInt();
497 State newState = new DecimalType(state);
498 updateState(CHANNEL_SESSION_SESSION_ID, newState);
502 int state = entry.getValue().getAsInt();
503 State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
504 updateState(CHANNEL_SETENERGY, newState);
509 } catch (JsonParseException e) {
510 logger.debug("Invalid JSON data will be ignored: '{}'", response);
515 public void handleCommand(ChannelUID channelUID, Command command) {
516 if ((command instanceof RefreshType)) {
517 // let's assume we do frequent enough polling and ignore the REFRESH request here
518 // in order to prevent too many channel state updates
520 switch (channelUID.getId()) {
521 case CHANNEL_MAX_PRESET_CURRENT: {
522 if (command instanceof QuantityType<?>) {
523 QuantityType<?> value = ((QuantityType<?>) command).toUnit("mA");
526 "curr " + String.valueOf(Math.min(Math.max(6000, value.intValue()), maxSystemCurrent)),
531 case CHANNEL_MAX_PRESET_CURRENT_RANGE: {
532 if (command instanceof OnOffType || command instanceof IncreaseDecreaseType
533 || command instanceof QuantityType<?>) {
534 long newValue = 6000;
535 if (command == IncreaseDecreaseType.INCREASE) {
536 newValue = Math.min(Math.max(6000, maxPresetCurrent + 1), maxSystemCurrent);
537 } else if (command == IncreaseDecreaseType.DECREASE) {
538 newValue = Math.min(Math.max(6000, maxPresetCurrent - 1), maxSystemCurrent);
539 } else if (command == OnOffType.ON) {
540 newValue = maxSystemCurrent;
541 } else if (command == OnOffType.OFF) {
543 } else if (command instanceof QuantityType<?>) {
544 QuantityType<?> value = ((QuantityType<?>) command).toUnit("%");
545 newValue = Math.round(6000 + (maxSystemCurrent - 6000) * value.doubleValue() / 100.0);
549 transceiver.send("curr " + String.valueOf(newValue), this);
553 case CHANNEL_ENABLED: {
554 if (command instanceof OnOffType) {
555 if (command == OnOffType.ON) {
556 transceiver.send("ena 1", this);
557 } else if (command == OnOffType.OFF) {
558 transceiver.send("ena 0", this);
565 case CHANNEL_OUTPUT: {
566 if (command instanceof OnOffType) {
567 if (command == OnOffType.ON) {
568 transceiver.send("output 1", this);
569 } else if (command == OnOffType.OFF) {
570 transceiver.send("output 0", this);
577 case CHANNEL_DISPLAY: {
578 if (command instanceof StringType) {
579 if (type == KebaType.P30 && (series == KebaSeries.C || series == KebaSeries.X)) {
580 String cmd = command.toString();
581 int maxLength = (cmd.length() < 23) ? cmd.length() : 23;
582 transceiver.send("display 0 0 0 0 " + cmd.substring(0, maxLength), this);
584 logger.warn("'Display' is not supported on a KEBA KeContact {}:{}", type, series);
589 case CHANNEL_SETENERGY: {
590 if (command instanceof QuantityType<?>) {
591 QuantityType<?> value = ((QuantityType<?>) command).toUnit(Units.WATT_HOUR);
593 "setenergy " + String.valueOf(
594 Math.min(Math.max(0, Math.round(value.doubleValue() * 10.0)), 999999999)),
599 case CHANNEL_AUTHENTICATE: {
600 if (command instanceof StringType) {
601 String cmd = command.toString();
602 // cmd must contain ID + CLASS (works only if the RFID TAG is in the whitelist of the Keba
604 transceiver.send("start " + cmd, this);