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 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 transceiver.unRegisterHandler(getHandler());
186 if (getThing().getStatus() == ThingStatus.ONLINE) {
187 ByteBuffer response = cache.get(CACHE_REPORT_1);
188 if (response != null) {
192 Thread.sleep(REPORT_INTERVAL);
194 response = cache.get(CACHE_REPORT_2);
195 if (response != null) {
199 Thread.sleep(REPORT_INTERVAL);
201 response = cache.get(CACHE_REPORT_3);
202 if (response != null) {
206 if (isReport100needed) {
207 Thread.sleep(REPORT_INTERVAL);
209 response = cache.get(CACHE_REPORT_100);
210 if (response != null) {
213 isReport100needed = false;
217 } catch (NumberFormatException | IOException e) {
218 logger.debug("An exception occurred while polling the KEBA KeContact '{}': {}", getThing().getUID(),
220 Thread.currentThread().interrupt();
221 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
222 "An exception occurred while while polling the charging station");
223 } catch (InterruptedException e) {
224 logger.debug("Polling job has been interrupted for handler of thing '{}'.", getThing().getUID());
228 protected void onData(ByteBuffer byteBuffer) {
229 String response = new String(byteBuffer.array(), 0, byteBuffer.limit());
230 response = StringUtils.chomp(response);
232 if (response.contains("TCH-OK")) {
233 // ignore confirmation messages which are not JSON
238 JsonObject readObject = JsonParser.parseString(response).getAsJsonObject();
240 for (Entry<String, JsonElement> entry : readObject.entrySet()) {
241 switch (entry.getKey()) {
243 Map<String, String> properties = editProperties();
244 String product = entry.getValue().getAsString().trim();
245 properties.put(CHANNEL_MODEL, product);
246 updateProperties(properties);
247 if (product.contains("P20")) {
249 } else if (product.contains("P30")) {
252 series = KebaSeries.getSeries(product.substring(13, 14).charAt(0));
256 Map<String, String> properties = editProperties();
257 properties.put(CHANNEL_SERIAL, entry.getValue().getAsString());
258 updateProperties(properties);
262 Map<String, String> properties = editProperties();
263 properties.put(CHANNEL_FIRMWARE, entry.getValue().getAsString());
264 updateProperties(properties);
268 int state = entry.getValue().getAsInt();
271 updateState(CHANNEL_WALLBOX, OnOffType.OFF);
272 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
273 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
277 updateState(CHANNEL_WALLBOX, OnOffType.ON);
278 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
279 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
283 updateState(CHANNEL_WALLBOX, OnOffType.ON);
284 updateState(CHANNEL_VEHICLE, OnOffType.OFF);
285 updateState(CHANNEL_PLUG_LOCKED, OnOffType.ON);
289 updateState(CHANNEL_WALLBOX, OnOffType.ON);
290 updateState(CHANNEL_VEHICLE, OnOffType.ON);
291 updateState(CHANNEL_PLUG_LOCKED, OnOffType.OFF);
295 updateState(CHANNEL_WALLBOX, OnOffType.ON);
296 updateState(CHANNEL_VEHICLE, OnOffType.ON);
297 updateState(CHANNEL_PLUG_LOCKED, OnOffType.ON);
304 int state = entry.getValue().getAsInt();
305 State newState = new DecimalType(state);
306 updateState(CHANNEL_STATE, newState);
307 if (lastState != state) {
308 // the state is different from the last one, so we will trigger a report100
309 isReport100needed = true;
315 int state = entry.getValue().getAsInt();
318 updateState(CHANNEL_ENABLED, OnOffType.ON);
322 updateState(CHANNEL_ENABLED, OnOffType.OFF);
329 int state = entry.getValue().getAsInt();
330 maxSystemCurrent = state;
331 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
332 updateState(CHANNEL_MAX_SYSTEM_CURRENT, newState);
333 if (maxSystemCurrent != 0) {
334 if (maxSystemCurrent < maxPresetCurrent) {
335 transceiver.send("curr " + String.valueOf(maxSystemCurrent), this);
336 updateState(CHANNEL_MAX_PRESET_CURRENT,
337 new QuantityType<ElectricCurrent>(maxSystemCurrent / 1000.0, Units.AMPERE));
338 updateState(CHANNEL_MAX_PRESET_CURRENT_RANGE, new QuantityType<Dimensionless>(
339 (maxSystemCurrent - 6000) * 100 / (maxSystemCurrent - 6000), Units.PERCENT));
342 logger.debug("maxSystemCurrent is 0. Ignoring.");
347 int state = entry.getValue().getAsInt();
348 maxPresetCurrent = state;
349 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
350 updateState(CHANNEL_MAX_PRESET_CURRENT, newState);
351 if (maxSystemCurrent != 0) {
352 updateState(CHANNEL_MAX_PRESET_CURRENT_RANGE, new QuantityType<Dimensionless>(
353 Math.min(100, (state - 6000) * 100 / (maxSystemCurrent - 6000)), Units.PERCENT));
358 int state = entry.getValue().getAsInt();
359 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
360 updateState(CHANNEL_FAILSAFE_CURRENT, newState);
364 int state = entry.getValue().getAsInt();
365 maxPresetCurrent = state;
366 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
367 updateState(CHANNEL_PILOT_CURRENT, newState);
371 int state = entry.getValue().getAsInt();
372 State newState = new QuantityType<Dimensionless>(state / 10.0, Units.PERCENT);
373 updateState(CHANNEL_PILOT_PWM, newState);
377 int state = entry.getValue().getAsInt();
380 updateState(CHANNEL_OUTPUT, OnOffType.ON);
384 updateState(CHANNEL_OUTPUT, OnOffType.OFF);
391 int state = entry.getValue().getAsInt();
394 updateState(CHANNEL_INPUT, OnOffType.ON);
398 updateState(CHANNEL_INPUT, OnOffType.OFF);
405 long state = entry.getValue().getAsLong();
406 State newState = new QuantityType<Time>(state, Units.SECOND);
407 updateState(CHANNEL_UPTIME, newState);
411 int state = entry.getValue().getAsInt();
412 State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
413 updateState(CHANNEL_U1, newState);
417 int state = entry.getValue().getAsInt();
418 State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
419 updateState(CHANNEL_U2, newState);
423 int state = entry.getValue().getAsInt();
424 State newState = new QuantityType<ElectricPotential>(state, Units.VOLT);
425 updateState(CHANNEL_U3, newState);
429 int state = entry.getValue().getAsInt();
430 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
431 updateState(CHANNEL_I1, newState);
435 int state = entry.getValue().getAsInt();
436 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
437 updateState(CHANNEL_I2, newState);
441 int state = entry.getValue().getAsInt();
442 State newState = new QuantityType<ElectricCurrent>(state / 1000.0, Units.AMPERE);
443 updateState(CHANNEL_I3, newState);
447 long state = entry.getValue().getAsLong();
448 State newState = new QuantityType<Power>(state / 1000.0, Units.WATT);
449 updateState(CHANNEL_POWER, newState);
453 int state = entry.getValue().getAsInt();
454 State newState = new QuantityType<Dimensionless>(state / 10.0, Units.PERCENT);
455 updateState(CHANNEL_POWER_FACTOR, newState);
459 long state = entry.getValue().getAsLong();
460 State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
461 updateState(CHANNEL_SESSION_CONSUMPTION, newState);
465 long state = entry.getValue().getAsLong();
466 State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
467 updateState(CHANNEL_TOTAL_CONSUMPTION, newState);
471 int state = entry.getValue().getAsInt();
472 State newState = new DecimalType(state);
473 updateState(CHANNEL_AUTHON, newState);
477 int state = entry.getValue().getAsInt();
478 State newState = new DecimalType(state);
479 updateState(CHANNEL_AUTHREQ, newState);
483 String state = entry.getValue().getAsString().trim();
484 State newState = new StringType(state);
485 updateState(CHANNEL_SESSION_RFID_TAG, newState);
489 String state = entry.getValue().getAsString().trim();
490 State newState = new StringType(state);
491 updateState(CHANNEL_SESSION_RFID_CLASS, newState);
495 int state = entry.getValue().getAsInt();
496 State newState = new DecimalType(state);
497 updateState(CHANNEL_SESSION_SESSION_ID, newState);
501 int state = entry.getValue().getAsInt();
502 State newState = new QuantityType<Energy>(state / 10.0, Units.WATT_HOUR);
503 updateState(CHANNEL_SETENERGY, newState);
508 } catch (JsonParseException e) {
509 logger.debug("Invalid JSON data will be ignored: '{}'", response);
514 public void handleCommand(ChannelUID channelUID, Command command) {
515 if ((command instanceof RefreshType)) {
516 // let's assume we do frequent enough polling and ignore the REFRESH request here
517 // in order to prevent too many channel state updates
519 switch (channelUID.getId()) {
520 case CHANNEL_MAX_PRESET_CURRENT: {
521 if (command instanceof QuantityType<?>) {
522 QuantityType<?> value = ((QuantityType<?>) command).toUnit("mA");
525 "curr " + String.valueOf(Math.min(Math.max(6000, value.intValue()), maxSystemCurrent)),
530 case CHANNEL_MAX_PRESET_CURRENT_RANGE: {
531 if (command instanceof OnOffType || command instanceof IncreaseDecreaseType
532 || command instanceof QuantityType<?>) {
533 long newValue = 6000;
534 if (command == IncreaseDecreaseType.INCREASE) {
535 newValue = Math.min(Math.max(6000, maxPresetCurrent + 1), maxSystemCurrent);
536 } else if (command == IncreaseDecreaseType.DECREASE) {
537 newValue = Math.min(Math.max(6000, maxPresetCurrent - 1), maxSystemCurrent);
538 } else if (command == OnOffType.ON) {
539 newValue = maxSystemCurrent;
540 } else if (command == OnOffType.OFF) {
542 } else if (command instanceof QuantityType<?>) {
543 QuantityType<?> value = ((QuantityType<?>) command).toUnit("%");
544 newValue = Math.round(6000 + (maxSystemCurrent - 6000) * value.doubleValue() / 100.0);
548 transceiver.send("curr " + String.valueOf(newValue), this);
552 case CHANNEL_ENABLED: {
553 if (command instanceof OnOffType) {
554 if (command == OnOffType.ON) {
555 transceiver.send("ena 1", this);
556 } else if (command == OnOffType.OFF) {
557 transceiver.send("ena 0", this);
564 case CHANNEL_OUTPUT: {
565 if (command instanceof OnOffType) {
566 if (command == OnOffType.ON) {
567 transceiver.send("output 1", this);
568 } else if (command == OnOffType.OFF) {
569 transceiver.send("output 0", this);
576 case CHANNEL_DISPLAY: {
577 if (command instanceof StringType) {
578 if (type == KebaType.P30 && (series == KebaSeries.C || series == KebaSeries.X)) {
579 String cmd = command.toString();
580 int maxLength = (cmd.length() < 23) ? cmd.length() : 23;
581 transceiver.send("display 0 0 0 0 " + cmd.substring(0, maxLength), this);
583 logger.warn("'Display' is not supported on a KEBA KeContact {}:{}", type, series);
588 case CHANNEL_SETENERGY: {
589 if (command instanceof QuantityType<?>) {
590 QuantityType<?> value = ((QuantityType<?>) command).toUnit(Units.WATT_HOUR);
592 "setenergy " + String.valueOf(
593 Math.min(Math.max(0, Math.round(value.doubleValue() * 10.0)), 999999999)),
598 case CHANNEL_AUTHENTICATE: {
599 if (command instanceof StringType) {
600 String cmd = command.toString();
601 // cmd must contain ID + CLASS (works only if the RFID TAG is in the whitelist of the Keba
603 transceiver.send("start " + cmd, this);